Publishing a release
Publishing means producing consistent tags on the six repositories with a single command, from the co-development workspace.
What each repository versions
Section titled “What each repository versions”| Repository | Source of truth | Rule |
|---|---|---|
darkone-nixos-framework | VERSION | Train reference |
dnf-generator | Cargo.toml | Own version, pinned by the framework |
dnf-fleet-update | package.json | Own version, packaged by the framework |
dnf-doc | package.json | Framework’s MAJEUR.MINEUR, own patch |
dnf-boilerplate, dnf-example | latest tag | Mirror of the framework |
All six repositories keep a generated CHANGELOG.md, templates included.
The private consumer project is never tagged: it stays in co-development and serves as a permanent integration testbed.
The commit vocabulary
Section titled “The commit vocabulary”The changelog is generated from commit messages. A type not in the list
silently disappears from the release notes, so the format is checked:
a single line <type>(<scope>): <subject>, 80 characters max, type from the
following list.
feat fix perf refactor docs test build ci chore security revertThe same script, scripts/check-commit-msg.sh, checks every push to
main and every pull request in continuous integration, and every commit
locally via the commit-msg hook, installed once per clone:
ln -s ../../scripts/check-commit-msg.sh .git/hooks/commit-msgA scope is never a type: feat(matrix): …, never matrix(admin): ….
| Type | Changelog section |
|---|---|
feat | Added |
fix | Fixed |
security | Security |
perf, refactor, revert | Changed |
docs | Documentation |
chore(deps) | Dependencies |
subject in drop or remove | Removed |
other chore, ci, test, build | not published |
chore(deps) is the train’s pinning commit: the framework entry names the
generator and fleet-update it bundles this way.
The recipes
Section titled “The recipes”| Recipe | Effect |
|---|---|
just versions | Version, latest tag and branch of each repository |
just changelog | Preview of the next entry, writes nothing |
just bump [level] | Version, changelog, commit and tag of one repository |
just release [level] | The full train across the six repositories (+ the root) |
just version | Version, revision and pinned generator of the framework |
The level defaults to auto: git-cliff 🡕 deduces
the next number from the commits. patch, minor, major or a literal
X.Y.Z force the decision. Only unpublished commits (a chore(deps), for
example) give a patch.
just bump never pushes: it prints the push command. It refuses to publish
if HEAD already carries the latest v* tag. Three flags pass to the
script:
--dry-run: shows the entry then restores the file, committing nothing;--yes: proceeds without confirmation or$EDITOR;--next: prints the version the release would take, then stops.
Writing by hand in an entry
Section titled “Writing by hand in an entry”The generated block only contains commit messages. A preamble, a companion
versions table or a migration note are written under ## [Unreleased] in the
CHANGELOG.md: on publish, the script empties this section and folds its
content at the head of the entry, before the generated sections. The train
doesn’t stop for editing: these notes are written before launching it.
The release train
Section titled “The release train”just release runs at the root of the co-development workspace, which must contain all six repositories. Each repository is processed in order of its dependencies, and only if it has changed since its last tag.
-
Pre-checks : verify required tools are present (
git-cliff,jq,nix-update), all repositories are clean, onmain, and up to date withorigin. No long-running tasks should be active before starting. -
Plan : determine the current and next version for each repository, preview changelog entries, then request a single confirmation. If declined, nothing is written. From this point onward, everything runs automatically.
-
Global gate :
just check(code and unit tests). -
Generator, if it has changed : run
just gen-test, release, and push. Since it owns the schemaetc/config.yaml, it runs before anything that depends on it. Its tag is then pinned indnf/flake.nix(commitchore(deps)), so the contract between the framework and the generator is visible in a diff. The pinned generator then regeneratesdnf/config/config.schema.json, the schema read by projects via theirdnf/link (commitchore(schema)if it changed). -
fleet-update, if it has changed : run
ciandauditgates using itsnix developenvironment, release, and push. Its tag is then pinned indnf/pkgs/fleet-updateviajust pkg-update, and the package is built (commitchore(deps)). -
Framework :
just simulate allruns after all pinning, so it uses the dependencies that were actually released. These simulations do not run in CI : a failing simulation stops the train here. Next, updateVERSION, pin the project template (templates/project/flake.nix), updateCHANGELOG.md, commit, tag, and push. -
Documentation :
just codegenregenerates the reference, then runs translation and normalization. If there is anything to publish, the site is built (before tagging, so any broken link blocks the release), the version is aligned to the framework’sMAJOR.MINORline, and the site is deployed. -
Boilerplate and example : the boilerplate is first copied from
templates/project/at the framework’s tag (CHANGELOG.mdandflake.lockare preserved), with a “published copy” banner placed under the title of its README files. As a result, it is never edited manually. For both the boilerplate and the example, the framework’s tag is written to theirflake.nix, theflake.lockis committed (as this ensures a reproducible clone), then changelog and tag are created to match the framework’s tag. Each host in the example is evaluated down tosystem.build.toplevelagainst the new framework, with hardware and secrets replaced by stubs (dnf/just/scripts/eval-template-hosts.nix) : a template has neither. The boilerplate, shipped withoutvar/generated/, has nothing to evaluate before runningjust generate. -
Root : run
just update-dnf, commit only theflake.lock, then push the current branch (with no tag), last of all : its lockfile points to the releases that were just published. The root repository is never tagged.
Only the framework follows the level passed to the release command. Generator and
fleet-update always use auto, documentation follows the framework’s line, and templates follow its version number.
Each repository is pushed with its own tag only (git push --atomic origin main refs/tags/vX.Y.Z), never with --tags : any local working tags remain local.
Continuous integration safeguards
Section titled “Continuous integration safeguards”On a v* tag, release.yml refuses to publish if :
- the
VERSIONfile does not match the tag; CHANGELOG.mddoes not contain a section for that version;- the lockfile points to a
dnf-generatorchecked out from a branch rather than from a published release.
The three checks in ci.yml (unit tests, evaluation, and lint) are rerun via workflow_call, and the corresponding changelog section becomes the GitHub release notes.
Installer test
Section titled “Installer test”The installer-test.yml workflow runs darkone new end-to-end in a QEMU VM (runner KVM, real network) : the generic ISO boots, the assistant is driven via its serial console (pexpect), then the installed system reboots and is controlled over SSH. Two scenarios run in parallel, around 25 minutes each :
- scripted : response file,
--non-interactive, unencrypted disk, backup passphrase stored in a file - interactive : each question answered on the console, encrypted disk, backup USB key unplugged then replugged (QMP), first boot unlocked via the initrd’s sshd
Checks : auto-start on tty1, system running with no failed units, clean /etc/nixos, home page installed, second backup via just backup-admin from the installed system (the first is kept by date), backup opened from the host. The serial log is attached to the run.
Three triggers :
- tag : called by
release.yml, a failure prevents the ISO from being included in the release - every Monday on `main** : catches breakage from nixpkgs unstable before the next tag
- manual :
gh workflow run installer-test.yml --ref <branch>, before a release or after ISO changes
Same test locally, on the current tree’s ISO or another :
just test-installer # both scenariosjust test-installer interactive /path/to/isoDisks and logs : ~/.cache/dnf-installer-test/<scenario>/, around 25 GB per scenario. Code : dnf/tests/installer/.
To try the current tree manually in a QEMU window :
just try # keep diskjust try reset path/to/dnf-generator # new disk, local generatorUnlike nix run …#try, the project created by the assistant is pinned to the ISO’s framework via path: rather than the template release : the installed system contains unpublished code. Disk : ~/.local/share/dnf-try-local.
The machine’s store acts as a package cache (nix-serve-ng on 127.0.0.1:5000, started by just try if the port is free). The ISO sets DNF_URL and DNF_CACHE : relaunching darkone new in the VM keeps them.
Shared configuration
Section titled “Shared configuration”A single git-cliff 🡕 for the six repositories, in
dnf/just/scripts/cliff.toml. The script dnf/just/scripts/bump.sh discovers
the repository’s version source (VERSION, Cargo.toml, package.json, or
failing that the latest tag), assembles the file around the generated block
and preserves the hand-written history.