Wheelhouse docs

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#

StepWhat it runs
Provide nodeNode 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
VersionA 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
Agentgo vet ./..., then go test -race -count=1 ./...
ToolsAll four Python suites: the importer, the seed reader, the installer's commands and its pty driver
Sitepython3 site/tests/test_build.py
Shellbash -n over the release scripts, install/, packaging/iso/build-iso.sh and the packaging shell, plus py_compile over the Python helpers
App cataloguecheck-images.py agent/catalog.json --offline
UInpm ci, tsc --noEmit, vite build, with VITE_WHEELHOUSE_VERSION set from the version step
Agent buildamd64 and arm64, static, trimmed, version stamped
PackageTwo .debs, two tarballs, CHANGELOG.md copied in, and a SHA256SUMS over the lot
Signinstall/sign.sh sign dist/SHA256SUMS, only when RELEASE_SIGNING_KEY is set
Upload artifactThe whole of dist/
Release (tags only)Creates the forge release, with this version's CHANGELOG.md section as the body, and uploads every artefact
Publish (tags only)publish.sh <tag> /srv/wheelhouse-releases dist/*

Three details worth knowing:

The version step runs before the UI build. VITE_WHEELHOUSE_VERSION is what ui/src/components/product.ts reads for the version shown in the sidebar, on the Dashboard, on System and on About. Without it the bundle falls back to a literal in the source, and every future image would keep reporting the version that literal was last edited to.

The shell syntax check is cheap and unflakeable. bash -n parses without running, and it exists because the release scripts are the ones that only fail at tag time — the worst possible moment to find out.

The release body is the changelog section for that version. A fixed sentence every time tells a subscriber nothing about what they just got, so 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).

StepWhat it runs
Provide node and the Docker CLINode from a tarball, Docker's static client binary
Checkout
VersionA tag gives both the version and the tag; a manual run that names a release takes its version from that name
Build the agent packageBuilds ui/dist with VITE_WHEELHOUSE_VERSION set, builds the amd64 agent, and packages the .deb into $BUILD_DIR
Build the ISOpackaging/iso/build-iso.sh, 30 to 60 minutes
Inspect the imageThe standard-library Python check described on Building the image
Upload artifactEverything in $BUILD_DIR/out matching the version
Attach to the release (when a tag is known)Creates the release if ci.yml has not yet, then uploads
Publish to the download hostThe same publish.sh

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.

The two jobs write to the same directory, on purpose#

ci.yml publishes the packages; iso.yml publishes the image and the corresponding-source files. Neither knows what the other published, so neither writes SHA256SUMS itself. publish.sh calls release-index.py, which rescans the whole tag directory every time — so whichever job finishes last 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. There are no UI tests; tsc and vite build are a type check and a bundle.
  • Whether it works against a router. No runner has one. agent/test.sh is 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#

Checked against#

.forgejo/workflows/ci.yml · .forgejo/workflows/iso.yml · .forgejo/workflows/publish.sh · .forgejo/workflows/release-index.py · ui/src/components/product.ts · packaging/README.md · agent/test.sh

Updated 2026-09-02 development ci forgejo workflows