From 6f8572d4a3536cff52171dc196df8a8c1a164101 Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 1 May 2026 14:25:11 -0700 Subject: [PATCH 1/2] Revert macOS signing workaround now that GoReleaser ships TeamID fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GoReleaser v2.15.4 bumps goreleaser/quill to a commit synced with anchore/quill upstream, which includes the TeamIdentifier fix from anchore/quill v0.7.0. The workaround from #392 (parallel signing via scripts/sign-darwin.sh plus a separate post-publish notarize step) is no longer needed. - Drop the scripts/sign-darwin.sh build hook from .goreleaser.yaml and restore the native notarize.macos block with an env-gated `enabled` template, so local dev with no secrets stays signing-free. - Remove the quill install, credential prep, separate notarize, and credential cleanup workflow steps. Restore MACOS_* env vars on the GoReleaser step and drop --skip=notarize. - Bump the GoReleaser action from v2.14.1 to v2.15.4 and pin the same version in .mise.toml so make test-release exercises the same binary CI uses. Both pins carry sync comments cross-referencing each other. - Drop the "macOS signing tradeoffs" section from RELEASING.md — the publish/notarize race window it described no longer exists, since signing and notarization run as one wait-blocking call again. The macos-verify post-release job is signing-method-agnostic and stays in place as ongoing CI coverage of TeamIdentifier, hardened runtime, and notarization. Closes #393 --- .github/workflows/release.yml | 44 +++++++---------------------------- .goreleaser.yaml | 22 +++++++++++------- .mise.toml | 2 ++ RELEASING.md | 28 +--------------------- scripts/sign-darwin.sh | 42 --------------------------------- 5 files changed, 24 insertions(+), 114 deletions(-) delete mode 100755 scripts/sign-darwin.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 318d797a..f08470b3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -225,25 +225,12 @@ jobs: MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - - name: Install quill for macOS signing - run: | - go install github.com/anchore/quill/cmd/quill@v0.7.1 - echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - - - name: Prepare signing credentials - env: - MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} - MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} - run: | - umask 077 - echo "$MACOS_SIGN_P12" | base64 -d > "$RUNNER_TEMP/codesign.p12" - echo "$MACOS_NOTARY_KEY" | base64 -d > "$RUNNER_TEMP/notary.p8" - - name: Install GoReleaser uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0 with: distribution: goreleaser - version: 'v2.14.1' + # Keep in sync with .mise.toml goreleaser version pin. + version: 'v2.15.4' install-only: true - name: Run GoReleaser @@ -251,33 +238,18 @@ jobs: CHANGELOG_FILE: ${{ steps.changelog.outputs.file }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_TOKEN: ${{ steps.sdk-token.outputs.token }} - QUILL_SIGN_P12: ${{ runner.temp }}/codesign.p12 - QUILL_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }} + MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }} + MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }} + MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} + MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} run: | RELEASE_CHANGELOG="" if [ -n "$CHANGELOG_FILE" ] && [ -f "$CHANGELOG_FILE" ]; then RELEASE_CHANGELOG=$(cat "$CHANGELOG_FILE") fi export RELEASE_CHANGELOG - goreleaser release --clean --skip=notarize - - - name: Notarize macOS binaries - env: - MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }} - MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }} - run: | - for bin in dist/basecamp_darwin_*/basecamp; do - echo "Notarizing $bin..." - quill notarize "$bin" \ - --notary-issuer "$MACOS_NOTARY_ISSUER_ID" \ - --notary-key-id "$MACOS_NOTARY_KEY_ID" \ - --notary-key "$RUNNER_TEMP/notary.p8" \ - --wait - done - - - name: Clean up signing credentials - if: always() - run: rm -f "$RUNNER_TEMP/codesign.p12" "$RUNNER_TEMP/notary.p8" + goreleaser release --clean - name: Attest build provenance uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a5cd60bd..ae44daa4 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -30,10 +30,6 @@ builds: - -X github.com/basecamp/basecamp-cli/internal/version.Version={{.Version}} - -X github.com/basecamp/basecamp-cli/internal/version.Commit={{.Commit}} - -X github.com/basecamp/basecamp-cli/internal/version.Date={{.Date}} - hooks: - post: - - cmd: scripts/sign-darwin.sh "{{ .Os }}" "{{ .Path }}" - output: true archives: - id: default @@ -98,13 +94,21 @@ signs: artifacts: checksum output: true -# Signing handled by scripts/sign-darwin.sh via build hook using anchore/quill -# CLI directly. The goreleaser/quill fork lacks the TeamIdentifier fix from -# anchore/quill v0.7.0 (https://github.com/anchore/quill/issues/147). -# Notarization runs as a separate workflow step after GoReleaser publishes. +# Sign and notarize macOS binaries (cross-platform via embedded quill) notarize: macos: - - enabled: 'false' + - enabled: '{{ and (ne .Env.MACOS_SIGN_P12 "") (ne .Env.MACOS_SIGN_PASSWORD "") (ne .Env.MACOS_NOTARY_KEY "") (ne .Env.MACOS_NOTARY_KEY_ID "") (ne .Env.MACOS_NOTARY_ISSUER_ID "") }}' + ids: + - basecamp + sign: + certificate: "{{.Env.MACOS_SIGN_P12}}" + password: "{{.Env.MACOS_SIGN_PASSWORD}}" + notarize: + issuer_id: "{{.Env.MACOS_NOTARY_ISSUER_ID}}" + key_id: "{{.Env.MACOS_NOTARY_KEY_ID}}" + key: "{{.Env.MACOS_NOTARY_KEY}}" + wait: true + timeout: 20m changelog: # Use GitHub's auto-generated release notes (categories configured in .github/release.yml) diff --git a/.mise.toml b/.mise.toml index f3ba969c..f03bf21a 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,2 +1,4 @@ [tools] go = "1.26" +# Keep in sync with .github/workflows/release.yml goreleaser-action version pin. +goreleaser = "2.15.4" diff --git a/RELEASING.md b/RELEASING.md index eca364e6..2bec6309 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -25,7 +25,7 @@ make release VERSION=0.2.0 DRY_RUN=1 - Generates AI changelog from commit history - Builds binaries for all platforms (darwin, linux, windows, freebsd, openbsd × amd64/arm64) - Builds `.deb`, `.rpm`, `.apk` Linux packages (amd64 + arm64) - - Signs macOS binaries via build hook (`anchore/quill` CLI), notarizes post-publish (see [tradeoff](#macos-signing-tradeoffs)) + - Signs and notarizes macOS binaries via GoReleaser's built-in notarize (embedded quill) - Signs checksums with cosign (keyless via Sigstore OIDC) - Generates SBOM for supply chain transparency - Updates Homebrew cask (`basecamp-cli`) in `basecamp/homebrew-tap` @@ -94,29 +94,3 @@ make update-nix-hash | deb/rpm/apk packages | GitHub Release assets | GoReleaser (nfpm) | | Nix flake | `flake.nix` in repo | Self-serve (`nix profile install github:basecamp/basecamp-cli`) | | go install | `go install github.com/basecamp/basecamp-cli/cmd/basecamp@latest` | Go module proxy | - -## macOS signing tradeoffs - -**Signing** uses `anchore/quill` CLI directly (not GoReleaser's embedded fork) -because the `goreleaser/quill` fork does not populate the `TeamIdentifier` field -in the CodeDirectory ([anchore/quill#147](https://github.com/anchore/quill/issues/147)). -The build hook signs darwin binaries before archiving, so archives, checksums, -and tap manifests all contain correctly signed binaries. - -**Notarization** runs after GoReleaser publishes. This means there is a brief -window where the GitHub release and Homebrew/Scoop manifests point at binaries -Apple has not yet accepted. This is accepted release debt, not a bug: -- The binary bytes are final — notarization for bare Mach-O is purely server-side -- GateKeeper checks Apple's servers at runtime, not a stapled ticket -- Bare Mach-O executables cannot be stapled (Apple limitation) -- This matches the previous GoReleaser/quill behavior - -The `macos-verify` post-release job runs on a real macOS runner for both amd64 -and arm64. TeamIdentifier and hardened runtime are hard assertions that fail -the workflow. The notarization check is best-effort telemetry — ticket -propagation can lag, so it warns rather than gates. - -**Reverting**: when `goreleaser/quill` syncs the fix from anchore/quill v0.7.0, -revert to the built-in notarize block: remove the build hook, re-enable -`notarize.macos`, remove the quill install/notarize workflow steps, and delete -`scripts/sign-darwin.sh`. diff --git a/scripts/sign-darwin.sh b/scripts/sign-darwin.sh deleted file mode 100755 index 8458c778..00000000 --- a/scripts/sign-darwin.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env bash -# Build hook: sign macOS binaries with anchore/quill CLI. -# Called by GoReleaser after each build. -# -# Two modes: -# CI (QUILL_SIGN_P12 is set): signing is mandatory, missing deps = hard fail -# Local dev (QUILL_SIGN_P12 unset): silently skip -# -# Inputs (env): -# QUILL_SIGN_P12 - path to .p12 certificate file (set = CI mode) -# QUILL_SIGN_PASSWORD - .p12 unlock password (quill reads this natively) -# Args: -# $1 - target OS (e.g. "darwin", "linux") -# $2 - path to built binary -set -euo pipefail - -os="$1" -path="$2" - -# Non-darwin targets: always skip -[ "$os" = "darwin" ] || exit 0 - -# No cert path configured: local dev, skip silently -[ -n "${QUILL_SIGN_P12:-}" ] || exit 0 - -# From here, CI has opted in to signing. Missing deps are errors. -if [ ! -f "$QUILL_SIGN_P12" ]; then - echo "ERROR: QUILL_SIGN_P12 set but file not found: $QUILL_SIGN_P12" >&2 - exit 1 -fi - -if [ -z "${QUILL_SIGN_PASSWORD:-}" ]; then - echo "ERROR: QUILL_SIGN_PASSWORD is not set" >&2 - exit 1 -fi - -if ! command -v quill >/dev/null; then - echo "ERROR: quill not found in PATH" >&2 - exit 1 -fi - -quill sign "$path" --p12 "$QUILL_SIGN_P12" From 682fc09cb93d1af562c77f876bcfaa81e3df4a5e Mon Sep 17 00:00:00 2001 From: Jeremy Daer Date: Fri, 1 May 2026 14:52:35 -0700 Subject: [PATCH 2/2] Clarify mise sync comment refers to GoReleaser CLI version The pinned value is the GoReleaser CLI version, not the goreleaser-action version (which is pinned by SHA at v7.0.0). Reword the cross-reference comment to make that distinction explicit. --- .mise.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.mise.toml b/.mise.toml index f03bf21a..ad684612 100644 --- a/.mise.toml +++ b/.mise.toml @@ -1,4 +1,4 @@ [tools] go = "1.26" -# Keep in sync with .github/workflows/release.yml goreleaser-action version pin. +# Keep in sync with .github/workflows/release.yml GoReleaser CLI version pin. goreleaser = "2.15.4"