diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2eecb2..d35237c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 --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 @@ -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 @@ -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 }}" - 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() diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 40c4202..22fe3df 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -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. @@ -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. diff --git a/scripts/release.sh b/scripts/release.sh index ed53fc3..ac8791d 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -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) @@ -13,8 +14,23 @@ set -euo pipefail cd "$(dirname "$0")/.." -VERSION="${1:?usage: scripts/release.sh }" +VERSION="${1:?usage: scripts/release.sh [--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)}" @@ -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" \ + --title "PR Monitor $VERSION" --generate-notes --verify-tag --latest +fi