One step picks its own mode, and the workflow is one job
Decision record 0077
The workflow people copied was four jobs, one per mode, joined by if: and needs:: if: github.event_name == 'workflow_dispatch' || (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'sluiceway')) on resolve, !cancelled() and a matrix on apply, always() and two needs on settle, and two more jobs and a longer condition for merge and deploy. Every line was right, and every line was one more thing a person had to copy exactly and keep in step with sluiceway.yaml. The owner, 2026-09-22: “these crazy ifs, cant we make sure this isnt necessary anymore, we want the ux as simple as possible, easy to install and easy to configure.” Build plan slice 5.12.
This amends 0014, 0017, 0025, 0035, 0042, 0061, 0065 and 0074.
Decision
Auto mode
-
modeis optional, and its default isauto. A step with no mode, or withmode: auto, readsGITHUB_EVENT_NAMEand the payload and runs what the event asks for.src/core/auto-mode.tsholds the rule,src/modes/auto.tsruns the modes one after the other, and the six explicit modes are unchanged.Event Runs pushto the default branchscanpushto any other refnothing, a notice schedulescanworkflow_dispatchresolve, thenscanissues,edited, on the dashboardresolveissues, any other issue or any other actionnothing, a notice pull_request,merge_groupcheckanything else, deploymentincludednothing, a notice -
What
resolveand the scan hand on is deployed in the same step. Eachmatrixentry goes toapply, one after the other, in the order it was handed on. When anything was handed on,settleruns last. A mode that ends red does not stop what follows: the deploys it started still run andsettlestill runs, as!cancelled()andalways()did, and the step goes red at the end with every reason. -
On a dispatch
resolveruns first, so the next layer of a chain (0056) goes out before the scan of that run previews, and the scan sees it deployed. -
The dashboard is known by its label from
sluiceway.yaml. The cheap check of 0017 runs before any mode, from the payload: open, written by the bot with a root marker, and then the label. The workflow names no label, and changingdashboard.labelneeds no change to it. -
dashboard.readOnlyis read too. With it on, a dispatch only scans and an issue edit ends with a notice, so the read-only trial is the same one-step workflow without theissuestrigger. -
A push to a branch that is not the default branch is not Sluiceway’s when the payload names the default branch, so a workflow without a branch filter scans only the default branch. The recommended workflow keeps
branches: [main], because the job loads credentials before Sluiceway runs. -
deploymentis not a trigger Sluiceway uses. A deployment record made with the workflow token starts no run (0017), soresolvecannot hand a deploy to another run that way. A dispatch is the one event the workflow token may start. -
Inputs. Auto mode accepts every input of the modes it may run:
strictfor its scan,dry-runanddeploy-timeoutfor its deploys,backendfor its check.deployment-idis an error in auto mode, as in every mode butapply. -
Outputs. The step sets what each mode it ran sets.
matrixis every deploy the step started. A step that deploys several stacks setsoutcome,stackandresult-fileonce per deploy, so a step after it sees the last one; the step is red when any failed. The job summary is the summaries of every mode it ran, in order. -
A post step settles a run that was cancelled mid deploy.
action.ymlgainspost: dist/post.jswithpost-if: always(). The file is written by hand, sets a flag and loads the one bundle. The main step saves a state when it hands a deploy on and another whensettleran. The post step runssettleonly in auto mode, only when a deploy was handed on andsettledid not run. Where the runner kept no state, nothing runs, and the next render ends an open record whose run is over (0003).
The one-step workflow
The README, docs/workflow.md, the read-only trial, the three example workflows and init all write the same shape, and a test holds each of them to it:
jobs: sluiceway: runs-on: ubuntu-latest concurrency: group: sluiceway-${{ github.event.issue.number }} queue: max steps: - uses: actions/checkout@v7 # the tool, the packages and the credentials - uses: sluiceway/sluiceway@v0- The triggers are the four of before:
pushto the default branch, the schedule,workflow_dispatchandissueswith the typeedited. The permissions block is the one of before. With merge and deploy,contents: writemoves into it, and the dispatch input of 0064 is declared as before. - The concurrency group is named after the edited issue, with
queue: max. Every run of a scan, a dispatch and a tick of the dashboard waits in line, and a waiting run is never dropped. A static group was tried first and rejected: GitHub starts the job for an edit of any issue, so on a busy or public repo those runs would wait behind scans and deploys and could flood the line ahead of a tick. With the issue in the name, an edit of another issue gets a group of its own and ends in seconds.queue: maxis needed because the dispatched runs now carry deploys (the next layer, the merged change), and a replaced run would lose them.cancel-in-progresswould stop a deploy half way. - Explicit modes and the split workflow stay, for anyone who wants what one job cannot do (below). They are documented on their own page,
docs/split-workflow.md, with theirif:andneeds:. It is the one page of the user docs that has any.
What one job cannot do, and what it costs
- One set of credentials. The job previews and deploys with the same credentials, so they must be able to write. 0014 recommended credentials that only read for
scan; that is the split workflow’s now. - The job an issue edit starts loads the credentials. 0014 called promise 4 “the real gain”: the job anybody with issue access can cause holds no infrastructure secrets. In one job it does. What the run executes is only code of the default branch, the event reaches no step, and Sluiceway ends it with a notice before it reads the dashboard when the issue is not the dashboard, so the credentials are loaded and not used. The cost is one runner minute and one load from the secret store per edit, on a public repo per edit by anyone. Promise 4 still holds of the modes:
resolveandsettleare handed no process runner and no tool environment in auto mode either. The split workflow keeps the stronger reading. - No environment per stack.
environment:is a key of the job. One job can name one environment, limited to the default branch (setup 2 of 0020), and cannot have required reviewers, because every run would wait for one. Setup 3 of 0020 needs the split workflow. - Deploys in turn. Stacks ticked in one edit deploy one after the other, and a tick waits for a run of the dashboard’s group that is deploying. The split workflow deploys them side by side.
- Ticks share the runner of scans. A self-hosted runner pool serves ticks too. The split workflow can keep
resolveandsettleon hosted runners. contents: writereaches the preview with merge and deploy, where the split workflow gives it toresolvealone.- Different permissions per mode are not possible in one job. The block is the union, which is the block the docs gave every workflow already.
Considered
- A reusable workflow at
sluiceway/sluiceway/.github/workflows. It could keep the four jobs, theirif:andneeds:and their groups inside Sluiceway’s repo, and the caller would write oneuses:. It was rejected as the harder one for users. A called workflow cannot run the caller’s steps, so the tool install, the packages and the credential step (0013) would have to become inputs: a script string at best, never an action such asaws-actions/configure-aws-credentialsor1password/install-cli-action, becauseuses:cannot be an expression. Secrets would pass throughsecrets: inherit, environments through more inputs, and a called workflow’s permissions are capped by the caller’s block anyway, so that block would still be the caller’s to write. The one job keeps the file the person’s own: the same steps they would write for any CI job, and one step of Sluiceway after them. - A static concurrency group (
group: sluiceway). Simpler to read, rejected for the queue above. - No concurrency group, and a lock of Sluiceway’s own. Two runs that resolve at once could make two records for one tick, and two deploys of one stack could run at once. A lock through the GitHub API is a new mechanism with its own failure modes, where GitHub’s queue already exists.
- Deploys side by side inside the step. The job log of two deploys at once interleaves, and the order of a chain matters. In turn is simpler to read and to reason about.
- Deploying from a
deploymentevent. Not possible, above. - Keeping the check in the same file.
pull_requestruns the check in auto mode, but the job would load credentials for the pull request’s code. The check keeps a workflow of its own, now with no mode either.
Consequences
- 0014 is amended: promise 4 holds of the modes, and “the job an issue edit starts holds no infrastructure secrets” holds of the split workflow only. The docs say so.
- 0017 is amended: the cheap check also runs in auto mode, before any mode, and in the one-step workflow an edit of an ordinary issue does start a runner, which ends with a notice.
- 0025 and 0035 are amended: the groups of the one-step workflow are one per edited issue and one for everything else, with
queue: max.settleruns in the step after the deploys, and in the post step after a cancelled run. - 0042, 0061 and 0074 are amended: the check reads a step with no mode as auto mode and works out the modes it runs from the triggers of its file. For such a job it warns about a missing trigger, a permission any of those modes needs, a missing group, a group without
queue: maxor withcancel-in-progress, and apull_requesttrigger next to it, with its own words. It no longer warns that a step has no mode.if:,needs:and a second apply job are not asked of it. - 0065 is amended:
initwrites the one-step workflow, withtimeout-minutes: 60on the job, no environment and one credential step that loads the env file that deploys. It refuses to write when any workflow runs Sluiceway beyond the check, a step with no mode included. CONTEXT.mdgains auto mode, the one-step workflow and the split workflow.docs/later.mdgains split credentials through a dispatched second workflow, one set of outputs per deploy, and a check that knows about busy issue trackers.