Skip to content

Merge pull request #17 from mohnya-org/fix/extract-certificates-synta… #13

Merge pull request #17 from mohnya-org/fix/extract-certificates-synta…

Merge pull request #17 from mohnya-org/fix/extract-certificates-synta… #13

name: Release macOS App
on:
push:
branches:
- main
workflow_dispatch:
inputs:
version:
description: "Release version without the v prefix. Omit to use CFBundleShortVersionString from Resources/Info.plist."
required: false
permissions:
contents: write
jobs:
prepare-release:
runs-on:
- self-hosted
- macOS
outputs:
version: ${{ steps.metadata.outputs.version }}
tag: ${{ steps.metadata.outputs.tag }}
zip_name: ${{ steps.metadata.outputs.zip_name }}
zip_path: ${{ steps.metadata.outputs.zip_path }}
checksum_path: ${{ steps.metadata.outputs.checksum_path }}
appcast_path: ${{ steps.metadata.outputs.appcast_path }}
should_release: ${{ steps.tag.outputs.should_release }}
skip_reason: ${{ steps.tag.outputs.skip_reason }}
steps:
- name: Check out repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Resolve release metadata
id: metadata
env:
INPUT_VERSION: ${{ github.event.inputs.version }}
run: |
set -euo pipefail
if [[ -n "${INPUT_VERSION:-}" ]]; then
VERSION="$INPUT_VERSION"
else
VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' Resources/Info.plist)"
fi
[[ -n "$VERSION" ]] || { echo "Could not resolve app version" >&2; exit 1; }
TAG="v$VERSION"
{
echo "version=$VERSION"
echo "tag=$TAG"
echo "zip_name=Prism Window-$VERSION-macOS.zip"
echo "zip_path=$GITHUB_WORKSPACE/dist/Prism Window-$VERSION-macOS.zip"
echo "checksum_path=$GITHUB_WORKSPACE/dist/Prism Window-$VERSION-macOS.zip.sha256"
echo "appcast_path=$GITHUB_WORKSPACE/dist/appcast.xml"
} >> "$GITHUB_OUTPUT"
- name: Create release tag if missing
id: tag
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ steps.metadata.outputs.tag }}
run: |
set -euo pipefail
git fetch --tags
if git rev-parse "$TAG" >/dev/null 2>&1; then
EXISTING_SHA="$(git rev-list -n 1 "$TAG")"
CURRENT_SHA="$(git rev-parse HEAD)"
if [[ "$EXISTING_SHA" == "$CURRENT_SHA" ]]; then
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=tag already exists for this commit" >> "$GITHUB_OUTPUT"
echo "Tag $TAG already exists for this commit. Skipping release."
exit 0
fi
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "skip_reason=tag already exists on another commit" >> "$GITHUB_OUTPUT"
echo "Tag $TAG already exists on another commit ($EXISTING_SHA). Skipping release."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git tag "$TAG"
git push origin "$TAG"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "skip_reason=" >> "$GITHUB_OUTPUT"
- name: Report skipped release
if: steps.tag.outputs.should_release != 'true'
run: |
echo "Release skipped: ${{ steps.tag.outputs.skip_reason }}"
release:
needs: prepare-release
if: needs.prepare-release.outputs.should_release == 'true'
runs-on:
- self-hosted
- macOS
steps:
- name: Check out repository
uses: actions/checkout@v5
- name: Import Developer ID certificate
env:
APPLE_CERT_BASE64: ${{ secrets.APPLE_CERT_BASE64 }}
APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }}
run: |
set -euo pipefail
CERT_PATH="$RUNNER_TEMP/developer-id.p12"
CERT_BASE64_PATH="$RUNNER_TEMP/developer-id.p12.b64"
KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db"
echo "CERT_PATH=$CERT_PATH" >> "$GITHUB_ENV"
echo "CERT_BASE64_PATH=$CERT_BASE64_PATH" >> "$GITHUB_ENV"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
printf '%s' "$APPLE_CERT_BASE64" > "$CERT_BASE64_PATH"
base64 -D -i "$CERT_BASE64_PATH" -o "$CERT_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 import "$CERT_PATH" \
-k "$KEYCHAIN_PATH" \
-P "$APPLE_CERT_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db
security default-keychain -s "$KEYCHAIN_PATH"
DEVELOPER_ID_G1="$RUNNER_TEMP/DeveloperIDCA.cer"
DEVELOPER_ID_G2="$RUNNER_TEMP/DeveloperIDG2CA.cer"
/usr/bin/curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDCA.cer -o "$DEVELOPER_ID_G1"
/usr/bin/curl -fsSL https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer -o "$DEVELOPER_ID_G2"
security import "$DEVELOPER_ID_G1" -k "$KEYCHAIN_PATH"
security import "$DEVELOPER_ID_G2" -k "$KEYCHAIN_PATH"
SIGNING_IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | awk '/Developer ID Application/ {print $2; exit}')"
[[ -n "$SIGNING_IDENTITY" ]] || { echo "Developer ID Application identity not found" >&2; exit 1; }
echo "SIGNING_IDENTITY=$SIGNING_IDENTITY" >> "$GITHUB_ENV"
- name: Build app bundle
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
SPARKLE_PUBLIC_ED_KEY: ${{ secrets.SPARKLE_PUBLIC_ED_KEY }}
run: |
set -euo pipefail
[[ -n "$SPARKLE_PUBLIC_ED_KEY" ]] || { echo "SPARKLE_PUBLIC_ED_KEY is required" >&2; exit 1; }
mkdir -p dist
VERSION="$VERSION" BUILD_NUMBER="${GITHUB_RUN_NUMBER}" SPARKLE_PUBLIC_ED_KEY="$SPARKLE_PUBLIC_ED_KEY" CODESIGN_IDENTITY="-" ./scripts/build-app.sh
rm -rf "dist/Prism Window.app"
cp -R ".build/release/Prism Window.app" "dist/Prism Window.app"
- name: Verify app architecture
run: |
set -euo pipefail
APP_BINARY="dist/Prism Window.app/Contents/MacOS/PrismWindow"
ARCH_INFO="$(file "$APP_BINARY")"
echo "$ARCH_INFO"
echo "$ARCH_INFO" | grep -q "arm64"
- name: Sign app bundle
run: |
set -euo pipefail
APP_PATH="dist/Prism Window.app" SIGNING_IDENTITY="$SIGNING_IDENTITY" KEYCHAIN_PATH="$KEYCHAIN_PATH" ./scripts/sign-release-app.sh
- name: Prepare App Store Connect API key
env:
APPLE_API_PRIVATE_KEY: ${{ secrets.APPLE_API_PRIVATE_KEY }}
run: |
set -euo pipefail
KEY_PATH="$RUNNER_TEMP/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8"
printf '%s' "$APPLE_API_PRIVATE_KEY" > "$KEY_PATH"
chmod 600 "$KEY_PATH"
echo "APPLE_API_KEY_PATH=$KEY_PATH" >> "$GITHUB_ENV"
- name: Create signed archive for notarization
env:
ZIP_PATH: ${{ needs.prepare-release.outputs.zip_path }}
run: |
set -euo pipefail
mkdir -p dist
ditto -c -k --keepParent "dist/Prism Window.app" "$ZIP_PATH"
- name: Notarize app archive
env:
ZIP_PATH: ${{ needs.prepare-release.outputs.zip_path }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
set -euo pipefail
[[ -n "$APPLE_TEAM_ID" ]] || { echo "APPLE_TEAM_ID is required" >&2; exit 1; }
xcrun notarytool submit "$ZIP_PATH" \
--key "$APPLE_API_KEY_PATH" \
--key-id "${{ secrets.APPLE_API_KEY_ID }}" \
--issuer "${{ secrets.APPLE_API_ISSUER_ID }}" \
--team-id "$APPLE_TEAM_ID" \
--wait
- name: Staple notarization ticket
run: |
set -euo pipefail
xcrun stapler staple "dist/Prism Window.app"
xcrun stapler validate "dist/Prism Window.app"
spctl --assess --type execute --verbose=4 "dist/Prism Window.app"
- name: Recreate release archive and checksum
env:
ZIP_PATH: ${{ needs.prepare-release.outputs.zip_path }}
CHECKSUM_PATH: ${{ needs.prepare-release.outputs.checksum_path }}
run: |
set -euo pipefail
rm -f "$ZIP_PATH" "$CHECKSUM_PATH"
ditto -c -k --keepParent "dist/Prism Window.app" "$ZIP_PATH"
shasum -a 256 "$ZIP_PATH" > "$CHECKSUM_PATH"
- name: Verify final release archive
env:
ZIP_PATH: ${{ needs.prepare-release.outputs.zip_path }}
run: |
set -euo pipefail
VERIFY_DIR="$RUNNER_TEMP/prism-window-release-verify"
rm -rf "$VERIFY_DIR"
mkdir -p "$VERIFY_DIR"
ditto -x -k "$ZIP_PATH" "$VERIFY_DIR"
codesign --verify --deep --strict --verbose=2 "$VERIFY_DIR/Prism Window.app"
codesign -d --extract-certificates="$RUNNER_TEMP/prism-window-cert-" "$VERIFY_DIR/Prism Window.app"
ls "$RUNNER_TEMP"/prism-window-cert-* >/dev/null
xcrun stapler validate "$VERIFY_DIR/Prism Window.app"
spctl --assess --type execute --verbose=4 "$VERIFY_DIR/Prism Window.app"
- name: Generate Sparkle appcast
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
BUILD_NUMBER: ${{ github.run_number }}
TAG: ${{ needs.prepare-release.outputs.tag }}
ZIP_PATH: ${{ needs.prepare-release.outputs.zip_path }}
OUTPUT_PATH: ${{ needs.prepare-release.outputs.appcast_path }}
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
set -euo pipefail
[[ -n "$SPARKLE_PRIVATE_KEY" ]] || { echo "SPARKLE_PRIVATE_KEY is required" >&2; exit 1; }
PRIVATE_KEY_PATH="$RUNNER_TEMP/sparkle_private_key"
printf '%s' "$SPARKLE_PRIVATE_KEY" > "$PRIVATE_KEY_PATH"
chmod 600 "$PRIVATE_KEY_PATH"
SPARKLE_PRIVATE_KEY_PATH="$PRIVATE_KEY_PATH" ./scripts/generate-appcast.sh
- name: Create or update GitHub Release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.prepare-release.outputs.tag }}
ZIP_PATH: ${{ needs.prepare-release.outputs.zip_path }}
CHECKSUM_PATH: ${{ needs.prepare-release.outputs.checksum_path }}
APPCAST_PATH: ${{ needs.prepare-release.outputs.appcast_path }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
gh release upload "$TAG" "$ZIP_PATH" "$CHECKSUM_PATH" "$APPCAST_PATH" --clobber
else
gh release create "$TAG" "$ZIP_PATH" "$CHECKSUM_PATH" "$APPCAST_PATH" \
--title "$TAG" \
--generate-notes
fi
- name: Cleanup signing keychain
if: always()
run: |
set +e
if [[ -n "${KEYCHAIN_PATH:-}" ]]; then
security default-keychain -s login.keychain-db || true
security list-keychains -d user -s login.keychain-db || true
security delete-keychain "$KEYCHAIN_PATH" || true
fi
rm -f "${CERT_PATH:-}" "${CERT_BASE64_PATH:-}" "${APPLE_API_KEY_PATH:-}" "${DEVELOPER_ID_G1:-}" "${DEVELOPER_ID_G2:-}"