Files
ngit-grasp/docs/how-to/publish-release.md
DanConwayDev b4bbd05238 docs(release): require stable branch updates for releases
The 3.0.9 release preparation omitted stable from the initial push instructions and left the local branch on the previous release. Add an agent reminder and a release checklist covering both local and remote stable refs.

Document fast-forward preparation, publishing master/stable/the version tag together, and comparing remote branch tips with the peeled tag. Keep stable on release commits rather than later development commits, exclude prereleases, and prohibit moving published tags. This documentation change does not publish or move release refs.

Validation: reviewed commands against the repository branch and annotated-tag workflow; git diff --check passed. No runtime code changed.

Assisted-by: GPT-6
2026-10-27 11:11:44 +00:00

7.2 KiB

Publish releases with native Nix CI

All CI workflows and their support files live in .ngit/nix/workflows/. default.nix groups commands by workflow and job; the root flake exports them as ngit-ci.commands, so YAML selects each command by its job and step names. A v* tag push runs release.yaml: a pure static-musl archive build and a container layout build, followed by OCI publication, a pull-and-run verification, and NIP-82 publication. The publisher copies the exact predecessor archive/layout; it does not rebuild release products. The container builder preserves the repository Dockerfile and scripts/build-container-layout.sh. Its base-image pulls retain that Dockerfile's existing update policy.

Publisher and container jobs begin with explicit tool builds. Their closures are shared through the provider cache before runtime commands start; credentials are used only by the publisher step. These commands pin Git, Python, ngit, Cargo and container tooling. The separate release-ngit input must support both release and container publication; checks.x86_64-linux.release-cli verifies its required flags. Checks and publishers share .ngit/nix/workflows/release-ngit.nix: on x86_64 Linux it installs the SHA-256-pinned static v3.0.3 release archive, avoiding a lengthy LTO rebuild in every fresh guest. The upstream source package disables its own tests, so recompiling it added no CLI test coverage. Other platforms retain the pinned source package. The archive pin trusts the upstream release build; it is not a claim of locally reproduced binaries. Keep the archive version/hash and source fallback revision aligned when updating this tooling. Container build and verification jobs reuse the deployment test's job-owned Docker daemon. They require a disposable root VM with the kernel facilities listed in their workflow requirements, and clean up the daemon on exit.

Rust CI, archive builds, container builds and Cargo publication require at least 4 GiB of guest RAM, including container verification. These are provider-selection floors, not memory caps.

Review the provider summary at the top of the final job log when tuning these minimums: observed guest RAM includes the guest OS, and swap activity or OOM kills signal pressure. Compare cold dependency builds with cached runs and leave headroom; a successful run alone does not measure peak memory.

Configure the repository NGIT_PUBLISHER_NBUNKSEC secret. Only the OCI and NIP-82 publisher jobs request it, and pass its private $NGIT_CI_SECRETS_DIR/NGIT_PUBLISHER_NBUNKSEC file directly to ngit. No secret value enters a Nix derivation, output artifact, or command argument. The existing .ngit/release.yaml and .ngit/containers.yaml retain the publication metadata, relays and Blossom servers. Archive contents are reproducible with a fixed tar timestamp; the NIP-82 release date comes from the actual Git tag.

Every release command requires an explicit tag context and fetches that tag from the Nostr repository. Ordinary tag pushes check the Cargo version and both the checkout and peeled tag against the signed NGIT_CI_COMMIT_SHA. A moved tag fails before publication. Branch or pull-request contexts cannot publish.

Stable release checklist

  1. Finish implementation changes and their changelog entries before the release commit. Run CI on the exact implementation commit and require successful Rust CI and container deployment checks; a successful push is not a CI result.
  2. Prepare the final version commit: update Cargo.toml, the root package entry in Cargo.lock, and nix/module.nix; the flake derives its version from Cargo. Move Unreleased notes into a dated release section, with Security before Fixed and any operator security guidance under Security. Validate locked Cargo metadata, Nix module parsing, formatting, and the diff.
  3. Prepare an annotated version tag and fast-forward the local stable branch to that same commit. Inspect branch/worktree state first; if stable has diverged, resolve that explicitly instead of forcing it. If further fixes are needed before publication, rerun CI and keep the release commit last.
  4. When publication is authorized, push all three refs: master, stable, and the exact version tag. A tag push starts release publication.
  5. Verify the remote branches and peeled tag point to the intended release commit, then check the release and crate publication workflows separately. Do not report publication complete just because Git accepted the refs.

For a new stable release, with a clean tree on master at the reviewed release commit, replace vX.Y.Z with the version being released:

release_tag=vX.Y.Z
# Check local and remote branch state before preparing the refs.
git fetch origin
git status -sb
git worktree list
git tag -a "$release_tag" -m "ngit-grasp $release_tag"
git switch stable
git merge --ff-only master
git switch master

# All three must resolve to the same commit before publication.
git rev-parse master stable "${release_tag}^{commit}"

# Run only when publication is authorized.
git push origin master stable "refs/tags/$release_tag"
git ls-remote origin refs/heads/master refs/heads/stable \
  "refs/tags/$release_tag" "refs/tags/$release_tag^{}"

An annotated tag's direct ref identifies the tag object; compare its peeled ^{} result with the branch commit IDs. If the local tag was already prepared, verify it instead of recreating it. Never move a published version tag. This is one push invocation, not a guarantee of atomic updates across Nostr relays and Git servers: inspect any partial failure and finish only the missing updates.

stable tracks the latest stable release commit, not subsequent development or documentation commits on master. Prerelease tags do not advance stable.

Manual container backfill

Replay .ngit/nix/workflows/container_backfill.yaml from a commit containing this workflow with an explicit refs/tags/v<version> context. The current reviewed command fetches only that historical tag, validates its Cargo version, and extracts its source into the job's private workspace. It builds and publishes only the exact OCI version tag, preserving latest and prerelease aliases, then pulls and verifies the image. It does not publish a crate or NIP-82 release.

This is the deliberate exception to ordinary release source matching: the signed allocation commit supplies the workflow, tools and publisher while the named historical tag supplies the Dockerfile and build context. A separate predecessor provenance artifact records the version, ref, historical source commit and allocation commit. The publisher refetches the tag and checks that record before copying the layout and publishing. Historical source never supplies the credentialed publisher code. Tags are neither moved nor created.

Crates.io publication remains independent; see publishing the crate. For nonpublishing validation run:

nix build .#checks.x86_64-linux.release-cli .#checks.x86_64-linux.release-scripts

These checks inspect CLI compatibility and exercise release guards, secret handling and artifact fan-in using fake external commands. They do not publish or perform a full Docker release run. Providers must allow enough predecessor artifact bytes for the OCI layout (including extracted archive transport).