A team can stop, rehearse and explain, and an empty fresh preview is not a failure
Decision record 0051
The first real user compared Sluiceway with the internal dashboard that inspired it, on 2026-09-22 (onboarding log, hurdle 23). That dashboard had five things a team misses here, and the owner decided to adopt all five (build plan, slice 2.20). This record fixes each one. Every part stands on its own and was built as its own commit.
1. An exclusion can say why
An ignore entry is a glob as text, as before, or a mapping with a glob and a reason:
ignore: - "apps/web:dev" - glob: "legacy/*" reason: Deployed by the platform team's pipeline until the move in MarchEvery stack an entry with a reason leaves out is listed with that reason in a fold of its own under the In sync heading: <details><summary>2 stacks left out by ignore</summary>, then one line per stack, - legacy/api:prod · Deployed by ..., by stack id. An exclusion written as text stays as quiet as it was. An exclusion written with a reason stays in sight on the page people open, so it does not rot unseen.
This amends 0010, where an ignored stack has no row and nothing else. It still has no row, no marker, no preview and no claim, and config still cannot give it settings. The line is not a row: it has no box, no marker and no state, and no count on the counts line or the header counts it.
- The key is
glob, because the otherignoreentries are globs and the docs call them that. A mapping withoutreasonis an error that says to write the glob as text instead: a reason is what the mapping is for. An empty reason is an error too. A mapping takes no other key. - A stack that several entries match gets the reason of the first entry in the file that matches it. When that first match is a glob as text, the stack is not listed. The first match decides because the file is read top to bottom, as a person reads it.
- The fold is regenerated by every writer from its own config and discovery, like the rest of the body outside the row blocks (0009, 0029).
scan,resolveandapplyall read the config and run discovery, so no marker has to carry the list. A narrowed scan lists them too. - The In sync heading is shown when there are in sync rows or listed stacks. With no in sync rows the heading holds only this fold.
- The reason is text a person wrote in a reviewed file. It is escaped like a stack id (0027), so it is never Markdown and can never start a row. It is shown on a redacted dashboard too: redact keeps the tool’s names out of the issue (0023), and a reason is not one of them.
- The check mode lists what each
ignoreglob leaves out, as before, and does not repeat the reason.
2. One reviewed line stops every deploy
deploys: false at the top of sluiceway.yaml stops every deploy from the dashboard. A team that wants a change freeze used to edit the workflow. Now it is one line in a reviewed pull request, and taking it out is the way back. The default is true.
resolveclears every ticked row box with a note under the first line of the row: “:information_source: deploys are turned off insluiceway.yaml, so this tick started nothing.” It creates no deployment record, hands on an emptymatrix, looks nobody up and writes no comment. A tick that nothing could deploy is not a refusal of a person (0018), so it gets a note on the row and not a mention. The job stays green: Sluiceway did its work (0012).- A tick on a stack that already has an open deployment is dropped as before. The switch does not end deploys that are running.
- The rescan box still starts a full scan. A scan deploys nothing, and a team in a freeze still wants the dashboard to be right.
applyreads the switch right after the config, before discovery, the version check and the fresh preview, so the tool and its credentials are never used. This covers a tick made before the switch was merged whoseapplyjob starts after it: it waited for a runner or a reviewer. The record getsin_progressfirst, as every recordapplytakes (0019), and then ends asfailurewith a new reason from the fixed list of 0022, “deploys are turned off in sluiceway.yaml”. The row keeps the failure line until a deploy of that stack succeeds, which says to the ticker what happened. Theoutcomeoutput isrefused, like a change that moved: nothing is wrong with the stack or the tool. The job is red, because a greenapplyjob means “this went out” (0035).applywrites no row in that case, as for every other way out before the fresh preview: the next scan previews the deploying row whose record ended (slice 2.1).- The switch is a config key and not an input, for the reason of 0045: a change of
sluiceway.yamlis reviewed, andresolveandapplyread the file of the default branch. - A scan does not show the switch on the dashboard. Boxes are still drawn, and a tick is cleared with the note. A line under the Pending heading was left out of this slice.
3. An empty fresh preview is nothing to deploy, not a failure
When the fresh preview of apply has no change at all, the stack is already as its code says. The usual cause is the outside deploy that 0016 calls legal: someone deployed the stack from a laptop or another pipeline between the scan and the tick. Until now the hash of the empty diff differed from the approved one, so the record ended as error, “the change moved since the tick”, the job went red and the row got a failure line, for a stack that was exactly where the person wanted it.
Now apply checks for an empty fresh preview after the preview and before the hash check:
- The record ends as
success, with the status description “nothing to deploy, already in sync”. The tool is not asked to deploy. - The job is green and the
outcomeoutput isin-sync, a new value next todeployed,refusedandfailed(0041).deployedwould say that something went out, which a notify step would announce. The result file’soutcometakes the same value, itsreasonandprevieware null. - The row is made from the empty fresh preview, so it is in sync. The newest record of the stack is a success, so the row has no failure line.
- The trail says it. The recently deployed list (0029) is where a person sees what went out and who ticked it. A success with that description is listed as
- <stack id> · ticked by alice · nothing to deploy, already in sync · <time> · [run](...), so it cannot be read as a deploy. - The summary of the
applysays the same in one line and why it most likely happened. - The description is the one status description a reader decides something from: the trail’s words. 0022 says nothing is decided from a failure reason. This is not a failure reason, it is a fixed word of Sluiceway’s own on a success, compared as a whole string. A person cannot write a deployment status with the workflow token of another repo, and one who can write statuses here can do far more than change a line on the dashboard.
- Attribution counts from this record like from any success (0026): the stack was in sync at that commit.
- A diff with only tracking changes (an import, a forget, a move) is not empty. It still goes through the hash check and deploys.
- With
scan.logDiffon, no tool diff is run for an empty preview, as before.
4. A moved change is told to the person who ticked
When the fresh preview gives another diff hash than the tick approved (0008), apply deploys nothing, the record ends as error, the job is red and the row gets the fresh diff with a failure line. None of that reaches the person who ticked: a red job in a run they did not start sends them nothing, and a failure line is only seen by someone who opens the dashboard. So apply writes one comment on the dashboard, the way 0018 does for a refused tick:
@alice ticked apps/web:prod, and the change moved since the tick, so nothing was deployed. The row on the dashboard shows the change as it is now. Tick it again to deploy that.
- The ticker is mentioned once, from the payload of the record (0003), which is the person the edit history named (0025). The stack is named in bold and escaped, as on a row. Nothing of the diff is in it: no type, no name, no property path and never a value (0021). The words are plain (0032).
- It is written after the row swap, because it says the row shows the fresh diff, as 0018 writes the refused tick’s comment after the body. With no dashboard, or a body that could not be written, no comment is written. The job is red already.
- A comment that cannot be written is one more line in the red job’s message, naming
issues: write. It changes nothing about the record or the row. - Only a moved change gets one. A deploy that failed does not: that is about the stack or the tool, the failure line says it, and later.md keeps that comment out. A refused re-run (0019), a record of another run and
deploys: falsedo not either: the first two are about the workflow, and the switch is a decision the team made in review. An empty fresh preview is not a moved change any more (part 3), so it gets none. - This amends 0018, whose comment for a refused tick was the only one Sluiceway wrote.
5. A tick can be rehearsed
A team that sets up Sluiceway, or changes its workflow, wants to see the whole path of a tick work before it lets a tick deploy: the edit history names the ticker, the tick rule allows them, resolve makes the record and hands it on, the apply job gets its credentials and a runner, the fresh preview runs, and its hash matches the row. The dry-run input of apply does all of that and stops before the deploy.
- uses: sluiceway/sluiceway@v0 with: mode: apply deployment-id: ${{ matrix.deployment }} dry-run: true- An input, not a config key, because it belongs to one step of one workflow, like
deployment-id, and is taken out of the workflow the same way when the rehearsal is over. It takestrueorfalseand nothing else, so a typo never deploys when a rehearsal was meant.dry-run: trueon any other mode is an error, asdeployment-idis (0035). The default,false, reaches every mode and says nothing. - Everything before the deploy runs as for a deploy, in the same order (0035, slice 2.5): the record’s status, the record,
in_progress, the config anddeploys: false, discovery, the version check, the fresh preview, the check for nothing to deploy, and the hash check. A record that is not open, a moved change, an empty preview and the switch end the way they end withoutdry-run, so a rehearsal proves those paths too. - After the hash check the record ends as
inactivewith the description “rehearsed, nothing was deployed”. GitHub has no state of its own for a rehearsal.successwas rejected: GitHub’s own views, its Slack and Teams apps (0041) and every notify step that watches deployments would show a deploy that never happened, and attribution would start from a commit that never went out.failureanderrorwere rejected because a rehearsal that worked is no failure and would put a failure line on the row.inactiveis GitHub’s word for a deployment that is not live, and GitHub writes it with an empty description when it supersedes a success, so the fixed words tell the two apart. Writinginactivedirectly withauto_inactive: falsewas checked on real GitHub on 2026-09-22 in the private lab repo: REST takes it, and GraphQL gives backINACTIVEwith the description. - A rehearsal is no deploy fact of the stack. 0003 reads
inactiveas “succeeded, then superseded”. A record with the rehearsal’s words is the exception: it is left out of the stack’s facts, so a failure line or a success before it stands, and attribution never starts from it. It is listed in recently deployed, the trail, as- <stack id> · ticked by alice · rehearsed, nothing was deployed · <time> · [run](...), so the team sees the rehearsal where it would have seen the deploy. - The row is made from the fresh preview, so it is pending again with its box and without the tick. It never says deploying: nothing was going to deploy.
- The job is green and the
outcomeoutput isrehearsed, a new value (0041). The result file’spreviewholds what a deploy would have sent, and the summary of theapplylists it under “What a deploy would send”. - The tool’s
upis never started, so a rehearsal needs the credentials a preview needs and no more. A team can rehearse with read-only credentials in theapplyjob. settlefinds nothing open after a rehearsal, as after anyapplythat gave its record a result.