From aa024e777067c243e0ed5e7a2c44a238f26ae303 Mon Sep 17 00:00:00 2001 From: Eleftheria Stein-Kousathana Date: Fri, 31 Jul 2026 12:28:40 +0200 Subject: [PATCH] Sign macOS thv binaries with Developer ID The macOS thv binary ships with only the ad-hoc signature Go's linker emits, so it carries no Team ID. Organizations that enforce binary allowlisting (Santa and similar) trust binaries by code-signing identity, leaving them to approve each release by CDHash or allow execution from a user-writable path. Both are fragile, so thv tends to get blocked in managed macOS environments. ToolHive Studio already signs with Developer ID Application: Stacklok, Inc, and the same certificate covers any binary from the team. Wire it into the CLI release. GoReleaser embeds anchore/quill, so signing happens on the existing Linux runner rather than requiring a macOS one. This is sign-only: omitting the notarize sub-block keeps Apple's notary service off the release critical path, and a Developer ID signature is all that Team ID allowlisting needs. Notarization can follow separately. Signing stays inert until the secrets are configured. Releases without them keep producing ad-hoc binaries and log a warning rather than failing, so this is safe to merge ahead of the credentials. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/releaser.yml | 22 ++++++++++++++++++++++ .goreleaser.yaml | 21 ++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/.github/workflows/releaser.yml b/.github/workflows/releaser.yml index 705ced0bae..c3cae4ffb7 100644 --- a/.github/workflows/releaser.yml +++ b/.github/workflows/releaser.yml @@ -193,6 +193,22 @@ jobs: owner: stacklok repositories: homebrew-tap + # Gates the notarize.macos pipe in .goreleaser.yaml, and warns when the + # signing secrets are absent so unsigned releases are visible in the log. + - name: Determine macOS signing configuration + id: macos-signing + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + run: | + set -euo pipefail + if [[ -n "$APPLE_CERTIFICATE" ]]; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + echo "✅ Apple signing credentials present — darwin binaries will be Developer ID signed" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + echo "::warning title=macOS binaries unsigned::APPLE_CERTIFICATE is not configured, so the darwin thv binaries ship ad-hoc signed with no Team ID. See issue #5862." + fi + - name: Run GoReleaser id: run-goreleaser uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7 @@ -208,6 +224,12 @@ jobs: COMMIT: ${{ needs.compute-build-flags.outputs.commit }} COMMIT_DATE: ${{ needs.compute-build-flags.outputs.commit-date }} TREE_STATE: ${{ needs.compute-build-flags.outputs.tree-state }} + # macOS signing, consumed by the notarize.macos pipe in + # .goreleaser.yaml. Same Developer ID cert toolhive-studio uses; no + # keychain needed, since quill reads the .p12 directly. + MACOS_SIGN_ENABLED: ${{ steps.macos-signing.outputs.enabled }} + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - name: Generate subject id: hash diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 50c3bcab35..e8cd721d57 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -3,7 +3,9 @@ project_name: toolhive version: 2 # This section defines the build matrix. builds: - - env: + # id is referenced by notarize.macos.ids below; matches the previous default. + - id: toolhive + env: - GO111MODULE=on - CGO_ENABLED=0 flags: @@ -24,6 +26,23 @@ builds: - arm64 main: ./cmd/thv binary: thv +# Developer ID signature for the macOS binaries. Go's linker emits only an +# ad-hoc one, which carries no Team ID to allowlist. Signing goes through +# GoReleaser's embedded quill, so no macOS runner is needed. +# +# Sign-only: omitting the `notarize` sub-block keeps Apple's notary service off +# the release critical path. +notarize: + macos: + # A flag rather than `isEnvSet "APPLE_CERTIFICATE"`, because an absent GitHub + # secret still sets the variable, to the empty string. Unset means ad-hoc + # binaries, not a failed release. + - enabled: '{{ .Env.MACOS_SIGN_ENABLED }}' + ids: + - toolhive + sign: + certificate: "{{ .Env.APPLE_CERTIFICATE }}" + password: "{{ .Env.APPLE_CERTIFICATE_PASSWORD }}" # This section defines the release format. archives: - formats: [ 'tar.gz' ]