What CI does#
Two workflows live in .forgejo/workflows/. ci.yml tests
everything on every push and pull request, and on a v* tag also builds the agent for two
architectures, packages both, publishes a release and copies it to the download host.
iso.yml builds the OS image on the same tags and attaches it to the same release.
Neither is exotic: both run in the golang:1.26-bookworm container on a self-hosted
runner, install Node from a tarball because that container has none, and drive the same
scripts you can run by hand.
ci.yml#
Triggered by a push to main, a v* tag, any pull request, or by hand from the Actions
tab. Two jobs.
Job build — test, package, release#
| Step | What it runs |
|---|---|
| Provide node | Node 22.12.0 from the upstream tarball (gzip, not xz — the Go image has no xz), and git config --global --add safe.directory '*' so git will touch a workspace owned by another uid |
| Checkout | |
| Version | A tag gives ${GITHUB_REF#refs/tags/v}; anything else becomes 0.0.0+<short sha>, so a bundle built off a branch cannot be mistaken for a shipped build. It runs first because the UI build stamps it into the bundle |
| Agent | go vet ./..., then go test -race -count=1 ./... |
| Tools | All four Python suites: the importer, the seed reader, the installer's commands and its pty driver |
| Site | python3 site/tests/test_build.py |
| Shell | bash -n over the release scripts, install/, packaging/iso/build-iso.sh and the packaging shell, plus py_compile over the Python helpers |
| App catalogue | check-images.py agent/catalog.json --offline |
| UI | npm ci, tsc --noEmit, vitest run (the page tests), npm run test:qr (the QR encoder's vectors), vite build, with VITE_WHEELHOUSE_VERSION set from the version step |
| Agent build | amd64 and arm64, static, trimmed, version stamped |
| Package | Two .debs, two tarballs, CHANGELOG.md copied in, and a SHA256SUMS over the lot |
| Sign | install/sign.sh sign dist/SHA256SUMS, only when RELEASE_SIGNING_KEY is set |
| Upload artifact | The whole of dist/ |
| Contract | tools/route-gaps.py — every route the mux serves is declared and either reached from the built UI or explained |
| Tools | tools/tests/*.py, including the release-tool suite |
Three details:
The version step runs before the UI build. VITE_WHEELHOUSE_VERSION is what
ui/src/components/product.ts stamps into the bundle
for the version shown in the account menu, on System and on About. Without it
the bundle has no version of its own, and every one of those surfaces falls back to the
product block on GET /api/system — or, on a box whose agent predates that block, to
the literal unknown. There is no hardcoded constant to fall back to.
The shell syntax check needs no network. bash -n parses without running, which
catches the release scripts — the ones that otherwise fail only at tag time.
The release body is the changelog section for that version. The step extracts the
## [0.5.1] … section up to the next ## heading and appends the verification
instructions.
Job audit — dependency scan#
govulncheck over the agent's module set, and npm audit --audit-level=high over the
UI's lockfile.
iso.yml#
Triggered by a v* tag, or by hand with two inputs: release_tag (attach to that
existing release; blank means the ISO stays a run artefact) and vyos_branch (default
rolling).
| Step | What it runs |
|---|---|
| Provide node and the Docker CLI | Node from a tarball, Docker's static client binary |
| Checkout | |
| Version | A tag gives both the version and the tag; a manual run that names a release takes its version from that name |
| Build the agent package | Builds ui/dist with VITE_WHEELHOUSE_VERSION set, builds the amd64 agent, and packages the .deb into $BUILD_DIR |
| Build the ISO | packaging/iso/build-iso.sh, 30 to 60 minutes |
| Inspect the image | The standard-library Python check described on Building the image |
| Upload artifact | Everything in $BUILD_DIR/out matching the version |
| Install the image | tools/release-readiness.sh — installs from a seed, takes the medium away, boots the disk, and checks the machine that comes back. Nothing below runs if this fails |
| Attach to the release (when a tag is known) | Creates the release, with this version's CHANGELOG.md section as the body, and uploads all nine artefacts |
| Publish to the download host | publish.sh <tag> /srv/wheelhouse-releases … |
| Verify | Reads the mirror back: checksums, the channel index and latest |
The job builds its own .deb rather than fetching the one ci.yml made, because a job
token cannot read another private repository's releases. The build tree lives at
/srv/wheelhouse-build on the runner host and is bind-mounted at the same path, so
docker -v inside the script sees what the job wrote. The runner mounts the Docker socket
into every job on its own — naming it again in the job's options is a duplicate mount
point and the container never starts.
Only one job publishes, and only after the image has installed#
ci.yml publishes nothing. It tests, and keeps the packages as a run artifact. Everything
that reaches a customer — the forge release, the nine attached files, the mirror, the
channel index and latest — is written by iso.yml, and only after the install gate has
installed that image from a seed and booted the disk. A green suite is not a working
image, and this is where that rule is enforced rather than remembered.
publish.sh still does not write SHA256SUMS itself:
release-index.py does, by rescanning the
whole tag directory every time, so a re-run of a partly published tag leaves a complete,
correctly signed set. The full sequence is on Making a release.
Running the ISO job by hand#
From the Actions tab: choose iso, then Run, and fill in the two inputs. Leave
release_tag blank to get an artefact without touching a release. That is the safe way to
test a vyos_branch change or a new vyos-build pin.
Everything else is reproducible locally with the commands on The test suites and Building the image. Nothing in either workflow only works on the runner, except the mounted host directories.
What CI cannot tell you#
- Whether the UI works. The UI step type-checks, runs the page tests and the QR vectors, and builds the bundle — but the configuration tree, reconcile, Support, About and the licence page have no test, and the rest run in jsdom, which has no layout and no real WebSocket. Nothing in the pipeline opens a browser.
- Whether it works against a router. No runner has one.
agent/test.shis the live check, and it is run by hand. - Whether the image installs. The inspection step reads the ISO's own headers; it does not boot it.
- Performance. Nothing is measured, in CI or on hardware.
See also#
- The test suites — every suite these steps invoke.
- Making a release — the tag-only half of
ci.yml, in order. - Building the image — the script
iso.ymlwraps. - Working agreements — the definition of done that a green pipeline does not satisfy on its own.
- Release history — what the tag-only steps end up producing.
- Download and verify — what a customer does with the artefacts these jobs publish.