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
19 changes: 13 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
name: Release

# Push a tag like v1.0.0 to build, sign, notarize, and publish a GitHub release.
# Releases are normally cut locally with `scripts/release.sh <version> --publish` (see
# docs/RELEASING.md). This workflow is an optional, manual alternative for when the signing
# secrets are configured; it never runs on its own.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: "Existing tag to release, e.g. v1.1.0"
required: true

permissions:
contents: write
Expand All @@ -14,6 +19,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}
fetch-depth: 0 # Build number is the commit count.

- uses: maxim-lobanov/setup-xcode@v1
Expand Down Expand Up @@ -50,14 +56,15 @@ jobs:
ASC_KEY_PATH: ${{ runner.temp }}/AuthKey.p8
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
run: scripts/release.sh "${GITHUB_REF_NAME}"
run: scripts/release.sh "${{ inputs.tag }}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep the dispatch tag out of shell source

When a write-level user dispatches this workflow with an existing tag containing shell syntax such as $(...)—which is valid in a Git ref—GitHub interpolates the input before the shell parses this line, and the surrounding double quotes do not prevent command substitution. Arbitrary commands can therefore run while the App Store Connect key and unlocked signing keychain are available; the direct interpolation is repeated at line 65 while GH_TOKEN is present. Pass the expression through an env entry and reference the resulting shell variable instead.

Useful? React with 👍 / 👎.


- name: Publish release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" dist/PRMonitor.dmg dist/PRMonitor.zip dist/SHA256SUMS \
--title "PR Monitor ${GITHUB_REF_NAME#v}" --generate-notes --verify-tag
TAG="${{ inputs.tag }}"
gh release create "$TAG" dist/PRMonitor.dmg dist/PRMonitor.zip dist/SHA256SUMS \
--title "PR Monitor ${TAG#v}" --generate-notes --verify-tag

- name: Clean up signing material
if: always()
Expand Down
70 changes: 34 additions & 36 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,62 @@

PR Monitor ships as a **Developer ID–signed, notarized** disk image on GitHub Releases. That's how Ice, Rectangle, Loop, Stats and AltTab distribute too. Since macOS 15, Gatekeeper no longer lets people right-click › Open an unsigned app, so notarization isn't optional for a download people can actually open.

`scripts/release.sh` does everything: archive, export with Developer ID, notarize and staple the app, build an APFS disk image with an Applications shortcut, sign, notarize and staple the image, and write `dist/SHA256SUMS`. CI runs the same script when a tag is pushed.
Releases are cut from your Mac with one command. `scripts/release.sh`:
1. Archives the app, exports it with your Developer ID, notarizes and staples it.
2. Builds an APFS disk image with an Applications shortcut, then signs, notarizes and staples the image.
3. Writes `dist/SHA256SUMS`.
4. With `--publish`, tags the release and publishes it on GitHub.

## One-time setup
## One-time setup (already done)

### On your Mac
- **The Developer ID Application certificate** is in your login keychain. Check with `security find-identity -v -p codesigning`.
- **Notarization credentials** are stored under the keychain profile `PRMonitor`. To redo them, create an app-specific password at [account.apple.com](https://account.apple.com) › Sign-In and Security, then run:

You need the **Developer ID Application** certificate in your login keychain (check with `security find-identity -v -p codesigning`). You also need notarization credentials, stored once in the keychain:

1. Create an app-specific password at [account.apple.com](https://account.apple.com) › Sign-In and Security › App-Specific Passwords.
2. Store the credentials under a profile name. The command prompts for the password:

```bash
xcrun notarytool store-credentials PRMonitor --apple-id YOUR_APPLE_ID --team-id G2537ZRGNU
```

### In GitHub (for automated releases)

Add these under **Settings › Secrets and variables › Actions**:

| Secret | How to get it |
| --- | --- |
| `DEVELOPER_ID_P12_BASE64` | In Keychain Access, export the Developer ID Application certificate *with its private key* as a .p12, then `base64 -i cert.p12 \| pbcopy` |
| `DEVELOPER_ID_P12_PASSWORD` | The password you set when exporting the .p12 |
| `APPLE_TEAM_ID` | `G2537ZRGNU` |
| `ASC_API_KEY_P8_BASE64` | App Store Connect › Users and Access › Integrations › Team Keys: create a key with the Developer role and download `AuthKey_XXXX.p8`, then `base64 -i AuthKey_XXXX.p8 \| pbcopy` |
| `ASC_KEY_ID` | The key's ID, shown next to it |
| `ASC_ISSUER_ID` | The Issuer ID at the top of the Team Keys page |
```bash
xcrun notarytool store-credentials PRMonitor --apple-id h4saamb@icloud.com --team-id G2537ZRGNU
```

## Cutting a release

1. Make sure `main` is green and `swift test` passes.
2. Tag and push:
1. Merge everything for the release into `main`, then check out a clean, up-to-date `main`:

```bash
git tag v1.0.0 && git push origin v1.0.0
git checkout main && git pull
```

The **Release** workflow builds, signs, notarizes, and publishes the release with `PRMonitor.dmg`, `PRMonitor.zip`, and `SHA256SUMS`. The README's Download button always points at the latest `PRMonitor.dmg`.

To release from your Mac instead:
2. Build, notarize, tag and publish in one step:

```bash
NOTARY_PROFILE=PRMonitor scripts/release.sh 1.0.0
gh release create v1.0.0 dist/PRMonitor.dmg dist/PRMonitor.zip dist/SHA256SUMS --generate-notes
NOTARY_PROFILE=PRMonitor scripts/release.sh 1.1.0 --publish
```

3. Verify before announcing. Download the DMG from the release page so it's quarantined like a user's copy, then check:
It refuses to publish from a dirty or out-of-date checkout, or to reuse an existing tag, so the release always matches the tagged source. Without `--publish`, it only builds into `dist/`, so you can check the build first.

3. Verify. Download the DMG from the release page so it's quarantined like a user's copy, then check it:

```bash
spctl --assess --type open --context context:primary-signature -vv PRMonitor.dmg # "Notarized Developer ID"
```

Then open it and make sure the app launches with no Gatekeeper warning.
Then open the DMG and make sure the app launches with no Gatekeeper warning. The README's Download button always points at the latest `PRMonitor.dmg`.

**Versions:** `MARKETING_VERSION` comes from the tag (SemVer). `CFBundleVersion` is the commit count, so it always increases, which update checks rely on.
**Versions:** `MARKETING_VERSION` comes from the version you pass (SemVer). `CFBundleVersion` is the commit count, so it always increases, which update checks rely on.

**Testing the pipeline without signing:** `SIGN_IDENTITY=- scripts/release.sh 0.0.0-test` builds an ad-hoc DMG and skips notarization.

## Optional: releasing from GitHub Actions

`.github/workflows/release.yml` can do the same build on GitHub's machines, but it only runs when started by hand (Actions › Release › Run workflow), and only once these repository secrets exist. It isn't needed for local releases.

| Secret | How to get it |
| --- | --- |
| `DEVELOPER_ID_P12_BASE64` | In Keychain Access, export the Developer ID Application certificate *with its private key* as a .p12, then `base64 -i cert.p12 \| gh secret set DEVELOPER_ID_P12_BASE64` |
| `DEVELOPER_ID_P12_PASSWORD` | The password you set when exporting the .p12 |
| `APPLE_TEAM_ID` | `G2537ZRGNU` |
| `ASC_API_KEY_P8_BASE64` | App Store Connect › Users and Access › Integrations › Team Keys: create a key with the Developer role and download `AuthKey_XXXX.p8`, then base64-encode it |
| `ASC_KEY_ID` | The key's ID, shown next to it |
| `ASC_ISSUER_ID` | The Issuer ID at the top of the Team Keys page |

## Homebrew

`packaging/homebrew/pr-monitor.rb` is a ready-made cask.
Expand All @@ -85,4 +83,4 @@ If it's ever wanted, add an `APPSTORE` build configuration that enables the sand

## Next: automatic updates

Add [Sparkle 2](https://sparkle-project.org): generate EdDSA keys, set `SUFeedURL` and `SUPublicEDKey` in `Info.plist`, and have the release workflow run `sign_update` and `generate_appcast`, attaching `appcast.xml` to the release. Until then, users update by downloading the new DMG, and a Developer ID signature keeps their keychain access working across versions.
Add [Sparkle 2](https://sparkle-project.org): generate EdDSA keys, set `SUFeedURL` and `SUPublicEDKey` in `Info.plist`, and have `scripts/release.sh` run `sign_update` and `generate_appcast`, attaching `appcast.xml` to the release. Until then, users update by downloading the new DMG, and a Developer ID signature keeps their keychain access working across versions.
30 changes: 27 additions & 3 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bash
# Builds a signed, notarized, stapled PR Monitor release: dist/PRMonitor.dmg and dist/PRMonitor.zip.
#
# scripts/release.sh 1.0.0
# scripts/release.sh 1.1.0 build, sign, notarize into dist/
# scripts/release.sh 1.1.0 --publish …then tag v1.1.0 on origin/main and publish the GitHub release
#
# Environment:
# TEAM_ID Apple Developer team (default: G2537ZRGNU)
Expand All @@ -13,8 +14,23 @@
set -euo pipefail

cd "$(dirname "$0")/.."
VERSION="${1:?usage: scripts/release.sh <version>}"
VERSION="${1:?usage: scripts/release.sh <version> [--publish]}"
VERSION="${VERSION#v}"
PUBLISH="${2:-}"
if [[ -n "$PUBLISH" && "$PUBLISH" != "--publish" ]]; then
echo "error: unknown option $PUBLISH" >&2; exit 1
fi
if [[ "$PUBLISH" == "--publish" ]]; then
# Publish only what's on main, exactly as pushed, so the release matches the tagged source.
git fetch -q origin main --tags
if [[ -n "$(git status --porcelain)" || "$(git rev-parse HEAD)" != "$(git rev-parse origin/main)" ]]; then
echo "error: --publish needs a clean checkout of origin/main" >&2; exit 1
fi
if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
echo "error: tag v$VERSION already exists" >&2; exit 1
fi
[[ "${SIGN_IDENTITY:-}" == "-" ]] && { echo "error: can't publish an ad-hoc build" >&2; exit 1; }
fi
TEAM_ID="${TEAM_ID:-G2537ZRGNU}"
SIGN_IDENTITY="${SIGN_IDENTITY:-Developer ID Application}"
BUILD_NUMBER="${BUILD_NUMBER:-$(git rev-list --count HEAD)}"
Expand Down Expand Up @@ -79,5 +95,13 @@ fi

ditto -c -k --keepParent "$APP" "$DIST/PRMonitor.zip"

step "Done"
step "Checksums"
( cd "$DIST" && LC_ALL=C shasum -a 256 PRMonitor.dmg PRMonitor.zip | tee SHA256SUMS )

if [[ "$PUBLISH" == "--publish" ]]; then
step "Publishing v$VERSION"
git tag -a "v$VERSION" -m "PR Monitor $VERSION"
git push -q origin "v$VERSION"
gh release create "v$VERSION" "$DIST/PRMonitor.dmg" "$DIST/PRMonitor.zip" "$DIST/SHA256SUMS" \
Comment on lines +103 to +105

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Allow publishing to resume after pushing the tag

If the tag push succeeds but gh release create then fails because of a transient network or asset-upload error, the next invocation fetches that tag and exits at the existing-tag guard before it can retry publication. This is particularly plausible because gh release create --help documents creation, asset upload, and publication as separate API calls. The advertised one-command flow can thus leave a remote tag or draft without a usable release; permit reuse when the existing tag resolves to the current origin/main commit and resume the release operation.

Useful? React with 👍 / 👎.

--title "PR Monitor $VERSION" --generate-notes --verify-tag --latest
fi
Loading