CI
On this page
- uses: actions/checkout@v6
- uses: nao1215/setup-block@v0
with:
sync: "true"
- run: block exec forge test
setup-block installs the CLI,
verifies its checksum, exports $BLOCK_HOME, caches the store on your
block.lock and runs sync. Without the action, the same thing by hand:
- uses: actions/checkout@v6
- name: Install block
env:
BLOCK_VERSION: 0.1.0
run: |
curl -sSfL "https://github.com/nao1215/block/releases/download/v${BLOCK_VERSION}/block_${BLOCK_VERSION}_linux_amd64.tar.gz" | tar xz
sudo install -m 0755 block /usr/local/bin/block
- uses: actions/cache@v4
with:
path: ~/.local/share/block
key: block-${{ runner.os }}-${{ hashFiles('block.lock') }}
- run: block sync
- run: block exec forge test
There is no CI-only flag and no CI mode. block sync means the same thing on a
runner as on a laptop, and it fails rather than resolving anything when
block.lockis missing;block.tomlandblock.lockdisagree — a tool added or removed, a constraint changed, a project-local source changed;block.lockhas no artifact for the runner’s platform;- a downloaded artifact’s SHA-256 does not match.
Each of those is a message that names the command that fixes it. The cookbook has them side by side.
Lock for every platform you use #
block lock resolves artifacts for the platforms block.toml lists, and for
the machine it runs on when it lists none. A team on macOS whose CI runs Linux
declares both once:
platforms = ["darwin/arm64", "linux/amd64"]
Otherwise the runner stops with
block: BLK1005: block.lock is stale; run "block lock"
foundry: block.lock has no artifact for linux/amd64
deliberately, because installing something the lockfile does not name is the
one thing sync must never do.
A matrix needs every platform in that list, and nothing else:
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v6
- uses: nao1215/setup-block@v0
with:
sync: "true"
- run: block exec forge test
platforms = ["linux/amd64", "darwin/arm64"]
Whether a given tool actually has a build for each of them is the upstream’s
decision, recorded in the registry — the Platforms column in
Tools. A job on a platform an upstream does not ship for is a
lockfile problem you find at block lock time, on your own machine, rather
than at 3 a.m. on a runner.
Caching #
Key the cache on block.lock: that file names every artifact and every digest
the job will use, so a hit means the store already holds exactly what sync
wants and the job downloads nothing.
- uses: actions/cache@v4
with:
path: ~/.local/share/block
key: block-${{ runner.os }}-${{ hashFiles('block.lock') }}
A partially restored cache is not a hazard. The store is content-addressed and a cache hit is re-hashed before it is used, so a truncated archive is discarded and fetched again rather than installed.
On a runner whose cache action wants a path inside the workspace, move the store there:
env:
BLOCK_HOME: ${{ github.workspace }}/.block
Guarding the lockfile #
A pull request that edits block.toml without re-locking already fails on the
sync step. Giving that its own step says so earlier and more clearly:
- name: block.lock matches block.toml
run: block sync
To also learn when the pins have fallen behind upstream, without failing the build for it:
- name: Report toolchain updates
continue-on-error: true
run: block lock --check
block lock --check resolves and writes nothing. It exits 0 when the lockfile
is current, 2 when it would change, 1 on error. Keep it out of the required
checks: a release upstream is news, not a broken build.
Keeping up with upstream #
Moving a pin is a change to block.lock, so it belongs in a pull request, not
in a job. A weekly workflow can open that pull request for you:
name: Toolchain updates
on:
schedule: [{ cron: "0 6 * * 1" }]
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
bump:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: nao1215/setup-block@v0
- id: check
# `cmd || echo ...` treats every failure as staleness, including exit 1
# — a network error or a broken manifest — and would open a pull
# request from a resolution that never finished. Only 2 means "the
# lockfile would change"; 1 is an error and has to fail the job.
run: |
set +e
block lock --check
status=$?
set -e
case "$status" in
0) ;;
2) echo "stale=2" >> "$GITHUB_OUTPUT" ;;
*) exit "$status" ;;
esac
- if: steps.check.outputs.stale == '2'
run: block lock
- if: steps.check.outputs.stale == '2'
uses: peter-evans/create-pull-request@v7
with:
title: "chore: move the toolchain pins forward"
branch: block/toolchain-update
The diff is block.lock, and it is reviewable: exact versions in, exact
versions out, with a URL and a digest per platform.
Other CI systems #
The install is a tarball and the commands are the same everywhere.
GitLab CI:
variables:
BLOCK_HOME: "$CI_PROJECT_DIR/.block"
toolchain:
cache:
key:
files: [block.lock]
paths: [".block"]
before_script:
- curl -sSfL "https://github.com/nao1215/block/releases/download/v0.1.0/block_0.1.0_linux_amd64.tar.gz" | tar xz -C /usr/local/bin block
- block sync
script:
- block exec forge test
CircleCI:
steps:
- checkout
- restore_cache: { keys: ["block-{{ checksum \"block.lock\" }}"] }
- run: curl -sSfL "https://github.com/nao1215/block/releases/download/v0.1.0/block_0.1.0_linux_amd64.tar.gz" | sudo tar xz -C /usr/local/bin block
- run: block sync
- save_cache:
key: block-{{ checksum "block.lock" }}
paths: ["~/.local/share/block"]
- run: block exec forge test
Anything else — Jenkins, Buildkite, a self-hosted runner — is the same three
lines: fetch the binary, block sync, block exec.
Inside a container image #
Copying block.toml and block.lock before the rest of the source makes the
toolchain a cacheable layer that only changes when the lockfile does — the same
trick as go.mod/go.sum:
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sSfL https://github.com/nao1215/block/releases/download/v0.1.0/block_0.1.0_linux_amd64.tar.gz \
| tar xz -C /usr/local/bin block
WORKDIR /src
COPY block.toml block.lock ./
RUN block sync
COPY . .
RUN block exec forge build
Tokens #
GITHUB_TOKEN is only relevant to block lock and block lock --check, which
call the GitHub API to discover versions; an unauthenticated runner gets 60
calls an hour, which a re-lock can exhaust.
- run: block lock --check
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
block sync and block exec never call the API, so a job that only builds and
tests needs no token at all — and cannot be broken by a rate limit.
The one exception is a tool from a private repository: its release asset is
downloaded through the API, with the token, because the public download URL
answers a browser session alone. block sync needs GITHUB_TOKEN for such a
pin and says so (BLK3001) when it is missing. The token goes to the GitHub
host it was given for and to no other host — not to the CDN a download
redirects to, and not to a vendor’s download server.