Releasing a New Version
Pushing the tag is the whole release: it triggers
.github/workflows/release.yml, which
builds the image, runs the E2E suite against it, publishes it to Docker Hub and
opens the GitHub release — and docs.yml,
which rebuilds the docs site and serves docs/public/ as of that tag at its
root. Docs are versioned per minor release, so v3.2.1 republishes the 3.2
documentation.
Finalize the changelog — in
CHANGELOG.md, rename## [Unreleased]to## [X.Y.Z] - YYYY-MM-DD(novprefix in the header), and replace the[Unreleased]compare link at the bottom of the file with the new version’s, pointing from the previous release’s endpoint to the new tag (vX.Y.Z). Do not add a fresh## [Unreleased]section here — the released tag should carry no empty section, since that is what the documentation site publishes. Step 4 reopens it afterwards.Release the in-app notes in the same commit — in
app/release-notes.ts, the first entry ofreleaseNotesis the one being prepared: replace itsversion: "Unreleased"with this version and add thedate. Nothing else moves.make testfails until the newest release the changelog names has an entry.Re-read those highlights against the finalized changelog first. They were written one at a time, before it was known what else the release would hold, so they are a draft, not a record: check that they are still the 3-5 changes an attendee or organizer would call this release’s highlights, and rewrite or replace any that are not.
Record the release’s database in the same commit, so future releases are tested against an upgrade from it:
make dump-release-db VERSION=vX.Y.ZIt writes no dump when the release ships no migration, only the
tests/fixtures/upgrade/releases.jsonentry pointing at the dump that already covers the upgrade.make testfails until the version the changelog now names appears there, so the fixture cannot be forgotten — and being in the release commit, it is inside the tag, where a patch release branched off that tag needs it. See Release-upgrade tests.Commit and merge this like any other change.
Tag the resulting commit on
mainand push the tag — the point of no return, since it publishes the documentation:jj git fetch jj tag set v3.0.0 -r main@origin jj git push --tag v3.0.0 # git: git fetch origin main # git tag v3.0.0 origin/main # git push origin v3.0.0Optionally run
make test-e2e-dockeron the release commit first. The release workflow runs it too, and refuses to publish anything if it fails — running it locally only buys you the chance to fix a failure before the tag is public.Watch the release workflow. It takes around fifteen minutes, most of it the E2E suite against the image.
Reopen the changelog — in a follow-up commit on
main, add an empty## [Unreleased]section above## [X.Y.Z]and an[Unreleased]compare link fromvX.Y.ZtoHEAD. Reopen the in-app notes too — inapp/release-notes.ts, add a new first entry withversion: "Unreleased"and nodate, above the one just released.
What the release workflow does
Triggered by a v*.*.* tag, in one job, so nothing is published until
everything before it has passed:
- Builds the image through
scripts/docker-build.sh— the same scriptmake docker-buildruns — withAPP_VERSIONtaken from the tag rather than fromscripts/app-version.js, which asksjjorgit describeand can’t be trusted on a CI checkout. - Runs the E2E suite against that image (
make test-e2e-docker, withIMAGEnaming the image just built, so it is not rebuilt). This is the only tier that runs the artifact we ship, and the last chance to catch a fault that exists only in it — a file the standalone build didn’t copy, a path that resolves differently under/data, a date rendered in the server’s timezone rather than the event’s. See Testing the Docker image. The report and the traces of failed attempts are uploaded as an artifact. - Pushes four tags to
schellingboard/schellingboard: the full version,major.minor,major, andlatest.scripts/release-tags.tsdecides the last one from the repository’s tags —:latesthas to keep meaning the newest published release, so a patch cut from an older line (v3.3.2released afterv3.4.0) publishes its own three tags and leaves:latestalone. - Creates the GitHub release, last, so a release exists only once its images
do. It is marked as GitHub’s “latest” on the same condition as the
:latesttag.
Images are linux/amd64 only, which is what has always been published.
Repository configuration
The workflow needs two repository secrets:
| Secret | Value |
|---|---|
DOCKERHUB_USERNAME |
The Docker Hub account that can push to schellingboard/schellingboard. |
DOCKERHUB_TOKEN |
A Docker Hub access token for that account with Read & Write scope — not the account password. |
Nothing else: the GitHub release is created with the automatic GITHUB_TOKEN,
which the workflow grants contents: write.
If it fails
Nothing has been published unless the run reached the “Publish the image” step,
so a failing build or E2E gate leaves Docker Hub and the releases page
untouched. The tag and the documentation are out, though, and re-cutting a tag
that people may already have fetched is worse than moving on: fix the problem on
main and release the next patch version.
A failure after the gate — a Docker Hub outage, a flake — is different: the tag is fine and only the publishing didn’t happen. Re-run the workflow (or dispatch it against the tag). Every step in it can be repeated safely.
Publishing by hand
Only if the workflow itself is unavailable. scripts/release-tags.ts prints
exactly what would be pushed:
VERSION=v3.0.0
docker login
jj new $VERSION # git: git checkout $VERSION
make clean
IMAGE="$(APP_VERSION=$VERSION bash scripts/docker-build.sh)"
for ref in $(bun scripts/release-tags.ts "$IMAGE"); do
docker tag "$IMAGE" "$ref"
docker push "$ref"
done
Patch Releases
A patch release is cut from the release branch of its minor version,
release/<major.minor> — release/3.2 for v3.2.1. These branches don’t have
to exist: create one from the release tag the first time that version needs a
fix, and a minor release that never needs one never gets a branch.
jj new v3.2.0
jj bookmark create release/3.2 # git: git switch -c release/3.2 v3.2.0
Commit the fix there (cherry-picking it from main if it landed there first),
then run the steps above against the branch instead of main, and merge the
branch back into main so the fix is not lost. Whether the patch takes over
:latest is not a decision to make: the workflow works it out from the tags.
Pushing the branch republishes that version’s documentation on its own, without
a tag — the docs build prefers release/<major.minor> over the tag for that
version, which is also how published docs are corrected between releases. See
Correcting published documentation.