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.
What is offline, and when
Section titled “What is offline, and when”| 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.
The two caches
Section titled “The two caches”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.
Key the cache on the Reed revision
Section titled “Key the cache on the Reed revision”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.
A GitHub Actions setup
Section titled “A GitHub Actions setup”The cache and prefetch steps look like this. steps.reed.outputs.rev is the commit of the
Reed source you built the CLI from:
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 distBoth 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.
Do not pipe a Reed command
Section titled “Do not pipe a Reed command”reed build --out dist | tee build.log # wrongThe 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.
Prefetch is per platform
Section titled “Prefetch is per platform”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.
Prove it, don’t assume it
Section titled “Prove it, don’t assume it”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:
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.