Skip to content

M7-10 Server release chain with a trial release

No Date Ruling Reason
D-M7-10-0 2026-10-15 The ticket was built on an integration base and ported onto main with only its own commits; the English docs and spec of main win. The runbook keeps main's layout: the server steps stand in release.md 2.1, the trial release in 2.3, the key pair of the private phase in 2.4, the verification commands in 5. On main the release workflow follows the stricter rules of tools/ci/check_runners.py (release jobs resolve the tag again, outputs of jobs without a release environment only in if:, artifacts only by id), so postgres-build-amd64, postgres-build-arm64 and scancode run in the environment release and resolve the tag themselves (D-M7-10-8). The tickets are built in parallel on integration bases; main takes each ticket as one pull request; merged rules win (D-W-100, D-W-114).
D-M7-10-1 2026-10-15 Trial release (Design note 1): make release-trial COMPONENT=server runs tools/ci/release_trial.py, which starts registry:3.1.2 pinned by digest on 127.0.0.1 with a random port, creates a docker-container buildx builder in the host network (so BuildKit reaches the registry on 127.0.0.1), runs make -C services/server release-build, pushes bothy-server (both platforms, one build) and bothy-postgres (one build per platform) by digest, joins the postgres digests, generates a cosign key pair in the temporary directory, and then calls the same tools/ci/release_chain.py functions as release.yml (sbom, sign --mode private, verify --mode private, tag). It checks every image infra/compose/compose.yaml names with ${BOTHY_VERSION} against the signed digest and runs three probes that must fail (changed file against SHA256SUMS, unsigned child digest against cosign verify, a release without one SBOM). cosign and syft get an empty DOCKER_CONFIG, so no credential of the host enters. TRIAL_PLATFORMS defaults to both platforms for both images, unlike the brief's default of the host platform for bothy-postgres: the merged Dockerfile of M7-01 builds the foreign platform of bothy-postgres without emulation (D-M7-01-2). Vulnerability scan and ScanCode are not part of the trial; they are the separate targets make trivy-image and make scancode. The trial and the target refuse to run when GITHUB_ACTIONS is set. D-W-127; the same code in trial and release is what makes the trial a proof; the merged code wins over the brief's default (D-W-100); a scan result depends on the vulnerability database of the day, not on the chain.
D-M7-10-2 2026-10-15 Signing commands (Design note 2, cosign 3.1.3 pinned): private phase cosign signing-config create --no-default-rekor --no-default-fulcio --no-default-tsa --no-default-oidc --out <file> (checked to name no service), cosign sign --yes --key env://COSIGN_PRIVATE_KEY --signing-config <file> --bundle <image>.sigstore.json <ref@digest> per image index and cosign sign-blob --yes --key env://COSIGN_PRIVATE_KEY --signing-config <file> --bundle <file>.sigstore.json <file> per binary and for SHA256SUMS; every bundle is checked to carry no transparency log entry and no timestamp. Verification cosign verify --key <pub> --insecure-ignore-tlog <ref@digest>, cosign verify-blob --key <pub> --insecure-ignore-tlog --bundle <bundle> <file>, sha256sum --check --strict SHA256SUMS. Public phase: the same commands without --key and signing configuration (keyless with the OIDC token of the job), verified with --certificate-identity-regexp '^https://github.com/<owner>/bothy/.github/workflows/release.yml@refs/tags/server/v' --certificate-oidc-issuer https://token.actions.githubusercontent.com, plus actions/attest per image digest; the mode follows github.event.repository.visibility. SHA256SUMS covers every file of the release except the signature bundles. Signing steps stand only in the job supply-chain with environment: release; check_runners.py rule signing-outside-release holds that for every workflow (environment name compared without case). ADR 0062 items 2 and 3 and its confirmation; the deprecated --tlog-upload=false is not used; the trial ran these commands against the pinned version.
D-M7-10-3 2026-10-15 ScanCode gate (Design note 3): ScanCode publishes no container image, so tools/scancode/Dockerfile builds one from python:3.13-slim-trixie pinned by digest and the release archive scancode-toolkit-v32.5.0_py3.13-linux.tar.gz pinned by sha256 (ADD --checksum); the archive's native wheels are linux/amd64 only, so the release job scancode runs on ubuntu-24.04 and an arm64 host runs it emulated. make scancode scans git archive HEAD with --license --copyright and tools/ci/scancode_policy.py fails on any file whose detected SPDX expression quality/licenses/allowed.txt does not satisfy (OR one side, AND both, WITH the license, LicenseRef-* never) and on scan errors, except paths in quality/licenses/scancode-exceptions.txt, each with a reason (only text that names or quotes a license). supply-chain needs scancode, so a finding stops the release before any tag; the report is a release artifact. Calibration: a run of ScanCode 32.5.0 over the tracked tree at this branch found 38 files outside the list, all text that names licenses or reviewed data (documentation, tool manifests and lock files, the OFL-1.1 fonts of docs/design/typography.md rule 5 in design/ and tools/design-tokens/, the Unlicense Noise vectors of D-M2-38-15, the OSV-Scanner overrides, the SPDX header templates of the server's code generators, DCO and CLA in CONTRIBUTING.md); scancode-exceptions.txt lists these paths with their reasons, and the same report then has no finding. That run used the PyPI package scancode-toolkit-mini 32.5.0 natively, because the container was killed twice for memory in the shared Docker VM of the build host; the release job runs the container on an amd64 runner. licenses.md section 4; a digest-pinned base plus a checksum-pinned archive fixes the image as rule 9 of the brief asks; a gate needs a decision rule, and documentation that lists forbidden licenses must not stop a release.
D-M7-10-4 2026-10-15 Tag form (Design note 4): server/vX.Y.Z stays. 18 section 2 rule 2 now quotes the Go modules reference as written: the subdirectory prefix of tags and the /vN suffix from major version 2 on are stated without a condition, and the reference gives their purpose ("If a module might be depended on by other modules, these rules must be followed so that the go command can find and download the module"); the server module is a main module that no module requires (P-05), so the go command never maps its versions to tags, and its version comes from VERSION. The old text attached the condition "only if loaded as a dependency" to the rule itself, which the source does not say (the plan's risk RK-57).
D-M7-10-5 2026-10-15 The public key of the private phase lives at quality/release/cosign-private-phase.pub (with a README there). This ticket does not create it; until the owner commits it and stores COSIGN_PRIVATE_KEY and COSIGN_PASSWORD in the environment release, a server release stops at its verification step, before any tag (release_chain.py verify names the missing file). Brief Design note 2 and Out of scope; a missing key must fail closed, not sign with nothing.
D-M7-10-6 2026-10-15 make release-prep (Design note 5): git-cliff 2.14.2 selects and parses the commits (--unreleased --context, --tag-pattern per prefix, --include-path <dir>/**, for server also docs/api/**), and tools/ci/release_prep.py applies the rule of 18 section 3: a break gives a major from 1.0.0 on and a minor before, feat a minor, fix or perf a patch, other types alone nothing to release; the first release is 0.1.0. --bumped-version is not used: in a fixture repository it gave 1.0.0 for a BREAKING CHANGE footer on 0.1.0 and a patch for a docs commit, and with no_increment_regex it ignored a break marked on a chore! commit. CHANNEL=beta appends -beta.N (N one above the highest existing beta tag of that version); stable after betas keeps the version and gets a section that points to the beta sections. The section Unreleased becomes [X.Y.Z] - <date> under a new empty Unreleased; nothing is written on any error; the script never commits or tags. ADR 0060 item 2 names the rule, and the tool did not follow it in three reproduced cases; git-cliff still does the path and tag selection and the Conventional Commits parsing ADR 0060 chose it for.
D-M7-10-7 2026-10-15 tools/ci/components.py release also rejects a tag whose component CHANGELOG.md has no section [X.Y.Z], so the job resolve stops before the gates and the build; publish keeps its own check. Requirement 15 of the brief: a missing changelog section aborts before anything is published; before, publish found it after the image was tagged.
D-M7-10-8 2026-10-15 bothy-postgres is built by two jobs, postgres-build-amd64 on ubuntu-24.04 and postgres-build-arm64 on ubuntu-24.04-arm (GitHub-hosted, environment release), each pushing its platform by digest, uploading the digest as the artifact postgres-digest-<arch> and passing its id as the output digest_id; supply-chain downloads both by id and joins them with release_chain.py join, which pushes the index by its own digest (computed from imagetools create --dry-run) and compares what the registry stores. scancode also runs in the environment release, so that its report_id may reach supply-chain. Images are pushed by digest only until the version tags. Brief requirement 13 and 18 section 7 rule 2; imagetools create needs a target and a tag would publish a name before the scan; main's rules release-artifact-by-id and release-untrusted-output refuse artifacts by pattern and outputs of jobs without a release environment, and a matrix has no output per leg.
D-M7-10-9 2026-10-15 services/server/Makefile release-build VERSION=<v> OUT=<dir> writes bothy_<v>_linux_amd64 and bothy_<v>_linux_arm64 with the flags of the Dockerfile (CGO_ENABLED=0, -trimpath -buildvcs=false -ldflags "-s -w"); the image builds stay with the root targets and release.yml. Component contract of 23 section 3; the binaries cover the platforms of the images (18 section 7 rule 1).