CI and Releases
Continuous integration — .github/workflows/ci.yml
Section titled “Continuous integration — .github/workflows/ci.yml”CI runs on pushes and pull requests against master, and it only runs the
jobs a change actually touches. A dorny/paths-filter step tags the diff
against four filters, and each job is gated on its own tag:
| Filter | Paths | Job |
|---|---|---|
go |
server/**, runner/**, internal/**, go.mod, go.sum, sqlc.yaml |
go-test |
web |
web/** |
web-test |
desktop |
desktop/** |
desktop-test |
website |
website/** |
website-build |
go-test— checks the committedsqlcgenoutput is current (sqlc vet+sqlc diff), builds, vets, then runs the coverage gate (make coverage). Uploadscoverage.filtered.outandcoverage.htmlas thego-coverageartifact.web-test— installs with a frozen lockfile, type-checks, lints, runs the test suite with coverage, then builds. Uploadsweb/coverage/lcov.infoasweb-coverage.desktop-test— same shape asweb-test, withELECTRON_SKIP_BINARY_DOWNLOAD=1so CI never downloads the Electron binary — the desktop unit tests never launch it.website-build— installs with a frozen lockfile and runs the Astro build, so a broken page or frontmatter fails the check.
A change that touches only web/ never spins up a Go job, and vice versa.
Releases — .github/workflows/release.yml
Section titled “Releases — .github/workflows/release.yml”Nexul has one version for the whole product, not one per component.
The VERSION file at the repo root holds the version line currently in
beta, with its suffix: 0.2.0-beta.
- Beta runs on every push to
master, so every squash-merge is a release. It tags a prereleasev<VERSION>-<NNN>(v0.2.0-beta-001,v0.2.0-beta-002, …) from the pushed commit. The number is the newest published beta on thatVERSIONline plus one, read from the releases list rather than from a file, so queued runs never collide and nothing commits back tomaster. Beta never touchesVERSION. - Stable is a manual
workflow_dispatch(channel: stable). It does not build master directly: it resolves the commit of the newest published beta and builds that commit, so stable only ever ships a commit a beta has already carried. It tags the releasev<VERSION>with the-betasuffix dropped (aversioninput can override this), then bumpsVERSION’s patch component, keeping the suffix (0.2.0-betabecomes0.2.1-beta), and pushes that to master aschore(release): prepare next version. Pushing a barev*.*.*tag by hand runs the same stable build against that exact tag, without theVERSIONbump — a hand-pushed tag can target an old commit, so it must never moveVERSIONforward. - What a release carries: four GHCR images (
nexul-server,-web,-runner,-automations, tagged with the release version plus the movingbetaorlatesttag; until the first stable release exists, betas carrylatesttoo, so a default install pulls the newest beta), one runner binary per supported OS/arch (nexul-runner-<goos>-<goarch>[.exe]: linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64), the same five targets as single-binary server tarballs (nexul-server-<goos>-<goarch>.tar.gz, embedding the web frontend viago:embed), and achecksums.txtcovering every binary asset. The runner binary naming is a contract: the server’s download proxy serves its own version’s assets under these exact names. - Beta image cleanup: a
prunejob runs after beta image pushes and deletes old beta-tagged GHCR image versions, keeping the ten newest per image and never touching alatestor stable-semver tag. - Every Go build in this workflow is stamped with the release version via
-ldflags -X .../internal/platform/version.Version=..., and the runner’s download proxy (GET /api/runners/download/{target}) now serves the server’s own version rather than a separately-versioned runner release.
Merge policy
Section titled “Merge policy”Merges to master are squash-only — a pull request lands as one commit,
whatever its branch history looked like.