Skip to content

Offline builds in CI

Prefetch once, then build with no network — and the cache mistake that quietly undoes it.

Reed’s build is offline by design, with exactly one exception. Getting that guarantee in CI takes one setup step and one correctly configured cache.

Command Network
reed check Never. No prerequisites at all.
reed build None, given a prior successful reed prefetch on the same machine.
reed dev None, same condition.
reed prefetch The only command that touches the network.

reed check is unconditionally offline: it is a read-only validation pass over docs.json and your content, and it never invokes the dependency-install step at all. It is safe in any sandbox, with no prior setup, which makes it the right check to run on every pull request.

reed prefetch populates two separate caches, and CI has to restore both:

Directory Holds
REED_STORE_DIR (default ~/.reed/pnpm-store) the pinned dependency store
COREPACK_HOME / REED_COREPACK_HOME Corepack’s cache of the pinned package-manager binary

Restoring only one leaves a hidden network call on a cold runner. Restore only the store and the package-manager binary is fetched; restore only the Corepack cache and the dependency store is cold. Either way the build still succeeds — on a networked runner — so the leak goes unnoticed until it runs somewhere without egress.

What prefetch stores depends on Reed’s pinned dependencies, its pinned pnpm version and the runner’s platform, architecture and libc. It does not depend on your content: prefetch does not even accept --dir. So key the cache on the Reed revision you build with, not on docs.json. A content key throws away a valid cache on every config edit, and it keeps a stale one when you move Reed to a new revision.

On an exact cache hit, skip prefetch. A restored store that no longer matches is not a silent problem: reed build stops with a diagnostic telling you to run reed prefetch.

The cache and prefetch steps look like this. steps.reed.outputs.rev is the commit of the Reed source you built the CLI from:

yaml
env:
REED_STORE_DIR: ${{ github.workspace }}/.ci-cache/reed-pnpm-store
COREPACK_HOME: ${{ github.workspace }}/.ci-cache/corepack
REED_COREPACK_HOME: ${{ github.workspace }}/.ci-cache/corepack
steps:
# ... check out your docs, set up Node 22, build the Reed CLI (id: reed) ...
- name: Restore both Reed caches
id: reed-cache
uses: actions/cache@v4
with:
path: |
${{ env.REED_STORE_DIR }}
${{ env.COREPACK_HOME }}
key: reed-${{ runner.os }}-${{ runner.arch }}-${{ steps.reed.outputs.rev }}
restore-keys: reed-${{ runner.os }}-${{ runner.arch }}-
- run: reed check
- name: reed prefetch (cache miss only)
if: steps.reed-cache.outputs.cache-hit != 'true'
run: reed prefetch
- run: reed build --out dist

Both directories go in one path list, under one key. Split them across two cache steps and one can hit while the other misses, which is exactly the hidden network call above.

For complete pipelines, including building the Reed CLI and deploying dist/, see Build in GitHub Actions and Build in GitLab CI.

Node 22 rather than latest is deliberate: prefetch and build shell out to Corepack, and Node 25 and newer no longer bundle it. On a runner without Corepack, reed build fails fast with a Reed diagnostic saying so, rather than silently reaching the network.

sh
reed build --out dist | tee build.log # wrong

The exit status of a pipeline is the status of its last command, so a failing build piped into tee reports success. Redirect instead, or set set -o pipefail.

The offline guarantee covers exactly the platform, architecture and libc that prefetch ran on — and Linux distinguishes glibc from musl, so a standard CI image and an Alpine runner are two different targets. Run prefetch on each combination your pipeline actually uses.

An offline build is a claim until you measure it. Running the build inside a network namespace with no outbound access turns the claim into a test:

sh
unshare --user --map-root-user --net -- \
bash -c 'ip link set lo up 2>/dev/null || true; exec reed build --out dist'

If that passes, the build genuinely does not need the network. If it fails, something reached out — better to learn it here than in an air-gapped environment.