8000 ci(release): notarize the shipped macOS binaries by norrietaylor · Pull Request #2003 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content

ci(release): notarize the shipped macOS binaries - #2003

Merged
norrietaylor merged 4 commits into
chore/release-ship-min-answererfrom
chore/release-notarize-macos
Oct 6, 2026
Merged

norrietaylor merged 4 commits into
chore/release-ship-min-answererfrom
chore/release-notarize-macos

Conversation

@norrietaylor
@norrietaylor norrietaylor commented Oct 5, 2026 •
Copy link
Copy Markdown
Member

Summary

Stacked on #2002, which ships minzoned; the base is that PR's branch. Merge #2002 first.

The release workflow signs every shipped macOS binary with the Developer ID but notarizes none of them. A binary that a user runs from a downloaded file can therefore fail Gatekeeper's notarization check. This PR closes that gap for min, minvmd and minzoned.

.github/workflows/release.yml (job build-release-macos-arm64)

  • New step Notarize binaries. It runs after the codesign and designated-requirement checks and the last minzoned link gate, before the uploads. For each of the three binaries, the step:
    • zips the binary with ditto -c -k --keepParent, because notarytool takes a zip, .pkg or .dmg and not a bare Mach-O; the zip is used only for the submission;
    • runs xcrun notarytool submit --wait --timeout 15m --output-format json;
    • reads status from the JSON and fails the release unless it is Accepted; on any other result, or if the submission itself failed, it prints xcrun notarytool log <id>.
  • Notarization does not modify the file, so the uploaded artifacts are the same signed, link-gated bytes as before.
  • No stapling. The release ships each binary as a bare Mach-O file (kind file rows in scripts/stage-release.sh, installed to bin/). stapler can attach a ticket only to an app bundle, a .pkg or a .dmg, so the workflow cannot staple these; Gatekeeper looks the ticket up online.
  • Credentials: a notarytool keychain profile in the existing signing keychain minimal-signing.keychain-db. The step unlocks that keychain with the existing KC_PW secret and passes it with --keychain. The repository variable MACOS_NOTARY_PROFILE names the profile; the workflow had no notary convention before this. If the variable is empty, the step fails before submitting anything.
  • The job timeout-minutes rises from 45 to 60. The build takes about 15 minutes, and the notary waits come on top of that.

docs/internal/release-pipeline.md: documents the step, why it does not staple, and its prerequisites. The doc also no longer points to a private tracker.

Not in scope: libkrun.1.dylib and the macOS gvproxy, which sign-macos-artifacts signs, are still not notarized. The doc says so.

Runner setup a human must do before merge

  1. On the self-hosted macOS runner, as the user the runner service runs as, store a notary profile in the signing keychain. Either form works:
    # App Store Connect API key (preferred for CI)
    xcrun notarytool store-credentials <profile> \
      --key AuthKey_XXXX.p8 --key-id <key-id> --issuer <issuer-uuid> \
      --keychain ~/Library/Keychains/minimal-signing.keychain-db
    # or an Apple ID with an app-specific password
    xcrun notarytool store-credentials <profile> \
      --apple-id <id> --team-id 3G47C5HY64 --password <app-specific-password> \
      --keychain ~/Library/Keychains/minimal-signing.keychain-db
  2. Check it: xcrun notarytool history --keychain-profile <profile> --keychain ~/Library/Keychains/minimal-signing.keychain-db.
  3. Set the repository variable: gh variable set MACOS_NOTARY_PROFILE --body <profile> --repo gominimal/minimal.

Until steps 1 and 3 are done, every release run with RUN_MACOS_CI not false fails at this step.

Validation

  • actionlint .github/workflows/release.yml: clean.
  • zizmor --offline .github/workflows/release.yml: no findings.
  • The YAML parses with python3 (yaml.safe_load).
  • I extracted the step's run: script and checked it with bash -n and shellcheck: clean.
  • I ran the script against a stubbed xcrun and security, using real ditto and plutil:
    • all three Accepted: exit 0;
    • one Invalid: exit 1, error annotation, notary log printed;
    • empty profile variable: exit 1 before any submission.
  • Confirmed on this host that notarytool submit and notarytool log accept --keychain, and that plutil -extract … raw reads the JSON output.
  • just lint-prose docs/internal/release-pipeline.md: 0 alerts.
  • No real notarization ran; the first release run after the runner setup proves it.

Review

This PR edits a frozen, CODEOWNER-gated workflow. A code owner reviews and merges it, after #2002.

After signing, build-release-macos-arm64 now submits min, minvmd and
min-answerer to the Apple notary service with
`xcrun notarytool submit --wait`, each in a ditto zip used only for the
submission. The release fails unless every result is Accepted, and the
step prints the notary log of a rejected submission.

The release ships bare Mach-O files, which stapler cannot staple, so
Gatekeeper looks the ticket up online.

Credentials come from a notarytool keychain profile in the signing
keychain, named by the MACOS_NOTARY_PROFILE repository variable. The
job timeout rises from 45 to 60 minutes to cover the notary waits.
Document the step and its runner prerequisites in release-pipeline.md.
@coderabbitai
coderabbitai Bot commented Oct 5, 2026 •
Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 8c0ef5d4-bc19-4b63-adb7-d07dcd2e3c2c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

# Conflicts:
#	docs/internal/release-pipeline.md
@norrietaylor norrietaylor changed the title ci(release): notarize the shipped macOS binaries ci(release): notarize the shipped macOS binaries Oct 6, 2026
@norrietaylor
norrietaylor marked this pull request as ready for review October 6, 2026 05:31
@norrietaylor
norrietaylor requested a review from a team as a code owner October 6, 2026 05:31
@minimal-foundry
minimal-foundry Bot commented Oct 6, 2026 •
Copy link
Copy Markdown
Contributor

Approved: no findings at 0523ef0eb6a2.

Nothing to do.

Clobber: none: git log on both files shows the change stacked cleanly on the #2002 branch with no recent fix reverted.

@minimal-foundry minimal-foundry Bot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

minimal-review — approved

Verdict APPROVE — No findings.

@minimal-foundry minimal-foundry Bot added the review:approved minimal-review approved the current head label Oct 6, 2026
@norrietaylor
norrietaylor merged commit 5bec2bc into chore/release-ship-min-answerer Oct 6, 2026
3 checks passed
@norrietaylor
norrietaylor deleted the chore/release-notarize-macos branch October 6, 2026 22:38
yermakoffivan pushed a commit to yermakoffivan/minimal that referenced this pull request Oct 7, 2026
…very platform (gominimal#2002)

* ci(release): ship min-answerer signed for every platform

The build, staging, packaging and install scripts already handle
min-answerer (gominimal#1971), but the release workflow never built, signed or
uploaded it, so stage-release.sh and package-nfpm.sh skipped it with a
warning.

Linux: rename the min-answerer artifacts dist-build.sh already builds,
re-run scripts/check-answerer-links.sh on the renamed files and upload
them as min-answerer-linux-{amd64,arm64}.

macOS: build min-answerer in its own -p min-answerer invocation,
link-gate it, rename it, Developer-ID sign it (hardened runtime,
timestamp, no entitlements, identifier dev.minimal.min-answerer),
verify it against the designated requirement min checks, re-run the
link gate on the signed bytes and upload it as min-answerer-macos-arm64.

Pass MINIMAL_ANSWERER_TEAMID and MINIMAL_ANSWERER_IDENTIFIER as job
env to the macOS build so the min it builds carries that identity.

* docs(release): describe min-answerer as built, signed and uploaded by the workflow

The release workflow now builds, link-gates, signs and uploads min-answerer,
so the pipeline doc no longer calls it a pending code-owner patch or a gap.

* refactor: rename min-answerer to minzoned

The host box-zone service binary becomes minzoned, matching minimald
and minvmd. It holds the box zone: the box names and the addresses
behind them, so a dns name would be too narrow once address
arbitration lands. The name stays narrow on purpose: a privileged,
config-less service should not invite more functions.

- crate, package and binary: crates/minzoned, minzoned
- launchd label dev.gominimal.zone, systemd units minzoned.socket and
  minzoned.service
- root-owned copies /Library/PrivilegedHelperTools/minzoned and
  /usr/local/lib/minimal/minzoned
- codesign identifier dev.minimal.minzoned; compile-time env
  MINIMAL_ZONED_TEAMID and MINIMAL_ZONED_IDENTIFIER
- release artifacts minzoned-{linux-amd64,linux-arm64,macos-arm64}
- scripts/check-zoned-links.sh (and its test), just zoned-build and
  zoned-link-check
- advisory lead-in: install the Minimal box-name service (DNS and
  addresses for boxes)

* ci(release): build, sign and ship the box-name service as minzoned

Follows the rename of min-answerer to minzoned: the artifact names, the link
check script, the cargo package and binary, the signing identifier
dev.minimal.minzoned and the MINIMAL_ZONED_* build env that min compiles in.

* test(e2e): match the renamed box-name service in the session e2e

The advisory command now installs minzoned, so the e2e's answerer-step
matches look for minzoned instead of the old zone-answerer label. The
install test's root-owned-path guard names the new program and plist
paths. Daemon log greps keep the zone-answerer component name.

* ci(release): notarize the shipped macOS binaries (gominimal#2003)

After signing, build-release-macos-arm64 now submits min, minvmd and
min-answerer to the Apple notary service with
`xcrun notarytool submit --wait`, each in a ditto zip used only for the
submission. The release fails unless every result is Accepted, and the
step prints the notary log of a rejected submission.

The release ships bare Mach-O files, which stapler cannot staple, so
Gatekeeper looks the ticket up online.

Credentials come from a notarytool keychain profile in the signing
keychain, named by the MACOS_NOTARY_PROFILE repository variable. The
job timeout rises from 45 to 60 minutes to cover the notary waits.
Document the step and its runner prerequisites in release-pipeline.md.

* test(minzoned): prove the macOS service install on a hosted runner (gominimal#2011)

* test(minzoned): prove the macOS service install on a hosted runner

The self-hosted Mac fleet runs PR code without sudo, so the minzoned
LaunchDaemon path had no CI proof. A new zoned job on macos-latest runs
the privileged install command min prints, then checks custody, the
system resolver, the channel (a script stands in for the node), the uid
gate, a restart, a re-run and a clean teardown.

A hidden debug-only verb, min debug-answerer-command, prints the command
without a session.

* test(minzoned): wait for the range job to apply every alias

* fix(scripts): spell the zoned restart check as an if so shellcheck passes

* docs(scripts): say release.yml now ships minzoned in the optional-artifact notes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review:approved minimal-review approved the current head

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0