Add buildkite ci pipelines

The pipelines are supposed to be a drop-in replacement for the current
github actions pipelines.

The github workflows have been restructured into 4 pipelines to take
advantage of the buildkite caching mechanism.
This commit is contained in:
Daniel Norman
2026-08-12 10:29:58 +02:00
committed by Rūdolfs Ošiņš
parent e20a1493cc
commit ab514fe0d4
6 changed files with 350 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
# Buildkite CI
The Garden Buildkite integration stores a bare `buildkite-agent pipeline
upload`, so the agent finds `pipeline.yml` by its default name. The integration
makes one pipeline per repo, so all suites land in the same build.
## Cache volumes and artifacts
A **cache volume** carries state between builds. It is an input: content that a
step needs, but that the repo does not contain. Volume names are scoped to the
pipeline.
An **artifact** is a file that one build produces. It is an output: evidence for
a person to read in the Buildkite UI. The agent uploads artifacts whatever the
exit code of the step, so a failed step still reports its results.
The visual suite uses both. The volume `visual-snapshots` holds the baseline
images, which are gitignored and exist nowhere else. `artifact_paths` uploads
the actual, expected, and diff images that a failed comparison writes. The two
cannot be swapped: a volume is not visible in the UI, and an artifact does not
persist as an input to the next build.
## One writer per volume
A job does not read its volume in place. It gets a private copy at start. That
copy becomes the version that the next job sees, but only when the job exits 0.
Every successful job is therefore a writer, whether or not it changed a file.
Two consequences follow.
Keep the volume names distinct across the uploaded files. A step that commits a
version discards what a concurrent step wrote to the same volume. This is why
`rust.yml` gives test, doc, and lint a separate volume each.
The visual suite keeps exactly one writer. `master` updates the baselines, and
`concurrency: 1` serializes the group. Other branches always exit non-zero, so
Buildkite discards their copy and no branch can republish a stale baseline set.
The branch step then cannot use the exit code to report the test result, so two
codes carry both: 100 means no differences, which `soft_fail` keeps green, and
any other non-zero code fails the build. An annotation reports the outcome in
both cases.
## Docker images
Two images cover all Node steps:
- `node:24-bookworm` tracks the major version in `.nvmrc` and the `engines`
field in `package.json`.
- `mcr.microsoft.com/playwright:v1.61.1-noble` ships the browsers at
`/ms-playwright`, so no step installs them. The tag must match
`@playwright/test` in `package.json`. Playwright refuses browsers built for
another version.
Node steps use no cache volume. Cache paths are relative to the step working
directory, and the agent home is not visible inside a container, so npm
downloads its dependencies on every run. The Rust steps run on the agent
instead, so their `~`-rooted paths work.
+31
View File
@@ -0,0 +1,31 @@
# No explicit build step: tests/support/globalSetup.ts runs `npm run build` in
# parallel with fixture creation, and asserts the pinned rad and radicle-httpd
# versions are on PATH.
env:
CI: "true"
steps:
- label: "🎭 e2e"
key: e2e
timeout_in_minutes: 30
artifact_paths:
- "tests/artifacts/**/*"
plugins:
# Ships the browsers at /ms-playwright, so nothing here installs them. The
# tag must match @playwright/test in package.json — Playwright refuses
# browsers built for another version.
- docker#v5.14.0:
image: "mcr.microsoft.com/playwright:v1.61.1-noble"
# Chromium exhausts Docker's default 64MB /dev/shm.
ipc: host
environment:
- CI
commands:
- npm ci
# The image ships no decompressors: install-binaries unpacks .tar.xz
# releases, and the repo fixtures are .tar.bz2 archives.
- apt-get update -qq && apt-get install -y -qq xz-utils bzip2
- mkdir -p tests/artifacts
- ./scripts/install-binaries
- npm run test:e2e -- --project chromium
+73
View File
@@ -0,0 +1,73 @@
# No cache volume: cache paths are relative to the step working directory, and
# the agent's home is not visible inside a container, so npm downloads its
# dependencies on every run.
env:
CI: "true"
__defs:
# Tracks the major version in .nvmrc and the engines field in package.json.
node_image: &node_image
docker#v5.14.0:
image: "node:24-bookworm"
environment:
- CI
# Ships the browsers at /ms-playwright, which is why no step installs them.
# The tag must match @playwright/test in package.json — Playwright refuses to
# run against browsers built for another version.
playwright_image: &playwright_image
docker#v5.14.0:
image: "mcr.microsoft.com/playwright:v1.61.1-noble"
# Chromium exhausts Docker's default 64MB /dev/shm.
ipc: host
environment:
- CI
steps:
- label: "🔎 check"
key: check
timeout_in_minutes: 10
plugins:
- *node_image
commands:
- npm ci
- npm run check
- label: "🧪 unit-tests"
key: unit-tests
timeout_in_minutes: 10
plugins:
- *node_image
commands:
- npm ci
- npm run test:unit
# The http-client suite talks to a real radicle-httpd, so it needs the pinned
# binaries on PATH even though it runs under vitest rather than Playwright.
- label: "🔌 http-client-tests"
key: http-client-tests
timeout_in_minutes: 15
artifact_paths:
- "tests/artifacts/**/*"
plugins:
- *node_image
commands:
- npm ci
- mkdir -p tests/artifacts
- ./scripts/install-binaries
- npm run test:http-client:unit
# playwright.buildSmoke.config.ts sets globalSetup to undefined, so this step
# needs a browser but no Radicle binaries or fixtures.
- label: "🏗 build-smoke"
key: build-smoke
timeout_in_minutes: 30
artifact_paths:
- "tests/artifacts/**/*"
plugins:
- *playwright_image
commands:
- npm ci
- npm run build
- npm run test:e2e -- --project chromium --config playwright.buildSmoke.config.ts
+16
View File
@@ -0,0 +1,16 @@
# Entry point for CI. garden-broker stores a bare `buildkite-agent pipeline
# upload` as the server-side configuration, so the agent looks for this file by
# its default name. The other files here are uploaded from below.
#
# The broker creates exactly one Buildkite pipeline per Radicle repo, so every
# suite lands in this single build. Keep cache volume names distinct across the
# uploaded files: volumes are scoped to the pipeline, and a step that commits a
# volume version discards what a concurrent step wrote to the same volume.
steps:
- label: ":pipeline: upload"
commands:
- buildkite-agent pipeline upload .buildkite/node-checks.yml
- buildkite-agent pipeline upload .buildkite/e2e.yml
- buildkite-agent pipeline upload .buildkite/visual.yml
- buildkite-agent pipeline upload .buildkite/rust.yml
+66
View File
@@ -0,0 +1,66 @@
# CI for the Rust workspace (crates/radicle-httpd, crates/radicle-search).
#
# Each step gets its own named cache volume. Clippy, test, and doc each produce a
# different target directory, and a shared volume would let whichever step
# commits last discard the other two, forcing a near-full rebuild every build.
#
# These steps run on the agent rather than in a container, so ~-rooted cache
# paths work. The toolchain version is not pinned here: rustup reads
# ./rust-toolchain.
#
# Every step needs network access to seed.radicle.dev as well as crates.io — the
# workspace patches radicle-job to a git dependency served from a Radicle seed.
env:
CI: "true"
CARGO_TERM_COLOR: "always"
__defs:
install_rust: &install_rust |
export PATH="$$HOME/.cargo/bin:$$PATH"
export CARGO_TARGET_DIR="$$HOME/.cache/cargo-target"
if ! command -v rustup >/dev/null 2>&1; then
curl -fsSL https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile minimal
fi
rustup component add rustfmt clippy
rustc --version
cargo_cache: &cargo_cache
- "~/.cargo"
- "~/.rustup"
- "~/.cache/cargo-target"
steps:
# RUSTFLAGS is set on the build only, not on the test run.
- label: "🦀 test"
key: test
timeout_in_minutes: 30
cache:
name: rust-test
paths: *cargo_cache
commands:
- *install_rust
- RUSTFLAGS="-D warnings" cargo build --workspace --all-features
- cargo test --workspace --all-features
- cargo test -p radicle-httpd
- label: "📚 doc"
key: doc
timeout_in_minutes: 30
cache:
name: rust-doc
paths: *cargo_cache
commands:
- *install_rust
- RUSTDOCFLAGS="-D warnings" cargo doc --workspace --all-features
- label: "🦀 lint"
key: lint
timeout_in_minutes: 30
cache:
name: rust-lint
paths: *cargo_cache
commands:
- *install_rust
- RUSTFLAGS="-D warnings" cargo clippy --workspace --all-features --tests
- cargo fmt --all -- --check
+107
View File
@@ -0,0 +1,107 @@
# Visual snapshot tests.
#
# tests/visual/snapshots is gitignored, so the baselines live only in this cache
# volume. Master updates them with --update-snapshots; every other branch
# compares against them.
#
# A job does not read the volume in place: it gets a private copy at start, and
# that copy becomes the version the next job sees if the job exits 0. So every
# successful job is a writer, whether or not it changed a file. The two steps
# below therefore keep exactly one writer:
#
# - Master updates the snapshots and commits, serialized by `concurrency: 1`
# so two master builds cannot overwrite each other.
# - Branches compare, and always exit non-zero so their copy is thrown away.
# They need this even though they do not update the snapshots: without
# --update-snapshots the mode is `missing`, under which Playwright still
# writes a baseline that does not exist yet. A branch therefore needs no
# concurrency limit, and any number of branch builds can run at once.
env:
CI: "true"
steps:
- label: "📸 visual update snapshots"
key: visual-update-snapshots
branches: "master"
timeout_in_minutes: 30
# One writer at a time. Buildkite runs a concurrency group in the order the
# jobs were created, so the newest master build commits last.
concurrency: 1
concurrency_group: "radicle-explorer/visual-snapshots"
cache:
name: visual-snapshots
paths:
- "tests/visual/snapshots"
artifact_paths:
- "tests/artifacts/**/*.png"
plugins:
# The image ships WebKit as well as Chromium, which this suite needs: the
# visual-mobile project uses the iPhone 13 Mini device descriptor, whose
# defaultBrowserType is webkit. The tag must match @playwright/test in
# package.json — Playwright refuses browsers built for another version.
- &playwright_docker
docker#v5.14.0:
image: "mcr.microsoft.com/playwright:v1.61.1-noble"
# Chromium exhausts Docker's default 64MB /dev/shm.
ipc: host
# buildkite-agent annotate runs inside the container.
mount-buildkite-agent: true
environment:
- CI
commands:
- npm ci
# The image ships no decompressors: install-binaries unpacks .tar.xz
# releases, and the repo fixtures are .tar.bz2 archives.
- apt-get update -qq && apt-get install -y -qq xz-utils bzip2
- mkdir -p tests/artifacts
- ./scripts/install-binaries
# No `|| true` here: --update-snapshots reports a re-generated snapshot as
# a pass, so a non-zero exit means the run itself broke. Failing is then
# what we want, because only a job that exits 0 commits a volume version,
# and a run that died halfway would publish a half-written baseline set.
# The visual projects set retries: 0, so one flaky test skips the update
# until the next push to master.
- npm run test:e2e -- --project visual-desktop visual-mobile --update-snapshots
- label: "📸 visual"
key: visual
branches: "!master"
timeout_in_minutes: 30
# No concurrency limit: this job never commits a volume version.
#
# The job must always exit non-zero to discard its copy of the volume, so
# the exit code cannot also report whether the tests passed. Two codes carry
# both: 100 means no differences, and soft_fail keeps the build green.
# Anything else fails the build — a real difference, but also a broken
# npm ci or a crashed browser. 100 is a sentinel that no tool in this step
# produces, so only the success path below is soft.
soft_fail:
- exit_status: 100
cache:
name: visual-snapshots
paths:
- "tests/visual/snapshots"
artifact_paths:
- "tests/artifacts/**/*.png"
plugins:
- *playwright_docker
commands:
- npm ci
- apt-get update -qq && apt-get install -y -qq xz-utils bzip2
- mkdir -p tests/artifacts
- ./scripts/install-binaries
# Both paths exit non-zero, so Buildkite discards this job's copy of the
# volume. A zero exit would revert an update that master committed while
# this job ran, and would also publish any baseline this run wrote itself
# for a screenshot that has none yet.
- |
if npm run test:e2e -- --project visual-desktop visual-mobile; then
buildkite-agent annotate "📸 visual: no differences" \
--style success --context visual
exit 100
else
buildkite-agent annotate "📸 visual: differences found, see the uploaded images" \
--style warning --context visual
exit 1
fi