8.3. Release process

SPEAR follows the branch-per-release model of its sibling projects, SO3 first among them: development happens on main, and every minor version gets a long-lived maintenance branch on which patch releases are tagged. A deployment — a workstation, a GPU host, a container image — pins a line, not a commit, and a fix to the line it runs can ship without everything main has gained since.

8.3.1. Overview

Three git objects work together, and GitHub surfaces them in different places:

Object

Example

Role

main branch

main

Continuous development (the next, unreleased version).

release/vX.Y branch

release/v0.2

Long-lived maintenance line for a minor version. Patch fixes land here and are tagged.

Tag vX.Y.Z

v0.2.0

Immutable point marking a delivered version. Release candidates use the -rcN suffix (v0.3.0-rc1).

GitHub Release

“SPEAR v0.2.0”

The release page (notes + assets), attached to a tag. Exactly one is flagged Latest; -rcN tags are published as pre-release.

8.3.2. Versioning

Versions follow semantic versioning vMAJOR.MINOR.PATCH. What a deployment relies on is SPEAR’s operator interface — the command line of spear-chat and the other entry points, the projects.json registry, machine.env and the other configuration files, the layout of the state directory, and the format of the standard store — so that is what the numbers are about:

  • MAJOR — breaking changes to that interface: an option removed or with a new meaning, a configuration key renamed, a state or store format that an existing installation can no longer read.

  • MINOR — new features, backward compatible (a new command, backend, guard, or configuration key with a default). Opens a new release/vX.Y branch.

  • PATCH — bug fixes only, cut on an existing release/vX.Y branch.

The served model is not part of the version: it is chosen per deployment (client/active-model.conf, machine.env) and changes without a release.

Release candidates append -rcN, numbered from 1 for each version, e.g. v0.3.0-rc1 then v0.3.0-rc2.

8.3.3. Which release is running?

Every entry point prints the release it belongs to when it starts, on stderr, once per invocation — also when it runs another one (spear-image runs the scripts under scripts/docker, spear-chat re-executes itself under systemd-run):

[spear v0.3.0-rc1] spear-chat --reds --auto

The version comes from scripts/spearversion.sh, which reads the latest v* release tag (git describe) and keeps only the base version: a tagged commit and development on top of v0.3.0-rc1 both report 0.3.0-rc1; the -rcN suffix is kept. A tree without git metadata — a tarball, a container image — falls back to the SPEAR_VERSION_FALLBACK constant of that script. spearversion.sh can also be run on its own. The documentation derives its version from the same helper.

8.3.4. Branch layout

main ──●──●──●──●──●──●───────►   development (next version)
        \
release/v0.2  ●──●──●             maintenance line for 0.2.x
              │  │  └─ v0.2.1     (tags live on the branch)
              │  └──── v0.2.1-rc1
              └─────── v0.2.0

While a minor line has not diverged from main yet (no work started on the next minor), its release/vX.Y branch and main may point at the same commit — that is expected.

8.3.5. Which fixes go on a release branch?

Being a bug fix is not what sends a change to a release branch. main is the continuous development line and carries both features and fixes as they land; anything committed there simply ships in the next version. There are two kinds of fix:

  • A fix for unreleased code, or one that can wait for the next version. Nothing special — it is an ordinary commit on main and ships in the next vX.Y.0. Do not touch any release branch.

  • A fix for an already-published version (e.g. a guard that lets through what it should refuse in v0.2.0) that must ship before the next version. Only this case uses the patch-release procedure below: the fix lands on release/v0.2 (tagged v0.2.1) and is also carried to main so it is not lost at the next minor.

In other words, what sends a change to release/vX.Y is the need to patch a live, already-released version — never the mere fact that it is a fix rather than a feature. A documentation-only change does not justify a patch release either: it rides the next version.

8.3.6. Cutting a patch release (vX.Y.Z)

Patch fixes are committed on the release branch, then tagged. If a fix was first merged into main, cherry-pick it onto the branch rather than fast-forwarding.

git checkout release/v0.2
git cherry-pick <sha>          # or commit the fix directly
# bump SPEAR_VERSION_FALLBACK in scripts/spearversion.sh to 0.2.1

# optional: publish a candidate first
git tag -a v0.2.1-rc1 -m "spear v0.2.1-rc1"
git push origin release/v0.2 v0.2.1-rc1
gh release create v0.2.1-rc1 --title "SPEAR v0.2.1-rc1" \
    --target release/v0.2 --prerelease --notes-file <notes>

# final release
git tag -a v0.2.1 -m "spear v0.2.1"
git push origin release/v0.2 v0.2.1
gh release create v0.2.1 --title "SPEAR v0.2.1" \
    --target release/v0.2 --latest --notes-file <notes>

8.3.7. Cutting a new minor release (vX.Y.0)

When main is ready for a new minor version, prepare it on main first (a commit carrying the CHANGELOG entry, the Maintained versions table and SPEAR_VERSION_FALLBACK set to the version being cut), then branch off and tag:

git checkout main
git checkout -b release/v0.3
git push -u origin release/v0.3

git tag -a v0.3.0 -m "spear v0.3.0"
git push origin v0.3.0
gh release create v0.3.0 --title "SPEAR v0.3.0" \
    --target release/v0.3 --latest --notes-file <notes>

The release notes are the version’s CHANGELOG entry.

A minor line may open with release candidates, as v0.3.0-rc1 did. The release commit on main then sets SPEAR_VERSION_FALLBACK to the candidate (0.3.0-rc1) and its Maintained versions row to Release candidate; the branch is created the same way, and the tag and Release carry the candidate name, published as a pre-release:

git tag -a v0.3.0-rc1 -m "spear v0.3.0-rc1"
git push origin release/v0.3 v0.3.0-rc1
gh release create v0.3.0-rc1 --title "SPEAR v0.3.0-rc1" \
    --target release/v0.3 --prerelease --notes-file <notes>

A later candidate, and the final vX.Y.0, are tagged on release/vX.Y like a patch release, with the fallback bumped in the commit they point at.

8.3.8. After tagging: propagate the release to main

A release is not finished when the tag is pushed. The references to the current version that live on main must be bumped right after every release (they drift silently otherwise):

  • the Maintained versions table in README.md (the Latest release column of the line, and the Status column when a new minor line starts);

  • a CHANGELOG entry summarizing the release (same content as the GitHub Release notes, kept in the repository for offline reference);

  • SPEAR_VERSION_FALLBACK in scripts/spearversion.sh, used only when the tree carries no git metadata.

Two version strings need no action: the entry points’ release banner and the documentation version both derive from the release tag (scripts/spearversion.sh, reused by doc/source/conf.py).

8.3.9. Before tagging: check the tree

The documentation workflow runs on every push to main and on pull requests, and must be green on the commit the tag will point at:

gh run list --workflow documentation --branch main

SPEAR has no test workflow of its own — the suite needs the project’s virtual environment and, for its sandbox tests, a host with bubblewrap and delegated cgroups — so it is run by hand, on the commit being tagged, from client/:

PYTHONPATH=. ./bin/python -m unittest discover -s tests -p "test_*.py"

It must pass in full; a test that only fails because an optional host (the remote embedding server) is unreachable is re-run once that host is back, not waved through. The strict documentation build must be clean as well:

cd doc && make clean && \
    sphinx-build -W --keep-going -b html -d build/doctrees source build/html

Check SPEAR_VERSION_FALLBACK too: it must already read the version being tagged, since the tagged tree is what a gitless copy reports. It appears twice in this page on purpose — bump it in the release commit set so the tag is right, and again when propagating to main so the next release does not start a version behind.

8.3.10. Rules of thumb

  • One release/vX.Y branch per minor, not per patch — patches are tags on the branch.

  • Tags are immutable: never move or delete a published vX.Y.Z tag. To correct a release, cut the next patch.

  • Exactly one GitHub Release carries the Latest flag; every -rcN Release is a pre-release so it never shadows the latest stable version.

  • Once a release/vX.Y branch has diverged from main, backport fixes with git cherry-pick — do not fast-forward the branch onto main.