Example workflows
These are complete workflows for common setups, ready to copy.
Most of a Sluiceway workflow is not about Sluiceway: checking out, installing a language and your dependencies, installing the tool, loading credentials. Each one is the workflow with those steps filled in, and each one is checked in this repo’s tests against the action’s inputs and the wiring Sluiceway needs.
| Setup | File | Credentials from |
|---|---|---|
Pulumi programs in TypeScript in one repo, with one package.json and lockfile at the root |
node-monorepo.yml | GitHub secrets |
| An env file of secret references, loaded with one call per run | secret-manager.yml and export-env.sh | A secret manager (1Password in the example) |
| A cloud account and a state bucket in that cloud | cloud-oidc.yml | OIDC, no stored cloud key (AWS in the example) |
Copy the file to .github/workflows/deploy-dashboard.yml on your default branch. The name of the file is yours to choose. Keep it different from sluiceway.yaml, which is Sluiceway’s own settings file at the repo root, so the two are never mixed up. Each one is one job with one Sluiceway step and no if:: the step reads the event of the run and scans, deploys a tick, or ends with a notice. For credentials that only read in scans, or an environment per stack with reviewers, start from the split workflow instead.
What to change
- The version of the action. The examples use
sluiceway/sluiceway@v0, which follows every release from 0.1.0 until 1.0.0. To review every update yourself, pin a full commit SHA instead, as Pin a commit says. - The branch. The examples scan after a push to
main. Use your default branch. - The secret and variable names.
PULUMI_ACCESS_TOKEN,OP_SERVICE_ACCOUNT_TOKEN,AWS_DEPLOY_ROLEand the others are names the examples made up. Create them under the repo’s settings, or rename them in the file. - The environment. The secret manager and the cloud examples name an environment,
sluiceway, so that the credentials can be secrets of a GitHub Environment that only the default branch may use (with GitHub Environments). Where your plan has no environments, remove theenvironment:block and keep those credentials as repository secrets. timeout-minutes. One run may hold a scan and the deploys it starts. A deploy has no time limit of Sluiceway’s unless thedeploy-timeoutinput gives it one. Set one that fits your slowest scan and deploys.- The runner. Change
runs-onfor a self-hosted runner. It needs runner version 2.328.0 or newer.
The monorepo
The dependencies are installed once, with one npm ci at the root, before the scan previews every stack. The plugin cache keeps the providers between runs: the first run fills it, which takes a while after a large first scan, and later runs reuse it. Your programs may need more than npm: a build step, a code generator, another language. Put it before the Sluiceway step, once.
The credentials sit on the Sluiceway step only, so the install scripts of your dependencies never see them.
A push previews only the stacks that claim a changed file, and a stack claims the files in its own directory. Two things in a monorepo lie outside every stack:
-
A shared package, such as
packages/ui, that several programs import. A push that changes only the package gives a full scan, because no stack claims it. List it under theinputsof the stacks that use it, and such a push previews only those:stacks:- path: apps/webinputs:- packages/ui/**- config/web.json -
The root lockfile and
package.json. No stack claims them, so every push that changes a dependency previews every stack. That is on purpose: a new version of a package can change any program, and Sluiceway cannot tell which. Keep them offscan.unrelated. The check, and the summary of a push that fell back to a full scan, list them among the files that no stack claims and say to keep them off that list.
The secret manager
The job resolves one env file of references with one op run, and export-env.sh masks the secrets and writes every value to the job environment. credentials.md explains the script and what it does not do. The service account’s token is a secret of the environment, and the account sees only the vault with the credentials the stacks need.
Every step after the loading step sees the credentials, because they are in the job environment from then on. Put every install step before it: npm ci, a build, anything that runs the install scripts of your dependencies. When you combine this example with the monorepo one, the order is checkout, the installs, the tool, then the loading step, then Sluiceway. The example has no install step, so it loads right before Sluiceway.
The cloud
The job assumes a role that can change things. Its trust policy names the environment, so no other branch can assume it. The state lives in a bucket (PULUMI_BACKEND_URL), and stack secrets use a passphrase from a repository secret. The workflow’s block adds id-token: write, which the role needs.
Before you merge one
Run the check in a pull request first. It tells you whether Sluiceway finds your stacks and understands sluiceway.yaml, with no credentials and no tool. What it cannot tell you is whether a preview works: whether the runner can fetch what your programs fetch (credentials), and whether every stack exists in the backend. The first scan shows that, one row per stack.
The files
cloud-oidc.yml
# Cloud credentials through OpenID Connect, so no cloud key is stored anywhere,# and the Pulumi state in a bucket of the same cloud. The example uses AWS;# docs/credentials.md says what changes for other clouds. Copy it to# .github/workflows/deploy-dashboard.yml on the default branch.## `@v0` follows every release until 1.0.0. To review every update yourself,# pin a full commit SHA instead, as "Pin a commit" in docs/workflow.md says.name: deploy-dashboard
on: push: branches: [main] schedule: - cron: "0 6 * * *" workflow_dispatch: issues: types: [edited]
permissions: contents: read issues: write deployments: write actions: write pull-requests: read checks: write id-token: write
jobs: sluiceway: runs-on: ubuntu-latest timeout-minutes: 60 # One run at a time, and none is dropped. An edit of any other issue gets # a group of its own, so it never waits for a scan or a deploy. concurrency: group: sluiceway-${{ github.event.issue.number }} queue: max # An environment that only the default branch may use. The role's trust # policy names it (the `environment:<name>` subject), so no other branch # can assume the role. environment: name: sluiceway deployment: false steps: - uses: actions/checkout@v7 - uses: pulumi/actions@v7 with: pulumi-version: ^3.229.0 # A role that previews and deploys, so it can change things. - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: ${{ vars.AWS_DEPLOY_ROLE }} aws-region: ${{ vars.AWS_REGION }} # Runs after a step above failed too, and then only says so on the # dashboard, so it never looks fresh while no scan could run. - uses: sluiceway/sluiceway@v0 if: ${{ !cancelled() }} env: PULUMI_BACKEND_URL: ${{ vars.PULUMI_BACKEND_URL }} PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}export-env.sh
#!/usr/bin/env bash# Loads an env file of secret references into the job environment, masked.# Run it inside your secret manager's `run` command, which resolves every# reference of the file into this process's environment, for example:## op run --env-file=ci.env --no-masking -- bash export-env.sh ci.env## It prints nothing but ::add-mask:: commands. Never add `set -x` or an echo.set -euo pipefailfile="$1"# A line whose value holds this is a secret. Every other line is a plain value.reference="${SECRET_REFERENCE:-op://}"
while IFS= read -r raw || [ -n "$raw" ]; do raw="${raw%$'\r'}" # Take NAME from lines like `NAME=...`, `NAME = ...` or `export NAME=...`. [[ "$raw" =~ ^[[:space:]]*(export[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*= ]] || continue name="${BASH_REMATCH[2]}" # The secret manager's own token and settings stay on this step, and the # runner does not let a step set its own names. case "$name" in OP_* | GITHUB_* | RUNNER_*) continue ;; esac value="${!name-}" [ -n "$value" ] || continue
# Mask first, every line on its own, because the log is matched line by line. if [[ "$raw" == *"$reference"* ]]; then while IFS= read -r line || [ -n "$line" ]; do line="${line%$'\r'}" [ -n "$line" ] || continue printf '::add-mask::%s\n' "${line//%/%25}" done <<<"$value" fi
# Then write, in the delimiter form so that newlines survive. delimiter="ghadelimiter_$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n')" if [[ "$value" == *"$delimiter"* ]]; then echo "The value of $name holds the delimiter. Run the step again." >&2 exit 1 fi printf '%s<<%s\n%s\n%s\n' "$name" "$delimiter" "$value" "$delimiter" >>"$GITHUB_ENV"done <"$file"node-monorepo.yml
# Pulumi programs in TypeScript, in one repo with a shared package.json, and# credentials stored as GitHub secrets. Copy it to# .github/workflows/deploy-dashboard.yml on the default branch, and read# docs/example-workflows.md for what to change.## `@v0` follows every release until 1.0.0. To review every update yourself,# pin a full commit SHA instead, as "Pin a commit" in docs/workflow.md says.name: deploy-dashboard
on: push: branches: [main] schedule: - cron: "0 6 * * *" workflow_dispatch: issues: types: [edited]
permissions: contents: read issues: write deployments: write actions: write pull-requests: read checks: write
jobs: sluiceway: runs-on: ubuntu-latest timeout-minutes: 60 # One run at a time, and none is dropped. An edit of any other issue gets # a group of its own, so it never waits for a scan or a deploy. concurrency: group: sluiceway-${{ github.event.issue.number }} queue: max steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version-file: .nvmrc cache: npm # Once for every program in the repo, not once per stack. - run: npm ci - uses: pulumi/actions@v7 with: pulumi-version: ^3.229.0 # The providers the programs use. The first run fills the cache. - uses: actions/cache@v6 with: path: ~/.pulumi/plugins key: pulumi-plugins-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }} # The credentials sit on this step only, so the install scripts of npm ci # never see them. They preview and deploy, so they must be able to change # things. # Runs after a step above failed too, and then only says so on the # dashboard, so it never looks fresh while no scan could run. - uses: sluiceway/sluiceway@v0 if: ${{ !cancelled() }} env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}secret-manager.yml
# Credentials and backend settings kept in a secret manager as an env file of# secret references, loaded once per run with one bulk call. The example uses# 1Password; docs/credentials.md has the same pattern for other managers.# Copy it to .github/workflows/deploy-dashboard.yml on the default branch, and# copy export-env.sh to .github/scripts/export-env.sh. The script is for a# file of references only: a file a step resolved, or one of plain values,# is loaded by the env-file input of the Sluiceway step, which masks every# value itself (docs/credentials.md, "An env file").## `@v0` follows every release until 1.0.0. To review every update yourself,# pin a full commit SHA instead, as "Pin a commit" in docs/workflow.md says.name: deploy-dashboard
on: push: branches: [main] schedule: - cron: "0 6 * * *" workflow_dispatch: issues: types: [edited]
permissions: contents: read issues: write deployments: write actions: write pull-requests: read checks: write
jobs: sluiceway: runs-on: ubuntu-latest timeout-minutes: 60 # One run at a time, and none is dropped. An edit of any other issue gets # a group of its own, so it never waits for a scan or a deploy. concurrency: group: sluiceway-${{ github.event.issue.number }} queue: max # An environment that only the default branch may use, which holds the # token of the secret manager. environment: name: sluiceway deployment: false steps: - uses: actions/checkout@v7 - uses: pulumi/actions@v7 with: pulumi-version: ^3.229.0 # Leave this out when op is part of your runner image. - uses: 1password/install-cli-action@v4 # One `op run` resolves the whole file. The service account can see only # the vault with the credentials Sluiceway previews and deploys with. # Every later step sees them, so put every install step (npm ci, a # build) before this one, and keep it right before Sluiceway. - name: Load the environment env: OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} run: op run --env-file=ci/deploy.env --no-masking -- bash .github/scripts/export-env.sh ci/deploy.env # Runs after a step above failed too, and then only says so on the # dashboard, so it never looks fresh while no scan could run. - uses: sluiceway/sluiceway@v0 if: ${{ !cancelled() }}