Writing workflows
Spindle workflows allow you to write CI/CD pipelines in a simple format.
They’re located in the .tangled/workflows directory at the root of
your repository, and are defined using YAML. This page shows each part
with a small example. The workflow reference
lists every field.
Before a workflow can run, the repository needs a spindle. Open the repository’s Settings → Pipelines and, under Spindle, run CI with Tangled or select your own spindle.
Your first workflow
Section titled “Your first workflow”Create .tangled/workflows/check.yml:
when: - event: push branch: main
engine: microvmimage: nixos
dependencies: - go
steps: - name: Test command: go test ./...Commit and push it:
git add .tangled/workflows/check.ymlgit commit -m "Add CI workflow"git pushThe push starts the first run. Open the repository’s Pipelines tab to watch it.
If a workflow file has a problem, git push prints it. Add
-o verbose-ci to see the report when there is no problem.
A workflow has four main parts:
| Part | Says |
|---|---|
when | When the workflow runs. |
engine | Where the workflow runs. |
dependencies | What tools it needs. |
steps | What commands to run. |
When it runs
Section titled “When it runs”The when field is a list of conditions. The workflow runs if any
condition matches.
Run on every push to main:
when: - event: push branch: mainRun on pull requests that target main:
when: - event: pull_request branch: mainRun when tags matching v* are pushed:
when: - event: push tag: "v*"Run only when files in docs/ change:
when: - event: push branch: main paths: ["docs/**"]Run every night, at a stable minute in the 02:00 hour:
when: - event: schedule schedule: - cron: "H 2 * * *"Let people start it by hand:
when: - event: manualCombine them. This runs on pushes to main or develop, on pull
requests that target main, and by hand:
when: - event: ["push", "manual"] branch: ["main", "develop"] - event: ["pull_request"] branch: ["main"]To skip CI for a push, pass a Git push option:
git push -o skip-ciWhere it runs
Section titled “Where it runs”The engine field picks the engine:
engine: microvmimage: nixosengine: nixerymicrovmruns the whole workflow inside its own microVM. Files, packages and services stay from one step to the next.nixeryruns each step in a fresh container. Only the workspace stays between steps.
Use microvm if you are not sure. With microvm, image picks the
image to boot: nixos, or a non-NixOS image such as alpine.
On microvm with a NixOS image, dependencies is a list of nixpkgs
packages. You can search for packages on https://search.nixos.org.
dependencies: - nodejs - pnpmTake a package from any flake with flakeref#attr:
dependencies: - github:nixos/nixpkgs#helloOn nixery, dependencies is a map from registry to packages:
dependencies: nixpkgs: - nodejs - goOn a non-NixOS image, install tools in a step:
steps: - name: "Install go" command: apk add goEach step has a name and a command. The step runs in a Bash shell.
Its logs show on the pipelines page.
steps: - name: "Build" command: go build ./... - name: "Test" command: go test ./...Use | for a command with more than one line:
steps: - name: "Lint and test" command: | pnpm run lint pnpm testEnvironment variables
Section titled “Environment variables”Set variables for every step with environment. Set variables for one
step with that step’s environment:
environment: NODE_ENV: "production"
steps: - name: "Build backend" command: "go build" environment: GOOS: "darwin" GOARCH: "arm64"Do not put secrets here, these environment variables are visible to anyone viewing the repository. Add secrets in the repository’s Settings → Pipelines → Secrets. They reach every step as environment variables. See Secrets.
The spindle also sets CI=true and TANGLED_* variables that describe
the run:
echo "$TANGLED_REF_NAME" # mainecho "$TANGLED_SHA" # the commit that triggered the pipelineCaching
Section titled “Caching”Keep directories between runs with cache:
cache: - key: go-mod hash: [go.sum] paths: [.gocache]hash folds the content of go.sum into the key, so editing go.sum
invalidates the cache. Caches are only used for trusted runs, and only when
the spindle operator configured cache storage.
Cloning
Section titled “Cloning”By default the spindle fetches only the commit that triggered the
workflow. Fetch more history when a tool needs it, such as
git describe:
clone: depth: 50Skip the clone when the workflow needs nothing from the repository:
clone: skip: trueRecipes
Section titled “Recipes”Lint, test and build a Node project
Section titled “Lint, test and build a Node project”when: - event: ["push", "pull_request"] branch: ["main"]
engine: microvmimage: nixos
dependencies: - pnpm
steps: - name: "Install dependencies" command: pnpm install --frozen-lockfile - name: "Lint and test" command: | pnpm run lint pnpm test - name: "Build" command: pnpm run buildCheck formatting
Section titled “Check formatting”when: - event: ["push", "pull_request"] branch: ["main"]
engine: microvmimage: alpine # slimmer image for checking the formatting
steps: - name: "Install go" command: apk add go - name: "Check formatting" command: test -z $(gofmt -l .)Build a Rust project that links OpenSSL
Section titled “Build a Rust project that links OpenSSL”when: - event: ["push", "pull_request"] branch: ["main"]
engine: microvmimage: nixos
dependencies: - gcc - cargo - rustc - clippy - rustfmt - pkg-config # exports PKG_CONFIG_PATH for the libraries below - openssl # the C library + headers openssl-sys links against
steps: - name: "Check formatting" command: cargo fmt --check - name: "Clippy" command: cargo clippy --all-targets -- -D warnings - name: "Test" command: cargo test --all - name: "Release build" command: cargo build --releaseRun migrations and integration tests against PostgreSQL
Section titled “Run migrations and integration tests against PostgreSQL”when: - event: ["push", "pull_request"] branch: ["main"]
engine: microvmimage: nixos
environment: DATABASE_URL: "postgresql:///spindle-workflow?host=/run/postgresql"
dependencies: - gcc - cargo - rustc - pkg-config - openssl - sqlx-cli
services: postgresql: enable: true # has to be same name as the user for peer auth to work automatically ensureDatabases: ["spindle-workflow"] ensureUsers: - name: spindle-workflow ensureDBOwnership: true
steps: - name: "Run migrations" command: sqlx migrate run - name: "Integration tests" command: cargo test --allBuild and push a Docker image on tag
Section titled “Build and push a Docker image on tag”REGISTRY_USER and REGISTRY_TOKEN are repository secrets.
when: - event: ["push"] tag: ["v*"]
engine: microvmimage: nixos
virtualisation: docker: true
steps: - name: "Build and push to ghcr.io" command: | set -euo pipefail
echo "$REGISTRY_TOKEN" | docker login ghcr.io -u "$REGISTRY_USER" --password-stdin image="ghcr.io/$REGISTRY_USER/myapp:$TANGLED_REF_NAME"
docker build -t "$image" -t "ghcr.io/$REGISTRY_USER/myapp:latest" . docker push "$image" docker push "ghcr.io/$REGISTRY_USER/myapp:latest"Deploy to Cloudflare Workers on tag
Section titled “Deploy to Cloudflare Workers on tag”# .tangled/workflows/deploy.ymlwhen: - event: ["push"] tag: ["v*"]
engine: microvmimage: nixos
dependencies: - pnpm
steps: - name: "Install dependencies" command: pnpm install --frozen-lockfile - name: "Deploy worker" # `wrangler` picks up `CLOUDFLARE_API_TOKEN` from the env. # set it under **Settings → Pipelines → Secrets**. command: pnpm exec wrangler deployAuthenticate to a cloud provider without a stored key
Section titled “Authenticate to a cloud provider without a stored key”If the spindle operator enabled OIDC, set audience and exchange
TANGLED_ID_TOKEN for short-lived cloud credentials. Configure the
provider to trust the spindle’s issuer and to check the token’s
repository and ref claims.
when: - event: push branch: main
engine: microvmimage: nixosaudience: sts.amazonaws.com
dependencies: - awscli2
steps: - name: "Deploy" command: | echo "$TANGLED_ID_TOKEN" > /tmp/token export AWS_ROLE_ARN=arn:aws:iam::123456789012:role/deploy export AWS_WEB_IDENTITY_TOKEN_FILE=/tmp/token aws s3 sync dist/ s3://my-bucket/