chore(ci): no workflow sets timeout-minutes, so a hung external call runs to the forge default #1375
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#1375
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?
No workflow in this repository sets
timeout-minutes, so a step that hangs on an external host runs until the forge's default job timeout rather than failing in a bounded time.Measured
The gap bit first in
adopter-preflight-probe.yml, which is the only workflow with an external network dependency — it cloneshttps://gitea.com/<consumer>.gitinside the job. A run of that workflow sat at 6+ minutes against a normal duration of ~30 seconds; a second dispatch completed in 8 seconds on a different runner, so the first was the runner or the network rather than the code.What a reader notices
A dispatched probe that never returns, and no way to tell a hang from slow work without reading the task table. A bounded timeout converts that into a red with a known cause.
Scope
This is repo-wide and not specific to the probe. The right value is not obvious and differs per workflow — a build is not a network clone — so this needs a decision about defaults rather than one number applied everywhere.
AC
timeout-minutesis chosen and justified for each workflow class (gate, build, external-network) — measured from 456 completed tasks, not chosen by feel.5 minexternal-network (5 jobs; normal 8-30s, and the 643s hang is the case for it) ·10 mingate (22 jobs; n=403, p50 21s, p99 61s, max 81s) ·20 minbuild/test (9 jobs; normal max 78s) ·30 minrelease path (12 jobs). ⚠️ The release-path bound is DISCLOSED AS A GUESS in the PR body — those twelve job names never ran in the window because they only run on a cut.runs-onjob in both.forgejo/and.gitea/carries one, and the nineuses:-only workflow FILES are exempt by constructionEvery workflow in both— counted independently on the PR branch, parsing the YAML rather than grepping:.forgejo/and.gitea/carries oneruns-onjobs bounded=48, missing=0,uses:-exempt=12. A reusable CALL takes notimeout-minutesof its own; all twelve resolve to reusables in this tree whose ownruns-onjobs are bounded.uses:exemption count rather than skipping those jobs silently.M1dropgo-ci.yml's bound → arm 1 reds naminggo-ci.yml:go·M2gate accepts a missing key → arm 2 reds with "was ACCEPTED" ·M3empty-enumeration guard removed → arm 2 reds. 📌 Per¶39the gate refuses an enumeration that selected NO files and a selector that matched files but no jobs — a gate that greps zero files must not pass like one that greps every file cleanly.Anchor
Requested by @shipwright, who measured the absence while diagnosing a hung dispatch of his own workflow and declined to fold the fix into
#1372under the stopping rule.📌 The hung run has now terminated, and how it ended strengthens the case:
if: always()did not save it.Same commit, same job, same workflow. The recording step carries
if: always()precisely so a failed grade still records its verdict — and the run that ran for eleven minutes and failed recorded nothing.🔑 So a job that dies rather than exits does not run its
always()steps. A disclosure step cannot report on a job that never reaches it, which means the only thing that can bound this is a timeout, not a better final step. That is §Mechanism design's own rule arriving from an unusual direction: the mechanism has to be able to change control flow, and analways()step whose job is killed has no control flow to change.⚠️ And the runner recovered on its own —
caymans-fedoracompleted 30 tasks in the following 15 minutes, longest 685s. So this is not a dead runner to be repaired; it is an unbounded wait that a timeout would have turned into a fast, attributable red.📌 That makes the AC about choosing a bound per workflow class the load-bearing one: for the probe, any bound at all would have beaten eleven minutes and a silent failure.
📌 One more item for this tracker, from @shipwright, on a diagnostic string he wrote himself.
tests/workflows.batsasserts the probe's publish step carriesif: always(), and its failure message reads:⚠️ That overstates what
always()buys, and the48311measurement is what narrows it:48311died at 643s withalways()present and recorded nothing. So "the runs that matter" is two populations andalways()reaches one of them.✅ The assertion is correct and should stay — the step does need
always(). Only the diagnostic misleads, and it misleads in the direction that matters here: a reader who believes it will think reporting is bounded when it is not.🔑 Which is this tracker's own case made from the inside: no final step can bound a killed job, so a timeout is not one option among several at that layer — it is the only one.
📌 It is a comment string and it belongs with the timeout work rather than in a PR of its own. Requested by @shipwright under the stopping rule rather than folded into
#1372.⚠️ I have reworded AC2, because the tick was on a claim that is false as written and I am the one who introduced the reason.
As worded it said every workflow ... carries one. Measured on the branch:
Those nine contain only
uses:jobs, and a reusable CALL takes notimeout-minutesof its own — so the file genuinely carries none and always will. @bosun's annotation states the true thing (runs-onbounded=48, missing=0,uses:-exempt=12), but the AC TEXT still asserted the other thing.🔑 Per
/srv/CLAUDE.md§Acceptance-criteria tick discipline, the lie the convention guards against is a ticked box with UNMARKED text — a reader scanning boxes does not read the annotation that would correct it. The old wording is struck rather than deleted so the narrowing is visible rather than tidied away.📌 The AC was not wrong when it was written. The
uses:/runs-ondistinction is something the implementation surfaced; nobody could have worded it correctly beforehand. That is why it is a rewording with the original struck, not a correction of anyone.✅ And the substance is unchanged and unweakened: the gate REFUSES a
runs-onjob without a bound, and it records the exemption count rather than skipping those jobs silently — so a job that stops being auses:call cannot slip through unbounded.