The canary and the Workflow test¶
The canary¶
The tests replace the engine, and the Action test runs everything but it, so the path that matters most, from an issue through the engine to the checked answer, ran only when someone opened an issue: here that is rare, and every repository that uses the action runs it with its own secret. A renamed output of the engine's action, a host that the firewall blocks, an expired secret, or a secret or variable that no longer reaches the called workflow would show as issues without an answer, in every repository at once. The Canary, .github/workflows/canary.yml, runs the whole path every Monday, after every change on main of what it runs, and by hand:
- It calls the reusable workflow as every repository does. Its job is the call of the template
templates/.github/workflows/issue-assistant.yml, with the same name, the same permissions and the engine's secrets passed by name, whichtests/test_templates.pykeeps equal to the template's. Only the workflow differs,./.github/workflows/reusable-issue-assistant.ymlof the commit, and the inputcanary. So the run takes the path of production: the job that calls can't name an environment, and the jobanalyzegets the value of the secret from the environmentissue-assistant; the called jobs see the variableISSUE_ASSISTANT_AIof the repository; and the engine runs on the firewall runner, with the steps and pins of every repository. Only withcanarydo the engines also accept a run that a bot started, as the canary's own runs after a push are. - The issue exists nowhere.
tests/fixtures/canary-issue.jsonholds an issue of the bug report form of this repository and a comment, as GitHub's REST API gives them. Withcanary, the environment variableISSUE_ASSISTANT_FIXTUREgives them tocontextandapplyin place of an issue on GitHub; the other issues, the discussions, the labels and the latest release come from GitHub, as for a real issue. The jobeventtakes the canary's issue in place of that of an event and sets no labels, and with a fixtureapplyis a dry run whateverdry-runsays, so nothing reaches an issue, although the token ofapplymay write, as in every repository. A test keeps the issue fitting the form and the labels of this repository. - Where a repository's run skips the AI, the canary fails. The job
eventfails when the called workflow doesn't seeISSUE_ASSISTANT_AIset toon, which the settings bot sets here from.github/repository.toml, and the jobanalyzefails without the engine's secret or without an answer. A repository's run would skipanalyzeorapplythere, and the canary would stay green. - Only this repository passes
canary. The jobeventfails a call withcanaryfrom another repository, which has no canary's issue. Every step and value of the canary in the reusable workflow namesinputs.canary, which every other caller leavesfalse, and a test keeps the jobanalyzeof the discussions equal to that of the issues without them. - The repository of the caller is that of the workflow. The Canary calls the workflow of its own repository, so it shows that the secret and the variables reach a called job, not whose they are when the two repositories differ: that a called job sees the caller's rests on GitHub's documentation, as decision 0015 says.
- The firewall policy is this repository's. The runner enforces
.github/egress-firewall.yaml, the blueprint's copy of the template of the release that it pins, so a change oftemplates/.github/egress-firewall.yamlreaches the canary with the blueprint's next update. - Only
mainruns it. Only the default branch may use the environmentissue-assistant, so no pull request runs the canary. A minor or major version of the engine's action waits for the maintainer in a pull request of its own, and a patch version merges on its own once the check engine contracts passed, as.github/renovate.json5says. The push of either merge starts the canary with the new version; the maintainer merges the next release only once the canary passed, and a failed update is reverted. A release of dependency updates alone merges itself, which decision 0020 names as a residual risk. The engine's action runs for a schedule or a run by hand, not for a push, so after a push the jobstart, whose token may only start workflows, starts the canary as a run by hand and waits for its result. - It measures the wait for the firewall runner. Its job the AI's wait for its runner runs
scripts/canary_queue.pybeside the call and reads the jobs of its own run untilanalyzestarts. The wait counts from the end of the jobevent, whichanalyzeneeds, so neither the event nor an earlier canary counts; the summary shows it, with a warning above 10 minutes and a failure above 30, also whileanalyzestill waits, which the PR coach explains from.github/checks.project.toml. Onmainthe job then opens the issue of the bot-health bot for the Canary at once, since the run, and with it GitHub's mail and bot-health's look, ends only whenanalyzedoes. - A red canary is seen. GitHub mails the failure of a scheduled run to whoever last changed its schedule, here the maintainer. A run that the job
startstarted has nobody to mail, so that job fails with it, and GitHub mails the failure of the push's run to the person who merged; a canary that a later push's canary replaced fails nothing. Every red run also shows under Actions.
The Workflow test¶
The tests and actionlint only read the reusable workflows, the Action test runs the action by itself, and the Canary calls only the workflow of the issues, only on main and with the engine's secret. So nothing ran the jobs that every repository calls on a pull request: a wrong needs, if, output or permission would show first in those repositories, after a release, often without a red run, since a job that its condition skips passes. The Workflow test, .github/workflows/workflow-test.yml, calls each of them on every pull request that changes what they run, and on every push to main; "Re-run all jobs" starts it again:
- Each call is that of its template. One job calls each reusable workflow of the commit,
./.github/workflows/reusable-*.yml, with the permissions of its template intemplates/anddry_run, whichtests/test_workflow_test.pykeeps equal. On a pull request from a fork GitHub gives every job a token that can only read, which a dry run needs; on one of this repository a token that may write, so a test makes sure that every command of the action that writes gets thedry_runof its call. - The labels, the findings and the lifecycle run in full.
sync-labels,findingsandsweepread this repository as every repository's morning run does, and write nothing. A dry run also waits for no run of its group and replaces none, so the pull requests don't queue behind each other or behind the real sweep. An open alert of code scanning onmainfails the dry run of the findings, as it fails the Findings workflow there, which reports it; since a pull request can't close it before its merge, the check passes then, with a warning, as long as open alerts are all that its job names. - The issues run without the AI. The input
smoke, which only this repository may pass, is the counterpart of the Canary's: the jobeventtakes the issue oftests/fixtures/canary-issue.jsonand sets no labels,analyzeis skipped, andapplychecks the answer oftests/fixtures/smoke-answer.jsonin place of the engine's, as a dry run whose summary shows the comment. A test runsapplywith this answer for that issue, so a change of the checks that refuses it fails before the workflow does. The engine, its secret and its runner stay the Canary's. - The discussions skip. A pull request is no event of theirs, and their job
eventruns only for a new discussion or a run by hand, so every job of the call is skipped. - Every job checks the commit of its workflow first. A reusable workflow can't pin the commit that it is part of, so each job checks out the issue assistant of the commit that GitHub names in its context,
job.workflow_repositoryandjob.workflow_sha(decision 0015). Without them,actions/checkoutwould take the caller's own repository at its default branch, and the job would run the caller'saction.yml, or fail with a puzzling error. The step before the checkouts stops the job unless GitHub names a repository and a full commit SHA, and says why; a test keeps it in every job of every reusable workflow. - The last job is the check. A job that its condition skips passes as well as one that runs, so the job reusable workflows, the check that
mainrequires, reads the jobs of its own run withscripts/workflow_test.py: each job passed, with the step that writes the plan of its dry run, or was skipped where its call has to skip it. Its summary shows a table of every job, and on a failure the PR coach explains it from.github/checks.project.toml. A pull request that changes nothing that the dry runs run or read, such as one of the documentation alone, skips the calls, and the check passes in seconds.