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
115 changes: 115 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ on:
description: 'Version to release (e.g., 1.3.1)'
required: true
type: string
dry_run:
description: 'Build, sign, notarize, and upload artifacts without publishing a GitHub Release'
required: false
default: true
type: boolean

# Grant GITHUB_TOKEN the permissions required to make releases
permissions:
Expand Down Expand Up @@ -66,6 +71,54 @@ jobs:
fi

echo "SPARKLE_PUBLIC_ED_KEY=$PUBLIC_ED_KEY" >> $GITHUB_ENV

- name: Validate Apple signing configuration
env:
DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
missing=0
for name in DEVELOPER_ID_APPLICATION MACOS_CERTIFICATE_P12 MACOS_CERTIFICATE_PASSWORD APPLE_ID APPLE_TEAM_ID APP_SPECIFIC_PASSWORD; do
if [ -z "${!name}" ]; then
echo "Error: Missing GitHub secret: $name"
missing=1
fi
done

if [ "$missing" -ne 0 ]; then
exit 1
fi

- name: Import Developer ID certificate
env:
DEVELOPER_ID_APPLICATION: ${{ secrets.DEVELOPER_ID_APPLICATION }}
MACOS_CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH="$RUNNER_TEMP/developer_id_application.p12"
KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db"
KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-temporary-password-${{ github.run_id }}}"

echo "$MACOS_CERTIFICATE_P12" | base64 --decode > "$CERTIFICATE_PATH"

security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | sed 's/"//g')
security default-keychain -s "$KEYCHAIN_PATH"
security import "$CERTIFICATE_PATH" -k "$KEYCHAIN_PATH" -P "$MACOS_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"

if ! security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -F "$DEVELOPER_ID_APPLICATION"; then
echo "Error: Developer ID identity not found in keychain: $DEVELOPER_ID_APPLICATION"
exit 1
fi

- name: Update version in code
run: |
Expand Down Expand Up @@ -111,6 +164,7 @@ jobs:
make package
env:
SPARKLE_PUBLIC_ED_KEY: ${{ env.SPARKLE_PUBLIC_ED_KEY }}
SIGN_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION }}

- name: Create DMG
run: |
Expand Down Expand Up @@ -138,6 +192,53 @@ jobs:
ls -la dist/
exit 1
fi
env:
SPARKLE_PUBLIC_ED_KEY: ${{ env.SPARKLE_PUBLIC_ED_KEY }}
SIGN_IDENTITY: ${{ secrets.DEVELOPER_ID_APPLICATION }}

- name: Capture signing diagnostics
run: |
set -o pipefail
mkdir -p dist/notary-logs
{
echo "DMG:"
ls -lh "dist/${{ steps.dmg_name.outputs.DMG_NAME }}.dmg"
codesign --verify --verbose=4 "dist/${{ steps.dmg_name.outputs.DMG_NAME }}.dmg"
codesign -dv --verbose=4 "dist/${{ steps.dmg_name.outputs.DMG_NAME }}.dmg"
echo
echo "App bundle verification:"
codesign --verify --strict --deep --verbose=4 dist/CiteBar.app
echo
echo "Main executable verification:"
codesign --verify --strict --verbose=4 dist/CiteBar.app/Contents/MacOS/CiteBar
codesign -dv --verbose=4 dist/CiteBar.app/Contents/MacOS/CiteBar
echo
echo "Sparkle framework binary verification:"
codesign --verify --strict --verbose=4 dist/CiteBar.app/Contents/Frameworks/Sparkle.framework/Versions/B/Sparkle
codesign -dv --verbose=4 dist/CiteBar.app/Contents/Frameworks/Sparkle.framework/Versions/B/Sparkle
echo
echo "Gatekeeper pre-notarization assessment:"
spctl -a -vvv -t exec dist/CiteBar.app || true
} 2>&1 | tee dist/notary-logs/signing-diagnostics.txt

- name: Notarize and staple DMG
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
NOTARY_TIMEOUT_SECONDS: 3600
run: |
scripts/notarize-dmg.sh "dist/${{ steps.dmg_name.outputs.DMG_NAME }}.dmg"

- name: Upload notarization diagnostics
if: always()
uses: actions/upload-artifact@v4
with:
name: notarization-diagnostics-${{ steps.get_version.outputs.TAG }}
path: |
dist/notary-logs/**
if-no-files-found: warn
retention-days: 14

- name: Build Sparkle sign_update tool
run: |
Expand Down Expand Up @@ -211,8 +312,22 @@ jobs:
# Generate release notes from AppVersion.swift
RELEASE_NOTES_MARKDOWN=$(swift scripts/extract-release-notes.swift markdown)
echo "$RELEASE_NOTES_MARKDOWN" > release-notes.md

- name: Upload dry-run release artifacts
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == true }}
uses: actions/upload-artifact@v4
with:
name: citebar-${{ steps.dmg_name.outputs.VERSION_FROM_FILE }}-dry-run-release
path: |
dist/${{ steps.dmg_name.outputs.DMG_NAME }}.dmg
appcast.xml
release-notes.md
dist/notary-logs/**
if-no-files-found: error
retention-days: 14

- name: Publish Release
if: ${{ github.event_name == 'push' || inputs.dry_run == false }}
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ steps.get_version.outputs.TAG }}
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Temporary Items

# Logs
*.log
notary-log*.json

# Sparkle signing keys
.sparkle/private_ed_key.txt
224 changes: 224 additions & 0 deletions APPLE_DISTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Apple Developer ID Distribution Setup

This project distributes CiteBar outside the Mac App Store. The production release path is:

1. Build a universal macOS app bundle.
2. Sign the app and embedded Sparkle framework with a `Developer ID Application` certificate.
3. Create the drag-to-Applications DMG.
4. Sign the DMG container with the same `Developer ID Application` certificate.
5. Submit the DMG to Apple's notary service with `notarytool`.
6. Staple the notarization ticket to the DMG.
7. Publish the notarized DMG and Sparkle appcast on GitHub Releases.

Apple references:

- [Signing Mac Software with Developer ID](https://developer.apple.com/developer-id/)
- [Notarizing macOS software before distribution](https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution)
- [Customizing the notarization workflow](https://developer.apple.com/documentation/security/customizing-the-notarization-workflow)
- [TN3147: Migrating to the latest notarization tool](https://developer.apple.com/documentation/technotes/tn3147-migrating-to-the-latest-notarization-tool)

## What You Need From Apple

You need these values and credentials. Do not commit private keys, `.p12` files, or app-specific passwords.

| Name | Where to get it | Used as |
| --- | --- | --- |
| Apple ID email | Apple Developer account login | `APPLE_ID` |
| Team ID | Apple Developer account membership page | `APPLE_TEAM_ID` |
| Developer ID Application identity | Keychain Access after importing the certificate | `DEVELOPER_ID_APPLICATION` |
| Developer ID Application `.p12` | Export from Keychain Access | `MACOS_CERTIFICATE_P12` |
| `.p12` export password | You choose this during export | `MACOS_CERTIFICATE_PASSWORD` |
| App-specific password | appleid.apple.com account security settings | `APP_SPECIFIC_PASSWORD` |
| Sparkle public/private EdDSA keys | Existing `RELEASING.md` Sparkle setup | `SPARKLE_PUBLIC_ED_KEY`, `SPARKLE_PRIVATE_KEY` |

The signing identity must look like:

```text
Developer ID Application: Your Legal Name or Company Name (TEAMID1234)
```

## Apple Developer Portal Setup

1. Sign in to <https://developer.apple.com/account/>.
2. Confirm the membership is active and you can access Certificates, Identifiers & Profiles.
3. Create or download a `Developer ID Application` certificate:
- Open Certificates, Identifiers & Profiles.
- Add a certificate.
- Choose `Developer ID Application`.
- If Apple asks for a CSR, create one in Keychain Access using Certificate Assistant.
- Download the generated `.cer` file.
- Double-click it so it imports into your login keychain.
4. In Keychain Access, find the certificate and verify it has a private key under it.
5. Export it as a `.p12`:
- Select the certificate and private key.
- File > Export Items.
- Format: Personal Information Exchange (`.p12`).
- Set a strong export password and save it somewhere secure.
6. Create an app-specific password:
- Go to <https://appleid.apple.com/>.
- Sign-In and Security > App-Specific Passwords.
- Generate one named `CiteBar Notarization`.

## Local Machine Setup

Install or update Xcode from the Mac App Store, then make sure command line tools use that Xcode:

```bash
xcode-select -p
xcodebuild -version
xcrun notarytool --help
```

Store notarization credentials in your local keychain:

```bash
xcrun notarytool store-credentials "CiteBar Notary" \
--apple-id "you@example.com" \
--team-id "TEAMID1234"
```

When prompted, paste the app-specific password.

Find the exact signing identity:

```bash
security find-identity -v -p codesigning | grep "Developer ID Application"
```

Build, sign, notarize, and staple locally:

```bash
make notarize \
SIGN_IDENTITY="Developer ID Application: Your Legal Name (TEAMID1234)" \
NOTARY_PROFILE="CiteBar Notary"
```

The final DMG will be in `dist/`.

## GitHub Secrets Setup

Base64-encode the `.p12` for GitHub Actions:

```bash
base64 -i DeveloperIDApplication.p12 -o DeveloperIDApplication.p12.base64
```

Configure these GitHub repository secrets:

```bash
gh secret set DEVELOPER_ID_APPLICATION --body "Developer ID Application: Your Legal Name (TEAMID1234)"
gh secret set MACOS_CERTIFICATE_P12 < DeveloperIDApplication.p12.base64
gh secret set MACOS_CERTIFICATE_PASSWORD --body "your-p12-export-password"
gh secret set APPLE_ID --body "you@example.com"
gh secret set APPLE_TEAM_ID --body "TEAMID1234"
gh secret set APP_SPECIFIC_PASSWORD --body "xxxx-xxxx-xxxx-xxxx"
```

Optional, only if you want to control the temporary CI keychain password:

```bash
gh secret set KEYCHAIN_PASSWORD --body "a-long-random-temporary-keychain-password"
```

Sparkle secrets are still required:

```bash
gh variable set SPARKLE_PUBLIC_ED_KEY --body "your-public-ed-key"
gh secret set SPARKLE_PRIVATE_KEY < .sparkle/private_ed_key.txt
```

## Release

After the secrets are configured, the existing release workflow will sign, notarize, staple, generate the Sparkle appcast, and publish the release when a version tag is pushed:

```bash
git tag v1.x.y
git push origin v1.x.y
```

You can also start the workflow manually from GitHub Actions and provide the version.

## Verification

For a local DMG:

```bash
codesign --verify --verbose=4 dist/CiteBar-*.dmg
xcrun stapler validate dist/CiteBar-*.dmg
spctl -a -vvv -t open --context context:primary-signature dist/CiteBar-*.dmg
```

For the app bundle before DMG creation:

```bash
codesign --verify --strict --deep --verbose=2 dist/CiteBar.app
codesign -dv --verbose=4 dist/CiteBar.app
spctl -a -vvv -t exec dist/CiteBar.app
```

Expected outcome:

- `codesign` verification succeeds.
- `spctl` shows accepted Developer ID assessment for the signed DMG.
- `stapler validate` confirms the ticket is attached.
- A fresh Mac can open the downloaded DMG, drag CiteBar to Applications, and launch without Terminal commands.

## Notarization Diagnostics

The notarization script writes diagnostics for every submission under:

```bash
dist/notary-logs/
```

Each run gets its own timestamped directory and `dist/notary-logs/latest` points to the newest run. Important files:

- `submit.json`: the upload response with the Apple submission ID.
- `submission-id.txt`: the Apple submission ID only.
- `poll-001.json`, `poll-002.json`, etc.: every status response from `notarytool info`.
- `latest-info.json`: the newest status response.
- `submission-log.json`: the Apple notary submission log, when Apple makes it available.
- `signing-diagnostics.txt`: GitHub Actions signing checks before notarization.

If notarization is slow, check the current status:

```bash
SUBMISSION_ID=$(cat dist/notary-logs/latest/submission-id.txt)
xcrun notarytool info "$SUBMISSION_ID" --keychain-profile "CiteBar Notary"
```

If notarization fails or remains stuck long enough to contact Apple Developer Support, collect:

```bash
SUBMISSION_ID=$(cat dist/notary-logs/latest/submission-id.txt)
xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "CiteBar Notary" \
"dist/notary-logs/latest/submission-log.json"
```

When contacting Apple Developer Support, include:

- Team ID.
- Submission ID.
- DMG filename.
- `latest-info.json`.
- `submission-log.json`, if available.
- A short note that this is a Developer ID notarization submission using `notarytool`.

Apple's Notary API also exposes a submission-log endpoint; `xcrun notarytool log` is the local CLI wrapper around the same diagnostic information.

In GitHub Actions, the release workflow uploads `notarization-diagnostics-*` as an artifact even if the notarization step fails or times out. Download that artifact from the failed workflow run before rerunning the release.

## Information To Provide To A Maintainer

Provide only these non-secret values in chat:

- `APPLE_TEAM_ID`
- Exact `DEVELOPER_ID_APPLICATION` string
- Whether you want releases built locally or by GitHub Actions

Provide these only through GitHub Secrets or another secure channel:

- `MACOS_CERTIFICATE_P12`
- `MACOS_CERTIFICATE_PASSWORD`
- `APP_SPECIFIC_PASSWORD`
- `SPARKLE_PRIVATE_KEY`
Loading
Loading