CI/CD
Status: Accepted
The pipeline, the release process, and the checks that gate a merge.
Satisfies: roadmap M10, E2, GOV-R enforcement.
PR pipeline — lemonfiber
Section titled “PR pipeline — lemonfiber”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 --> doneOrdered 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 gate in CI
Section titled “The comment gate in CI”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.
cargo-deny
Section titled “cargo-deny”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.
Secret scanning
Section titled “Secret scanning”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.
Release — cargo-dist
Section titled “Release — cargo-dist”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).
Real cross-platform testing
Section titled “Real cross-platform testing”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-stack—spec-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.
Branch protection
Section titled “Branch protection”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).
Requirements
Section titled “Requirements”| 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. |
Reaching zero from a backlog
Section titled “Reaching zero from a backlog”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.
Related
Section titled “Related”- testing-strategy.md — what the test stages run
- security.md — why
cargo-denyand secret scanning matter - 50-governance/cross-repo-ci.md — spec-check
- roadmap M10
This page lives in another repository Rendered from lemonfiber/spec at 1d10402, 2026-09-09. Read the source of this page