A dispatched resolve waits in line, and settle ends the records of a cancelled one
Decision record 0123
Amends 0025 (the split workflow’s
resolveusesqueue: maxagain), 0035 (settlealso runs whenresolvewas cancelled) and 0074 (the check warns about aresolvegroup withoutqueue: maxor withcancel-in-progress, and about asettlethat skips a cancelledresolve).
Found on 2026-10-20 on a repo that runs the split workflow. Twice, a tick through the Sluiceway app (0116) opened a record for the run it dispatched, as 0109 and 0122 describe. Each time the resolve job of that run ended as cancelled about ten seconds later without a runner and without a step. Its record stayed queued, its row said deploying, and nothing deployed. A manual re-run of the cancelled run deployed it.
The jobs of the runs around it show the cause. The app’s tick also edits the dashboard, so an issues run’s resolve held sluiceway-resolve when the dispatched run arrived, and the dispatched resolve waited in the group. A second edit of the dashboard a few seconds later started a third run. Its resolve queued in the same group, and GitHub keeps one waiting job per group: it cancelled the older waiting one, the dispatched resolve. cancel-in-progress played no part. The workflow did not set it, and the cancelled job never started.
0025 chose that group without queue: max because any resolve handles every ticked box, so a replaced run lost nothing. That stopped being true for dispatched runs. Only the resolve of the run a record names hands that record on (0109), and the dispatched resolve is also what starts a stack that waited behind another (0056) or for its window (0104). An issues run’s resolve does neither. So a replaced dispatched resolve loses its deploy. settle did not help, because its if: ran it only when resolve handed something on, and a cancelled resolve hands nothing on. The record stayed open until a render after the end of the run ended it as the run ended without a result. In a dispatched run that waited for a long scan, that was the next push or the next day’s scan.
Decision
- The split workflow’s
resolvewaits in line withqueue: max. Its group issluiceway-resolveas before, in the long form withqueue: max, so every waitingresolveruns, in order, one at a time. Nevercancel-in-progress. The cost is anissuesrun per edit of the dashboard by a person, each a few seconds on a hosted runner. The workflow token’s own writes start no run (0017), so the line holds people’s edits and dispatches only. settlealso runs whenresolvewas cancelled. Itsif:gainsneeds.resolve.result == 'cancelled'as one more way in. It ends every open record of its run, as it always does, with the failure on the row and a full scan after it (0035, 0113). A run that was cancelled by hand, or aresolvethat waited past a timeout, ends loudly within its own run and does not stay deploying until a later scan. A run with nothing open costssettleone request per environment name, as before.settleends such a record. It does not dispatch the workflow again. Withqueue: max, a newer run no longer cancels aresolve. Only a person or a timeout does, and a deploy that a person cancelled must not start again by itself. The failure line says the deploy did not go out, and a fresh tick deploys it.- The check warns about each part. A split
resolvewhose group has noqueue: max, a splitresolvewhose group hascancel-in-progress, and asettlethat waits forresolveand whoseif:is notalways()alone and does not readneeds.<resolve>.result. Each warning names what is lost and the line to add. - The scan keeps its group without
queue: max. A replaced scan loses nothing: a merge waits for any scan (0054, 0064), and a full scan runs again on the next push, the schedule or the rescan box. - The one-step workflow is unchanged. Its one group already has
queue: max(0077), and its post step settles a cancelled run.
Considered
- A group per event,
sluiceway-resolve-${{ github.event_name }}. Rejected: two app ticks in a row are two dispatched runs, and the newer one’sresolvewould still drop the older one’s, with its record. - A group per run for dispatched runs. Rejected: two
resolvejobs would then write the dashboard at the same time, which the group exists to avoid. - Dispatching the run again from
settle, or from the next render, when itsresolvewas cancelled. Rejected for the reason above. A cancel by a person is an answer, and a deploy that starts again after it is a surprise. - Running
settleonalways()alone. Rejected: every edit of the dashboard and every push would start one more job that finds nothing. Theresultofresolvesays when one is needed.
Consequences
docs/split-workflow.mdshowsresolvewithqueue: maxand the widerif:ofsettle, in the workflow and in the merge and deploy section, and says why each is there.- The check has three new warnings:
resolve-no-queue,resolve-cancelsandsettle-skips-cancelled-resolve. - A repo on the split workflow sees those warnings in the check until it adds the two lines. Nothing in the action changes, so its runs keep working, and the gap stays open until the workflow changes.
- The e2e run gains two steps on the split workflow. An app’s tick whose dispatched
resolveruns after theresolveof a later edit still deploys. An app’s tick whose dispatchedresolvewas cancelled is ended bysettle: the record ends as error,settlesays so in its log and starts a full scan, and the row comes back pending, not deploying.