A resolve that no issue edit started hands on the open records that name its own run
Decision record 0109
Amends 0003 (a record
applytakes may be one that a writer outside Sluiceway opened, and the run rule is what ends one nobody handed on), 0035 (applydeploys a record of its own run, whoever opened it, not only one itsresolvecreated), 0056 and 0104 (a dispatched or scheduledresolvereads the records of every environment, one page each, also without a dependency, a phase or a window) and 0077 (a dispatched run may skip its scan). Built as slice 5.44, for issue 256.
Found on 2026-09-25 by a reader of the published shape (0096) that opens deployment records itself and starts the workflow with a dispatch, the way the action’s own resolve does. A run that workflow_dispatch started runs resolve and then a scan (0077). That resolve handed on only queued records (behind, 0056) and deploy-window records (window, 0104), and only when the config had a dependency, a phase or a window. A record that was already open, named this very run in its payload and waited behind no stack and for no window was handed to nobody: apply got only what resolve and the scan put in matrix, so the record sat until a later render ended it as “the run ended without a result” (0003), a failure line and no deploy. The action already knew how to find exactly those records: openRecordsOfRun in src/core/settle.ts.
Decision
- A
resolvethat no issue edit started hands on every open record that names its own run, carries neitherbehindnorwindow, and was opened by a login thatrecordWritersnames. Such a record is an outside record: it was opened in the published shape by a writer other than Sluiceway, before the run got toresolve. Nothing of Sluiceway’s opens a record that names a run other than its own, and in a dispatched or scheduled runresolvecomes first, so any such record is another writer’s. It goes toapplyas it is, inmatrix, in stack id order with the queued stacks the run starts and under the cap of 0035.resolvewrites the deploying row for it, with the ticker the record names, and writes no status on it.applydoes what it always does: claims it, previews again, compares the hash and the value fingerprint, deploys or refuses. The record is the lock (0003), and a record with a hash the fresh preview does not give deploys nothing. The ticker on it is the writer’s word, as it is on a queued record that a later run starts (0056). recordWriters, a new top level key, is the list of logins whose records go on. A person as their login, an app asname[bot], matched in lower case. The writer is the login GitHub records as the creator of the deployment record, which the port reads with the record; nothing in the payload decides it, because the payload is the writer’s own text. The default is empty, so a repo that names nobody behaves exactly as before this record: no outside record is ever handed on, and the job log says the record was left alone and why. The tick rule is not asked instead, for two reasons. A bot has no repo permission to judge: the tick rule reads a person’s access level, and an app that opens records has none of its own. And the list is the repo’s reviewed word on who may open records: a change tosluiceway.yamlgoes through a pull request on a protected branch, where a record and a dispatch do not. Without the list, anyone with write access could deploy past a tick rule that narrows below write access (admin, or named people) by opening a record and dispatching. The check names the writers the config lists, and says nothing when the list is empty.- What is left alone, each with a line in the log and nothing else. A record when
recordWritersis empty, and one whose creator is not on the list or whom GitHub does not name. A record whose payload this version cannot read (0003). A record of a stack discovery does not know, or thatignoreleaves out. A record whose stack has a newer record, open or ended: an open one means the stack is taken, an ended one means the outside record is stale. A merge record (0054) and a record withbehindorwindoware what they were. Each of these ends as every open record of a run that is over does: bysettleat the end of the run when the run handed something else on, else by the next render. - Every dispatched or scheduled
resolvereads the records. One GraphQL page per environment name the stacks use (0003), and the REST fall back only for the stacks a dependency or a window involves, as before; an outside record is fresh and on the page. Without an outside record, a dependency, a phase or a window it says so and stops. This amends 0056 and 0104: such a run costs one request per environment name, no longer none. The schedule is included because it is aresolvethat no issue edit started and the code is one path; a writer that names a scheduled run is unlikely and does no harm. - A dispatched run whose
resolvehanded on nothing but outside records skips its scan, and the log says why. The deploys write their own rows, and a run that exists to deploy has nothing to scan for; a full scan per deploy is the cost the split workflow’sresolveavoided. The scan runs as before when the dispatch carries thesluiceway-mergedinput (0064), whose scan hands the merged change on; whenresolvealso started a queued stack, because the run after a layer always scanned andsettlemay have ended records whose failure rows that scan writes; and on the schedule, whose scan is the drift check and the run inside the window. The rule isscanSkippedAfterResolveinsrc/core/auto-mode.ts, andresolvetells auto mode how many outside records it handed on next tomatrix, which is unchanged. The split workflow’sscanjob is the workflow’s own and is never skipped. - A read-only dashboard never resolves (0045, 0077). An outside record on such a repo is never handed on, and ends as any open record of a run that is over.
Considered
- Handing on any open record that names the run, with write access as the only guard. The first cut of this slice. Rejected by the coordinator on the pull request: it let anyone with write access deploy past a tick rule that narrows below write, where a config change is reviewed and a record is not.
recordWritersis the answer. - Checking the creator of the record against the tick rule. Rejected: the tick rule reads a person’s access level (0018), and an app that opens records has none. A list of logins the repo reviews says who may open records, which is the question here.
- Skipping the scan on any dispatch that handed something on. Rejected: a dispatch by
settleafter a layer is also the full scan that writes the failure rows of the records it ended (0035, slice 2.6). - Keeping the scan on every dispatch. Rejected: the row of the deployed stack is written by
apply, and a full scan per outside deploy is exactly the cost a writer that opens records itself is trying to avoid. - Reading the records only when the dispatch payload says a writer started it. Rejected: the sender of a dispatch is the token’s identity, a person or an app, and nothing in the payload says that a record waits.
- Handing on a record that is not the newest of its stack. Rejected, above.
- A
stackinput onworkflow_dispatch, where Sluiceway opens the record. The entry pointdocs/later.mdnamed for a deploy without a tick. Not built: the outside record is that entry point now, with GitHub recording who opened the record and who dispatched, and a second way to say the same thing would need its own rules for the ticker and the hash.
Consequences
CONTEXT.mdgains outside record, and auto mode says that such a run may skip its scan.sluiceway.yamlgainsrecordWriters, in the schema, the configuration reference and the plan. The deployment record as the port reads it carriescreator, the login GitHub names, from REST and from the GraphQL page.docs/what-sluiceway-writes.mdsays how to open a record yourself, that the row’s value fingerprint goes in the payload too, and what happens to the record.docs/workflow.mdanddocs/split-workflow.mdsay what a dispatchedresolvecosts now.docs/later.md: the line on unattended deploys names the outside record as the entry point that exists and keeps thestackinput out.resolvereturns what it handed on beyondmatrixto auto mode. The split workflow’sresolvejob ignores it. No input, output, marker or payload key changes.- The e2e run gains a step: the repo lists an app in
recordWriters, a record opened in that app’s name for app:prod with the row’s hash and fingerprint, the dispatched run deploying it with the real tool, settling, and skipping its scan. The first run of that step, without the fingerprint, was refused as a value that changed since the tick (0102): a writer copies the fingerprint from the row too. - A writer has to open the record after the dispatch and before that run’s
resolvereads the records. With the concurrency group’s queue there is usually time, and a record that comes late sits until a render ends it, as before this record.