Skip to content

A stack waits only on the stacks it names, and a chain deploys one layer per run

Decision record 0056

Record 0009 reserved the row state queued and said the fact behind it is a deployment record with status queued and the blocking stack ids in its payload. The brief’s rule for dependencies was: refuse a tick whose upstream has pending changes that are not ticked, and when both are ticked, deploy the first layer and re-trigger resolve after it. Build plan slice 4.4 adds the lesson of the earlier internal dashboard that inspired Sluiceway (onboarding log, hurdle 23): only refuse on things that can hold state, and a gate that never applies must not stay green and silent. This record fixes part 1.

Decision

  • stacks[].dependsOn is a list of stack ids, exact, no globs. An entry without a name gives its list to every stack in its path, and entries add up like inputs. A stack waits only on the stacks it names, not on theirs.
  • Every id is checked against discovery at config load. A stack that discovery did not find, one that ignore leaves out, the stack itself and a circle are config errors, so check, scan and every other mode say so. This is the second half of the lesson: a dependency that could never hold anything back would be a gate that stays green and says nothing, so it is refused before it exists. A circle is named once, from its smallest stack id.
  • A tick is refused only while a dependency’s row is pending and nobody ticked it in the same run. That is the first half of the lesson, read for Sluiceway: the only thing that can change what a stack reads from its dependency is a change of the dependency that has not gone out yet. A dependency that is in sync, whose preview failed, or whose row is of a state this version does not know holds nothing back. A refusal travels down a chain in one run: a stack whose dependency was refused waits on a change that is not going out either.
  • A refused tick gets a note, not a comment. The box is cleared, and a line under the row names the dependencies: “this tick started nothing: it depends on network:prod, which has a change waiting. Tick both to deploy them in order, or deploy network:prod first.” No record is made, the job stays green, and the log says it. It is a fact about the stacks, like deploys: false (0051), not about the person, so nobody is mentioned (0018 keeps its comment for the tick rule).
  • Ticks in one chain deploy one layer at a time. Every allowed tick gets its deployment record in the same resolve run, in stack id order, first the layer that has nothing to wait for and then the rest. The first layer goes into matrix as always. Every other stack gets a record with status queued and behind in its payload: the stack ids of its dependencies that were ticked in this run or have an open deployment of any run. It is not handed on. The same goes for a single tick whose dependency is deploying: it is queued behind that deploy.
  • The payload gains behind, and its version stays 1. A key is added only to a queued record, as a non-empty list of stack ids. A version that does not know it reads the record as an open deployment, which is what it is.
  • A queued row is a deploying row that waits. Its marker state is queued, its text is queued behind **app:prod** · ticked by alice · [run](...), and it has no box. It is placed in the Deploying section and counted as deploying on the counts line, in the header state, in the destroy sign and in the result file. No header picture of its own in part 1 (docs/later.md). A scan makes the row from the queued record, as it makes a deploying row from an open one, so a queued row survives a scan.
  • settle starts the next layer by starting the workflow again. A job cannot hand a later job of its own run a new matrix, and apply deploys only a record of its own run (0035). So when a queued record anywhere is ready, settle dispatches this same workflow on this same ref, the way it starts a full scan (0035, slice 2.6). The workflow’s resolve job runs on workflow_dispatch as well as on issues for this. A resolve that no issue edit started does one thing: it starts every ready queued stack.
  • Ready means that every stack in behind went out. The newest record of each is read. Open: the queued record waits. success or inactive (a rehearsal included, so a rehearsed chain rehearses on): it goes on. failure, error, or no record at all: it can never start.
  • A queued stack starts under a record of the run that starts it. resolve creates a new record with the hash and the ticker of the queued one, run of its own run and no behind, hands it on in matrix, and then ends the queued record as inactive with the words “started in a later run”. Like a rehearsal, that status is told apart by its words, and unlike a rehearsal it is not even a line of the trail: the new record says everything. The ticker is not looked up again. The tick rule was checked when the tick was made, and every record apply takes is trusted the same way.
  • A queued record outlives its run on purpose. The rule of 0003, that an open record whose run is over gets error, does not apply to it, because between the settle of one run and the resolve of the next it is the only thing that says the stack is taken. It is ended instead, by settle for its own run and by the late read of any writer for any run, as failure with the new reason “a stack it depends on did not deploy” (0022), as soon as a stack it waits behind did not go out. The rows then show the failure line, so a chain that stops is never silent.
  • apply never deploys a queued record. resolve never hands one on. If one is handed on by hand, apply leaves it alone and the job goes red, as for a record of another run.

Consequences

  • A tick is now also a request to deploy after the dependencies. The hash it approved is the one checked when the stack finally deploys. When a dependency’s deploy changes what the stack reads, the fresh preview gives another hash, apply refuses it as moved, and the ticker gets the comment of 0051. That is right: the tick approved the old diff.
  • A layer costs a workflow run of its own. On a hosted runner the resolve and settle jobs take seconds.
  • Every dispatch of the workflow now starts a resolve job as well. Without dependsOn in the config it loads the config, finds nothing to do and asks GitHub nothing.
  • A queued stack whose dependencies went out while nobody was going to start it waits for the next resolve run. That happens when the workflow’s resolve job does not run on workflow_dispatch, when a dispatch failed, and in the short moment where a dependency ends between resolve reading it as open and writing the queued record. Ticking the rescan box starts a resolve run and so starts it. The row keeps saying what it waits behind. Detecting it and saying more is left for part 2.
  • The row state is still a cache, with one exception that can only hold a deploy back: resolve reads a dependency’s row state to see whether it is pending. It never previews, so the live body is the one source it has. A wrong state can at worst refuse a tick that could have gone out, or let one out that a person could have ticked by hand anyway.

This record amends 0003 (the payload key behind, the inactive status “started in a later run”, and the exception to the run rule), 0009 (the queued state is now used, and the exception to “nothing is decided from the row state”), 0019 and 0035 (resolve on a dispatch, and apply refusing a queued record) and 0022 (a new deploy failure reason).