Releasing
Status: Accepted
How a release happens — the one page that answers “I want to cut a version, what do I do and what happens?” The version model lives in versioning; this is the process.
Satisfies: roadmap M10, E1, E2.
Each repo releases on its own clock
Section titled “Each repo releases on its own clock”The implementation repos are the independent release streams — that’s the point of the
decoupling. (Distinct from the three version identifiers the binary and manifest
carry — lemonfiber, stack_version, schema_version — defined in
versioning; a “stream” is a repo
that tags, a “version” is a number it carries.)
| Repo | Release trigger | Produces |
|---|---|---|
lemonfiber |
git tag vX.Y.Z |
Binaries, installers, updated formula, changelog, docs |
lemonfiber-media-stack |
git tag vX.Y.Z |
A pinned stack the binary can embed; stack_version moves |
lemonfiber-web |
git tag vX.Y.Z |
A pinned web surface the binary embeds at build time |
sdk-ts |
git tag vX.Y.Z |
@lemonfiber/sdk-ts published to npm. Not yet tagged; consumers pin a commit |
sdk-php |
git tag vX.Y.Z |
The PHP client, published to Packagist. Not yet tagged |
brand |
git tag vX.Y.Z |
@lemonfiber/brand published to npm. Not yet tagged; consumers pin a commit |
homebrew-tap |
— | Never released directly; regenerated by lemonfiber’s pipeline |
A brand recolour does not force a binary release; a stack service bump does not force one either — the binary picks up a new embedded stack only when it is released with the submodule re-pinned.
Cutting a lemonfiber release
Section titled “Cutting a lemonfiber release”The binary is the one with real release engineering. The flow is tag-triggered — everything downstream is automatic:
flowchart TD tag["git tag v0.4.0 && push"] --> plan[cargo-dist plan] plan --> build[Build matrix:<br/>macOS arm64+x86 · Linux gnu+musl · Windows] build --> smoke[Smoke test: run the binary on each OS] smoke --> art[Signed artifacts + checksums] art --> rel["Draft release: installers, artifacts, attestations"] art --> tap[Regenerate homebrew-tap formula → PR/push] art --> inst[Shell + PowerShell installers] rel --> log[git-cliff: changelog from commits] log --> body["Release body: the changelog, then the installers"] body --> pub["A maintainer publishes"] pub --> docs[Docs site rebuilds on main]Concretely, a maintainer does three things; CI does the rest:
- Merge the version bump. The workspace’s
Cargo.tomlmust already declare the version being released, onmain, before anything is tagged —cargo-distreleases the tag whose version the workspace carries, so a tag cut over a stale one names a version nothing will build. This is a step in its own right rather than a thing to remember while doing the next one, because it is the one part of the sequence whose order matters and whose failure is silent until the release job runs.execute-versionrefuses to tag a repo that declares anything else, so getting it wrong costs a re-run rather than a deleted tag. - Dispatch
execute-versionwith the version,dry_runfirst. It checks the goal gate, cross-stream compatibility, release blockers and the declared version, then tags every repo the manifest names. A version left atreleasableblocks the next one from being staged, so this is not optional bookkeeping. - Review the drafted GitHub Release, and the homebrew-tap PR once that publish
job is on (
L1-R3), then publish. Publishing firesrelease-finalize, which records what shipped, pins the stack commit the release embedded, and regenerates the public roadmap.
Everything between step 2 and step 3 is cargo-dist and git-cliff. A draft is
published by a person on purpose: it is the one step that reaches the outside
world, and it is the last place to notice that what was built is not what was
meant.
The version-bump decision
Section titled “The version-bump decision”What moves, and when:
| You changed… | Bump |
|---|---|
| A bug fix, no behaviour change | binary patch |
| New behaviour, backward-compatible CLI/flags | binary minor |
A removed/renamed flag, changed default, dropped schema_version support |
binary major |
| A service image tag, a new service or form | lemonfiber-media-stack minor (or major if a form’s meaning changes) |
| A manifest field added-optional | schema_version unchanged, stack_version minor |
| A manifest field required/removed/retyped, or a new enum value | schema_version +1 (why) |
| A token value (recolour) | brand minor |
| A token renamed/removed | brand major |
The one that needs coordination is schema_version: bumping it means the next
lemonfiber release must support it, and build.rs will fail the build until the
embedded stack and the binary agree (ARCH-R6).
That failure is the safety net — an incompatible pairing cannot ship.
Every release is verifiable
Section titled “Every release is verifiable”Artifacts don’t just carry checksums — they carry provenance:
| Artefact | What it proves |
|---|---|
| SHA-256 checksums | The download wasn’t altered in transit |
| SLSA build provenance (attestation) | The binary was built by this pipeline from this commit, not tampered with |
Both are carried today: cargo-dist emits the attestation via GitHub’s artifact
attestations, so a consumer can verify a downloaded binary came from the real
pipeline. It also raises the OpenSSF Scorecard signal
the project already publishes.
OPS-R20 also obliges an SBOM (software bill of materials) per release, and
that half is not produced yet — answering “are we affected?” still means reading
Cargo.lock at the tag rather than a manifest of what went in.
The changelog is generated, not written
Section titled “The changelog is generated, not written”Commits follow a conventional-commit shape and carry Spec: trailers, so
git-cliff produces the changelog from history. Nobody maintains
CHANGELOG.md by hand — which also means a commit message is the release note,
so write it for a reader.
It is generated after the build, onto the draft the pipeline leaves, because
the pipeline is generated code: cargo-dist owns release.yml and rewrites it
whole, so the changelog cannot live inside it. The release body ends up as the
changelog first and cargo-dist’s install instructions second — the install
half is the same every time, and the announcement that fires on publish is the
body, so what changed is what a reader meets first.
Pre-1.0 versioning
Section titled “Pre-1.0 versioning”Until 1.0.0, the binary is 0.y.z: y moves for features, z for fixes, and
breaking changes are allowed within 0.x as the roadmap milestones land. 1.0.0
is cut when the M10 exit criteria are met — a non-contributor can install and run
on all three platforms from the README alone.
What a release must not do
Section titled “What a release must not do”| Never | Because |
|---|---|
| Ship a floating image tag in the embedded stack | Defeats reproducibility (E1-R1) |
| Publish artifacts without checksums | Integrity (Q-R44) |
| Release a binary that fails its own smoke test | “Builds” ≠ “runs” (Q-R36) |
Bump schema_version without the binary supporting it |
build.rs blocks it, but don’t force it |
| Overwrite the homebrew formula by hand | It’s generated (REPO-R24) |
The one-time setup
Section titled “The one-time setup”Releasing needs two things configured once (they can’t be scaffolded):
| Setup | Where |
|---|---|
A token for the pipeline to push to homebrew-tap |
lemonfiber repo secret |
npm publish auth for @lemonfiber/brand |
brand repo secret |
Both are documented as manual steps (Q-R60), like SONAR_TOKEN.
Requirements
Section titled “Requirements”| ID | Requirement |
|---|---|
| OPS-R1 | A lemonfiber release MUST be triggered by a version tag and MUST require no manual build steps beyond tagging and publishing the drafted release. |
| OPS-R2 | The release pipeline MUST regenerate the Homebrew formula and open a change against homebrew-tap; the formula MUST NOT be edited by hand. |
| OPS-R3 | The changelog MUST be generated from commit history, not hand-maintained. |
| OPS-R4 | A release MUST NOT proceed if the binary fails its cross-platform smoke test. |
| OPS-R5 | The four version streams MUST be releasable independently; one MUST NOT force a release of another. |
| OPS-R6 | Secrets required for releasing MUST be documented as one-time manual setup. |
| OPS-R20 | Every release MUST publish SLSA build provenance (an attestation) and an SBOM alongside the checksummed artifacts. |
Related
Section titled “Related”- staging.md — the spec-led, cross-repo release train this cut sits inside
- versioning — the version model this process applies
- ci-cd.md — the pipeline stages
- tooling.md — cargo-dist, git-cliff
- 30-repos/homebrew-tap.md — the generated formula
- roadmap M10
This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page