Skip to content

CI/CD

Status: Accepted

The pipeline, the release process, and the checks that gate a merge.

Satisfies: roadmap M10, E2, GOV-R enforcement.


flowchart LR
pr[PR] --> spec[spec-check]
spec --> fmt[rustfmt]
fmt --> lint[clippy strict]
lint --> arch[arch tests]
arch --> unit[unit + golden]
unit --> integ[integration mocked]
integ --> sec[secret scan + cargo-deny]
sec --> e2e{Docker available?}
e2e -->|yes| boot[boot forms]
e2e -->|no| done[pass]
boot --> done

Ordered cheapest-first so a formatting failure doesn’t wait on a compile. Every stage blocks merge.

Stage Gate
spec-check Citation present, resolves, spec merged first (GOV-R)
rustfmt Formatting (Q-R19)
clippy The full strict set; warnings are errors (Q-R12)
arch tests Boundaries, no #[allow] in src/, comment policy (Q-R13, Q-R7)
unit + golden Logic and command construction (Q-R23)
integration Mocked Docker and service APIs
secret scan No credential in any tracked file
cargo-deny Licences, advisories, banned/duplicate deps
coverage 100% of applicable lines (cargo-llvm-cov, Q-R61)
SonarCloud Analysis ingested; zero open issues enforced in CI (Q-R64)
e2e (conditional) Boot forms where Docker is present

The comment policy runs as an arch test, and — critically — against its fixture tree (Q-R7): it must find each planted violation and pass the compliant control. A comment gate that only runs over production source passes vacuously on a young repo. Running it over deliberate violations is what proves it still works.

Supply chain is a real threat (security), enforced not exhorted:

Check Fails on
advisories A dependency with a known RUSTSEC advisory
licenses A dependency licence outside the allow-list
bans A banned crate, or a telemetry-carrying one (G8-R11)
sources A dependency from an unapproved registry

G8-R11 — dependencies must not introduce telemetry — is checked here rather than hoped for.

Runs over all tracked files including tests (Q-R29). A real key in a fixture is a leak whatever the intent. This is defence-in-depth behind the allow-list redaction (C4) — the redaction protects the operator’s secrets; this protects the project’s.

SonarCloud — enforced in CI, not by the plan gate

Section titled “SonarCloud — enforced in CI, not by the plan gate”

Code quality and coverage run through SonarQube Cloud, which ingests the cargo-llvm-cov report (Q-R52, Q-R61). But the free plan cannot set the quality gate to this project’s standard: its gate is fixed around an 80% new-code coverage default and cannot be configured to require 100% coverage or zero open issues. Relying on Sonar’s own gate would let a sub-standard change merge.

So the standard is enforced in CI directly, independently of the Sonar plan gate, and both checks block the merge:

Enforced in CI How Fails on
Coverage cargo-llvm-cov --fail-under-lines 100 over the applicable set Any applicable line uncovered (Q-R61)
Issues A step that reads the summary SonarCloud posts on the PR when its analysis finishes Any open issue — bug, vulnerability or code smell (Q-R64)

The issues check blocks on a counted issue and on nothing else. A summary that never arrived, and one whose shape the check no longer understands, are the analysis’s problem or the check’s own; blocking a contributor’s pull request on either would punish the wrong person, and — worse — an unreadable summary that fails looks exactly like a summary that failed, so the real finding hides behind the noise. Both cases warn instead, in the log and in the verdict comment, which says plainly that Q-R64 is not being enforced until someone fixes the check.

It also waits for a summary of the commit under test. SonarCloud edits one comment in place, so after a push the previous analysis’s comment is still there with its old count — and a check that reads the first count it finds fails the push that fixed the issue it is reporting, then passes on a manual re-run. A re-run as the remedy is the bug wearing a hat, so the comment is believed only once it is newer than the commit it describes.

This is the documented cap Q-R63 calls for. Reason: the free plan’s gate is not configurable. Lift condition: a paid plan or self-hosted SonarQube whose gate can be set to 100% coverage and zero issues — at which point Sonar’s gate and the CI checks say the same thing and the CI issue check becomes belt-and-braces rather than the enforcement.

The issue check reads what the analysis already reports rather than asking the SonarCloud API for it: SonarCloud posts a summary on the PR when its run finishes, and the CI step reads that summary and fails on any open issue — turning “Sonar found something” from advisory into blocking without a second credential or a separate query. Where the scan did not run there is no summary to read, and the check does not fail for something it could not observe.

Two kinds of run reach it that way. A pull request from a fork is given no secrets at all. A pull request Dependabot opened reads secrets from the Dependabot store rather than the Actions store, and SONAR_TOKEN is not in it — so a CI-driven scan there receives an empty token and can only fail, on a step that has nothing to do with the bump, holding a required check red on every dependency update. The scan is skipped on those runs instead, keyed on pull_request.user.login — the field Q-R55 already keys on, and the only one the pull request’s opener cannot write.

Neither run changes what the coverage gate measures: it needs no token and still runs, so Q-R61 is enforced on a dependency update exactly as it is elsewhere. Both leave Q-R64 unenforced, and both say so — in the log, and in the verdict the gate writes to the run summary as well as to the pull request. A skipped scan reports itself under its own name too, because a step missing from a log reads the same as a step that ran and found nothing.

A tagged release on lemonfiber triggers the three-platform build:

flowchart TD
tag["tag v0.4.0"] --> build[cargo-dist build matrix]
build --> mac["macOS<br/>aarch64 + x86_64"]
build --> lin["Linux<br/>gnu + musl"]
build --> win["Windows<br/>x86_64"]
mac & lin & win --> art[Signed artifacts + checksums]
art --> gh[GitHub Release]
art --> tap[Regenerate homebrew-tap formula]
art --> inst[Shell + PowerShell installers]
Output For
Per-platform archives + checksums Direct download, and every installer
homebrew-tap formula brew (homebrew-tap)
install.sh / install.ps1 curl | sh, irm | iex
Signed release Integrity

No web build runs here. The app is compiled in lemonfiber-web’s CI and carried as a pinned submodule, so the Rust build sees files (ARCH-R19, ADR-0012).

Not “it compiles” — actually run. The release matrix builds all targets; a smoke-test job runs the binary on macOS, Linux and Windows (roadmap M10 exit). A binary that builds for Windows and panics on first launch has been tested for the wrong thing.

lemonfiber-media-stack and homebrew-tap pipelines

Section titled “lemonfiber-media-stack and homebrew-tap pipelines”
  • lemonfiber-media-stackspec-check, then the structural checks in its repo spec. No stack boot in CI (no credentials).
  • homebrew-tap — the shared gates only. It holds one generated file and no formula-specific check yet.

Every repo: required checks must pass, spec-check among them, before merge (roadmap M0.5). The override bypasses spec-check only, never the build, tests, or review (GOV-R19).

ID Requirement
Q-R30 PR CI MUST run spec-check, format, strict clippy, arch tests, unit, golden, integration, secret scan, cargo-deny, the coverage gate and SonarCloud analysis, all blocking.
Q-R31 The comment and boundary arch tests MUST run against their fixture trees, not only production source.
Q-R32 cargo-deny MUST fail on advisories, disallowed licences, banned crates, and telemetry-carrying dependencies.
Q-R33 Secret scanning MUST cover all tracked files.
Q-R34 Releases MUST build macOS (arm64 + x86_64), Linux (gnu + musl) and Windows, with checksums.
Q-R35 The release MUST regenerate the Homebrew formula and produce shell and PowerShell installers.
Q-R36 A release smoke test MUST run the binary on all three platforms, not merely build it.
Q-R37 Every repository in the org MUST require passing checks before merge; the override MUST bypass only spec-check.
Q-R64 Open SonarCloud issues MUST be zero, enforced as a blocking CI check independent of the Sonar plan’s own quality gate, since the free plan’s gate cannot be configured to this standard (Q-R63).
Q-R67 A repository that has not yet reached zero MUST declare what it still carries, in its own workflow, as a number that MUST NOT increase.

Q-R64 names a number the shared gate did not read for months: it counted the new issues on a pull request, and new is not open, so findings that predated the gate were invisible to it. Sixty-one of them accumulated across the fleet under checks that were green throughout.

The gate reads both now. New issues on a pull request are the contributor’s and block immediately. The open total is the repository’s, and blocks when it rises above the number that repository declares — allowed-open in its own sonar.yml, in the tree, readable without a SonarCloud login.

That declaration is a ratchet and not an exemption, and the gate enforces it rather than trusting it. A pull request runs the workflow file its own head declares, so the diff that brings the issues could raise the number that permits them; the gate reads the same declaration on the base branch and refuses any run whose number is higher, a first declaration where there was none included. A base it cannot read is refused too wherever the run declares anything above zero — not knowing whether the ratchet held is not the same as it having held — and zero needs no comparison, since no count is below it. The gate also says so in its verdict when the true count is below the declaration, and a repository that declares its backlog rather than reducing it is failing Q-R67 whatever the check reports.

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