Skip to content
Go to console
Go to console

A resolve that no issue edit started hands on the open records that name its own run

Decision record 0109

Amends 0003 (a record apply takes may be one that a writer outside Sluiceway opened, and the run rule is what ends one nobody handed on), 0035 (apply deploys a record of its own run, whoever opened it, not only one its resolve created), 0056 and 0104 (a dispatched or scheduled resolve reads 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 resolve that no issue edit started hands on every open record that names its own run, carries neither behind nor window, and was opened by a login that recordWriters names. Such a record is an outside record: it was opened in the published shape by a writer other than Sluiceway, before the run got to resolve. Nothing of Sluiceway’s opens a record that names a run other than its own, and in a dispatched or scheduled run resolve comes first, so any such record is another writer’s. It goes to apply as it is, in matrix, in stack id order with the queued stacks the run starts and under the cap of 0035. resolve writes the deploying row for it, with the ticker the record names, and writes no status on it. apply does 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 as name[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 to sluiceway.yaml goes 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 recordWriters is 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 that ignore leaves 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 with behind or window are what they were. Each of these ends as every open record of a run that is over does: by settle at the end of the run when the run handed something else on, else by the next render.
  • Every dispatched or scheduled resolve reads 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 a resolve that 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 resolve handed 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’s resolve avoided. The scan runs as before when the dispatch carries the sluiceway-merged input (0064), whose scan hands the merged change on; when resolve also started a queued stack, because the run after a layer always scanned and settle may 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 is scanSkippedAfterResolve in src/core/auto-mode.ts, and resolve tells auto mode how many outside records it handed on next to matrix, which is unchanged. The split workflow’s scan job 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. recordWriters is 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 settle after 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 stack input on workflow_dispatch, where Sluiceway opens the record. The entry point docs/later.md named 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.md gains outside record, and auto mode says that such a run may skip its scan.
  • sluiceway.yaml gains recordWriters, in the schema, the configuration reference and the plan. The deployment record as the port reads it carries creator, the login GitHub names, from REST and from the GraphQL page.
  • docs/what-sluiceway-writes.md says 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.md and docs/split-workflow.md say what a dispatched resolve costs now.
  • docs/later.md: the line on unattended deploys names the outside record as the entry point that exists and keeps the stack input out.
  • resolve returns what it handed on beyond matrix to auto mode. The split workflow’s resolve job 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 resolve reads 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.