Repository navigation
Report failed unattended runs to Slack, and check every repository does - #12
Conversation
A scheduled run has nobody watching it, so when it fails the only record is a red entry in a list of runs nobody opens. Each repository gets one small watcher, triggered by `workflow_run` on the workflows it names, that calls this; the filter on schedule-started runs and on the conclusion lives here so no caller can get it wrong. Every conclusion but success, skipped and neutral is reported, so a conclusion nobody anticipated is posted rather than passed over. A post that fails - no secret, a webhook Slack refuses, Slack unreachable - fails the watcher's run with Slack's answer, so a broken webhook is visible rather than silent. A call from anything but `workflow_run` fails in its first step instead of being skipped. Slack's body is written to a file rather than standard output: run against a server that answered 500 and then 200, curl's retry appended the second body to the first, giving `server_errorok`, and a post Slack had accepted was reported as refused. Written to a file, the retry truncates it. Tested locally by running the step's script against a fake Slack server: accepted, refused with 404, refused with 200 and an error body, 500 then 200, unreachable, no secret, a call from `push`, the jobs listing failing, and no failed jobs, with `&`, `<` and `>` in the workflow name. The job-level filter and a real post are untested until a repository's watcher is on its default branch, which is the only place `workflow_run` fires. Signed-off-by: Gavin Lucas <Gavin@L337.org>
…its own A run started by a push to the default branch or by a release has nobody watching it, just as a scheduled one has not, so the reporter now takes `events`, the triggers to report, instead of reporting schedules alone. A push is reported only on the default branch, because a push to any other branch has its pusher watching. The post now names the trigger. The hygiene check gains "unattended runs report their failures": every workflow triggered by `schedule`, `push` or `release` must be named in a watcher that calls the reporter with that trigger among its events, must have a `name:` for the watcher to match, and every name in a watcher must belong to a workflow, since GitHub matches on the name and a rename otherwise leaves the list reporting nothing. It is in the shared check because the convention is every repository's, and because the repositories adopt it by moving their pin, so the pin move and the watcher land together. Parsing the workflows moves out of `workflow_jobs` into `workflow_documents` so both checks share it. This repository gets a watcher too, calling the reporter by local path, for its own pushes to main; it has no repository secret, so it posts to the organisation's channel. Tested: the check's function against fourteen fixtures - a clean repository, a local call, pull-request-only workflows, `on:` as a list and as a string, no watcher, a workflow left off, a trigger missing from `events`, a stale name, no `name:`, `events` that is not JSON, a watcher with no `workflows:`, another reusable workflow mistaken for none, and no `on:` (refused) - and the whole script over a fresh clone of each repository that pins this one, where it names exactly the push, schedule and release workflows each has. The post step was re-run against the fake Slack server. The job-level filter is still untested until a watcher fires on a default branch. Signed-off-by: Gavin Lucas <Gavin@L337.org>
Several repositories run their checks on a push to main in a concurrency group with `cancel-in-progress`, so two merges landing close together cancel the first run. The reporter posts cancelled runs, because a run cancelled by its timeout is a real failure, so each such pair would have put a false failure in Slack. A cancelled run is now skipped, with a notice, when the newest run of the same workflow for the same branch and trigger has a higher run number: that run replaced it and is reported in its own right if it fails. A run cancelled by its timeout has no newer run and is still posted, and if the runs cannot be listed the post goes anyway, with a warning. Tested against the fake Slack server with a stub `gh`: cancelled and replaced (skipped), cancelled and newest (posted), no runs listed (posted), the listing failing (warning, posted), a failed run with a newer one (posted, since only a cancellation is skipped), and a branch name needing URL-encoding. The live endpoint was queried against quaffed's own checks to confirm it returns the newest run first, numbered per workflow. Signed-off-by: Gavin Lucas <Gavin@L337.org>
Every other repository's pull requests get the Claude review, and the repository that defines it did not, so changes to the shared workflows were the ones reviewed least. The caller is every other repository's, calling the review by local path rather than by a pinned commit, so there is no pin to bump. On a pull request GitHub runs the version on the pull request's branch, so a change to the review is exercised by reviewing the pull request that makes it. A broken change can review itself badly, which costs little because the review is advisory. A comment request runs from the default branch, so it uses main's version. The token is the organisation's secret, already visible to this repository, and a fork pull request gets no secrets, which the review's gate already skips. Signed-off-by: Gavin Lucas <Gavin@L337.org>
Claude review: changes requestedSummaryThe PR adds three things: the Slack reporter ( Findings1. Bug: a tag-triggered release is required by the hygiene check, but the reporter never posts its failures.
2. Bug: a watcher without
3. Bug (documentation untrue): "without it the post still goes".
4. Bug (minor): suppressing replaced cancelled runs can also hide a timeout, and the comment says it can't.
5. Maintainability (recorded state): configuration held elsewhere.
6. Preference.
What I checked
What I did not check
full review of |
…E promises From the review of this branch: - A push was reported only on the default branch, so a release started by pushing a tag, which the hygiene check requires to be listed, was never posted. For a push, `head_branch` is the branch or the tag's name, so the step now looks the name up among the tags: the default branch or a tag is posted, another branch is not, and if the tags cannot be listed the post goes anyway. That needs `contents: read`. - A watcher without `types: [completed]` is also started when a run is requested and while it runs, with no conclusion, which passed the conclusion filter and would have posted every run as it started. The job now requires the `completed` action itself. - The README said a caller without `actions: read` still got a post. A called job cannot have more than its caller grants, so GitHub refuses to start it; the README now says the caller must grant `actions: read` and `contents: read`, and this repository's watcher does. - The comment and README said a run cancelled by its timeout is always posted. One cancelled while a newer run was already queued looks like a replaced one and is not, which is now stated as an accepted limitation. - The README described the organisation's secret's channel and this repository's own files, which those hold themselves; it now states the rule instead. Tested against the fake Slack server with a stub `gh`: a push to the default branch (posted), to another branch (not posted), of a tag (posted), of a name that only prefixes a tag (not posted), with the tags unlistable (warning, posted), a scheduled run on another branch (posted), and the cancelled cases again. The live `git/matching-refs/tags` endpoint returns prefix matches, and an empty list for none, which the exact comparison relies on. Signed-off-by: Gavin Lucas <Gavin@L337.org>
|
Addressed in 4d2d875:
Tested against a fake Slack server with a stub |
Claude review: approveResolved
Still open
Declined
New
What I checked
What I did not check
delta review of |
… review caller with other repositories' The reporter cannot tell a branch from a tag of the same name from the run, so a push to such a branch is posted; that is now said where the lookup is. The review caller's comment compared it with other repositories' callers, which goes stale when one of them changes; it now says only what this file does. Signed-off-by: Gavin Lucas <Gavin@L337.org>
|
Second round, in the commit after 4d2d875:
|
Claude review: approveResolved
Still open
Declined
New
What I checked
What I did not check
delta review of |
…here when it runs A review requested by comment runs after the comment, carrying the token, and checked out the pull request's head as it was when the job started. A push in between put code the requester never saw under that token. CodeQL raised it as an untrusted checkout and a time-of-check race once this repository's own caller made the reusable workflow visible to it; it applies equally to every repository calling the review. On a comment request the gate now reads when the branch was last pushed from GitHub's activity record, which the pusher cannot set, unlike a commit's own date, and refuses the request with a reply saying to ask again if that push is at or after the comment, is not of the current head, or cannot be shown at all. A failed lookup fails the job, naming what it was reading. The gate passes the head it checked to the review job, which checks that out instead of looking the head up again. The comparison is two `date -d` timestamps, the form CodeQL recognises as a comment-versus-push check; with the caller's `author_association` condition, that is the pair of guards its untrusted-checkout query asks for on `issue_comment`. Tested by running the gate's script with a stub `gh` and GNU date: a pull request event, a request after the push (runs, passes the head), a push after the request, a push in the same second, a latest push of another commit, no activity, no comment time (each refused with a reply), and the activity lookup failing (job fails, naming it). The live activity endpoint, queried for a branch with a slash in its name, returns its latest push with the branch's head as `after`. Signed-off-by: Gavin Lucas <Gavin@L337.org>
Claude review: approveResolved
Still open
Declined
New
What I checked
What I did not check
delta review of |
What
.github/workflows/slack-on-failure.yaml, a reusable workflow called from a repository's watcher workflow, whichworkflow_runstarts when one of the workflows it names completes. It posts a failed run to the channel behindSLACK_WEBHOOK, naming the repository, the workflow, its trigger, its conclusion and the jobs that did not succeed, and linking the run.schedule,pushorreleasemust be named in a watcher reporting that trigger, must have aname:, and every name in a watcher must belong to a workflow.report-failures.yaml, for its pushes to main, calling the reporter by local path.actions/untrusted-checkoutandactions/untrusted-checkout-toctouonclaude-review.yamlonce this repository's own caller made it visible; it applies to every repository calling the review, and moves with their pins.code-review.yaml, the same caller as every other repository's but calling the review by local path, so a change to the review is exercised by reviewing the pull request that makes it. A comment request runsmain's version, since GitHub runs comment-triggered workflows from the default branch.Why
A run started by a schedule, a push to the default branch or a release has nobody watching it, so its failure is a red entry in a list nobody opens. One watcher per repository plus this workflow keeps the filtering and posting in one place. The check is in the shared hygiene script because every repository adopts the convention by moving its pin, so the pin move and the watcher land in the same pull request, and a workflow added later without a watcher entry fails CI.
Behaviour
events. A push is reported on the default branch and on a tag. Only completed runs are reported, with every conclusion but success, skipped or neutral. The filters are in this workflow, not the caller.cancel-in-progresswhen two merges land together - is skipped with a notice; the newer run is reported if it fails. A run cancelled by its timeout has no newer run and is posted.workflow_runfails in its first step rather than being skipped.actions: readandcontents: read, or GitHub refuses to start the job. If the failed jobs cannot be listed, the post still goes and says so.SLACK_WEBHOOKis passed explicitly by the caller. GitHub uses the lowest level of a secret defined at several: "If a secret with the same name exists at multiple levels, the secret at the lowest level takes precedence." So a repository secret overrides the organisation's.workflow_jobsnow reads its documents fromworkflow_documents, which the new check shares; its behaviour is unchanged.Testing
actionlint clean.
The post step's script run locally against a fake Slack server: accepted; refused with 404; refused with 200 and an error body; 500 then 200; unreachable; no secret; called from
push; job listing failing; no failed jobs;&,<and>in the workflow name. The 500-then-200 case found that curl's retry appended the second body to the first on standard output, so an accepted post was reported as refused; the body now goes to a file, which the retry truncates (observed with curl 8.7.1, not stated in curl's manual).The new check's function against fourteen fixtures, each passing or producing its intended finding: a clean repository, a local call, pull-request-only workflows,
on:as a list and as a string, no watcher, a workflow left off, a trigger missing fromevents, a stale name, noname:,eventsnot JSON, a watcher withoutworkflows:, another reusable workflow, and noon:(refused as cannot evaluate).The whole script over a fresh clone of each repository pinning this one: each gets exactly one finding per push, schedule or release workflow it has, and nothing else. Run over this repository's workflows, the new check finds nothing.
The superseded-cancellation skip, with a stub
gh: replaced (skipped), newest (posted), nothing listed (posted), listing failing (warning, posted), a failed run with a newer one (posted), a branch needing URL-encoding. The live runs endpoint, queried against quaffed's checks, returns the newest run first, numbered per workflow.The gate's script with a stub
ghand GNU date: a pull request event; a request after the push (runs); a push after the request, in the same second, of another commit, no activity, no comment time (each refused with a reply); the lookup failing (job fails, naming it). The live activity endpoint returns a slashed branch's latest push with its head asafter.Not yet tested: the job-level filter and a real post; and the comment-request gate live, since a comment-triggered run uses the default branch's workflow, which has no caller here until this merges.
workflow_runfires only from a default branch, so both are tested once a watcher is merged. This repository's own watcher will be the first, on its next push to main.