Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,37 @@ jobs:
exit 1
fi
echo "RELEASE_TOKEN is configured"
-
# Signing is optional: what makes a homebrew install run is shipping a
# formula rather than a cask, since a cask quarantines what it installs.
# Half-configured is worth saying out loud though, because goreleaser
# silently skips signing when the certificate is absent.
name: check macos signing credentials
env:
MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
run: |
missing=""
present=""
for name in MACOS_SIGN_P12 MACOS_SIGN_PASSWORD MACOS_NOTARY_ISSUER_ID MACOS_NOTARY_KEY_ID MACOS_NOTARY_KEY; do
value="${!name}"
if [ -z "$value" ]; then
missing="$missing $name"
else
present="$present $name"
fi
done
if [ -z "$present" ]; then
echo "the darwin binaries will not be signed, which is supported: see the release section of the README"
elif [ -n "$missing" ]; then
echo "::error::macos signing is half configured, missing:$missing. goreleaser skips signing entirely when the certificate is absent, so set all five or none of them."
exit 1
else
echo "macos signing credentials are configured"
fi
-
name: install goreleaser
uses: goreleaser/goreleaser-action@v6
Expand All @@ -45,7 +76,24 @@ jobs:
run: |
for config in .goreleaser/.goreleaser.*.yml; do
echo "checking $config"
goreleaser check --config "$config"
# `brews` is deprecated in favour of homebrew_casks, which is not
# usable here: a cask quarantines what it installs and the binary
# is then killed. goreleaser fails the check for a deprecation as
# well as for a mistake, so let its own wording tell them apart.
if out="$(goreleaser check --config "$config" 2>&1)"; then
echo "$out"
continue
fi
echo "$out"
# matched on goreleaser saying DEPRECATED and not saying the
# configuration is invalid, rather than on one exact sentence. If
# the wording changes past recognising, this fails the release
# rather than waving something through.
if echo "$out" | grep -q "DEPRECATED" && ! echo "$out" | grep -q "configuration is invalid"; then
echo "::warning::$config uses deprecated goreleaser properties, tolerated because the replacement quarantines what it installs"
continue
fi
exit 1
done
-
# created once here so the platform jobs can upload in parallel. Left to
Expand Down Expand Up @@ -131,6 +179,13 @@ jobs:
run: make ${{ matrix.target }}
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
# only the darwin target signs, so only that job is given the
# credentials to do it with
MACOS_SIGN_P12: ${{ matrix.target == 'release-mac' && secrets.MACOS_SIGN_P12 || '' }}
MACOS_SIGN_PASSWORD: ${{ matrix.target == 'release-mac' && secrets.MACOS_SIGN_PASSWORD || '' }}
MACOS_NOTARY_ISSUER_ID: ${{ matrix.target == 'release-mac' && secrets.MACOS_NOTARY_ISSUER_ID || '' }}
MACOS_NOTARY_KEY_ID: ${{ matrix.target == 'release-mac' && secrets.MACOS_NOTARY_KEY_ID || '' }}
MACOS_NOTARY_KEY: ${{ matrix.target == 'release-mac' && secrets.MACOS_NOTARY_KEY || '' }}
-
# the linux and windows targets build as root inside the container, so
# the archives come back owned by root and unreadable to the next step
Expand Down
41 changes: 36 additions & 5 deletions .goreleaser/.goreleaser.darwin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,22 +34,53 @@ archives:
formats:
- tar.gz

# Signing the binaries with a Developer ID certificate, for anyone who would
# rather have it than not. It is off unless the certificate is configured, and
# it is not what makes a homebrew install run: a bare binary has nowhere to
# staple a notarization ticket, and on macos 26 an unstapled ticket did not
# satisfy gatekeeper for a quarantined binary in testing. What fixes that is the
# brews block below, by not being quarantined in the first place.
notarize:
macos:
- enabled: '{{ isEnvSet "MACOS_SIGN_P12" }}'
ids:
- darwin-arm64
- darwin-amd64
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 }}"
# the archives are uploaded from this same run, so the binaries they
# carry must be signed and accepted before it moves on
wait: true
timeout: 20m

checksum:
name_template: "checksums_darwin.txt"

homebrew_casks:
# A formula rather than a cask, because homebrew marks what a cask installs
# com.apple.quarantine and gatekeeper then kills a bare binary that carries no
# stapled notarization ticket, which a bare binary has nowhere to keep. A
# formula is not quarantined, which is how every other go command line tool in
# homebrew arrives able to run.
#
# goreleaser calls this deprecated in favour of homebrew_casks. The cask is what
# certreader used until it turned out not to run when installed, so this stays
# until there is somewhere to staple a ticket to.
brews:
- name: certreader
# IMPORTANT: these are *archive* IDs, not build IDs:
ids:
- darwin
binaries:
- certreader
homepage: https://github.com/jonhadfield/certreader
description: Output detailed information about TLS certificates...
commit_msg_template: "Brew cask update for {{ .ProjectName }} {{ .Tag }}"
commit_msg_template: "Brew formula update for {{ .ProjectName }} {{ .Tag }}"
# a prerelease must not become what brew installs
skip_upload: "auto"
directory: Casks
directory: Formula
repository:
owner: jonhadfield
name: homebrew-certreader
Expand Down
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,8 +565,22 @@ workflow; it does not say the source is good.

### brew

- brew tap jonhadfield/certreader
- brew install --cask certreader
```shell script
brew tap jonhadfield/certreader
brew trust jonhadfield/certreader
brew install certreader
```

Homebrew 6 refuses to load anything from a third-party tap until the tap is trusted, so `brew trust`
comes before the install rather than after it fails.

certreader was a cask until v0.24.0 and is a formula from v0.25.0. If you installed the cask, replace
it once:

```shell script
brew uninstall --cask certreader
brew install certreader
```

### go

Expand Down Expand Up @@ -607,7 +621,7 @@ git tag -a -m "add super cool feature" v1.0.0
git push --follow-tags
```

### required secret
### required secrets

Platform builds run in parallel, so a release takes about as long as its slowest target rather than
the sum of all five. A prerelease tag (one containing a hyphen, such as `v1.0.0-rc1`) is published as
Expand All @@ -617,12 +631,53 @@ Release notes come from the annotated tag message, so write the tag with the not

The workflow needs a `RELEASE_TOKEN` repository secret: a personal access token with `repo` scope on
both `jonhadfield/certreader` and `jonhadfield/homebrew-certreader`. The token built into Actions
cannot write to another repository, and the darwin build pushes the cask update to the tap. Without
the secret the workflow stops at its preflight job and publishes nothing.
cannot write to another repository, and the darwin build pushes the formula update to the tap.

Signing the darwin binaries is optional, and off unless all five of these are set. It is not what
makes an install work — the formula is, see [why a formula and not a cask](#why-a-formula-and-not-a-cask)
— so treat it as something to have rather than something to fix:

| secret | what it is |
| --- | --- |
| `MACOS_SIGN_P12` | base64 of the Developer ID Application certificate, exported as `.p12` |
| `MACOS_SIGN_PASSWORD` | the password that `.p12` was exported with |
| `MACOS_NOTARY_KEY` | base64 of an App Store Connect `.p8` key |
| `MACOS_NOTARY_KEY_ID` | that key's id, also in its filename |
| `MACOS_NOTARY_ISSUER_ID` | the issuer uuid shown when the key was created |

```shell script
gh secret set MACOS_SIGN_P12 --repo jonhadfield/certreader < <(base64 -i DeveloperID.p12)
gh secret set MACOS_NOTARY_KEY --repo jonhadfield/certreader < <(base64 -i AuthKey_XXXXXXXXXX.p8)
```

Without `RELEASE_TOKEN` the workflow stops at its preflight job and publishes nothing. The signing
secrets are all-or-nothing: GoReleaser skips signing entirely when the certificate is absent, so
preflight fails a half-configured set rather than letting it publish quietly unsigned.

Run the workflow manually from the Actions tab to check the secret and the GoReleaser configs
Run the workflow manually from the Actions tab to check the secrets and the GoReleaser configs
without cutting a tag; a manual run stops after preflight.

### why a formula and not a cask

Homebrew marks what a **cask** installs with `com.apple.quarantine`, and gatekeeper kills a
quarantined binary that carries no stapled notarization ticket. The command exits 137 and prints
nothing, which reads like a broken build rather than a refused one:

```shell script
$ certreader -version
$ echo $?
137
```

A bare executable has nowhere to keep a ticket — `stapler` needs an app bundle, a disk image or an
installer package — so signing and notarizing the binary does not settle it. On macOS 26 a notarized
but unstapled binary was still refused in testing.

A **formula** is not quarantined, which is how every other Go command line tool in Homebrew arrives
able to run. GoReleaser calls `brews` deprecated in favour of `homebrew_casks`; the cask is what
certreader shipped as up to v0.24.0, and it did not run when installed, so the formula stays until
there is something to staple a ticket to.

### releasing by hand

The same builds can be run locally, which is useful when debugging a release failure:
Expand Down
Loading