|
root / docs / superpowers / specs / 2026-05-05-versioning-and-release-tarball-design.md
2026-05-05-versioning-and-release-tarball-design.md markdown 216 lines 10.7 KB

Versioning and Release Tarball — Design

Date: 2026-05-05
Status: Approved (brainstorm complete); implementation plan to follow
Scope: Establish a single canonical version for the zyginit suite (zyginit + zygctl + sysv-wrapper), wire --version flags into all three binaries, and add scripts to bump the version and produce a source release tarball for vendoring into Hammerhead.

1. Goal

Hammerhead consumes zyginit as a vendored source drop into hammerhead-userland (per docs/SMF_MIGRATION.md). To make that drop reproducible and auditable, zyginit needs:

  1. A single canonical version, tracked in source.
  2. A scripted bump that updates every place the version appears, in lock-step.
  3. A scripted release that produces releases/zyginit-<version>-source.tar.xz plus a .sha256 companion.
  4. A runtime --version flag on every shipped binary, so an installed Hammerhead system can answer "which zyginit am I running?".

This work intentionally mirrors Coral's scripts/make-release.sh pattern (single tarball, sha256, --transform-prefixed paths) — the Hammerhead team gets a consistent shape across Zygaena projects.

2. Non-goals

  • No CI/CD release automation. Bump and release are run by hand by a maintainer; no GitHub/Hg-server hooks, no signing, no upload step.
  • No reproducible-build flags (--mtime, --owner=0, --sort=name). Coral doesn't bother and Hammerhead's packaging step is what produces the OS-level reproducible artifact. Adding these is a trivial follow-up if needed.
  • No multi-tarball releases. The whole suite (zyginit + zygctl + sysv-wrapper) ships and versions together, in one tarball.
  • No CHANGELOG.md integration. The bump script doesn't touch a changelog. (We can add this later; there is no changelog file today to integrate with.)
  • No GPG signing. Hammerhead handles signing at the OS package layer.

3. Versioning policy

Semver: MAJOR.MINOR.PATCH.

Bump Reason
MAJOR Incompatible socket protocol change, incompatible service-definition (TOML) schema change, removal of a published zygctl subcommand
MINOR Backward-compatible additions: new service-definition fields with defaults, new zygctl subcommands, new runlevel features
PATCH Bug fixes only; no API or protocol changes

While zyginit is pre-1.0, breaking changes may land in MINOR per semver convention. Once 1.0 ships (target: when SMF removal from Hammerhead is complete and the suite has been running unmodified for one release cycle), strict semver applies.

4. Source of truth

The root reef.toml [package].version is canonical. Every other location is derived and regenerated by the bump script.

Today's drift inventory (rev 82, version 0.1.0) — the bump script must keep these all consistent:

# Location Form
1 reef.toml (root) version = "0.1.0" (canonical)
2 tools/zygctl/reef.toml version = "0.1.0"
3 src/version.reef (new, generated) pub fn VERSION(): string returning "0.1.0"
4 tools/zygctl/src/version.reef (new, generated) same
5 tools/sysv-wrapper/version.h (new, generated) #define ZYGINIT_VERSION "0.1.0"

Generated files (version.reef × 2 and version.h) are committed to the repository so a tarball recipient can build without rerunning the bump script. They carry a "Generated by scripts/bump-version.sh — do not edit by hand" comment.

5. scripts/bump-version.sh

Usage: scripts/bump-version.sh <new-version>

Behavior:

  1. Validates <new-version> matches ^[0-9]+\.[0-9]+\.[0-9]+$. Refuses if not.
  2. Reads current version from root reef.toml. Refuses if <new-version> equals current.
  3. Rewrites all five locations listed in §4.
  4. Prints a summary diff (hg diff --stat) and a suggested commit command:
    $ hg ci -m 'Bump version to <new-version>'
    
  5. Does not auto-commit. The maintainer reviews and commits by hand.

Generated file templates:

src/version.reef (and identical-content tools/zygctl/src/version.reef):

/* SRCHEADER... */

module version

// Generated by scripts/bump-version.sh — do not edit by hand.
pub fn VERSION(): string
    return "0.1.0"
end VERSION

end module

tools/sysv-wrapper/version.h:

/* SRCHEADER... */
/* Generated by scripts/bump-version.sh — do not edit by hand. */
#ifndef ZYGINIT_VERSION_H
#define ZYGINIT_VERSION_H
#define ZYGINIT_VERSION "0.1.0"
#endif

Both templates use the project's standard SRCHEADER.txt header. The bump script fills in ${project} from each subproject's own reef.toml [package].name (i.e., zyginit for src/version.reef, zygctl for tools/zygctl/src/version.reef), ${file.name} from the filename, and ${file.description} with a fixed string ("Generated version constant").

6. scripts/make-release.sh

Usage: scripts/make-release.sh [version-override]

Behavior (mirrors Coral's make-release.sh):

  1. Reads version from root reef.toml, or uses [version-override] if provided.
  2. Creates releases/ if missing.
  3. Produces releases/zyginit-<version>-source.tar.xz via tar --transform "s,^,zyginit-<version>-source/," -cJf ....
  4. Generates releases/zyginit-<version>-source.tar.xz.sha256.
  5. Prints final paths and the sha256.

Tarball contents (top-level paths, --transform-prefixed into zyginit-<version>-source/):

Include Why
reef.toml Build manifest (carries the canonical version)
README.md Project orientation
ROADMAP.md Forward-looking roadmap
CLAUDE.md Project context (useful even outside Claude — accurate in-tree docs)
SRCHEADER.txt Source-header template (referenced by all source files)
src/ Main daemon Reef source
docs/ Architecture docs (DESIGN.md, SMF_MIGRATION.md, etc.)
tests/ Unit + integration tests
services/ Example + Hammerhead service TOMLs (the v2 BSD-pivot tree)
tools/ zygctl/, sysv-wrapper/ (subprojects)
utils/ contract_probe and any other utility tools
scripts/ bump-version.sh, make-release.sh, install-to-be.sh

Tarball excludes:

Exclude Why
build/ (root and any tools/*/build/) Build artifacts
releases/ Output of this very script — don't ship past tarballs
.hg/, .hgignore, .hgtags VCS metadata
*.o, *.a Compiled object files
*.swp Editor cruft
resume/ Local scratch / resume notes (per user instruction)
ss/ Local scratch (per user instruction)
stub/ Old Rust-like design archeology, kept for reference only (per CLAUDE.md)

tar's --exclude patterns are passed in for each item. The exclusion of tools/*/build/ is critical — the global build/ exclude only covers the repo-root build dir.

7. Code changes (one-time)

These ride along with the introduction of the bump/release scripts:

File Change
src/main.reef:67-69 Drop the inline fn VERSION(): string definition; import version and use version.VERSION()
src/socket.reef:248 Replace literal "zyginit 0.1.0\n" with "zyginit " + version.VERSION() + "\n"
tools/zygctl/src/main.reef:54-57 Replace literal "zygctl 0.1.0" with "zygctl " + version.VERSION()
tools/zygctl/src/main.reef Add import version
tools/sysv-wrapper/wrapper.c Add --version / -V handling: prints "<argv[0]-basename> <ZYGINIT_VERSION>\n" and exits 0. #include "version.h".
tools/sysv-wrapper/Makefile Add version.h to the dependency list of wrapper.o

The argv[0]-basename trick in sysv-wrapper means halt --version prints halt 0.1.0, reboot --version prints reboot 0.1.0, etc. — consistent with how each personality identifies itself in normal output.

8. Output format

A single line, <binary> <version>, exit 0. Matches Coral and the GNU/BSD common case:

$ zyginit --version
zyginit 0.1.0
$ zygctl --version
zygctl 0.1.0
$ halt --version
halt 0.1.0

zygctl version (no dashes — already a subcommand) continues to work and prints the same line. Both zygctl --version and zygctl version are strictly local — they print zygctl's compiled-in version and never round-trip the socket. This is the standard Unix convention and keeps the answer trustworthy when the daemon is wedged.

Querying the running daemon's version (useful during a partial upgrade) is an existing socket command (socket.reef:247) but is currently unreachable from zygctl. Wiring up a separate zygctl daemon-version subcommand to surface it is out of scope for this design — see §10.

9. End-to-end workflow

Maintainer cuts release 0.2.0:

$ scripts/bump-version.sh 0.2.0
zyginit: bumping 0.1.0 -> 0.2.0
  rewriting reef.toml
  rewriting tools/zygctl/reef.toml
  regenerating src/version.reef
  regenerating tools/zygctl/src/version.reef
  regenerating tools/sysv-wrapper/version.h
done. review with `hg diff` and commit:
  hg ci -m 'Bump version to 0.2.0'

$ hg diff --stat
 reef.toml                          | 2 +-
 tools/zygctl/reef.toml             | 2 +-
 src/version.reef                   | 2 +-
 tools/zygctl/src/version.reef      | 2 +-
 tools/sysv-wrapper/version.h       | 2 +-
 5 files changed, 5 insertions(+), 5 deletions(-)

$ hg ci -m 'Bump version to 0.2.0'

$ scripts/make-release.sh
Creating release: zyginit-0.2.0-source
Created: releases/zyginit-0.2.0-source.tar.xz
-rw-r--r-- 1 ctusa ctusa 1234567 May  5 15:30 releases/zyginit-0.2.0-source.tar.xz
Checksum: releases/zyginit-0.2.0-source.tar.xz.sha256
abc123...  releases/zyginit-0.2.0-source.tar.xz

Hammerhead then consumes the tarball + sha256 into hammerhead-userland, per the integration steps in docs/SMF_MIGRATION.md.

10. Open questions / future work

  • scripts/install-to-be.sh already exists. Out of scope for this design — it's an in-place dev install, not a release artifact path.
  • Reproducible tarballs (--mtime, --sort=name, --owner=0, --group=0). Add when the Hammerhead build needs byte-identical tarballs across maintainer machines.
  • CHANGELOG.md — add when there's enough release history to warrant one. The first one or two release notes can live in commit messages.
  • GPG signing — Hammerhead handles this at the OS package layer; no zyginit-side signing planned.
  • Pre-release versions (e.g., 0.2.0-rc1). Not supported by the regex in §5. Add when needed.
  • zygctl daemon-version subcommand. The socket already exposes a version command (socket.reef:247) that returns the running daemon's version, but no client surfaces it. Add a zygctl daemon-version (or similar) subcommand to make partial-upgrade diagnosis easy. Until then, socket.reef's version handler stays as accessible-but-unused (an echo version | nc -U /var/run/zyginit.sock still works).