Skip to content

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.


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.

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:

  1. Merge the version bump. The workspace’s Cargo.toml must already declare the version being released, on main, before anything is tagged — cargo-dist releases 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-version refuses to tag a repo that declares anything else, so getting it wrong costs a re-run rather than a deleted tag.
  2. Dispatch execute-version with the version, dry_run first. It checks the goal gate, cross-stream compatibility, release blockers and the declared version, then tags every repo the manifest names. A version left at releasable blocks the next one from being staged, so this is not optional bookkeeping.
  3. Review the drafted GitHub Release, and the homebrew-tap PR once that publish job is on (L1-R3), then publish. Publishing fires release-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.

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.

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.

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.

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.

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)

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.

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.

This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page