Releases ​
CodeLotse is released as a set of container images (quavon-app, quavon-worker, quavon-postgres, quavon-valkey, quavon-caddy) together with release notes, a versions.json file and one CycloneDX SBOM per image. The pipeline is .github/workflows/release.yml.
Publication is disabled. No container registry and no signing identity have been chosen yet. Until the repository variable QUAVON_RELEASE_REGISTRY is set, the release workflow only builds, inventories and scans the images and never pushes, signs or creates a release. All registry and repository names below are placeholders.
Version scheme and channels ​
Versions follow Semantic Versioning. A release is started by pushing a tag v<version>, for example v1.2.0 or v1.2.0-rc.1.
| Tag | Channel | Image tags |
|---|---|---|
v1.2.0 | stable | 1.2.0, stable |
v1.2.0-rc.1 (any prerelease suffix) | edge | 1.2.0-rc.1, edge |
Channel tags move with every release; pin deployments to a version or, better, to a digest. Prereleases are marked as prereleases on GitHub and can never be published to stable.
versions.json ​
Each release carries a versions.json asset:
{
"version": "1.2.0",
"channel": "stable",
"minUpgradableFrom": "<release>",
"migrations": ["0000_core_tables", "0001_tenant_rls", "..."]
}minUpgradableFrom: the oldest release that can be upgraded to this one directly; it comes from the upgrade test matrix (tests/upgrade/matrix.json). Older installations must upgrade in steps.migrations: the database migrations shipped with this release, in the order they are applied.
Generate it locally with node apps/cli/src/bin.ts release versions --version <version> [--channel stable|edge] [--out versions.json].
Release notes ​
Release notes are generated from the commit subjects between the previous v* tag and the release commit (node apps/cli/src/bin.ts release notes [--from <ref>] [--to <ref>] [--version <v>], or pnpm release:notes). Merge commits are excluded.
| Section | Commits |
|---|---|
| Breaking changes | type!: subjects or a BREAKING CHANGE: footer, whatever the type |
| Security | type or scope security/sec, or a subject mentioning security, a vulnerability, a CVE or a GHSA id |
| Features | feat |
| Fixes | fix |
| Performance | perf |
| Documentation | docs |
Other types (refactor, test, chore, ci, build, …) and non-conventional subjects are not listed; the notes state how many there were.
Only subject lines are used. Commit bodies and trailers are never copied, and any line that looks like a co-author trailer or a "generated with/by" footer is dropped, so the notes contain no attribution footers.
Holding back a contract migration ​
The migration runner applies every .sql file in packages/db/migrations, and release versions lists every file, so there is no setting that holds a migration back. A contract migration that must ship one release later is held back with a release branch. This is currently needed for 0030_wave16a_posted_comments_contract (why).
Release N, the first published release that contains 0029_wave16a_posted_comments_scope:
- Create the release branch from
main:git switch -c release/<version> <commit>. - Remove the contract migration on that branch only:
git rm packages/db/migrations/0030_wave16a_posted_comments_contract.sql. Keeppackages/db/migrations/meta/; onlydrizzle-kitreads the journal and snapshots, the runner does not. - Commit with the subject
docs: migration 0030 ships in the next release, upgrade through this release, so the release notes of N carry the upgrade note. - Check that
node apps/cli/src/bin.ts release versions --version <version>lists0029_wave16a_posted_comments_scopeand not0030_wave16a_posted_comments_contract. - On this branch,
packages/publisher/src/publisher.int.test.ts("records equal comment ids from two connections of one tenant") fails by design, because the old key still exists. Every other gate must pass. - Tag
v<version>on the release branch and generate the notes with--to release/<version>.
Release N+1 is cut from main as usual. It contains 0030, and versions.json lists it. Generate its notes with --from v<N>; commits of N are reachable from that tag and are not repeated. Before tagging N+1, commit on main with the subject docs: this release applies migration 0030, upgrade from the previous release, so the notes of N+1 carry the upgrade note. Never remove an applied migration from main: init refuses to start when an applied migration is missing.
SBOMs and vulnerability scans ​
Every image gets a CycloneDX JSON SBOM produced with Syft (the version pinned in CI) and is scanned with Grype; a fixable vulnerability of severity high or critical fails the release. When publishing is enabled, each SBOM is attached to the image in the registry as a signed attestation and uploaded as a release asset (<image>.cyclonedx.json). See SBOM production and delivery.
CI builds the images for linux/amd64.
Dry run ​
pnpm release:dry-run -- --version 0.1.0-rc.1 runs the pipeline locally and publishes nothing. It writes the release notes, versions.json, the SBOMs and the Grype reports to dist/release/, builds the images with Docker, and runs Syft and Grype from their pinned container images when the binaries are not installed. Push, signing, provenance, SBOM attestations and the GitHub release are reported as skipped, because they need a CI OIDC identity and a registry. --skip-build skips the image builds, SBOMs and scans. The command exits non-zero if any step that ran failed.
In GitHub Actions, run the workflow manually with dry_run left at true for the same result on CI runners.
Enabling publication ​
Once a registry and the signing identity are decided:
- Set the repository variable
QUAVON_RELEASE_REGISTRYto the registry and namespace, for example<REGISTRY>/<NAMESPACE>. - For registries other than GitHub Container Registry, set the variable
QUAVON_RELEASE_REGISTRY_USERNAMEand the secretQUAVON_RELEASE_REGISTRY_TOKEN. - Push a
v*tag.
The publish job then pushes each image, signs it by digest with cosign keyless signing (the workflow's GitHub OIDC identity), records SLSA build provenance and the SBOM as GitHub artifact attestations stored in the registry next to the image, and the release job creates the GitHub release with the notes, versions.json and the SBOMs. Only these two jobs receive write permissions. GitHub artifact attestations for private repositories need GitHub Enterprise Cloud.
Verifying an image ​
Replace <REGISTRY>/<NAMESPACE>, <OWNER>/<REPO> and the digest with the values of your release. Always verify by digest.
Signature:
cosign verify <REGISTRY>/<NAMESPACE>/quavon-app@sha256:<DIGEST> \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/<OWNER>/<REPO>/\.github/workflows/release\.yml@refs/tags/v'Build provenance (SLSA) with the GitHub CLI:
gh attestation verify oci://<REGISTRY>/<NAMESPACE>/quavon-app@sha256:<DIGEST> \
--repo <OWNER>/<REPO> \
--signer-workflow <OWNER>/<REPO>/.github/workflows/release.ymlSBOM attestation:
gh attestation verify oci://<REGISTRY>/<NAMESPACE>/quavon-app@sha256:<DIGEST> \
--repo <OWNER>/<REPO> \
--predicate-type https://cyclonedx.org/bomAdd --format json to inspect the attested SBOM or provenance. A release asset can be compared with the attested SBOM, and versions.json with the release you are upgrading from, before upgrading.