Skip to content

Commit 0202739

Browse files
committed
ci: switch notarization auth to App Store Connect API key, gate on environment
Two changes that significantly reduce the CI signing risk profile: 1. Replace --apple-id/--team-id/--password (app-specific password) with --key/--key-id/--issuer (App Store Connect API key). The API key is a service credential scoped to the developer team and a role - it cannot sign in to icloud.com or appleid.apple.com, cannot touch iCloud data or payment methods, and is independent of any personal Apple ID password. Apple recommends API keys over app-specific passwords for automation. 2. Bind the build job to a GitHub Environment named `release`. A tag push now pauses the workflow at the environment gate; the runner does not decrypt any signing material until a manually-approved reviewer clicks through. An accidental or attacker-pushed tag does not auto-release. scripts/release-notarize.sh accepts both --key/--key-id/--issuer (used by CI) and --keychain-profile NAME (recommended for local releases via `xcrun notarytool store-credentials`), with strict validation that exactly one auth mode is passed. Updated docs/releasing.md to cover the new prerequisites, the Approve-deployment step, and a local-release fallback that uses keychain-stored credentials. Required repo secrets after this change: BUILD_CERTIFICATE_BASE64, P12_PASSWORD, KEYCHAIN_PASSWORD, SIGNING_IDENTITY, APP_STORE_CONNECT_KEY_BASE64, APP_STORE_CONNECT_KEY_ID, APP_STORE_CONNECT_ISSUER. Removed: APPLE_ID, APPLE_TEAM_ID, APPLE_APP_SPECIFIC_PASSWORD.
1 parent a20da2a commit 0202739

3 files changed

Lines changed: 141 additions & 69 deletions

File tree

.github/workflows/release.yml

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ name: Release
77
# P12_PASSWORD - password protecting the .p12
88
# KEYCHAIN_PASSWORD - random string for the temporary keychain
99
# SIGNING_IDENTITY - "Developer ID Application: Name (TEAMID)"
10-
# APPLE_ID - Apple ID email used for notarization
11-
# APPLE_TEAM_ID - 10-char Developer team identifier
12-
# APPLE_APP_SPECIFIC_PASSWORD - from appleid.apple.com
10+
# APP_STORE_CONNECT_KEY_BASE64 - base64 of the .p8 App Store Connect API key
11+
# APP_STORE_CONNECT_KEY_ID - 10-char Key ID
12+
# APP_STORE_CONNECT_ISSUER - issuer UUID
1313
#
14-
# The published zip is built by ditto post-staple so the notarization
15-
# ticket travels with the archive and Gatekeeper accepts the bundle
16-
# offline on first launch.
14+
# Required GitHub Environment: `release`. The job binds to it so a tag push
15+
# pauses for manual approval before any signing material is decrypted on the
16+
# runner. Configure the environment at Settings -> Environments -> release
17+
# with required reviewer = the repo owner.
18+
#
19+
# The published zip is built by `zip -ry` post-staple so the notarization
20+
# ticket travels with the archive and Gatekeeper accepts the bundle offline
21+
# on first launch.
1722

1823
on:
1924
push:
@@ -25,6 +30,7 @@ permissions:
2530
jobs:
2631
build:
2732
runs-on: macos-15
33+
environment: release
2834
steps:
2935
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
3036
with:
@@ -52,6 +58,15 @@ jobs:
5258
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
5359
rm -f "$CERT_PATH"
5460
61+
- name: Write App Store Connect API key
62+
env:
63+
APP_STORE_CONNECT_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }}
64+
run: |
65+
set -euo pipefail
66+
KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
67+
echo "$APP_STORE_CONNECT_KEY_BASE64" | base64 --decode > "$KEY_PATH"
68+
chmod 600 "$KEY_PATH"
69+
5570
- name: Build, sign, and zip
5671
env:
5772
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
@@ -60,14 +75,13 @@ jobs:
6075

6176
- name: Notarize, staple, and refresh zip
6277
env:
63-
APPLE_ID: ${{ secrets.APPLE_ID }}
64-
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
65-
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
78+
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
79+
APP_STORE_CONNECT_ISSUER: ${{ secrets.APP_STORE_CONNECT_ISSUER }}
6680
run: |
6781
scripts/release-notarize.sh \
68-
--apple-id "$APPLE_ID" \
69-
--team-id "$APPLE_TEAM_ID" \
70-
--password "$APPLE_APP_SPECIFIC_PASSWORD"
82+
--key "$RUNNER_TEMP/AuthKey.p8" \
83+
--key-id "$APP_STORE_CONNECT_KEY_ID" \
84+
--issuer "$APP_STORE_CONNECT_ISSUER"
7185
7286
- name: Verify signature and notarization
7387
run: |
@@ -81,10 +95,12 @@ jobs:
8195
with:
8296
files: .build/MicGuard.zip
8397

84-
- name: Clean up keychain
98+
- name: Clean up secrets on runner
8599
if: always()
86100
run: |
87101
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
102+
KEY_PATH="$RUNNER_TEMP/AuthKey.p8"
88103
if [[ -f "$KEYCHAIN_PATH" ]]; then
89104
security delete-keychain "$KEYCHAIN_PATH" || true
90105
fi
106+
rm -f "$KEY_PATH"

docs/releasing.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,31 +12,49 @@ How to publish a new MicGuard version to Homebrew.
1212

1313
The app version is derived from the latest git tag at build time. `scripts/bundle.sh` runs `git describe --tags` and stamps the result into `Info.plist` via `PlistBuddy`. The source `Info.plist` contains a `0.0.0-dev` placeholder — do not hardcode a version there.
1414

15+
## Prerequisites
16+
17+
The release workflow needs these one-time-setup repo secrets:
18+
19+
- `BUILD_CERTIFICATE_BASE64` - base64 of the Developer ID Application `.p12`
20+
- `P12_PASSWORD` - password protecting the `.p12`
21+
- `KEYCHAIN_PASSWORD` - random string for the temporary runner keychain
22+
- `SIGNING_IDENTITY` - `Developer ID Application: <Name> (<TEAMID>)`
23+
- `APP_STORE_CONNECT_KEY_BASE64` - base64 of the `.p8` App Store Connect API key
24+
- `APP_STORE_CONNECT_KEY_ID` - 10-char Key ID
25+
- `APP_STORE_CONNECT_ISSUER` - issuer UUID
26+
27+
Plus a GitHub Environment named `release` with required reviewer set to the repo owner. The workflow binds the build job to this environment, so a tag push pauses for manual approval before any signing material is decrypted on the runner.
28+
1529
## 1. Tag and push
1630

1731
```bash
1832
git tag v<version>
1933
git push origin v<version>
2034
```
2135

22-
GitHub Actions builds the app and creates a release with `MicGuard.zip`. The tag determines the version embedded in the built app.
36+
The `Release` workflow starts and pauses on the `release` environment, waiting for approval.
2337

24-
## 2. Get the SHA-256
38+
## 2. Approve the deployment
39+
40+
Open the workflow run on GitHub. The `build` job will be in `Waiting` state with a `Review deployments` button. Click it, select `release`, and approve. The job then imports the certificate, builds, signs, notarizes, staples, verifies, and publishes the release with `MicGuard.zip`.
41+
42+
## 3. Get the SHA-256
2543

2644
Once the release is published, compute the checksum:
2745

2846
```bash
2947
curl -sL https://github.com/pszypowicz/MicGuard/releases/download/v<version>/MicGuard.zip | shasum -a 256
3048
```
3149

32-
## 3. Update the Homebrew cask
50+
## 4. Update the Homebrew cask
3351

3452
In the [`homebrew-tap`](https://github.com/pszypowicz/homebrew-tap) repo, edit `Casks/mic-guard.rb`:
3553

3654
```ruby
3755
cask "mic-guard" do
3856
version "<version>"
39-
sha256 "<sha256 from step 2>"
57+
sha256 "<sha256 from step 3>"
4058

4159
url "https://github.com/pszypowicz/MicGuard/releases/download/v#{version}/MicGuard.zip"
4260
# ...
@@ -45,9 +63,30 @@ end
4563

4664
Update both the `version` and `sha256` fields, then commit and push.
4765

48-
## 4. Verify
66+
## 5. Verify
4967

5068
```bash
5169
brew update
5270
brew upgrade mic-guard
5371
```
72+
73+
## Local release (fallback)
74+
75+
If the CI path is broken or you want to release from your Mac, use [`scripts/release-notarize.sh`](https://github.com/pszypowicz/MicGuard/blob/main/scripts/release-notarize.sh). One-time setup:
76+
77+
```bash
78+
xcrun notarytool store-credentials MicGuard \
79+
--key /path/to/AuthKey_XXXXXXXXXX.p8 \
80+
--key-id XXXXXXXXXX \
81+
--issuer YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY
82+
```
83+
84+
Per-release:
85+
86+
```bash
87+
SIGNING_IDENTITY='Developer ID Application: <Name> (<TEAMID>)' \
88+
ENTITLEMENTS=Resources/MicGuard.entitlements \
89+
make zip
90+
scripts/release-notarize.sh --keychain-profile MicGuard
91+
gh release create v<version> .build/MicGuard.zip --generate-notes
92+
```

scripts/release-notarize.sh

Lines changed: 68 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,73 +4,98 @@
44
# resulting ticket onto the .app, and re-zip so the published archive ships
55
# with offline-verifiable notarization.
66
#
7-
# Inputs (named flags - positional args are not accepted):
8-
# --app PATH path to the signed .app bundle (default: .build/MicGuard.app)
9-
# --zip PATH path to the zip uploaded for notarization (default: .build/MicGuard.zip)
10-
# --apple-id EMAIL Apple ID used for notarization
11-
# --team-id TEAM 10-char Apple Developer team identifier
12-
# --password PWD app-specific password (NOT the Apple ID account password)
13-
# -h, --help show usage and exit
7+
# Two authentication modes (pick one):
148
#
15-
# Exit codes:
16-
# 0 notarized + stapled, zip refreshed
17-
# 1 notarization rejected by Apple, or stapler failed
18-
# 2 bad usage (missing flag, unknown flag)
9+
# 1. App Store Connect API key (recommended for CI):
10+
# --key PATH --key-id ID --issuer UUID
11+
#
12+
# 2. Keychain profile (recommended for local: run
13+
# `xcrun notarytool store-credentials NAME ...` once, then reference NAME):
14+
# --keychain-profile NAME
1915
#
2016
# notarytool's --wait flag polls until Apple returns a terminal status, so
21-
# this script does not implement its own polling loop. Submission log is
17+
# this script does not implement its own polling loop. The submission log is
2218
# fetched on rejection so the failure reason ends up in CI output.
19+
#
20+
# Exit codes:
21+
# 0 notarized + stapled, zip refreshed
22+
# 1 notarization rejected by Apple, or stapler failed
23+
# 2 bad usage (missing/conflicting flags, file not found)
2324

2425
set -euo pipefail
2526

2627
usage() {
2728
cat <<'USAGE'
28-
Usage: release-notarize.sh --apple-id EMAIL --team-id TEAM --password PWD \
29-
[--app PATH] [--zip PATH]
29+
Usage:
30+
release-notarize.sh --key PATH --key-id ID --issuer UUID [--app PATH] [--zip PATH]
31+
release-notarize.sh --keychain-profile NAME [--app PATH] [--zip PATH]
3032
3133
Submit a signed .app/.zip to Apple notarization, staple the ticket, and
3234
refresh the zip so the published archive carries the notarization offline.
3335
34-
Required flags:
35-
--apple-id EMAIL Apple ID used for notarization
36-
--team-id TEAM 10-char Apple Developer team identifier
37-
--password PWD app-specific password (appleid.apple.com)
36+
Authentication (choose exactly one):
37+
--key PATH path to App Store Connect API key (.p8)
38+
--key-id ID 10-char Key ID from App Store Connect
39+
--issuer UUID issuer UUID from App Store Connect
40+
--keychain-profile NAME profile name set up via `notarytool store-credentials`
41+
42+
Optional:
43+
--app PATH path to the .app bundle (default: .build/MicGuard.app)
44+
--zip PATH path to the zip to submit (default: .build/MicGuard.zip)
45+
-h, --help show this help and exit
3846
39-
Optional flags:
40-
--app PATH path to the .app bundle (default: .build/MicGuard.app)
41-
--zip PATH path to the zip to submit (default: .build/MicGuard.zip)
42-
-h, --help show this help and exit
47+
Examples:
48+
# CI / explicit credentials
49+
release-notarize.sh --key ./AuthKey_ABCDEF1234.p8 \
50+
--key-id ABCDEF1234 --issuer 12345678-1234-1234-1234-123456789012
4351
44-
Example:
45-
release-notarize.sh --apple-id me@example.com --team-id ABCDE12345 \
46-
--password "$APPLE_APP_SPECIFIC_PASSWORD"
52+
# Local, after running `xcrun notarytool store-credentials MicGuard ...`
53+
release-notarize.sh --keychain-profile MicGuard
4754
USAGE
4855
}
4956

5057
APP=".build/MicGuard.app"
5158
ZIP=".build/MicGuard.zip"
52-
APPLE_ID=""
53-
TEAM_ID=""
54-
PASSWORD=""
59+
KEY=""
60+
KEY_ID=""
61+
ISSUER=""
62+
KEYCHAIN_PROFILE=""
5563

5664
while [[ $# -gt 0 ]]; do
5765
case "$1" in
58-
--app) APP="$2"; shift 2 ;;
59-
--zip) ZIP="$2"; shift 2 ;;
60-
--apple-id) APPLE_ID="$2"; shift 2 ;;
61-
--team-id) TEAM_ID="$2"; shift 2 ;;
62-
--password) PASSWORD="$2"; shift 2 ;;
63-
-h|--help) usage; exit 0 ;;
64-
*) echo "error: unknown flag: $1" >&2; usage >&2; exit 2 ;;
66+
--app) APP="$2"; shift 2 ;;
67+
--zip) ZIP="$2"; shift 2 ;;
68+
--key) KEY="$2"; shift 2 ;;
69+
--key-id) KEY_ID="$2"; shift 2 ;;
70+
--issuer) ISSUER="$2"; shift 2 ;;
71+
--keychain-profile) KEYCHAIN_PROFILE="$2"; shift 2 ;;
72+
-h|--help) usage; exit 0 ;;
73+
*) echo "error: unknown flag: $1" >&2; usage >&2; exit 2 ;;
6574
esac
6675
done
6776

68-
missing=()
69-
[[ -z "$APPLE_ID" ]] && missing+=(--apple-id)
70-
[[ -z "$TEAM_ID" ]] && missing+=(--team-id)
71-
[[ -z "$PASSWORD" ]] && missing+=(--password)
72-
if (( ${#missing[@]} > 0 )); then
73-
echo "error: missing required flag(s): ${missing[*]}" >&2
77+
# Pick auth mode: keychain profile XOR (key + key-id + issuer).
78+
api_key_mode_count=0
79+
[[ -n "$KEY" ]] && api_key_mode_count=$((api_key_mode_count + 1))
80+
[[ -n "$KEY_ID" ]] && api_key_mode_count=$((api_key_mode_count + 1))
81+
[[ -n "$ISSUER" ]] && api_key_mode_count=$((api_key_mode_count + 1))
82+
83+
if [[ -n "$KEYCHAIN_PROFILE" ]]; then
84+
if (( api_key_mode_count > 0 )); then
85+
echo "error: --keychain-profile cannot be combined with --key/--key-id/--issuer" >&2
86+
usage >&2
87+
exit 2
88+
fi
89+
auth_args=(--keychain-profile "$KEYCHAIN_PROFILE")
90+
elif (( api_key_mode_count == 3 )); then
91+
[[ -f "$KEY" ]] || { echo "error: API key not found: $KEY" >&2; exit 2; }
92+
auth_args=(--key "$KEY" --key-id "$KEY_ID" --issuer "$ISSUER")
93+
elif (( api_key_mode_count == 0 )); then
94+
echo "error: missing authentication; pass --keychain-profile NAME or all of --key/--key-id/--issuer" >&2
95+
usage >&2
96+
exit 2
97+
else
98+
echo "error: --key, --key-id, and --issuer must be passed together" >&2
7499
usage >&2
75100
exit 2
76101
fi
@@ -83,9 +108,7 @@ submit_log=$(mktemp)
83108
trap 'rm -f "$submit_log"' EXIT
84109

85110
if ! xcrun notarytool submit "$ZIP" \
86-
--apple-id "$APPLE_ID" \
87-
--team-id "$TEAM_ID" \
88-
--password "$PASSWORD" \
111+
"${auth_args[@]}" \
89112
--wait \
90113
--output-format plist \
91114
> "$submit_log"; then
@@ -94,9 +117,6 @@ if ! xcrun notarytool submit "$ZIP" \
94117
exit 1
95118
fi
96119

97-
# Parse the submission ID + status from the plist output. PlistBuddy on macOS
98-
# is the standard tool for this; fall back to grep if PlistBuddy is missing
99-
# (notarytool always runs on macOS so PlistBuddy should be present).
100120
status=$(/usr/libexec/PlistBuddy -c "Print :status" "$submit_log" 2>/dev/null || echo "")
101121
submission_id=$(/usr/libexec/PlistBuddy -c "Print :id" "$submit_log" 2>/dev/null || echo "")
102122

@@ -106,10 +126,7 @@ if [[ "$status" != "Accepted" ]]; then
106126
echo "error: notarization not accepted" >&2
107127
if [[ -n "$submission_id" ]]; then
108128
echo "==> fetching submission log:" >&2
109-
xcrun notarytool log "$submission_id" \
110-
--apple-id "$APPLE_ID" \
111-
--team-id "$TEAM_ID" \
112-
--password "$PASSWORD" >&2 || true
129+
xcrun notarytool log "$submission_id" "${auth_args[@]}" >&2 || true
113130
fi
114131
exit 1
115132
fi

0 commit comments

Comments
 (0)