Releasing Spexso
@flexso/spexso ships on two npm channels, both driven by git tags:
| Channel | Install | Tag form |
|---|---|---|
| Stable (default) | npm install @flexso/spexso | v1.6.0 |
| Experimental | npm install @flexso/spexso@experimental | v1.6.0-experimental.3 |
The git tag is the source of truth for the published version — CI stamps package.json from the tag at publish time, so package.json stays on the last stable version.
From the pipeline: prepare-release (stable only)
On any green main pipeline, run the manual prepare-release job. It tags the next stable version from the latest stable tag and pushes the tag, which starts the tag pipeline (release-gate → publish → gitlab-release → pages). The bump is inferred from the Conventional Commits since that tag: a breaking change (feat!: or BREAKING CHANGE) → major, any feat: → minor, otherwise (only fix:, docs:, chore: …) → patch — the same rule as npm run release. To override, add the variable RELEASE_BUMP=major|minor|patch when you run the job; the job log prints which bump it used and why. Experimentals are never cut from the pipeline — use npm run release for those.
It refuses when main's HEAD is already the latest stable tag, or when the target tag exists. If an experimental cycle is open (e.g. v1.8.0-experimental.2), a pipeline minor effectively promotes HEAD to v1.8.0; the next local experimental cut then starts the following cycle.
One-time setup: the job pushes with a project access token, because a tag pushed with CI_JOB_TOKEN does not start a pipeline. Create a project access token (role Maintainer, scope write_repository) and store it as CI/CD variable RELEASE_TOKEN (masked + protected). The token's bot user must be allowed to create the protected v* tags.
The easy path: npm run release
npm run release is an interactive helper (repo-only — not shipped to projects). It reads the git tags, proposes the right next tag, and creates + pushes it after you confirm:
- Cut experimental — increments the counter mid-cycle, or (at a new cycle) infers the bump from your commits and proposes
vX.Y.Z-experimental.1. - Promote to stable — turns the chosen experimental into the clean
vX.Y.Ztag and, when there is newer work onmain, also creates the re-baseline experimental tag for the next version (the bump — major/minor/patch — is inferred from the commits ahead).
It always shows a summary and asks before pushing, refuses if the tag exists or the target commit is not yet on origin/main, and aborts outside an interactive terminal. The manual commands below are the underlying mechanism if you prefer to tag by hand.
Cut an experimental release
After your feature is merged to main:
git tag v1.6.0-experimental.1
git push origin v1.6.0-experimental.1 # CI publishes → @experimentalThe tag pipeline also registers the GitLab Release (see Release notes).
Promote an experimental point to stable
Promotion is two tags. Say experimental .2 is good and you want it stable:
# 1. Tag the clean stable version on the chosen commit:
git tag v1.6.0 <commit-of-experimental.2>
git push origin v1.6.0 # CI publishes 1.6.0 → @latest
# 2. Re-baseline the experimental line to the next minor at HEAD:
git tag v1.7.0-experimental.1
git push origin v1.7.0-experimental.1 # CI publishes → @experimentalStep 2 is required: without it v1.6.0-experimental.3 sorts below stable v1.6.0, so @experimental would point behind @latest.
Release notes and changelog
Both are generated from the git tags — never hand-written, and nothing is committed:
- GitLab Release — the
gitlab-releasejob (afterpublish, every release tag) creates the Release with the commits since the previous stable tag, grouped likespexso changelog. An experimental's notes are therefore cumulative for its cycle. The job is idempotent: an existing Release is left alone. - Changelog page —
docs:build(anddocs:dev) regeneratesdocs/framework/changelog.mdfrom all stable tags (node scripts/release.js --history <file>; the file is gitignored).pagesalso runs on stable tag pipelines, so the site shows a release as soon as it ships.
The CHANGELOG.md that spexso init creates and /spexso-finish maintains belongs to a target project, not to spexso itself.
Hotfix a stable release
The only branch that lives longer than a feature branch:
git switch -c hotfix/1.6.1 v1.6.0
# fix, commit
git tag v1.6.1 && git push origin v1.6.1 # CI publishes → @latest
git switch main && git merge hotfix/1.6.1 # forward-merge the fix into the trunkGreen to ship — the release gate
A release tag only publishes after this bar (in order of authority). Level 1 is enforced by CI (release-gate job — publish depends on it); Levels 2–3 need live agent CLIs and are the operator's checklist half:
- Level 1 fully green —
npm test(unit + contract suites, dist-staleness, canary phrases, doc counts, manifest validation, install smoke). Hard fail, no exceptions; CI blocks the publish. - Level 3 on Claude Code (reference harness):
node test/parity/run.js --harness claude-code— P1–P5 pass; P6 is advisory. - Level 3 on opencode/Codex: no check regresses vs that harness's last-release scorecard (
parity-report.jsondelta — parity is a ratchet, not a day-one bar). - Level 2 loop probes:
node test/evals/run.js --probe 4,5 --runs 3— both terminate correctly 3/3 (the B1 endless-testing pathology as a permanent regression gate). Any candidate always-on rule must have beaten the current arm on these evals (median task success, ≤ +20% tokens).
Branching rules
mainis the trunk and is always publishable. Short-lived feature branches; merge fast.- Parallel features are fine only if they touch different files. Avoid editing shared index files (
docs/.vitepress/config.mts,CLAUDE.md,bin/spexso.jshelp) from two branches at once. - Never hand-write release notes — CI generates them from the tag range (same grouping as
spexso changelog).