0050 — Release pipeline + code-signing (tag-driven, SignPath for Windows, gated Apple)
- Status: accepted
- Date: 2026-07-14
- Deciders: bioedca
- PRD anchor: §9 M9 (packaging & docs), §12.7 (release pipeline), §4.1 (installers)
- Milestone: M9
Context and problem statement
M9 must publish Tether as signed, self-contained desktop installers driven by a
signed v* tag, with provenance and a bill-of-materials (PRD §9 M9, §12.7). The
constructor recipe (ADR-0049) already
builds the 3-OS installers unsigned; this decision homes the release pipeline and,
specifically, how the installers get code-signed without the maintainer holding a
traditional certificate. How do we sign a GPL open-source project's installers, keep the
merge gate green before any signing secret exists, and never let the heavy release job
become a required PR check?
Decision drivers
- OSS-appropriate, low-cost signing. Trusted OV/EV Authenticode certs now require a
hardware/cloud HSM; a raw
.pfxin a secret is no longer issuable for a trusted cert. - Green-before-secrets. The pipeline (and the PR that lands it) must pass CI before any signing credential is provisioned — signing is enrolled by the maintainer later.
- Never a required/gating check. Building + signing a full napari/PySide6 + sidecar
installer is heavy and per-OS; it must not sit in the required PR matrix (the
deep-gpu.yml/packaging.ymlposture, ADR-0047 / ADR-0049). - Provenance travels with the artifact — the release-suite counterpart of the data-model provenance invariant (ADR-0001).
Considered options
- A — SignPath (free for OSS) for Windows; gated Apple Developer ID for macOS.
- B — Azure Trusted Signing for Windows. Modern cloud HSM, but needs a paid Azure subscription + verified identity — heavier onboarding for an academic lab.
- C — Traditional
.pfx/token secret +signtool. No longer issuable as a trusted OV/EV cert (HSM requirement); only a self-signed/internal cert would fit — no real SmartScreen benefit. - D — Ship everything unsigned. Fails the §9 M9 "signed installers" requirement.
Decision outcome
Chosen: Option A. A tag-driven release.yml (verify → build+sign → release)
with signing gated on non-secret repository variables so the pipeline is inert-but-wired
until the maintainer enrolls:
- Windows — SignPath (
signpath/github-action-submit-signing-request). Afterconstructorbuilds the.exe, it is uploaded as a workflow artifact and submitted to SignPath, which returns the signed.exe. The step runs only whenvars.SIGNPATH_ORGANIZATION_IDis set (a repository variable —secretscannot be read in anif:), with the API token assecrets.SIGNPATH_API_TOKEN. Unset ⇒ the.exeships unsigned with a build warning. - macOS — Developer ID
productsign+notarytoolstaple, gated onvars.APPLE_SIGNING_ENABLED == 'true'. Until an Apple Developer account + a Developer ID Installer cert + an App Store Connect notary key exist as secrets, the.pkgships unsigned (Gatekeeper warns) — the maintainer's explicit "wire it, enable later" choice. - Linux — no OS-level signing; the SHA-256 checksums + the build-provenance attestation are the integrity anchor.
- Tag verification without shipping keys to the runner.
verifyrequires an annotated tag whose signature GitHub reports verified (via the REST API — GitHub holds bioedca's registered SSH signing key), and whose commit is an ancestor oforigin/main. Aworkflow_dispatchdry-run builds + signs + checksums but does not publish, satisfying the §9 M9 "dry-run on a pre-release tag first" clause. - Release assets — the signed installers, per-platform + combined
SHA256SUMS, a CycloneDX SBOM, and the frozen Tether GUI/runtime (conda-lock.yml), sidecar (sidecar-conda-lock.yml), and deep (deep-conda-lock.yml) source-lock assets (the authoritative reproducibility bill of materials), a Conventional-Commits changelog, and anactions/attest-build-provenanceattestation over the installers. Constructor consumes rendered forms of the GUI/runtime and sidecar locks as itstetherandsidecarextra environments; its ownbaseis the live-solved Python + conda bootstrap and is excluded from that bill of materials. The deep lock recreates ADR-0047's optional environment and does not place that environment in the installers.
Consequences
- Good: the full pipeline lands + passes CI before any secret exists; the maintainer
turns signing on by enrolling in SignPath and adding the vars/secret (documented in
docs/release.md) — no code change. Third-party actions are SHA-pinned (supply-chain). - Bad / trade-off: SignPath requires a one-time OSS-project enrollment; the macOS path is
authored-but-inert until an Apple Developer account is funded. The heavy build cannot be
end-to-end-validated in the PR (no secrets, no signed tag) — real validation is the
maintainer's
workflow_dispatchdry-run, then thev1.0.0cut (M9 PR-6). - Follow-up:
tests/test_marker_contract.pylocks the release shape (nopull_request/merge_grouptrigger → never a required check; tag-filteredpush; no bareexitunderbash -el). The GUI menu shortcut is a later slice. macOS enablement is a real follow-up, not a flag flip: the current stepproductsigns only the outer.pkg; notarization also requires the bundled conda payload's Mach-O binaries to be Developer ID Application-signed with a hardened runtime (elsenotarytool→ Invalid), so a payload deep-sign (or constructor's nativesigning_identity_name/notarization_identity_name) must land beforeAPPLE_SIGNING_ENABLEDis turned on.
More information
- Recipe: ADR-0049 (the installers this
pipeline signs);
packaging/construct.yaml;.github/workflows/packaging.yml(the advisory build leg this pipeline'sbuildjob mirrors). - SignPath GitHub Action:
docs.signpath.io/trusted-build-systems/github(upload artifact → submitgithub-artifact-id→ download signed artifact). - Maintainer setup (variables/secrets + SignPath enrollment):
docs/release.md.