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
90 changes: 77 additions & 13 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,6 @@ jobs:
exit 1
fi

BUILD_VERSIONS="$(printf '%s\n' "$BUILD_SETTINGS" | awk '$1 == "CURRENT_PROJECT_VERSION" && $2 == "=" { print $3 }' | sort -u)"
if [[ "$BUILD_VERSIONS" != "$MARKETING_VERSIONS" ]]; then
echo "CURRENT_PROJECT_VERSION must equal MARKETING_VERSION ($MARKETING_VERSIONS), but found: $BUILD_VERSIONS"
echo "Sparkle compares CFBundleVersion to decide whether an update is newer, so the build version must advance with every release."
exit 1
fi

- name: Import Apple signing certificate
env:
Expand Down Expand Up @@ -84,7 +78,12 @@ jobs:
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db

- name: Build macOS app archive
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
# CURRENT_PROJECT_VERSION override: the Sparkle feed compares
# CFBundleVersion, so released builds stamp the marketing version
# there. The project file itself keeps a plain build number.
xcodebuild archive \
-project ComputerSolitaire.xcodeproj \
-scheme ComputerSolitaire \
Expand All @@ -95,15 +94,45 @@ jobs:
CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \
OTHER_CODE_SIGN_FLAGS="--keychain $KEYCHAIN_PATH" \
CURRENT_PROJECT_VERSION="${RELEASE_TAG#v}" \
ARCHS=arm64

# Exporting re-signs all nested code (Sparkle's Autoupdate, Updater.app,
# and XPC services ship ad-hoc signed) with the Developer ID identity and
# secure timestamps. Copying the app straight out of the archive leaves
# those ad-hoc signatures in place and notarization rejects them.
- name: Export Developer ID app
run: |
cat > build/ExportOptions.plist <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Developer ID Application</string>
<key>teamID</key>
<string>TEAM_ID_PLACEHOLDER</string>
</dict>
</plist>
EOF
sed -i '' "s/TEAM_ID_PLACEHOLDER/${{ secrets.APPLE_TEAM_ID }}/" build/ExportOptions.plist

xcodebuild -exportArchive \
-archivePath build/ComputerSolitaire.xcarchive \
-exportOptionsPlist build/ExportOptions.plist \
-exportPath build/export

- name: Create DMG
run: |
mkdir -p dmg-contents

APP_PATH="$(find build/ComputerSolitaire.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
APP_PATH="$(find build/export -maxdepth 1 -name '*.app' -print -quit)"
if [[ -z "${APP_PATH}" ]]; then
echo "No .app bundle found in archive output."
echo "No .app bundle found in export output."
exit 1
fi

Expand Down Expand Up @@ -146,11 +175,29 @@ jobs:
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
run: |
DMG_PATH="ComputerSolitaire-${{ github.ref_name }}-arm64.dmg"
xcrun notarytool submit "$DMG_PATH" \

# notarytool's exit code reflects "submission completed", not the
# verdict — an Invalid result can still exit 0. Check the status
# explicitly and surface Apple's per-file log on failure.
SUBMIT_OUTPUT="$(xcrun notarytool submit "$DMG_PATH" \
--key "$NOTARY_API_KEY_PATH" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" \
--wait
--wait | tee /dev/stderr)"

SUBMISSION_ID="$(printf '%s\n' "$SUBMIT_OUTPUT" | awk '$1 == "id:" { print $2; exit }')"
STATUS="$(printf '%s\n' "$SUBMIT_OUTPUT" | awk '$1 == "status:" { status = $2 } END { print status }')"

if [[ "$STATUS" != "Accepted" ]]; then
echo "Notarization verdict: ${STATUS:-unknown} — fetching Apple's log for submission ${SUBMISSION_ID:-unknown}"
if [[ -n "$SUBMISSION_ID" ]]; then
xcrun notarytool log "$SUBMISSION_ID" \
--key "$NOTARY_API_KEY_PATH" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID"
fi
exit 1
fi

- name: Staple notarization ticket
run: |
Expand All @@ -159,19 +206,36 @@ jobs:
xcrun stapler validate "$DMG_PATH"

- name: Verify signed artifact
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
APP_PATH="$(find build/ComputerSolitaire.xcarchive/Products/Applications -maxdepth 1 -name '*.app' -print -quit)"
APP_PATH="$(find build/export -maxdepth 1 -name '*.app' -print -quit)"
if [[ -z "${APP_PATH}" ]]; then
echo "No .app bundle found in archive output."
echo "No .app bundle found in export output."
exit 1
fi

DMG_PATH="ComputerSolitaire-${{ github.ref_name }}-arm64.dmg"
DMG_PATH="ComputerSolitaire-$RELEASE_TAG-arm64.dmg"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
spctl --assess --type execute --verbose=4 "$APP_PATH"
codesign --verify --verbose=2 "$DMG_PATH"
xcrun stapler validate "$DMG_PATH"

# Sparkle's nested helpers must carry the Developer ID signature,
# not the ad-hoc one they ship with.
SPARKLE_BIN="$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
if codesign -dvv "$SPARKLE_BIN" 2>&1 | grep -q "Signature=adhoc"; then
echo "Sparkle helper is still ad-hoc signed; export did not re-sign nested code."
exit 1
fi

# The Sparkle feed keys on the marketing version.
BUNDLE_VERSION="$(/usr/libexec/PlistBuddy -c 'Print CFBundleVersion' "$APP_PATH/Contents/Info.plist")"
if [[ "v$BUNDLE_VERSION" != "$RELEASE_TAG" ]]; then
echo "CFBundleVersion ($BUNDLE_VERSION) does not match the release tag ($RELEASE_TAG)."
exit 1
fi

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
Expand Down
Loading