Release process¶
A release here is a git tag, a green build of the tagged tree, and a GitHub
Release whose description is the changelog. There is no CHANGELOG.md: the
2026-08-01 revision of the org standard made the Release description the only
canonical history, on the grounds that a file and a Release description
describing the same release is two places to forget.
Where the version lives¶
VERSION, at the repository root, one line, no v prefix.
Every other repository in this org spells its version in the :version of
its .asd, and fromAsdSystem
reads it from there. cl-nix-forge ships no .asd — it is Nix, not Lisp —
so it has nothing for that function to read. Until the same revision the
version of record was the first ## [x.y.z] heading in CHANGELOG.md, which
docs/default.nix parsed; abolishing that file removed the version source
along with it, and a plain VERSION file replaces it.
Two alternatives were rejected:
version = "0.5.0"inflake.nix. The org conformance rule is literally "version is not hardcoded inflake.nix".- Derive it from the git tag. Unavailable under pure evaluation, which
is the only mode
nix flake checkand every consumer's lock file use.
docs/default.nix reads VERSION and passes it to
mkDocsSite, so the published
documentation site carries the release number without a second place to edit.
What keeps the file and the tag in step¶
A VERSION file requires a matching update when a release is cut. The same is
true of a .asd :version; the gate below keeps the two values synchronized:
# .github/workflows/release.yml
- name: Verify tag matches VERSION
run: |
tag="${GITHUB_REF_NAME#v}"
file="$(tr -d '[:space:]' < VERSION)"
[ "$tag" = "$file" ]
A tag that disagrees with the file fails the release job before anything is
published. Without this step the two drift apart silently, which is the exact
failure the .asd rule exists to prevent.
What the tag gate actually builds¶
The release job then runs nix flake check --print-build-logs on the tagged
tree without --no-build. This builds the checks and catches missing module
arguments that evaluation alone may not force.
checks.default is the aggregate this repository's own suite hangs from —
the same statement mkPackageFlake
makes on an adopter's behalf, namely "this package's tests pass". It names
every member of the suite in its build script, so each one enters its input
closure and is built before the command runs, and its output is a manifest of
what was gated:
checks.formatting is not a member. treefmt over the whole tree
does not replace the library checks. mkPackageFlake keeps it as a sibling
of default rather than a
component of it.
checks.docs is the same derivation as packages.docs: mkdocs build
--strict fails on a broken link or a page missing from the nav, so a broken
docs tree fails the release rather than the deploy that follows it.
Cutting one¶
- Decide the number. The public API in
lib/may change in a breaking way in any0.xrelease (see Home); within that, a change to whatlib/exports or to what the flake declares is a minor bump, and a fix or a documentation correction is a patch. - Bump
VERSIONand merge that tomainalong with the release's content. - Tag the merge commit and push the tag:
release.ymlverifies the tag againstVERSION, builds every check, and opens the Release as a draft with an empty body. The draft is load-bearing: a published release with no notes would make "the maintainer forgot the changelog" a user-visible state, and a draft appears neither under "Latest release" nor in the default output ofgh release list.- Write the notes and publish:
What a consumer pins¶
A release tag, never the branch:
inputs.cl-nix-forge = {
url = "github:nerima-lisp/cl-nix-forge/v0.6.1";
inputs.nixpkgs.follows = "nixpkgs";
};
A bare github:nerima-lisp/cl-nix-forge follows main, so a push here would
change a downstream build with no lock-file change to review.