Skip to main content
Version: Next

dispat in CI

dispat is a release tool. You will nearly always run it on a CI runner. Choose one of three ways to install it based on where your pipeline runs:

You are onUse
GitHub Actionsthe composite action
GitLab CI, Jenkins, Buildkite, anything with a job imagea container image
anything else, or a custom imagethe install script

All three methods install the same binary. The container images use the install script internally.

This page covers installing the binary on a runner. Read Pipeline patterns to learn what to run next. That guide covers everything from per-commit test windows to a fully gated release pipeline.

The GitHub Action

- uses: yohimik/dispat@v1
- run: dispat --log-format json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The action installs the CLI and puts it on PATH. It does not run dispat. You run it in your own step, like you would with setup-go and setup-node. This lets you pass your own flags, environment and working directory.

Inputs

InputDefaultMeaning
versionthe latest stable releaseVersion or tag to install: 1.2.3, v1.2.3 or services/dispat/v1.2.3.
bin-dir$HOME/.dispat/binWhere to install. The action adds it to PATH either way.
github-token${{ github.token }}Only raises the API rate limit; dispat's releases are public.

Pin a version to make a release reproducible a year from now:

- uses: yohimik/dispat@v1
with:
version: 1.2.3

Outputs

The version output contains the installed version. The path output contains the binary's full path. Use the path when a later step needs an explicit location instead of relying on PATH:

- id: setup
uses: yohimik/dispat@v1
- run: echo "installed ${{ steps.setup.outputs.version }}"

The action supports ubuntu-*, macos-* and windows-* runners.

A full release job

name: Release
on: workflow_dispatch

permissions:
contents: write # the release lock, tags, the release commit, releases

jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # dispat reads tags and commit ranges; a shallow clone is refused (E196)
- uses: yohimik/dispat@v1
- run: dispat --log-format json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Set up your stage scripts in steps before this one. If your scripts shell out to node, go, cargo, or docker, you must install those tools first. dispat itself needs only git and a POSIX shell.

Grant contents: write even for a job that pushes nothing. The run claims the repository with a release lock tag on the remote before it plans. This refuses a second job instead of racing it. You can safely trigger the job on every merge because the second run stops immediately.

Gating a pipeline on the plan

A repository with nothing pending releases nothing and exits 0. This keeps your pipeline green when a merge only contains a docs: commit. But a stage whose point is that something shipped needs the opposite. A deploy, an announcement, or a downstream trigger will deploy nothing if it passes quietly on an empty plan.

Pass --require-release to gate these steps. The release and status commands exit 3 when the plan releases nothing. This gives you a dedicated code separate from exit 1 failures:

dispat status --require-release # 0 releasing, 3 nothing to release, 1 something is wrong

Only packages the run will actually publish count. A package held by Release-As: none gets no version this run. A package withheld until its providers release or excluded by your --package/--space/--group selection also counts as "not releasing".

The simplest pipeline uses one job that decides and one that acts:

jobs:
plan:
runs-on: ubuntu-latest
outputs:
releasing: ${{ steps.plan.outputs.releasing }}
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: yohimik/dispat@v1
- id: plan
run: |
rc=0
dispat status --require-release --log-format json || rc=$?
case "$rc" in
0) echo "releasing=true" >> "$GITHUB_OUTPUT" ;;
3) echo "releasing=false" >> "$GITHUB_OUTPUT" ;;
*) exit "$rc" ;;
esac

release:
needs: plan
if: needs.plan.outputs.releasing == 'true'
runs-on: ubuntu-latest
# ... the release job above

Run status as often as you like. It takes no lock and writes nothing. On release, dispat answers the flag before it takes the release lock and before beforeAll runs. This means dispat --require-release in a single-job pipeline refuses the run without claiming the repository or executing your scripts.

Capture and map the exit code in your shell script. Running it bare with set -e would end the job on 3 before you can read the answer. Use a case statement rather than an if statement. An if statement folds every nonzero code together, so a broken configuration would look like "nothing to release" and skip the job instead of failing. Exit 3 means nothing released, while everything else is a real failure. Use the bare form only when a step should fail the pipeline when nothing releases.

Gating a step on what changed

Use dispat if --changed to gate one step inside a script. This uses the same selection dispat run covers packages by, while --require-release gates a whole job. You can build the docs in a PR pipeline only when the docs changed:

dispat if --changed -p docs --consumers --since origin/main --then 'dispat run build-docs'

The condition holds when changes reach the selection. It runs the --then script and passes the exit code through. It runs nothing and exits 0 when nothing relevant changed, keeping the step green. You need fetch-depth: 0 to count changes against a branch base.

File tests gate on artifacts the same way, with no repository involved. You can require a test report before deploying the docs:

dispat if -f packages/docs/data/report.json \
--then 'dispat run deploy-docs' \
--else 'echo "no test report; run the test job first" >&2; exit 1'

The container images

Choose from four images on linux/amd64 and linux/arm64:

ImageBaseSize
yohimik/dispat-alpinealpine15 MB
yohimik/dispat-ubuntuubuntu59 MB
yohimik/dispat-debiandebian69 MB
yohimik/dispat-dinddocker:dind142 MB

Each image carries the CLI, git, CA certificates, an SSH client and tzdata. Add your pipeline's toolchain yourself. Every extra package in a base image is one more thing to patch.

# GitLab CI
release:
image: yohimik/dispat-alpine:1
script:
- dispat --log-format json
# Anywhere with a Docker daemon
docker run --rm -v "$PWD:/workspace" yohimik/dispat-alpine:1 dispat status

Tags

TagExampleMoves
exact version1.4.2never
channelstable, rcevery release on that channel
major, major.minor1, 1.4stable releases only
lateststable releases only

A release publishes every tag from one build. The tags 1, 1.4 and latest share the exact digest of the version they follow.

Pin 1 in a pipeline. The latest tag never points at a prerelease. Use the channel tags for prereleases.

Two things worth knowing

The repository you mount is owned by another uid. Every git call would fail with "dubious ownership" without configuration. The images set git config --system --add safe.directory '*' to make a bind-mounted checkout work.

Only dispat-dind declares an ENTRYPOINT. It inherits this from docker:dind to keep behaving like that image. You still need --privileged or a services: docker:dind companion. The other three images declare no entrypoint so GitLab CI and GitHub Actions can run scripts through the image's shell.

dispat self-update is not the way to upgrade one. That command replaces a binary that dies with the container. Change the image tag instead. The images set DISPAT_UPDATE_CHECK=0 to disable update warnings.

The install script

The container images use this install script. It is the same script Getting started hands to a human:

With curl
curl -fsSL https://raw.githubusercontent.com/yohimik/dispat/main/install.sh | sh
With wget
wget -qO- https://raw.githubusercontent.com/yohimik/dispat/main/install.sh | sh

The script resolves the latest stable release or takes a --version flag. It downloads the binary for the current platform and verifies it against the GitHub checksum. Then it installs the binary into --bin-dir. The script requires only curl or wget, with no package manager or Go toolchain needed.

OptionDefaultMeaning
--versionlatest stable1.2.3, v1.2.3 or services/dispat/v1.2.3.
--bin-dir/usr/local/bin, else $HOME/.local/binWhere to install.
--os, --archthis machine'sOverride the platform, for building an image for another one.
--token$GITHUB_TOKENRaises the API rate limit.

The script prints the resolved version to stdout and everything else to stderr. Capture the version in your own script:

VERSION=$(curl -fsSL https://raw.githubusercontent.com/yohimik/dispat/main/install.sh | sh)

Pin the URL to a tag rather than main to make the installer reproducible. Use a URL like https://raw.githubusercontent.com/yohimik/dispat/services/dispat/v1.2.3/install.sh.

In your own image

FROM ubuntu:24.04
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
ADD https://raw.githubusercontent.com/yohimik/dispat/main/install.sh /tmp/install.sh
RUN sh /tmp/install.sh --version 1.2.3 && rm /tmp/install.sh

The dispat images run this script in a throwaway first stage and copy the binary out. This keeps the downloader out of the published image. Read docker/README.md to see how this works.

Somewhere other than GitHub Actions

Read The release job on other providers for full release jobs on GitLab CI, CircleCI, Jenkins, Buildkite and Azure Pipelines. That guide includes the clone settings and tokens each provider needs.