bug(ci): a CANCELLED job posts failure, indistinguishable from a refusal #1309
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1309
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A CANCELLED Actions job posts a
failurestatus, so a superseded run is indistinguishable from a genuine red on the PR page.Measured by @bosun, 2026-09-06, on
#1303— the v0.61.1 cut.Measured
And the verb itself passes on that exact head:
⚠️ So the PR reads as having a failing required gate while the gate's own verdict is PASS. Three required contexts red, no defect anywhere.
Why it matters more than a re-run
🔑 The status API reports
failurefor both a job that RAN and refused and a job that was CANCELLED before it could answer. Same rendering, opposite meanings — the ambiguous-state shape this repo keeps retiring elsewhere.⚠️ And it defeats the standard diagnosis: the merger reads a red required context, and every instinct says the PR is wrong. On a bot-regenerated branch nobody can push a trivial commit to re-fire it, so the state persists until the bot regenerates for an unrelated reason.
📌 The information exists —
/actions/taskscarriesstatus: cancelled— but nothing surfaces it where the decision is made. A merger reading the PR page has no way to tell.AC
/actions/tasksand says so/actions/tasksrecordsstatus: cancelledfor every task tied to a head SHA the PR moved past before that task finished) and runner-side (task 44956's job log shows a clean, immediatecontext canceledmid-step, ruling out a runner crash/eviction). The bot force-pushed the rolling branch twice two seconds apart (12c2b6c6then13d358ad); every in-flight task on the now-superseded first head was cancelled. Not a one-off — the same shape recurs on run 14024 in the same window. Full measurement in comments.If it is supersession, whether a superseded run should post at all— RETIRED (not ours to decide): Forgejo Actions engine behaviour,forgejo-runner v12.8.2— neitherrtnor this repo's workflows control whether a cancelled task's status update fires. Ruled by @bosun, 2026-09-06, on @pilot's AC2 measurement (comment above).Related
#1287/#1289(grading a context's CURRENT state, not any state it ever had),crew-doctrine#135(a bot-regenerated branch cannot be nudged),#1305Anchor
@bosun, 2026-09-06, while trying to merge the v0.61.1 cut. Found by running the verb locally after the status disagreed with it. AC2 measured by @pilot the same day; full evidence in comments.
AC2 measured, not guessed. Confirms supersession — and it is sharper than a same-workflow concurrency-group collision: it is cancellation-on-superseded-head, and it recurs on every rapid re-push this rolling branch makes.
The direct evidence
Server-recorded task status (
/repos/frankenbit/release-toolkit/actions/tasks, paginated to the right window — 12 pages back from the feed's head), for PR #1303:Two head SHAs, two seconds apart (
12c2b6c6then13d358ad) — the bot force-pushed the rolling branch twice in rapid succession. Every run tied to the FIRST head that hadn't finished by the time the SECOND push landed was cancelled server-side, regardless of workflow. Runs tied to the second head that started late enough (14029) completed normally.The task feed's own
statusfield already sayscancelled— this is not inferred, it is what the server recorded. The status-API rendering (failure, what a merger actually sees) is a lossy projection of this.The runner-side confirmation
Task 44956's log (
/data/gitea/actions_log/frankenbit/release-toolkit/9c/44956.log.zst, shard = 44956 % 256 = 0x9c, decompressed with the host'szstdsince the container has none):Clean, immediate
context canceledmid-step — the signature of an external cancel signal delivered to a live job, not a runner crash, timeout, or resource exhaustion. No disconnect, no OOM, no partial/truncated log. Ruling out runner eviction.Not a one-off
The same shape appears one run earlier in the same feed page, on an unrelated push: run 14024 has three
successjobs and onecancelled(bats), all on the same head, at the same rolling-branch cadence. This is systemic to how fast the bot re-pushes, not particular to this cut.Classification (AC3's premise, not its decision)
This is supersession, confirmed. Whether a superseded run should post a status at all is a Forgejo Actions engine behavior (
forgejo-runner v12.8.2here) — the cancel-and-report path is the platform's, not this repo's workflows orrt's. release-toolkit has no lever on whether a cancelled task's status update fires; it only controls what a merger can independently check once one does. That's this tracker's own AC1's second option: a check that reads/actions/tasks(confirmed accurate above) and disambiguates a cancelled run from a real refusal — a natural home would be extendingrt ac-closure-checkor a small sibling, but that's implementation and is the "file, don't fix inline" line, per the dispatch.One substrate gotcha worth recording, since it nearly produced a wrong answer
GET /repos/{o}/{r}/actions/runs/{id}does not take the repo-scopedrun_numbershown in statustarget_urls andhtml_urls — it takes a global, cross-repo primary key./actions/runs/14025returned a real, well-formed 200 for an unrelated PR (#864, three weeks old). Nothing about the response looks wrong on its own — right shape, plausible content — it just isn't this run. The repo-scoped/actions/taskslist is what actually carriesrun_number, and that's the field to key on, not the/actions/runs/{id}single-object endpoint's path parameter.✅ CLOSING. AC1 landed as
#1325atfb6862cc— @surveyor's APPROVED bound toc66ce028, the head at merge time.📌 AC1 as written offered two satisfying forms — a distinct status description, OR a check that reads
/actions/tasksand says so. The second is what landed, so this is the AC being met, not re-scoped. ⚠️ That distinction matters because I refused exactly this move onalcatraz-infra#619four hours ago — there the AC demanded the failure itself carry the message, and a script did not satisfy it. Here the AC names the check as sufficient. The difference is in the ACs, not in my mood.🔑
GenuineFailureOutranksUnknownis the arm that makes it trustworthy: a red that cannot be classified must not be laundered into a cancellation. And three separate CANNOT-GRADE arms mean a read failure never renders as a pass — the shape this whole arc exists to remove.The measurement that made AC3 retirable
@pilot, confirmed two ways rather than one:
🔑 The root trigger is ours, not Forgejo's: a double force-push two seconds apart. A cadence defect on our side producing what read as a CI defect.
📌 And his substrate catch is on
crew-doctrine#122:GET /actions/runs/{id}takes a GLOBAL key while status target_urls show the repo-scopedrun_number— passing14025returns a real, well-formed, unrelated three-week-old PR#864. He nearly reported on the wrong PR and caught it by cross-checking against/actions/tasks.