Skip to content

Build in GitLab CI

The Reed pipeline as .gitlab-ci.yml, with both caches, dist/ as an artifact, and a main-only deploy job.

This page is the GitLab version of Build in GitHub Actions: check, prefetch on a cache miss, build, keep dist/ as an artifact, and deploy from the default branch only.

It was written from GitLab’s CI/CD YAML reference and Reed’s cache layout, not from a pipeline that has run on GitLab. Read the checklist at the end before you trust the first green run.

Reed is not published to npm yet. The job clones the Reed source, builds it, and runs node .ci-reed/packages/cli/dist/bin.js. Two inputs control what it builds:

Name Where Value
REED_REPO_URL CI/CD variable, in project settings Clone URL of the Reed source. Mark it masked if it carries a token.
.reed-ref a file committed to your repository One line: the Reed commit SHA to build.

The pin lives in a file rather than a variable for a reason. GitLab’s cache:key:files hashes file contents into the cache key, so changing the pin automatically starts a fresh cache. The Reed revision is what decides what reed prefetch stores. Your content does not.

Save this as .gitlab-ci.yml:

yaml
stages:
- build
- deploy
variables:
# Both caches that `reed prefetch` fills. GitLab only caches paths inside the project
# directory, so both live under it. REED_COREPACK_HOME points Reed's Corepack calls at
# the same directory as COREPACK_HOME.
REED_STORE_DIR: "$CI_PROJECT_DIR/.ci-cache/reed-pnpm-store"
COREPACK_HOME: "$CI_PROJECT_DIR/.ci-cache/corepack"
REED_COREPACK_HOME: "$CI_PROJECT_DIR/.ci-cache/corepack"
build-docs:
stage: build
# Node 22, not "latest": prefetch and build shell out to Corepack, and Node 25 and
# newer no longer bundle it.
image: node:22-bookworm
cache:
key:
files:
- .reed-ref
prefix: reed-node22-bookworm
paths:
- .ci-cache/reed-pnpm-store
- .ci-cache/corepack
script:
- |
set -eu
if [ -z "${REED_REPO_URL:-}" ]; then
echo "error: set the CI/CD variable REED_REPO_URL to the Reed source repository." >&2
exit 1
fi
# Build the Reed CLI from source. A throwaway COREPACK_HOME keeps this from
# warming the cache the job is about to depend on.
git clone --quiet "$REED_REPO_URL" .ci-reed
git -C .ci-reed checkout --quiet "$(cat .reed-ref)"
(
export COREPACK_HOME="$(mktemp -d)"
cd .ci-reed
corepack enable pnpm
pnpm install --frozen-lockfile
pnpm run build
)
REED_BIN="$CI_PROJECT_DIR/.ci-reed/packages/cli/dist/bin.js"
test -f "$REED_BIN"
# Offline, no prerequisites.
node "$REED_BIN" check
# GitLab has no cache-hit flag. A missing cache directory is the miss.
if [ -d "$REED_STORE_DIR" ] && [ -d "$COREPACK_HOME" ]; then
echo "Reed caches restored; skipping prefetch."
else
node "$REED_BIN" prefetch
fi
node "$REED_BIN" build --out dist
artifacts:
paths:
- dist/
expire_in: 1 week
deploy-docs:
stage: deploy
image: node:22-bookworm
needs:
- job: build-docs
artifacts: true
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual # REPLACE ME: switch to `when: on_success` once the script below uploads dist/
script:
- |
# REPLACE ME with your host's upload command for dist/. The job stays manual
# until you do, so a merge never looks deployed when nothing was uploaded.
echo "no deploy command configured yet; upload dist/ to your host here"

Both caches in one cache: block. reed prefetch fills the dependency store at REED_STORE_DIR and Corepack’s cache of the pinned package-manager binary at COREPACK_HOME. Cache only one and the build still passes on a runner with network access. It fetches the other directory silently, every run.

Paths inside the project. GitLab resolves cache:paths and artifacts:paths relative to $CI_PROJECT_DIR and does not reach outside it. Reed’s default store location, ~/.reed/pnpm-store, would never be cached. That is why the variables move both directories under .ci-cache/.

Prefetch only on a miss. When the key changes, nothing is restored, the directories do not exist, and the job runs reed prefetch. GitLab saves the cache only when the job succeeds, so a failed prefetch is never stored as a warm cache.

A stale cache fails loudly. If a restored store was prefetched for a different Reed pin or platform, reed build stops with a diagnostic telling you to run reed prefetch. It does not fall back to the network.

No pipes. Each Reed command runs on its own line. Piping one into tee reports the exit status of tee, and a failed build passes. Redirect with > if you need a log file.

Artifacts, not a rebuild. deploy-docs uses needs with artifacts: true, so it receives the exact dist/ that build-docs produced and checked.

The rules: entry adds deploy-docs to a pipeline only when the commit is on the project’s default branch. Every other branch still runs build-docs, so each push is checked and built, and nothing but the default branch reaches your host.

What replaces the placeholder depends on your host. Reed has no deploy command. Keep the host’s token in a masked CI/CD variable that only this job uses. If docs.json sets base, stage dist/ under a matching directory before uploading, and keep _headers at the root of the served tree. See Deploy and Response headers.

To fail on warnings too, change the check line:

sh
node "$REED_BIN" check --strict-components --strict-tokens

--strict-components escalates DS-D* design warnings. --strict-tokens escalates every other warning. See Diagnostics.

This pipeline has not been run on GitLab by the people who wrote it. Verify it once:

  1. Prefetch wrote inside the project. The first run logs [reed] prefetch complete → store: followed by a path. It must end in .ci-cache/reed-pnpm-store. A path under ~/.reed means the variables did not reach the job, and nothing will be cached.
  2. Both paths were uploaded. The end of the job log shows the cache being created for both .ci-cache paths. A warning that a path matched no files means that cache is empty.
  3. The second run skips prefetch. It logs Reed caches restored; skipping prefetch. and still builds. If it fails with a Reed diagnostic naming reed prefetch, the restored cache is stale or came from a different platform.
  4. The artifact is the site. Browse the build-docs job artifacts and confirm dist/index.html and dist/llms.txt are there.
  5. Deploy is gated. A pipeline on a feature branch shows no deploy-docs job.

Two cache behaviors will show up as extra prefetches. A cache is stored where the runner is installed unless distributed cache is enabled, so a job on a different runner sees a miss. And by default protected and unprotected branches do not share caches, so your default branch and your feature branches each warm their own. Both cost time, not correctness.

Prefetch covers exactly the platform, architecture and libc it ran on. The cache prefix names the image for that reason. If you add a job on an Alpine image, an ARM runner or a macOS runner, give it a different prefix so the two never share a store.