M7-10: Server release chain with a trial release (ADR 0036, 0060, 0062)¶
Status: ready | Depends on: M7-01 | Milestone: M7
Goal¶
After this ticket the component server can be released by a tag, and the whole chain is proven before anyone tags: release.yml builds bothy-server (cross compiled) and bothy-postgres (per architecture on its own runner, no emulation) for linux/amd64 and linux/arm64, joins each into one multi-arch manifest, scans, runs ScanCode over the tree, creates SBOMs (SPDX and CycloneDX per image and binary), signs every digest and SHA256SUMS (key pair without a transparency log while the repository is private, keyless with attestations when it is public, ADR 0062), writes checksums and only then sets the version tags on the images (18 6 and 7). A missing SBOM aborts; VERSION must equal the tag. The trial release (make release-trial COMPONENT=server, D-W-127) runs the same component targets on the build host against throwaway local targets: images into a local registry, signatures with a throwaway key pair, SBOMs and checksums into a temporary directory, then cosign verify --key and sha256sum --check; it never pushes to a public registry, never creates a GitHub release and never tags. make release-prep implements ADR 0060 with git-cliff as a pinned binary: the next version per component from Conventional Commits under its path, the beta counter, the changelog section renamed, VERSION written and the checklist of commits without a changelog entry. M7-10 also settles the tag form for the server module and corrects the rationale in 18 (the plan's risk on services/server/ tags). Why: every later release (product bundle M7-29, host service M7-23, plugin M7-26, apps M7-12) builds on this chain; signatures, SBOM and checksums are owner requirements, and the chain must have run once for real before the owner sets the first tag.
Read first¶
docs/spec/18-release.md(all: 2, 2.1, 3, 4, 5, 6, 7, 11, 13),docs/operations/release.md(all, section 5 gets the exact private-phase verification command).docs/adr/0036-releases-semver-per-component-signed-sbom-provenance.md,docs/adr/0060-release-automation-in-the-monorepo.md(all, "Confirmation"),docs/adr/0062-release-signatures-while-the-repo-is-private.md(all),docs/adr/0039-only-github-hosted-runners-public-from-the-first-push.md, ADR 0041 (pins)..github/workflows/release.yml(jobsresolve,gates,build,publish,portability),tools/ci/components.py(release,pins),tools/ci/check_runners.py(rules on release jobs and environments),tools/bin/tools.lock.jsonandmake tools-lock,docs/quality/toolchain.md,docs/quality/licenses.md(ScanCode before every release),docs/quality/public-switch.md(keyless switch),docs/quality/push-discipline.md.docs/RISKS.md(RK-57),docs/OPEN-QUESTIONS.md(OQ-12: placeholderOWNER).docs/DECISIONS.md: D-W-100 and its extension to M7 (D-W-125), the trial release (D-W-127), D-W-52, D-W-76, D-W-95, D-W-98, D-W-110, D-W-111; section 10 (format ofD-M7-10-<n>rows).- Briefs (merged names win, D-W-100):
M1-23-server-image.md(make image, Dockerfile rules,trivy-image),M7-01-postgres-image.md(make postgres-image, per-architecture build). - Code on the base branch:
Makefile(image,release-build,trivy-image,licenses,tools-lock),services/server/Dockerfile,services/server/images/bothy-postgres.
This ticket touches no table, no action and no event.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; the repository is writtenOWNER/bothy; no company, host or personal names. - Every tool is pinned with a checksum (
tools.lock.json) or by digest (images, actions); no moving tag. - Specification first: a change to the release rules updates
docs/spec/18-release.mdanddocs/operations/release.mdin the same change. The core contract is not edited by this ticket. Everything this ticket adds is English. - No cost (D-W-125): GitHub-hosted standard runners (free for public repositories) and local throwaway targets only.
- Never tag, never create a release, never publish to a registry (D-W-125). The trial release uses only local targets; the first real tag of
serveris an owner step after the merge.
Product requirements implemented here:
- Images are built per architecture without emulation (18 7 rule 2) and joined into one multi-arch manifest per image; the version tag is set only after the vulnerability scan and the ScanCode run passed.
- Every image digest, every binary and
SHA256SUMSis signed; in the private phase with the key pair and a signing config without Rekor, Fulcio and timestamp service, in the public phase keyless with attestations (github.event.repository.visibility); signing steps only in jobs withenvironment: release(ADR 0062 confirmation,make runners-checkholds it). - A missing SBOM, a
VERSIONother than the tag, or a missing changelog section aborts before anything is published. make release-prepfollows ADR 0060 items 1 to 5 exactly; it writes nothing on an error.
Scope¶
.github/workflows/release.yml: the server path buildsbothy-server(existing step) andbothy-postgresin a matrix ofubuntu-24.04andubuntu-24.04-arm, joins manifests, scans each digest (make trivy-image), runs ScanCode (Design note 3), creates SBOMs with Syft (pinned action or binary), signs (Design note 2), then tags;publishattaches SBOMs, checksums and signature bundles.- Component Makefile
services/server/Makefilewithrelease-build VERSION=<v> OUT=<dir>(binarybothyper platform, image builds through the root targets) as the component contract of 23 3 requires. Makefile: targetsrelease-prep(callstools/ci/release_prep.py) andrelease-trial(Design note 1).tools/ci/release_prep.pywith tests (tools/ci/tests/test_release_prep.py) against a fixture repository built in a temporary directory.tools/ci/release_trial.py(or a shell script undertools/ci/) with tests.- Pins: git-cliff, cosign and syft in
tools/bin/tools.lock.json(make tools-lock), the local registry image by digest;docs/quality/toolchain.mdrows andtest_toolchain_doc.pystay green; acliff.tomlper ADR 0060. tools/ci/check_runners.py: the addition of ADR 0062 confirmation (signing steps only in jobs withenvironment: release) with tests, if the rule is not there yet.- Public key file of the private phase: the path is fixed in the ruling (for example
quality/release/cosign-private-phase.pub); this ticket does not create the owner's real key pair (Design note 2). - Docs:
docs/spec/18-release.md2 (tag form rationale, Design note 4), 6 and 7 as built;docs/operations/release.md2.1 and 5 (exact commands for the private and the public phase, the trial release);docs/RISKS.mdRK-57 (mitigation now the trial release);CHANGELOG.mdandservices/server/CHANGELOG.mdunder[Unreleased];docs/DECISIONS.mdsection 10 rowsD-M7-10-<n>.
Out of scope¶
- The product bundle,
install.shandrelease-buildof the componentbothy(M7-29); the portability test (M7-14); app distribution (M7-12, M7-13); the host service release (M7-23); the plugin release (M7-26). - Creating the owner's signing key pair and the GitHub environment secrets (owner step; the runbook names it).
- Any real tag, release or registry push.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M7-10-server-release-chain; one writer per worktree. - Conventional Commits with the component as scope (for example
ci(release): ...,feat(tools): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag, never publish an image or a release, never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules). The local registry binds only
127.0.0.1on a random port and is removed at the end. - Stop only processes and containers you started, by PID or container ID. Temporary directories only via
mktemp -d, removed at the end. - Never read or print secrets; the trial key pair is generated in the temporary directory and deleted with it.
- Docker with buildx must be available for
make release-trial; run it once on the build host or stop withVerdict: BLOCKED docker missing. - Never lower a threshold, disable a gate or delete a test.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M7 by D-W-125). Where the merged code of an earlier ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Trial release (D-W-127).
make release-trial COMPONENT=server(other components join in their tickets): start a pinned registry container on127.0.0.1:0; build both images for both platforms (the build host may build the foreign platform only for the server binary image, which cross compiles;bothy-postgresfor the foreign platform comes from the CI job of M7-01, so the trial acceptsTRIAL_PLATFORMSand defaults to the host platform forbothy-postgres); push by digest; create the manifests; generate a throwaway cosign key pair; sign with the private-phase signing config; Syft SBOMs;SHA256SUMS; sign it; then verify everything as an instance operator would (cosign verify --key,cosign verify-blob,sha256sum --check) and print the digests. Negative cases in the tests: without an SBOM the trial aborts; a changed file fails the checksum; an unsigned digest fails verification. The target refuses to run whenGITHUB_ACTIONSis set, so it never runs with release credentials. -
Signing paths (ADR 0062). Private:
cosign signing-config createwithout default Rekor, Fulcio, TSA and OIDC;cosign sign --key env://...with the key from the environmentrelease; verification with--keyand--insecure-ignore-tlog(check the flags of the pinned cosign version and write the exact commands intorelease.md5). Public: keyless with the workflow identity andactions/attest. The trial proves the private path; the keyless path can only run in GitHub Actions with OIDC and is proven at the first real public release (report it as not proven). -
ScanCode (licenses.md 4). ScanCode Toolkit as a container pinned by digest over the tree; the report is a release artifact; a finding outside the allowed licenses of
quality/licenses/allowed.txtstops the release before tags. -
Tag form (18 2 rule 2; plan risk). Keep
server/vX.Y.Z(the module is a main module that is never loaded, P-05); correct the rationale text in 18 so that it states the Go rule as written in the source and why it does not apply; record the ruling. -
release-prep(ADR 0060). git-cliff--bumped-versionwith--include-pathper component and--tag-patternper prefix; before 1.0.0 a break bumps the minor;CHANNEL=betaappends-beta.Nwith N from existing tags; the changelog sectionUnreleasedbecomes[X.Y.Z] - <date>;VERSIONwritten; the checklist of commits of the component without a change to itsCHANGELOG.mdprinted. Fixture repository tests cover patch, minor and breaking change before and after 1.0.0, the beta counter, commits of other components, and a commit without a changelog change. -
Rulings for
docs/DECISIONS.md(D-M7-10-<n>): trial release shape (note 1), signing commands (note 2), ScanCode gate (note 3), tag form (note 4), the public key path.
Tests (write them first)¶
tools/ci/tests/test_release_prep.py: the cases of ADR 0060 confirmation (Design note 5).tools/ci/tests/test_release_trial.py: argument checks, refusal underGITHUB_ACTIONS, abort without an SBOM, checksum and signature negatives (with fakes of the tools where a container is not needed).tools/ci/tests/test_check_runners.py: signing steps outside a job withenvironment: releaseare a finding.- A test that
release.ymlsets version tags only after the scan and ScanCode steps (parse the workflow).
Commands that must pass, from the repository root:
uv run --project tools --locked pytest -q tools/ci/testsmake release-trial COMPONENT=serveron the build hostmake lint,make runners-check,make licenses,make docs,make linkcheckmake check
Acceptance criteria¶
-
cosign verifyandsha256sum --checkagainst the trial release are green; a missing SBOM aborts;VERSIONequals the tag in the workflow check. - The ScanCode report is an artifact; tags are set only after scan and ScanCode.
- The trial publishes every image that
infra/compose/compose.yamlnames with${BOTHY_VERSION}under exactly this version (bothy-server:<version>,bothy-postgres:pg18-<version>) in the local registry. -
make release-preppasses the tests of ADR 0060 confirmation against the fixture repository. - git-cliff, cosign and syft are pinned;
toolchain.mdmatches. - 18, the runbook, RK-57, the CHANGELOGs and the rulings are updated.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why.
docs/operations/release.mdnames the exact verification commands for both phases.- Rulings of Design note 6 recorded in
docs/DECISIONS.mdsection 10. CHANGELOG.mdentries under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make checkand ofmake release-trial COMPONENT=serverwith the digests. - Review and red team verdicts and the findings fixed.
- The exact verification commands of both phases as written into the runbook.
- What is not proven (keyless signing and attestations, a push to the real registry; both come with the owner's first tag).
- Owner steps before the first tag (key pair, environment secrets), each with a recommendation; follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.