Skip to content

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 March

Every 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 other ignore entries are globs and the docs call them that. A mapping without reason is 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, resolve and apply all 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 ignore glob 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.

  • resolve clears every ticked row box with a note under the first line of the row: “:information_source: deploys are turned off in sluiceway.yaml, so this tick started nothing.” It creates no deployment record, hands on an empty matrix, 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.
  • apply reads 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 whose apply job starts after it: it waited for a runner or a reviewer. The record gets in_progress first, as every record apply takes (0019), and then ends as failure with 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. The outcome output is refused, like a change that moved: nothing is wrong with the stack or the tool. The job is red, because a green apply job means “this went out” (0035).
  • apply writes 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.yaml is reviewed, and resolve and apply read 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 outcome output is in-sync, a new value next to deployed, refused and failed (0041). deployed would say that something went out, which a notify step would announce. The result file’s outcome takes the same value, its reason and preview are 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 apply says 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.logDiff on, 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: false do 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 takes true or false and nothing else, so a typo never deploys when a rehearsal was meant. dry-run: true on any other mode is an error, as deployment-id is (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 and deploys: 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 without dry-run, so a rehearsal proves those paths too.
  • After the hash check the record ends as inactive with the description “rehearsed, nothing was deployed”. GitHub has no state of its own for a rehearsal. success was 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. failure and error were rejected because a rehearsal that worked is no failure and would put a failure line on the row. inactive is 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. Writing inactive directly with auto_inactive: false was checked on real GitHub on 2026-09-22 in the private lab repo: REST takes it, and GraphQL gives back INACTIVE with the description.
  • A rehearsal is no deploy fact of the stack. 0003 reads inactive as “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 outcome output is rehearsed, a new value (0041). The result file’s preview holds what a deploy would have sent, and the summary of the apply lists it under “What a deploy would send”.
  • The tool’s up is never started, so a rehearsal needs the credentials a preview needs and no more. A team can rehearse with read-only credentials in the apply job.
  • settle finds nothing open after a rehearsal, as after any apply that gave its record a result.