Merge pull request #17 from mohnya-org/fix/extract-certificates-synta… #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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:-}" |