Skip to content

Publishing a release

Publishing means producing consistent tags on the six repositories with a single command, from the co-development workspace.

RepositorySource of truthRule
darkone-nixos-frameworkVERSIONTrain reference
dnf-generatorCargo.tomlOwn version, pinned by the framework
dnf-fleet-updatepackage.jsonOwn version, packaged by the framework
dnf-docpackage.jsonFramework’s MAJEUR.MINEUR, own patch
dnf-boilerplate, dnf-examplelatest tagMirror 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 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 revert

The 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:

Fenêtre de terminal
ln -s ../../scripts/check-commit-msg.sh .git/hooks/commit-msg

A scope is never a type: feat(matrix): …, never matrix(admin): ….

TypeChangelog section
featAdded
fixFixed
securitySecurity
perf, refactor, revertChanged
docsDocumentation
chore(deps)Dependencies
subject in drop or removeRemoved
other chore, ci, test, buildnot published

chore(deps) is the train’s pinning commit: the framework entry names the generator and fleet-update it bundles this way.

RecipeEffect
just versionsVersion, latest tag and branch of each repository
just changelogPreview 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 versionVersion, 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.

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.

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.

  1. Pre-checks : verify required tools are present (git-cliff, jq, nix-update), all repositories are clean, on main, and up to date with origin. No long-running tasks should be active before starting.

  2. 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.

  3. Global gate : just check (code and unit tests).

  4. Generator, if it has changed : run just gen-test, release, and push. Since it owns the schema etc/config.yaml, it runs before anything that depends on it. Its tag is then pinned in dnf/flake.nix (commit chore(deps)), so the contract between the framework and the generator is visible in a diff. The pinned generator then regenerates dnf/config/config.schema.json, the schema read by projects via their dnf/ link (commit chore(schema) if it changed).

  5. fleet-update, if it has changed : run ci and audit gates using its nix develop environment, release, and push. Its tag is then pinned in dnf/pkgs/fleet-update via just pkg-update, and the package is built (commit chore(deps)).

  6. Framework : just simulate all runs 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, update VERSION, pin the project template (templates/project/flake.nix), update CHANGELOG.md, commit, tag, and push.

  7. Documentation : just codegen regenerates 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’s MAJOR.MINOR line, and the site is deployed.

  8. Boilerplate and example : the boilerplate is first copied from templates/project/ at the framework’s tag (CHANGELOG.md and flake.lock are 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 their flake.nix, the flake.lock is 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 to system.build.toplevel against the new framework, with hardware and secrets replaced by stubs (dnf/just/scripts/eval-template-hosts.nix) : a template has neither. The boilerplate, shipped without var/generated/, has nothing to evaluate before running just generate.

  9. Root : run just update-dnf, commit only the flake.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.

On a v* tag, release.yml refuses to publish if :

  • the VERSION file does not match the tag;
  • CHANGELOG.md does not contain a section for that version;
  • the lockfile points to a dnf-generator checked 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.

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 :

Fenêtre de terminal
just test-installer # both scenarios
just test-installer interactive /path/to/iso

Disks 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 :

Fenêtre de terminal
just try # keep disk
just try reset path/to/dnf-generator # new disk, local generator

Unlike 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.

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.