settle writes the row of a deploy it ended, with the failure line
Decision record 0113
Amends 0004 and 0035 (
settleswaps a row again, the one of each deploy it ended, and still starts the full scan), 0027 (a row form of its own for a stack whose deploy ended and that no scan has previewed since) Promise 4 of 0014 holds, and this record says why. Built as slice 5.50, for issue 272.
A deploy whose run is cancelled, or whose apply job a reviewer rejects, never reports. settle ends its record as error, “the run ended without a result”, and until now left the body alone: it runs no tool (0014), so it has no diff, and 0035 left the row to the full scan it starts. That scan has to get a runner and install the tools before it writes anything. The release verification of issue 272 measured 44, 56 and 63 seconds from the cancel to the failure line on hosted runners, against the minute that docs/acceptance.md promises, and on a busy runner pool it takes longer. Until then the row says deploying for a deploy that is over. Decided by the owner on 2026-09-25: settle writes the row itself, with no tool and no second runner, and the scan still follows.
Decision
settleswaps one row block per record it ended, through the write loop (0004), after every record has its result and before it starts the full scan. It is the swapresolveandapplymake: the live body is read, the block of each such stack takes the place of the stack’s first block, every other block is carried byte for byte, the trail is drawn from the deployment records as the late read of each try finds them, and everything around the blocks is drawn again. The attribution walk (0026) runs for the trail’s shipped lines, as in every swap, and needs the workflow token only.- Only a live block that says deploying or queued is swapped, and only while the stack’s newest record at the late read is the one this run ended, failed, with no deploy after it that took the failure’s place (0076). That is the row
resolveorapplywrote for the deploy. A stack whose live block is anything else, or a block this version does not know, or no block, keeps what it has, and the scan writes it. So does a stack a newer record took over, such as a tick made since. - The block is the live one, with its first line written again. The first line and the marker are made from facts only: the stack id, and
failed="true". The failure line is made from the record, the wayapplymakes it after a failed deploy and with the same function::x: last deploy failed: the run ended without a result · ticked by alice · 2026-09-25 10:09 UTC+2 · [run](…), in the dashboard zone (0089),merged byfor a deploy on merge (0095). It goes right under the first line. Every other line of the block, the attribution line and the fold of the changes outside the stack, is carried as it is, below it. A writer that cannot tell the lines of a block apart puts a line of its own right under the first line, as the note of a cleared tick does (0025), and the next scan renders the row in the order of 0027. - The first line reads
- **apps/api:prod** · no preview since its deploy ended, the next scan previews it, with no box, no counts, no spinner and no link: the failure line under it links the run. The marker state ispreview-failed. It is the state that fits what the row is: there is no preview of the stack to show since its deploy ended, it cannot be deployed from here until a scan succeeds, which is what the Preview failed section says of its rows, and every scan previews it, a narrowed one too (0010). The header is failing and the counts line counts a failed deploy, as for any failure line (0031, 0040). - The full scan follows, as before:
settledispatches it after its write, whether that write went through or not. It previews every stack and gives the row its fresh diff, with the failure line. The line on the row does not wait for it. - A write that does not happen is one line of the job log, and never turns the job red. No open dashboard, a live body this version did not write, a body that would not fit, or a write GitHub refuses: the records are ended and the scan follows, which writes the row, as it did before this record. The line decides nothing. A
settlethat ended nothing writes nothing, as before. - The job log says it:
Wrote the failure line on the row of apps/api:prod, before the full scan previews it again (record 0113). - The cost is the requests of one swap: the find, the read, one page of deployment records per environment name, the attribution walk, the write and the read back, all well inside the budget of 0017. It is spent only by a run that ended a record.
Why this is safe with the carried-row rule
A carried row is never read inside (0004, 0009): a writer that did not render a block moves it whole and decides nothing from its text. settle keeps to that. It reads the marker of the live block, which is what every writer reads, to know that the block says deploying or queued. It does not read the text of any line: the first line and the marker are made again from the stack id and the record, and the lines under them are moved as they are, byte for byte. The failure line comes from the record, as apply’s does, never from the old row.
What it writes cannot lead to a deploy. The row has no box, no diff hash and no value fingerprint, so there is nothing to tick, no bulk box counts it and no confirm box names it (0083): the deploy that settles the stack starts from the fresh row the next scan writes, through the fresh preview and the hash check, as every deploy does. The facts behind the row are the deployment records, which settle itself just wrote, and the late read of each try reads them again, so a tick that opened a newer record meanwhile keeps its own row.
Once written, the row is itself a row other writers carry. A narrowed scan that does not preview the stack would carry it with its failure line, and that is right: the failure stands until a later deploy ends it (0076). But no scan carries it for long: its state makes every scan preview the stack, a narrowed one too, so the next scan writes the fresh row even when the full scan settle starts never comes. A scan whose preview of the stack started before the record was ended keeps the live row at its late read, because its preview predates the deploy (0004), and that live row is now this one, with the failure line, instead of a deploying row it would have had to preview again.
Promise 4 of 0014 holds: settle is handed no tool environment and no process runner, and still discovers from files only. What it writes is made from the deployment records and the live body.
Rejected
- A new row state for the row. It would be honest in the marker, but it adds a state to the published shape (0096), a section or a place in one, a rule in the scan plan and a count, for a row that lives about a minute.
preview-failedmeans the same to every writer and every reader that matters here: no box, no diff, previewed by the next scan. - Keeping the marker state
deployingand only adding the failure line. A row says deploying exactly as long as a deployment is open (0004), and the counts line would say 1 deploying for a deploy that is over, which is what issue 272 is about. - The pending row the tick was made on, from the edit history. The body before
resolve’s write holds the ticked row with its diff. Finding it costs a read of the history, and it is not there for a deploy on merge, an outside record or a history past its 100 entries, so the row form above is needed anyway. A diff that may be half deployed is also not one to show with a box. - Leaving the row to the scan, and saying so in the checklist. The row said deploying for a minute or more after the deploy was over, and the promise of the checklist was the owner’s.
Consequences
- The row of a cancelled or rejected deploy shows the failure line within seconds of
settle, and the fresh diff when the scan it starts ends, as before. - The edit history gets one bot entry more per run whose
settleended a record. A bot entry inside a stretch is normal for the walk (0025). docs/later.mdloses its line onsettlewriting the row of a stack whose record it ended.