feat: add the CI and release pipelines - #7
Merged
Merged
Conversation
monmux had no CI workflow, no release pipeline and no published artifact; docs/release.md said so in its first line. This turns that design into files, and reconciles the document with what now exists. One rule frames all of it: no job runs monmux against hardware. Nothing in either workflow executes ddcutil, m1ddc or `monmux switch`, and the test suite refuses to start a process by construction, so a runner needs neither tool installed. CI (.github/workflows/ci.yaml) runs the tests on a Linux and a macOS runner, the cross targets, govulncheck, `goreleaser check`, and the reviewdog hook jobs on pull requests. The linter also runs natively on macOS: each backend sits behind a build tag, so a linter on one OS does not analyse the other's files, and `make go-lint-cross` aborts in GOROOT when it is older than the toolchain, which is a bad thing to have as the only line of defence. Both jobs read the linter version out of .pre-commit-config.yaml, so there is one source of truth. Releases (.github/workflows/release.yaml) are fully automatic from one workflow_dispatch, with an optional version and a dry-run switch. The job order exists because tags here are immutable: the repository ruleset blocks tag deletion with no bypass actors, so the familiar "delete the tag on failure" cleanup cannot work, and everything that can fail without a tag therefore happens before the tag exists. Everything after the push is idempotent, so a re-run with the same explicit version recovers a failed publish. The version comes from the Conventional Commits since the last tag, via svu, with one project rule on top: a change to models.yaml that adds or moves a byte monmux can send is never a patch. A new tool, internal/catalog/internal/generate/cmd/relnotes, compares the two catalogs and prints both the release-notes section and the flag the workflow enforces that rule with. A recorded entry counts, because --unsafe-model reaches it through Model.UnsafeOperation. What a release produces: static binaries for linux/amd64, linux/arm64 and darwin/arm64; tarballs; .deb and .rpm packages, published to Cloudsmith as well as attached to the release; a Homebrew cask in leinardi/homebrew-tap depending on the m1ddc formula; a checksum file signed keyless with cosign; and a build provenance attestation. Three deliberate decisions, each with its reasoning in docs/release.md: a cask rather than a formula, because goreleaser hard-deprecated brews: and a cask is macOS-only by nature; an unversioned ddcutil dependency, because a hard 2.2 floor would make the package uninstallable on Debian 12 and Ubuntu 24.04 while preflight enforces the floor properly anyway; and the quarantine-stripping postflight hook, without which Gatekeeper refuses an unsigned binary outright. The supply chain is pinned rather than floating: every action is a full commit SHA, goreleaser and svu are exact versions, everything stamped into a binary comes from the commit rather than the clock (so the publish rebuild reproduces what the dry run inspected), and the documented verification commands pin the signing workflow and its ref rather than matching any workflow in the repository. Guards on the release job, in the order they fire: it refuses to run from any ref but main; it refuses when the publishing credentials are missing; it refuses a version that is not higher than every released version, except the recovery re-run of the version on HEAD; it is serialized by a concurrency group that is never cancelled; and it checks the remote one last time before pushing the tag. Also: the issue and pull-request templates now ask monmux's questions - redaction warnings, which catalog entry --unsafe-model named, and a monitor report that becomes catalog evidence - and SECURITY.md says the latest release is the supported version, which it could not say before.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
monmux had no CI workflow, no release pipeline and no published artifact —
docs/release.mdsaid so in its first line. This turns that design into files and reconciles the document with what now exists.One rule frames all of it: no job runs monmux against hardware. Nothing in either workflow executes
ddcutil,m1ddcormonmux switch, and the test suite refuses to start a process by construction, so a runner needs neither tool installed.CI —
.github/workflows/ci.yamlteston Linux and macOS,cross,lint-macos,govulncheck,release-config(goreleaser check), plus the reviewdog hook jobs on pull requests (actionlint,pre-commit-hooks,markdownlint,shellcheck,yamllint,conventional-commits). The linter runs natively on both OSes because each backend sits behind a build tag, andmake go-lint-crossaborts inGOROOTwhen it is older than the toolchain — a bad thing to have as the only line of defence. Both linter jobs read the version out of.pre-commit-config.yaml, so there is one source of truth.Releases —
.github/workflows/release.yaml+.goreleaser.yamlFully automatic from one
workflow_dispatch, with an optionalversionand adry_runswitch. The job order exists because tags here are immutable: the ruleset blocks tag deletion with no bypass actors, so the familiar "delete the tag on failure" cleanup cannot work. Everything that can fail without a tag happens before the tag exists, and everything after the push is idempotent, so a re-run with the same explicit version recovers a failed publish.Produces: static binaries for
linux/amd64,linux/arm64,darwin/arm64; tarballs;.deband.rpm(Cloudsmith and attached to the release); a Homebrew cask inleinardi/homebrew-tapdepending onm1ddc; a cosign-signed checksum file; a build provenance attestation.Versioning, and the catalog rule
The version comes from the Conventional Commits since the last tag via
svu, with one project rule on top: a change tomodels.yamlthat adds or moves a byte monmux can send is never a patch. A new tool,internal/catalog/internal/generate/cmd/relnotes, compares the two catalogs, prints the release-notes section, and prints the flag the workflow enforces that rule with."Can send" is deliberately wider than "is write-enabled":
catalog.Model.UnsafeOperationignores the flag and--unsafe-modelcalls exactly that, so recording a model or an input also counts. A release that only adds disabled entries is still at least a minor.Three decisions worth reviewing
brews:in v2.16, and a cask is macOS-only by nature. It quarantines its download, and the binaries carry no Apple Developer ID, so the cask stripscom.apple.quarantinein apostflighthook. That is a real bypass and is documented as one indocs/security.md.ddcutildependency — a hard 2.2 floor would make the package uninstallable on Debian 12 and Ubuntu 24.04 (both ship 1.4), including for somebody with a source-built 2.2 in/usr/local. Preflight enforces the floor properly against the binary that is actually installed.Supply chain
Every action is pinned to a full commit SHA;
goreleaserandsvuare exact versions; everything stamped into a binary comes from the commit rather than the clock, so the publish rebuild reproduces what the dry run inspected (verified by building the same commit twice and comparing SHA-256); and the documented verification commands pin the signing workflow and its ref rather than matching any workflow in the repository.Guards on the release job, in firing order
main(workflow_dispatchlets the caller pick a branch, and this job holdscontents: write, OIDC and the tap token).HEAD.releaseconcurrency group that is never cancelled.Pull request checklist
mainbranchmake checklocally before creating the commit and it has run successfullyWIPcommits in this PRMonitor writes
internal/catalog/models.yaml, the evidence is in the entry, and the Summary above says which model and which input it enables — no catalog entry is touched by this PRType of changes
Before the first release
Hand setup, tracked in
docs/release.md: thecataloglabel, thereleaseenvironment's deployment-branch rule, the required status checks once these workflows have run, and — before a real (non-dry) release —leinardi/homebrew-tapplus theHOMEBREW_TAP_TOKENsecret. Cloudsmith andCLOUDSMITH_SERVICE_SLUGare already configured.