Get started
Go one step at a time. Each step shows you something before the next one can change anything.
- Check your setup. A pull request check that reads your files and says which stacks Sluiceway found and whether
sluiceway.yamlis valid. No credentials, no tool, no write. Step 1. - Scan, read only. One job that previews your stacks and writes the dashboard, with nothing that can deploy.
dashboard.readOnly: trueinsluiceway.yamldraws the dashboard without boxes, so nothing looks as if it could be ticked. Start read only. - The whole loop. The workflow with all four jobs, so a tick deploys. Step 2, then your stacks and your credentials.
What new users ran into, so you do not have to:
- A stack config file with no stack in the backend becomes a red row. Leave it out with
ignore, and write its full id:apps/web:dev, neverapps/web. The check warns about a glob that leaves out nothing. - A program that pulls from a private registry works on your laptop and fails on the runner. Log in to that registry in the workflow. Credentials has recipes.
resolveandsettlehold no secrets. Keep them on hosted runners even whenscanandapplyare self-hosted, so a tick shows on the dashboard in seconds instead of waiting for a busy runner.
Pin a commit
Every example here says sluiceway/sluiceway@v0. v0 moves with every release until 1.0.0, so your workflow always runs the newest 0.x release. To review every update before it runs, pin the full commit SHA of a release instead, with its version as a comment:
- uses: sluiceway/sluiceway@f417adda434806ed641f551aa126402c923516a3 # v0.1.1The releases page lists every version. Dependabot and Renovate can raise a pull request when a new one is out. A branch such as @main runs code that is not released yet.
Requirements
- A GitHub repo with issues turned on. The dashboard is an issue.
- GitHub Actions runners with runner version 2.328.0 or newer. Hosted runners qualify. Self-hosted runners need that version at least, and ARM32 is not supported.
- Pulumi CLI 3.229.0 or newer on the runners that preview and deploy, for Pulumi stacks.
pulumi/actionsinstalls it. With an older one every preview fails, and the job log says which version is needed. - OpenTofu 1.11.0 or newer, for OpenTofu stacks, installed without a wrapper (credentials). A repo with only Pulumi stacks never needs it.
- Your programs’ own needs: a language runtime, dependencies, credentials. The workflow installs and loads them, the same way your own CI or laptop does.
Setup
Four steps. The first one needs no credentials and changes nothing, so you learn whether Sluiceway understands your repo before anything can deploy.
The examples use the action at @v0. Pin a commit says how to pin a release by its commit SHA instead.
What goes where
Two files have nearly the same name and do different jobs. Keep the workflow file’s name different from sluiceway.yaml; the examples call it deploy-dashboard.yml.
| File | Belongs to | What it says |
|---|---|---|
.github/workflows/deploy-dashboard.yml |
GitHub Actions | When Sluiceway runs, on which runners, with which permissions, and the steps that install your tools and load your credentials before it. You choose the name. All four jobs stay in this one file. |
.github/workflows/deploy-dashboard-check.yml |
GitHub Actions | The check that runs on every pull request. |
sluiceway.yaml |
Sluiceway, optional, at the repo root | Settings about your stacks: who may tick them, which ones to leave out, which files outside a stack’s directory it reads. Never credentials, never runner settings. Reference. |
| Your secrets | GitHub secrets, your cloud, your secret manager | Credentials, backend settings, anything your programs read. They reach the job through the workflow, never through Sluiceway. Credentials. |
1. Check your setup
Start here. The check mode tells you, in a pull request, whether Sluiceway will understand your repo, before any workflow that previews or deploys is merged. It reads files and nothing else: no credentials, no infrastructure tool, no GitHub API, no write. It needs contents: read, so it is safe on pull_request, also from forks. Put this in .github/workflows/deploy-dashboard-check.yml:
name: deploy-dashboard-check
on: pull_request:
permissions: contents: read
jobs: check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 with: persist-credentials: false - uses: sluiceway/sluiceway@v0 with: mode: checkThe job log and the summary of the run say:
- whether
sluiceway.yamlis valid, with the same messages a scan gives, - every stack that discovery found, with its environment, its tick rule and its inputs,
- which stacks each
ignoreglob leaves out. A glob that leaves out nothing is a warning.ignorematches the stack id, soapps/webignores nothing, and the warning names the glob that would work (apps/web:*), - the files that no stack claims, grouped by directory. A push that changes one of them previews every stack. A ready-to-paste
scan.unrelatedblock covers the ones that look like docs and tooling. Sluiceway never decides this for you, so leave out any file one of your programs reads.
The job is red only when the config is not valid or discovery fails. A check cannot say that a preview will work: a stack that does not exist in the backend, a missing credential or a registry the runner cannot reach shows only in a scan. The check reads the files of the checkout, so run it right after actions/checkout, before anything writes files into the workspace.
If your repo uses a merge queue and you make this check required, add merge_group: next to pull_request: in this file, so the queue gets its result. This is the only Sluiceway workflow that may have it.
2. Add the workflow
This is the whole workflow. It goes in .github/workflows/deploy-dashboard.yml on the default branch. The comments mark where your own steps go. docs/example-workflows.md has it complete for a Node monorepo, a secret manager and a cloud with OIDC.
name: deploy-dashboard
on: push: branches: [main] schedule: - cron: "0 6 * * *" # keep this: the daily full scan is part of the design workflow_dispatch: issues: types: [edited]
# This block is everything Sluiceway can do in your repo.permissions: contents: read issues: write deployments: write actions: write pull-requests: read checks: write
jobs: scan: if: github.event_name != 'issues' runs-on: ubuntu-latest concurrency: sluiceway-scan steps: - uses: actions/checkout@v7 - uses: pulumi/actions@v7 # without a command this only installs the CLI with: pulumi-version: ^3.229.0 # Install what your programs need, once, for example: npm ci # Load your credentials and your state backend settings into the job # environment here. Sluiceway passes the environment to the tool and # never looks inside. Whatever loads a secret must also mask it. - uses: sluiceway/sluiceway@v0 with: mode: scan
resolve: if: github.event_name == 'workflow_dispatch' || (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'sluiceway')) runs-on: ubuntu-latest concurrency: sluiceway-resolve outputs: matrix: ${{ steps.resolve.outputs.matrix }} steps: - uses: actions/checkout@v7 # No tool and no credentials in this job. It never runs the tool. - id: resolve uses: sluiceway/sluiceway@v0 with: mode: resolve
apply: needs: resolve if: ${{ !cancelled() && needs.resolve.outputs.matrix != '' && needs.resolve.outputs.matrix != '[]' }} strategy: fail-fast: false matrix: include: ${{ fromJson(needs.resolve.outputs.matrix) }} runs-on: ubuntu-latest concurrency: group: sluiceway-apply-${{ matrix.stack }} queue: max steps: - uses: actions/checkout@v7 - uses: pulumi/actions@v7 with: pulumi-version: ^3.229.0 # Same install and credential steps as in the scan job. These # credentials must be able to change things. - uses: sluiceway/sluiceway@v0 with: mode: apply deployment-id: ${{ matrix.deployment }}
settle: needs: [resolve, apply] if: always() && needs.resolve.outputs.matrix != '' && needs.resolve.outputs.matrix != '[]' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: sluiceway/sluiceway@v0 with: mode: settleWhat the parts are for:
- All four jobs stay in one file. A scan looks for waiting ticks among the runs of its own workflow, and the rescan box and
settlestart a scan by starting that same workflow again. Name the file as you like, and keep the name different fromsluiceway.yaml. - The daily schedule stays. A push previews only the stacks that claim a changed file. A program can read something that is not a file in the repo (another stack’s output, a remote chart, a secret), and the daily full scan is what catches that.
- Never add
pull_requestormerge_groupto this workflow. A scan writes the dashboard from the code it checked out, and on a pull request or in a merge queue that is code that is not on the default branch yet. A merge queue ends in a push to the default branch, and the scan runs on that push. actions: writelets the rescan box andsettlestart a scan, and lets a scan see whether a run is still on its way.id-token: writeis not in the block. Add it only to the jobs that run the tool, and only if your credential step uses OIDC. A job’s ownpermissions:replace the workflow’s, so repeat the whole block there.sluiceway-scanmakes scans run one at a time. A running scan finishes, and of the waiting ones only the newest runs.sluiceway-resolvedoes the same for ticks. Anyresolverun handles every ticked box it finds, so a replaced run loses nothing. Replaced runs show as cancelled in the Actions list. That is normal.queue: maxonapplykeeps a waiting deploy from being cancelled by a newer one. Never addcancel-in-progressto this job.- The
if:onresolvekeeps an edit of an ordinary issue from starting a runner. If you changedashboard.label, change it here too.resolvealso runs when the workflow is dispatched: withdependsOninsluiceway.yaml,settlestarts the workflow again once a stack went out, and that run’sresolvestarts the stacks that were queued behind it. WithoutdependsOnit finds nothing to do in a few seconds and asks GitHub nothing. resolvehandsapplya deployment record. It creates one record per ticked stack in GitHub’s Deployments list and puts{ stack, environment, deployment }inmatrix.applydeploys only while that record is still open. “Re-run failed jobs” therefore deploys nothing. To try again, tick the box again.!cancelled()onapplylets the deploys thatresolvestarted go ahead whenresolveitself ended red, for example because one of several ticks could not be verified or the dashboard could not be written. Without a status check in itsif:, GitHub skips a job whoseneedsfailed. Every entry inmatrixis a record thatresolvecreated after it checked the ticker, so nothing else can get through here.settlegives a deploy a result when its job was cancelled or rejected, so a row never stays “deploying” for ever. It touches only the deployment records of its own run. When it ended one it starts a full scan, which writes the row again with the failure line, so it needsactions: writeas well. WithdependsOn, it also starts the workflow again when a stack went out that others are queued behind.- A deploy has no time limit of Sluiceway’s. Set
timeout-minuteson theapplyjob. @v0follows every release from 0.1.0 until 1.0.0. A commit SHA stays the choice if you want to review every update (Pin a commit).
Self-hosted runners work the same way: change runs-on for scan and apply. resolve and settle hold no infrastructure secrets, so they can stay on hosted runners.
With GitHub Environments
The tick is always a gate. Where your plan has environments, they make it a stronger one: store the credentials that can change things as secrets of an environment that is limited to the default branch, and add required reviewers where you have them. Give every stack an environment in sluiceway.yaml, and add this to the apply job:
environment: name: ${{ matrix.environment }} deployment: false # Sluiceway already records the deployGitHub lists an environment for every name a deployment record uses, so your repo settings will show one named sluiceway (or the names you configured) even if you never use the feature. That entry is only a label.
Without deployment: false GitHub records every deploy a second time. Custom deployment protection rules do not work with deployment: false. If you use them, leave it out and accept the second record. Sluiceway ignores it.
docs/security.md has the three setups, from what every repo has to required reviewers, and what each one protects against.
Merge and deploy
With mergeAndDeploy.authors in sluiceway.yaml, routine pull requests by those authors, such as Renovate’s, get a row of their own under “Updates waiting to merge”, and one tick merges the pull request and deploys its stack (configuration). It is off by default, and it needs three changes to the workflow above.
The merge. resolve merges with the workflow token, which needs contents: write. Give the resolve job its own block. A job’s own permissions: replace the workflow’s, so it repeats the rest:
resolve: permissions: contents: write issues: write deployments: write actions: write pull-requests: read checks: writeThe deploy. A merge made with the workflow token starts no run of its push, so resolve starts the workflow again instead, and the scan of that run hands the merged change on through its own matrix output. resolve runs in that run too, so the scan’s matrix gets an apply job of its own. Give the scan step an id, add a copy of the apply job that takes the scan’s matrix, and let settle wait for both:
scan: outputs: matrix: ${{ steps.scan.outputs.matrix }} # ... the steps as above, with `id: scan` on the Sluiceway step
# A copy of the apply job. Only these three keys differ: runs-on, # concurrency and the steps are the ones of apply. apply-merged: needs: scan if: ${{ !cancelled() && needs.scan.outputs.matrix != '' && needs.scan.outputs.matrix != '[]' }} strategy: fail-fast: false matrix: include: ${{ fromJson(needs.scan.outputs.matrix) }}
settle: needs: [scan, resolve, apply, apply-merged] if: always() && ((needs.resolve.outputs.matrix != '' && needs.resolve.outputs.matrix != '[]') || (needs.scan.outputs.matrix != '' && needs.scan.outputs.matrix != '[]'))The pull requests. The scan reads them with pull-requests: read, which the block above already gives.
A merge never skips a check: branch protection and required reviews apply to the merge as to any other, and the deploy after it goes through the fresh preview and the hash check like every tick. When the change moved between the scan after the merge and the deploy, nothing is deployed, the row shows the fresh diff and the ticker gets a comment.
3. Tell it about your stacks
Optional. Without sluiceway.yaml every stack that discovery finds gets a row and anyone with write access can tick. A stack’s id is its directory and its name, apps/web:prod, and that is what ignore matches. A typical file:
# Only maintainers may tick, unless a stack says otherwise.tickers: maintain
# A stack config file with no stack in the backend.ignore: - "apps/web:dev"
# Files no program reads: changing them previews nothing.scan: unrelated: - "**/*.md"
stacks: # The program in apps/web also reads packages/ui. - path: apps/web inputs: - packages/ui/**Every key, its default and its messages are in docs/configuration.md. The check of step 1 tells you whether the file is valid.
4. Load your credentials
The workflow puts everything the tool needs into the job environment, in steps before Sluiceway: the state backend, the cloud credentials, anything your programs read. Sluiceway passes that environment to the tool as it is and never loads a credential itself.
- Only
scanandapplyload credentials.resolveandsettlenever run the tool, so the job an issue edit starts holds no infrastructure secrets. scanneeds no more than read access. Keep the credentials that change things forapply.- The step that loads a secret has to mask it. Sluiceway never sees a secret as a secret.
- What your programs fetch, the runner has to be able to fetch: private packages, plugins, charts, images. Log in to those registries before Sluiceway too.
docs/credentials.md has recipes for GitHub secrets, a cloud with OIDC, a secret manager and private registries, and how Sluiceway fits next to the tooling you already have.
Start read only
You can run the scan alone first, to see your dashboard with nothing that can deploy. It is the workflow of step 2 with everything that can deploy taken out.
name: deploy-dashboard
on: push: branches: [main] schedule: - cron: "0 6 * * *" workflow_dispatch:
# This block is everything Sluiceway can do in your repo.permissions: contents: read issues: write deployments: write actions: read pull-requests: read checks: write
jobs: scan: runs-on: ubuntu-latest concurrency: sluiceway-scan steps: - uses: actions/checkout@v7 - uses: pulumi/actions@v7 # without a command this only installs the CLI with: pulumi-version: ^3.229.0 # Install what your programs need, once, for example: npm ci # Load your credentials and your state backend settings into the job # environment here. Credentials that can only read are enough. Whatever # loads a secret must also mask it. - uses: sluiceway/sluiceway@v0 with: mode: scanAnd tell Sluiceway that nothing acts on a box, in sluiceway.yaml at the repo root:
dashboard: readOnly: trueWhat this does and does not do:
- Nothing can be deployed. The workflow has no
resolveand noapplyjob, and it does not listen to issue edits. Withdashboard.readOnly: truethe dashboard shows that: pending rows have no box, there is no rescan box, and a line under the Pending heading says the dashboard is read only. Without it the rows get boxes that do nothing, and a tick sits there until the next scan clears it. A scan only ever asks the tool for a preview. The token can read the code, write issues, read and write deployment records, and write check runs, and nothing else. A scan reads the deployment records, which is where Sluiceway keeps who deployed what and when, and with noresolvejob there are none.actions: readlets it see whether a workflow run is over, and whether a run that an issue edit started is still on its way.pull-requests: readlets a row name the pull requests that made it pending.checks: writegives every pending stack its preview page. - The header image is served from an exact release tag or commit SHA. Never from one that can move, so that a picture never changes behind a dashboard that was already written. Started from
@v0or a branch, Sluiceway names the release tag of its own version, such asv0.1.1. Started from a copy inside your own repo (uses: ./), it names a commit that this repository does not have, and the picture is broken while the scan still works.dashboard.personality: falseinsluiceway.yamltakes the picture out. - A push gives a narrowed scan: only the stacks that claim a changed file are previewed, and every other row stays as it is. The schedule and “Run workflow” give a full scan. The first scan is always full.
To turn it into the whole workflow later, replace the file with the one of step 2, change actions: read to actions: write, and take readOnly: true out of sluiceway.yaml. The change to sluiceway.yaml makes the next push a full scan, and every pending row gets its box back.