Skip to content

release: add MacOS signing to CI - #49

Merged
guygrigsby merged 9 commits into
mainfrom
guy/ci-macos-signing
Sep 23, 2026
Merged

guygrigsby merged 9 commits into
mainfrom
guy/ci-macos-signing

Conversation

@guygrigsby

Copy link
Copy Markdown
Contributor
  • release: strip resource forks and xattrs from notarized zips
  • release: sign and notarize macOS builds in CI
  • release: add curl installer and document the macOS install paths

ditto -c -k stores the built binary's xattrs (notably com.apple.provenance,
which modern macOS stamps on every built file) as AppleDouble ._* entries
inside the zip. Archive Utility on current macOS extracts those as literal
files, a pattern already observed breaking Gatekeeper assessment of
Finder-extracted downloads elsewhere. The binaries are bare Mach-O: no
resource forks or xattrs need preserving, so --norsrc --noextattr removes
the pollution at the source.
The certificate lives on one laptop, so every release depends on that
machine being around and awake. With the certificate and notarization
credentials in GitHub secrets, a macOS job can run the same Makefile
targets the local flow uses: setup-macos-signing.sh builds a temporary
keychain (adapted from the unmerged APT-331 branch), then
release-mac-notarized and upload-mac produce and publish the signed zips.
One implementation of asset replacement keeps CI and local identical.

Tradeoff: GoReleaser still publishes unsigned darwin tarballs first, and
they sit on the release until upload-mac replaces them, the length of one
notarization. Cutting darwin from the goreleaser config would close that
window but fork the local and CI release definitions. Revisit if anyone
ships a workaround that scrapes the tarballs in that gap.

Not exercised end to end until the secrets exist and a tag is pushed; the
Makefile path itself is the one that produced v0.0.13.
Browser downloads set the quarantine attribute, and managed Macs can fail
Gatekeeper's online notarization lookup even for properly notarized
binaries: reproduced on a corp Mac with terraform's own signed release.
So the README needs two things the go install line never had: a
quarantine-free install path and the Open Anyway instructions for people
who do click the zip in a browser.

install.sh resolves the latest tag, downloads the matching asset and
verifies it against the release checksums.txt before installing.
Verified on linux amd64 against v0.0.13: latest-tag resolution, checksum
accept, checksum reject on a corrupted download, and -version on the
installed binary. The darwin branch is untested here (no Mac in the
sandbox); the zip layout it expects was confirmed against the published
v0.0.13 archive.
The box recreated two commits after origin already held their signed
versions. sand replayed the replacements over the published additions
and hit a workflow conflict.

Keep the published commits and record the intended changes as one
follow-up. Its tree is identical to 9686877. Rebase would repeat the
original additions; replacing origin would discard signed history.
Revisit only if the published commits themselves must be removed.
The two-job design published unsigned darwin tarballs and replaced them
afterwards, so for minutes every release carried binaries Gatekeeper
would block, and a failed signing job left them there permanently.

Move signing into the pipeline: one macOS job runs goreleaser with a
build post-hook (scripts/sign-macos.sh) that codesigns and notarizes
each darwin binary before archiving, failing the run on anything but
Accepted. Linux cross-compiles with CGO off. GoReleaser's native
notarization is Pro-only, which is why this is a hook script. PR dry
runs get no keychain setup, so the hook passes binaries through
unsigned. The keychain prep and the fake-Apple-tool hook test are
adapted from the unmerged APT-331 branch, with the keychain created
once per job instead of once per binary. upload-mac stays as the local
fallback for a release the workflow could not sign.
CI now ships darwin as goreleaser tarballs like linux, so the installer
looks for aperture-cli_darwin_<arch>.tar.gz first and falls back to the
zips the local upload-mac flow still produces. The README releasing
section describes the hook-based workflow and marks the Makefile flow
as the fallback.

Verified end to end on linux and against the v0.0.13 darwin zip
fallback: download, checksum, extract, install, -version.
Missing signing setup let direct GoReleaser releases publish unsigned Darwin binaries. An exact JSON grep also rejected valid Accepted responses while discarding notarytool failures.

Use the snapshot flag to permit unsigned builds only when GoReleaser cannot publish. Require a successful submission and one Accepted JSON object through jq. Widening the grep would still accept nested or malformed results. The exit trap removes submissions on failure too.

Keep the hook gate until the release tool provides equivalent built-in signing and notarization checks. Regression tests failed before the fix; make check and the fresh security review now pass. Real GoReleaser snapshots package all four targets, while a non-snapshot run without signing setup stops before archiving. Apple signing still needs the credentialed macOS run.
@guygrigsby guygrigsby changed the title add MacOS signing to CI release: add MacOS signing to CI Sep 23, 2026
Workflow edits started a full macOS snapshot build on PRs even though Linux CI already runs make check. Release execution belongs to tag pushes and explicit release dispatches.

Removing the PR trigger avoids conditional secret handling and snapshot branches in the release workflow. Local snapshots remain available. Revisit separate packaging validation only if the existing PR gate misses a concrete packaging regression.

The regression test failed before the change. make check, actionlint and an independent security review pass. Hosted Actions execution remains unverified from this sandbox.
Setup failures could leave certificate material without an exported cleanup path, and notarization credentials stayed in the login keychain outside teardown. Store both in the temporary keychain and remove the exported certificate before setup succeeds.

The workflow teardown now reports deletion failure instead of hiding it. Setup failure cleanup still suppresses secondary deletion errors so the original setup failure remains visible. Revisit the local fallback separately; it intentionally uses the developer login keychain.

Regression tests cover the hook requiring the temporary keychain and passing it to notarytool. shellcheck, make check and a fresh independent security review pass. Apple credential storage and hosted teardown remain unverified until the credentialed macOS run.
@guygrigsby
guygrigsby merged commit 81441f7 into main Sep 23, 2026
2 checks passed
@guygrigsby
guygrigsby deleted the guy/ci-macos-signing branch September 23, 2026 22:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant