Releasing
How a release is cut, how versions are chosen and how to roll back.
Releases are automatic. Every push to main whose ci run is green goes through .github/workflows/release.yml. Nobody tags by hand and nothing red is ever released.
What a release contains
| Asset | Where |
|---|---|
docker images ghcr.io/findmalek/skiffr-api, -web, -dashboard, -docs | tags vX.Y.Z, X.Y and latest, for linux/amd64 and linux/arm64 |
skiffr-extension-vX.Y.Z.zip | the GitHub release, and baked into the api image at /downloads |
docker-compose.yml, install.sh, skiffr (the helper) | the GitHub release |
SHA256SUMS | the GitHub release. install.sh and ./skiffr update check the compose file and the helper against it. It guards against a truncated or corrupted download, not against a compromised release, since the sums come from the same place as the files |
| release notes | the squash titles since the last release, grouped by type, each with its pull request link |
The version is written into the package.json of every app at build time from the tag, which is the single source: the extension manifest, GET /version on the api and the image labels all carry it. A vX.Y.Z image tag is never moved to a different build: if it already exists the run only continues when it was built from the same commit, otherwise it stops.
How the next version is chosen
From the commit titles on main since the last vX.Y.Z tag. While the major version is 0:
| Title | Bump |
|---|---|
feat(scope): ... | minor, 0.2.1 to 0.3.0 |
fix, perf, refactor, revert | patch, 0.2.1 to 0.2.2 |
a ! after the type (feat(api)!: ...) or a BREAKING CHANGE: line in the body | minor (major once the project is at 1.0) |
anything with the scope deps (chore(deps): ...) | patch, dependencies change the images |
a title that does not follow type(scope): subject | patch, listed under Other changes |
docs, ci, test, chore, build, style | nothing |
When only the last group landed there is no release, so a docs fix does not make everyone update. The first release is v0.1.0. A push to main that is not the newest commit by the time the workflow runs is skipped, the newer commit carries its changes.
Forcing, rehearsing, rolling back
- Rehearse: Actions, release, Run workflow.
dry_runis on by default: it plans the version, builds the extension and all eight images and pushes nothing. The plan and the notes are in the run summary. - Force a release (for example after a docs-only history): run the workflow with
dry_runoff and abumpofpatch,minorormajor. It still needs commits since the last tag. - Rerun is safe. The release is created as a draft, every asset is uploaded, and only then it is published as the latest one, so
install.shnever sees a half uploaded release. A version that is already published is skipped. A run that died before that leaves a draft or untagged images: run it again, it replaces the draft's assets and publishes it, and the existingvX.Y.Zimages are reused when they come from the same commit. - Roll a self-hosted server back:
SKIFFR_VERSION=v0.1.2in.env, thendocker compose up -d, or./skiffr update --version v0.1.2. Migrations are meant to only add (a rule for reviewers, not enforced yet), so the older code runs on the newer schema. - Undo a bad release: fix forward with a
fixcommit, that releases a new patch. Do not delete tags or move image tags.
Checks before the first run in a fork or a new repo
- the workflow only runs on
FindMalek/skiffr, change the repository check inrelease.ymland theIMAGE_PREFIXfor a fork - ghcr packages are private after their first push: open each of the four packages, Package settings, and make them public so self hosters can pull. The workflow checks an anonymous pull and stops before publishing the release when one fails, so make them public and run it again
- it needs no secret beyond the automatic
GITHUB_TOKEN, withcontents: writeonly in the publish job andpackages: writeonly in the image jobs
The Chrome Web Store listing, which would also update the extension itself, is a separate piece of work (issue 26).
Testing the logic
The version and notes logic is a plain module, scripts/release/lib.mjs, with tests in scripts/release/lib.test.mjs: pnpm test:release. See what the next release would be from your checkout without changing anything: node scripts/release/plan.mjs.