A stack may deploy on merge, and the default stays a tick
Decision record 0095
Amended by 0105: one more reason a stack set to on-merge waits for a tick, after a destroy and drift and before a scan no merge started: its change costs more a month than the repo’s
cost.threshold, or the threshold is set and the cost could not be estimated. The row says which.
Amends 0003 (the payload key
onMerge), 0018 (a deploy that is not a tick), 0025 (a scan may open a deployment record for a stack set to on-merge, not only after a merge from the dashboard), 0054 (the scan’smatrixcarries these deploys too), 0056 and 0091 (a queued record carriesonMerge), and 0061 (the check warns when the split workflow has no job for the scan’s matrix). Built as slice 5.31.
Every tool Sluiceway is compared with deploys on merge by default: Terrateam, Digger, Spacelift, env0. DORA’s research reads a human approval per change as a cost to lead time and deploy frequency. Sluiceway’s tick is not an approval board, but from outside it looks like one: a gate on every change to every stack. The honest answer is that the gate belongs to the stack. A boring stack, such as a dashboard, an exporter or a test namespace, can go out on merge. A stack that holds the cluster, the database or the network waits for a person. Until now the only ways to get that were to tick everything, or to leave a stack off the dashboard (issue 213).
The owner, 2026-09-23: the default stays as it is, a person ticks, and a stack may choose otherwise. “defaulting to checking but u can still choose per stack”. So this is opt in, per stack, and a repo that does not use it is byte for byte what it was.
Decision
stacks[].deploytakeson-tick, the default, oron-merge. A stack no entry sets carries no key at all, in the config, the payload, the rows and the check, so nothing a repo without it writes or reads changes. An entry with a name wins over one without, as forenvironment. Any other value fails the config: a typo here would change what deploys without a person.- Only the scan of a merge deploys on merge. A merge is a push to the default branch, and the scan it starts is the scan of the merge. When that scan finds a stack set to on-merge pending, it opens the stack’s deployment record at its late read, right after it hands on the merges of 0054, and puts it in its
matrix. In the one-step workflow the same step deploys it (0077). In the split workflow the second apply job of 0054 takes it. A scheduled scan, a dispatch, the rescan box and the scan after a merge from the dashboard never deploy on merge: a change they find has no merge to deploy on, and its row says it waits for a tick. The one exception is the stack a merge from the dashboard itself handed on: that deploy is the ticker’s, through the merge record, as before. - Nothing about the deploy forks. The record is opened by the same
openRecordas a tick’s, on the scanned commit, with the diff hash the scan previewed.applyclaims it, previews again, compares the hash and deploys, refuses a change that moved, ends the record, swaps the row and writes the trail line exactly as for a tick. The job, the concurrency group,settle, the post step, a timeout anddry-runall apply unchanged. The only new fact is who: the payload’stickerholds whoever merged, and the added keyonMerge: truesays it was not a tick. The version stays 1. A reader that does not know the key reads a tick by that person, which deploys the same. - Whoever merged is the sender of the push, as GitHub names it: the person who pressed merge, an app that merges such as
renovate[bot], or whoever pushed directly. Only a push whoserefis the default branch the payload names counts. An app is named as GitHub names it, and allowed: the repo put the stack on on-merge, and what may land on the default branch is the default branch’s rules. A push the payload names no sender for deploys nothing on merge. - A destroy always waits for a tick, whatever the setting says, and the row says so:
this stack deploys on merge, and this change waits for a tick: it deletes or replaces a resource.The destroy alert exists because a destroy deserves a look (0024, 0062), and a setting that deploys without anyone looking must not reach it. A replace counts as a destroy, as the glossary already defines one: the old object goes away, with its data, its address or its identity, before or after the new one exists, and a database replaced by a changed name loses the same data as one deleted. A tracking change alone, an import, a forget or a move, is no destroy and goes. - A preview failure never deploys, as for every tick: the stack has no diff, so no hash and no record. Its row is a preview failure row, with no note.
- Drift waits for a tick. A pending row whose hash covers drift is not deployed on merge, and a drifted row with nothing from the code never is: a drifted row is no change of the code, so no merge asked for it. Drift means reality moved, not the code. Someone changed something by hand, perhaps on purpose during an incident, and a deploy on merge would put it back as a side effect of an unrelated change. Repairing it is a decision about the real infrastructure, which is a person’s. The argument for repairing by itself, that the config says the code is the truth for this stack, holds for the code’s own changes and not for what someone did to the resources. The row says:
the stack drifted, and a deploy would also put back what changed outside the code. dependsOnand phases decide the order, by the rules of a tick. The decision hands the stacks that would go to the sameplanDeploysa tick’s judgement uses (0056), as if they were ticked together, with every other pending row as a change nobody ticked. Two stacks set to on-merge in one chain go out one layer per run: the first now, the next under a queued record withbehindandonMerge, started by theresolveof the runsettlestarts. A stack set to on-merge whose dependency is deploying is queued behind it. A stack set to on-merge whose dependency has a change waiting for a tick, because it is on a tick, or because its own change waits, waits too, and its row names it, in the words of a refused tick:it depends on **network:prod**, which has a change waiting. Tick both to deploy them in order, or deploy **network:prod** first.With a phase, the phase is named. A dependency that is in sync, whose preview failed, or whose row this scan did not preview and is not pending holds nothing back, as 0056 says.deploys: falseand a read-only dashboard stop everything. Withdeploys: falsenothing goes on merge and the row saysthis stack deploys on merge, and deploys are turned off in sluiceway.yaml.A read-only dashboard deploys nothing on merge and writes no note: it has no boxes, and the line under the Pending heading already says it is read only (0045).- On the dashboard a deploy on merge never looks like a stack nobody ticked, and never like a tick. While it goes out its row says
waiting to start on mergeordeploying on merge, thenmerged by alicewhere a tick saysticked by alice. Queued behind a stack it saysqueued behind **network:prod** · merged by alice. Its failure line saysmerged by. On the trail a ticked deploy names the ticker alone and a deploy on merge saysmerged by alice, with the result word before it as for any line (failed · merged by alice). The summary ofapplyand its job log line saymerged bytoo. - A change that moved tells whoever merged, in one comment as for a tick (0051), in words that do not speak of a tick:
@alice merged a change that **app:prod** deploys on merge, and the change moved before the deploy, so nothing was deployed. The row on the dashboard shows the change as it is now. Tick it to deploy that. tickerskeeps deciding who may tick the stack, and does not judge the merge. Every change of the stack that waits, a destroy, drift, a dependency, a scan that no merge started, is deployed by a tick, and the tick rule judges that tick as always. The merge itself is the ask, and who may merge is GitHub’s to decide: write access, and the branch protection and required reviews of the default branch. Judging the person who merged againsttickerswas considered and left out: it would take a permission lookup in every scan of a merge, it would refuse every app that merges, and it would put a gate that acts like branch protection behind a key that reads like a stack setting. A stack only named people may deploy stays on a tick.- A GitHub Environment with required reviewers still gates it, and that is the honest answer to who may deploy such a stack (0093). The deploy runs in the job that deploys ticks: in the split workflow the second apply job, a copy of
applythat names the same environment. The reviewers approve a deploy on merge as they approve a tick, and until they do the record stays open and the row says waiting to start on merge. Without an environment, whoever may merge may deploy the stack, and the docs say so. In the one-step workflow an environment on the one job holds every run, as 0077 says. - The check lists it. A stack set to on-merge says
deploys on mergeon its line of the check, and the summary’s table gets a columnDeploysonly when a stack does. A split workflow with a stack set to on-merge and an apply job, but none that takes the scan’smatrix, gets a warning, because that deploy would never start. With merge and deploy on as well, the one warning is merge and deploy’s.
The promise, reworded
The product said: nothing deploys unless a person asks. It now says, everywhere it says it: nothing deploys unless a person asks, or unless the repo’s own config says this stack goes out on merge. The README, SECURITY.md, the security page, the configuration page, using the dashboard, the split workflow page and the glossary say it in those words. The setting lives in sluiceway.yaml and nowhere else, never on the dashboard or in an input, so turning a stack to on-merge is itself a reviewed change on the default branch.
Considered
- On-merge as the default, as the tools compared with do it. Rejected by the owner, and it would break the promise for every repo that upgrades.
- A top level
deploykey for every stack at once. Left out: the point of the decision is that the gate belongs to the stack. A repo that wants every stack on merge writes an entry per path, which keeps the choice visible stack by stack. - Deploying on merge from any scan that finds the stack pending, a schedule included. Rejected: there is no merge to attribute it to, and a change a push scan could not preview, and that a later scan finds, is exactly the change a person should look at. It waits for a tick with a note.
- Letting a replace through, since some replaces lose nothing. Rejected: the preview cannot tell which, and the glossary already calls a replace a destroy.
- Repairing drift on merge. Rejected above.
- A result file field or an output that says on merge. Left for later (
docs/later.md): the result file keepstickeras the name on the record, and its schema is published, so a field is a change of its own. - Its own header picture or result dot. Not needed: a deploy on merge is a deploy, and the words on the row and the trail tell it apart.
Consequences
- The scan of a merge can now open deployment records and hand deploys on, for stacks set to on-merge. It already had
deployments: writefor the hand-off of 0054. In the split workflow it needs the scan’soutputs:, the second apply job and thesettleof 0054’s section, and the docs say so. - The deploy runs with the credentials of the job that scanned, in the one-step workflow, as every tick of that workflow already does.
- The queued-record hand-off carries
onMergeby name, the way 0091 made it carrydrift, and a test holds it. CONTEXT.mdgains Deploy on merge, and the ticker, the queued stack and the trail say how a deploy on merge differs.