Skip to content

feat: stable macOS code-signing in the reusable release workflow + darwin-gate - #24

Merged
rianjs merged 5 commits into
mainfrom
feat/23-macos-codesign-infra
Jun 3, 2026
Merged

feat: stable macOS code-signing in the reusable release workflow + darwin-gate#24
rianjs merged 5 commits into
mainfrom
feat/23-macos-codesign-infra

Conversation

@rianjs

@rianjs rianjs commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Shared-infra ticket for the stable macOS code-signing initiative (convention: cli-common distribution.md §2A). Lets every keychain-backed CLI get a stable code-signing identity so the Keychain "Always Allow" grant survives brew upgrade.

What's here

  • New actions/macos-codesign-setup composite + codesign-darwin.sh: imports the cert (apple-actions/import-codesign-certs@v6), stages the per-build signing script to $RUNNER_TEMP (outside the repo, so it can't dirty goreleaser's git-state check), exports SIGN_IDENTITY / TOOL_IDENTIFIER=org.open-cli-collective.<binary> / CODESIGN_DARWIN_SCRIPT / REQUIRE_SIGNING. All-or-none self-gate on the cert inputs (secrets.* isn't usable in if:).
  • darwin-gate: new pure, unit-tested assert-dr (reads codesign -d -r- on stdin, parses only the designated => line, asserts cert leaf + identifier, no cdhash, case-insensitive leaf, ignores CDHash= metadata) + a macOS check-signature wrapper. Self-gated on expected-leaf.
  • release.yml: 4 optional workflow_call secrets; macos-codesign-setup before the snapshot build; expected-leaf/identifier passed to the gate. Atomic per caller — signing + enforcement both off, or both on, so the rolling v1 bump can't break untouched callers.

Verification

  • bash actions/darwin-gate/test_darwin_gate.sh — 16/16 pass (6 new assert-dr cases).
  • shellcheck clean; actionlint clean; all YAML parses.

On merge

The rolling v1 tag is advanced to this commit so the @v1 callers pick up the new action + gate, then the CLI tickets (#184, #154, #128, #106, #408) opt in.

Closes #23

rianjs added 2 commits June 3, 2026 15:30
New macos-codesign-setup composite: imports the family code-signing cert
(apple-actions/import-codesign-certs), stages codesign-darwin.sh onto the runner,
and exports SIGN_IDENTITY / TOOL_IDENTIFIER / CODESIGN_DARWIN_SCRIPT /
REQUIRE_SIGNING. Self-gated all-or-none on the cert inputs, so an opt-out caller
no-ops and keeps releasing unsigned.

Extend darwin-gate with a pure, unit-tested assert-dr subcommand and a macOS
check-signature wrapper: parse only the `designated =>` requirement line and
assert it pins the expected cert leaf + org.open-cli-collective.<binary>
identifier with no cdhash. Self-gated on expected-leaf.

Wire both into release.yml behind four optional workflow_call secrets, called
unconditionally (secrets.* is not available in if:); signing setup and the
signature check activate together exactly when a caller passes the cert secrets,
keeping the rolling v1 bump from breaking untouched callers.

Closes #23
macos-codesign-setup now counts cert-leaf-sha in its gate, so a caller that
passes p12/password/cn but omits leaf-sha fails fast instead of signing while
darwin-gate (which gates check-signature on leaf-sha) silently skips DR
enforcement. Keeps signing and enforcement strictly atomic per caller.
@rianjs

rianjs commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Codex PR review (9a). STATUS: blockers=0 majors=1 — the major (all-or-none must span all four secrets) is fixed in the follow-up commit; re-review below.

Findings:

  • Major: signing and enforcement are not actually all-or-none across the four macOS secrets. macos-codesign-setup only gates on cert-p12, cert-password, and cert-cn (action.yml), while darwin-gate independently gates check-signature only on expected-leaf (action.yml). That means a caller that passes the first three secrets but forgets macos-cert-leaf-sha will sign the release but skip DR enforcement, violating the approved “signing + enforcement both off or both on” invariant. Fix by passing macos-cert-leaf-sha into macos-codesign-setup too and making that action enforce all four inputs atomically, or add an equivalent shared preflight that fails unless all four are present together before either signing or signature-check skipping can happen.

Verification note: I could not rerun the shell tests in this read-only sandbox because temp-file creation was denied; this review is based on the diff and file inspection.

@rianjs

rianjs commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

TDD coverage assessment

All 16 tests pass on Linux (macOS toolchain tests correctly skipped).

assert-dr — the main testable unit

Coverage is solid for a shell utility. All six cases that matter are present:

Case Covered
Valid requirement (leaf + identifier, CDHash= metadata ignored) yes
Case-insensitive leaf match (uppercase H in DR) yes
ad-hoc cdhash requirement → must fail yes
Wrong leaf hash → must fail yes
Wrong identifier → must fail yes
No designated => line at all → must fail yes

Gaps worth considering (not blocking):

  1. Multiple designated => lineshead -1 means only the first line is evaluated. If a malformed / multi-statement requirement somehow emits two designated => lines, the second is silently ignored. A test piping two such lines (second with a bad leaf) would confirm the intended head -1 behaviour is deliberate and not accidentally hiding a bug.

  2. Empty identifier arg — calling assert-dr $LEAF "" with an empty $ident. The case "$req" in *"identifier \"\""* pattern would match any requirement string that contains identifier "", which is unlikely in practice but worth a single negative test to confirm an empty identifier is rejected rather than vacuously accepted.

  3. Whitespace variant in the designated => prefix — codesign occasionally emits a leading tab before the line in older SDK output. Currently sed -n 's/^designated => //p' requires the line to start at column 0 with no leading whitespace. This is almost certainly fine for goreleaser output, but worth a note in the comment rather than a test.

macos-codesign-setup all-or-none gate

The bash gate logic (count secrets, fail on 1–3, no-op on 0, proceed on 4) is pure and testable but currently has no unit tests. For a CI action gate that controls whether signing is enforced or silently skipped this is the one place worth adding a small test. Suggested cases:

  • all four secrets present → enabled=true
  • zero secrets → enabled=false (unsigned no-op)
  • one or three secrets → exit 1 (partial config error)
  • all four present but binary empty → exit 1

These could live in a test_macos_codesign_setup.sh that sources just the gate logic (extract to a function or invoke via env vars against a stripped-down script). Not blocking for merge given the all-or-none pattern is simple and easy to audit by reading, but it's the one testable branch left entirely uncovered.

Correctly NOT tested (exercised at release time)

  • check-signature: calls codesign --verify --strict + codesign -d -r- — macOS-only, can't unit-test on Linux CI.
  • codesign-darwin.sh: wraps codesign --force --sign — same constraint.
  • macos-codesign-setup/action.yml composite steps after the gate (apple-actions/import-codesign-certs, env export) — GitHub Actions runtime, not unit-testable.

Verdict: the core assert-dr unit is well-covered. The two optional additions (multiple-DR-lines edge case and empty-identifier guard) are low-priority nits. The one meaningful gap is the absence of any tests for the all-or-none gate in macos-codesign-setup, which is testable pure bash controlling a security-relevant branch.

Move the all-or-none decision into a unit-tested gate.sh (the action calls it and
fails fast on a partial config) and add test_gate.sh + a CI job. This is the
invariant that keeps signing and DR enforcement atomic, so it deserves coverage.
Also add two assert-dr edge cases (first designated => line wins; empty identifier
is rejected).

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: f047db2

Summary

Reviewer Findings
harness-engineering:harness-architecture-reviewer 3
harness-engineering:harness-enforcement-reviewer 1
harness-engineering:harness-knowledge-reviewer 1
harness-engineering:harness-architecture-reviewer (3 findings)

⚠️ Should Fix - actions/macos-codesign-setup/action.yml:59

Third-party action apple-actions/import-codesign-certs@v6 is pinned to a floating major-version tag rather than a commit SHA. This action receives the raw P12 bytes and password (the private signing key). If the upstream v6 tag is moved to a malicious commit, signing key material can be exfiltrated silently. Pin to a full commit SHA to make the code handling cert material immutable.

💡 Suggestion - actions/macos-codesign-setup/action.yml:57

No guard enforcing that the job runs on a macOS runner when signing is enabled. If a caller wires all four secrets but runs on ubuntu-latest (e.g., a matrix job that forgot to filter), the workflow fails with an opaque keychain error from the upstream action. Adding if: steps.gate.outputs.enabled == 'true' && runner.os == 'macOS' on the cert-import step (or a check inside gate.sh) would surface the misconfiguration immediately.

💡 Suggestion - actions/macos-codesign-setup/codesign-darwin.sh:19

After signing, the script only checks that the designated requirement does not pin cdhash; it does not verify the cert leaf SHA or identifier against expected values. A misconfigured cert CN (wrong cert in the keychain) produces a structurally valid signature that passes this check but fails only at the darwin-gate step — after a full multi-target goreleaser run has already completed. Asserting the leaf and identifier immediately after signing would catch cert misconfiguration at the first binary.

harness-engineering:harness-enforcement-reviewer (1 findings)

💡 Suggestion - actions/darwin-gate/darwin-gate.sh:135

check_signature silences codesign diagnostic output with 2>/dev/null. When verification fails, the custom ::error:: names the binary but discards the codesign reason (e.g., 'code object is not signed at all', 'a sealed resource is missing'). Redirect stderr to a variable or remove the suppression so the codesign message appears alongside the custom error without polluting the happy-path output.

harness-engineering:harness-knowledge-reviewer (1 findings)

💡 Suggestion - actions/macos-codesign-setup/gate.sh:14

The ::error:: workflow commands in gate.sh are written to stderr (>&2), but GitHub Actions only processes workflow commands from stdout. The step will still exit 1 correctly, but the structured annotations will not appear in the Actions UI — they will show as plain stderr text. Remove >&2 so annotations render correctly.

3 info-level observations excluded. Run with --verbose to include.

2 PR discussion threads considered.


Completed in 8m 06s | $1.12 | sonnet | daemon 0.2.121 | Glorfindel
Field Value
Model sonnet
Reviewers hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 8m 06s wall · 12m 54s compute (Reviewers: 6m 28s · Synthesis: 1m 34s)
Cost $1.12
Tokens 197.9k in / 45.7k out
Turns 8

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 32.2k 6.4k 18.6k 13.5k (1h) $0.16
harness-engineering:harness-architecture-reviewer sonnet 71.4k 8.5k 49.1k 22.2k (1h) $0.24
harness-engineering:harness-enforcement-reviewer sonnet 49.2k 22.7k 6.9k 42.3k (1h) $0.51
harness-engineering:harness-knowledge-reviewer sonnet 45.2k 8.1k 23.0k 22.2k (1h) $0.22

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

Comment thread actions/macos-codesign-setup/action.yml Outdated
Comment thread actions/macos-codesign-setup/action.yml Outdated
Comment thread actions/darwin-gate/darwin-gate.sh
Comment thread actions/macos-codesign-setup/gate.sh
Comment thread actions/macos-codesign-setup/codesign-darwin.sh
- SHA-pin apple-actions/import-codesign-certs (fe74d46, v6.1.0): it receives the
  raw .p12 + password, so a moved @v6 tag would be silent key exfiltration.
- macos-codesign-setup: fail with a clear error if signing is enabled on a
  non-macOS runner instead of an opaque keychain failure later.
- gate.sh now signals via exit code (0 enabled / 10 disabled / 1 invalid) and
  emits its ::error:: on stdout so the Actions UI renders the annotation; the
  action branches on the code.
- darwin-gate check-signature: stop suppressing codesign stderr so the failure
  reason ('not signed', 'sealed resource missing', ...) is visible.
@monit-reviewer
monit-reviewer dismissed their stale review June 3, 2026 19:54

Superseded by updated review

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: 0f37420 | Previous: f047db2 (incremental)

Summary

No issues found.

7 PR discussion threads considered.


Completed in 47s | $0.73 | sonnet | daemon 0.2.121 | Glorfindel
Field Value
Model sonnet
Mode Re-review · Cycle 2 · Session resumed
Reviewers hybrid-synthesis, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 47s wall · 42s compute (Reviewers: 6s · Synthesis: 18s)
Cost $0.73
Tokens 194.4k in / 2.2k out
Turns 4

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 42.4k 839 18.6k 23.8k (1h) $0.11
harness-engineering:harness-architecture-reviewer sonnet 37.0k 74 4.0k 33.0k (1h) $0.13
harness-engineering:harness-enforcement-reviewer sonnet 51.2k 76 4.0k 47.1k (1h) $0.18
harness-engineering:harness-knowledge-reviewer sonnet 36.6k 73 4.0k 32.6k (1h) $0.12
discussion-summarizer 27.2k 1.1k 0 22.4k (1h) $0.19

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

@rianjs

rianjs commented Jun 3, 2026

Copy link
Copy Markdown
Contributor Author

Codex final pass (9e) — post-daemon. STATUS: blockers=0 majors=0 minors=0 nits=0 — no findings.

No findings.

The post-daemon changes preserve the approved architecture: the all-or-none gate now spans all four signing inputs, the runner guard is correctly scoped to the enabled path, gate.sh gives testable 0/10/1 behavior without hiding annotations, and darwin-gate still parses only the designated => line while preserving codesign --verify diagnostics.

@rianjs
rianjs merged commit 779a537 into main Jun 3, 2026
18 checks passed
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.

Stable macOS code-signing in the reusable release workflow + darwin-gate

2 participants