Skip to content

Notarize macOS

Notarize macOS #32

name: Notarize macOS
on:
workflow_run:
workflows: ["Release macOS"]
types: [completed]
workflow_dispatch:
inputs:
version:
description: "Version (auto-detected from latest release if empty)"
required: false
run_id:
description: "Release macOS workflow run ID (auto-detected if empty)"
required: false
permissions:
contents: write
actions: write
concurrency:
group: notarize-${{ github.event.workflow_run.head_branch || github.run_id }}
cancel-in-progress: true
jobs:
# ===================================================================
# Resolve version + macOS build run_id automatically
# ===================================================================
resolve:
runs-on: ubuntu-latest
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
startsWith(github.event.workflow_run.head_branch, 'v'))
outputs:
version: ${{ steps.resolve.outputs.version }}
run_id: ${{ steps.resolve.outputs.run_id }}
steps:
- name: Resolve version and run_id
id: resolve
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
RUN_ID="${{ github.event.inputs.run_id }}"
if [ -z "$VERSION" ]; then
TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q '.tagName')
VERSION="${TAG#v}"
fi
if [ -z "$RUN_ID" ]; then
RUN_ID=$(gh run list --repo "$GITHUB_REPOSITORY" \
--workflow="Release macOS" --branch="v${VERSION}" --limit=1 \
--json databaseId,conclusion --jq '.[] | select(.conclusion=="success") | .databaseId')
fi
else
TAG="${{ github.event.workflow_run.head_branch }}"
VERSION="${TAG#v}"
RUN_ID="${{ github.event.workflow_run.id }}"
fi
if [ -z "$VERSION" ] || [ -z "$RUN_ID" ]; then
echo "::error::Failed to resolve version='${VERSION}' run_id='${RUN_ID}'"
exit 1
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "run_id=${RUN_ID}" >> $GITHUB_OUTPUT
echo "Resolved: version=${VERSION} run_id=${RUN_ID}"
# ===================================================================
# Notarize Yao binaries (arm64 + amd64)
# ===================================================================
notarize:
needs: resolve
runs-on: macos-latest
strategy:
matrix:
arch: [arm64, amd64]
steps:
- name: Download Yao Binary
uses: actions/download-artifact@v4
with:
name: yao-darwin-${{ matrix.arch }}
path: bin
run-id: ${{ needs.resolve.outputs.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Certificates
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
mkdir -p certs
echo "${{ secrets.APPLE_DEVELOPERIDG2CA }}" | base64 --decode > certs/DeveloperIDG2CA.cer
echo "${{ secrets.APPLE_DISTRIBUTION }}" | base64 --decode > certs/distribution.cer
echo "${{ secrets.APPLE_PRIVATE_KEY }}" | base64 --decode > certs/private_key.p12
security verify-cert -c certs/DeveloperIDG2CA.cer
security verify-cert -c certs/distribution.cer
- name: Import Certificates
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
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 ./certs/DeveloperIDG2CA.cer -k $KEYCHAIN_PATH -T /usr/bin/codesign
security import ./certs/distribution.cer -k $KEYCHAIN_PATH -T /usr/bin/codesign
security import ./certs/private_key.p12 -k $KEYCHAIN_PATH -P "${{ secrets.APPLE_PRIVATE_KEY_PASSWORD }}" -T /usr/bin/codesign
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Verify Signature
run: codesign --verify --deep --strict --verbose=2 bin/yao
- name: Notarize Yao ${{ matrix.arch }}
timeout-minutes: 15
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAME_ID: ${{ secrets.APPLE_TEAME_ID }}
APPLE_APP_SPEC_PASS: ${{ secrets.APPLE_APP_SPEC_PASS }}
run: |
zip -j bin/yao.zip bin/yao
SUBMIT_OUT=$(xcrun notarytool submit bin/yao.zip \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAME_ID" \
--password "$APPLE_APP_SPEC_PASS" \
--wait --timeout 10m --output-format json 2>&1) || true
echo "$SUBMIT_OUT"
STATUS=$(echo "$SUBMIT_OUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))" 2>/dev/null || true)
SUB_ID=$(echo "$SUBMIT_OUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
if [ "$STATUS" != "Accepted" ]; then
echo "::error::Yao ${{ matrix.arch }} notarization failed (status: $STATUS)"
[ -n "$SUB_ID" ] && xcrun notarytool log "$SUB_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAME_ID" \
--password "$APPLE_APP_SPEC_PASS" || true
exit 1
fi
echo "Yao ${{ matrix.arch }} notarization accepted."
# ===================================================================
# After both architectures finish: wait for Linux R2, then trigger CDN
# ===================================================================
finalize:
needs: [resolve, notarize]
runs-on: ubuntu-latest
if: success()
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
R2_ENDPOINTS: ${{ secrets.R2_ENDPOINTS }}
R2_BUCKET: ${{ secrets.R2_BUCKET || 'releases' }}
steps:
- name: Checkout (for gh CLI context)
uses: actions/checkout@v4
with:
sparse-checkout: .github
- name: Configure AWS CLI
run: |
aws configure set default.region us-east-1
aws configure set default.s3.signature_version s3v4
- name: Wait for all platform assets on R2
run: |
VERSION="${{ needs.resolve.outputs.version }}"
PREFIX="yao/${VERSION}"
# Format: "os-arch:ext" (ext is empty for non-Windows platforms)
PLATFORMS=(
"darwin-arm64:"
"darwin-amd64:"
"linux-amd64:"
"linux-arm64:"
"windows-amd64:.exe"
)
for ATTEMPT in $(seq 1 30); do
MISSING=0
for ENTRY in "${PLATFORMS[@]}"; do
P="${ENTRY%%:*}"
EXT="${ENTRY#*:}"
KEY="${PREFIX}/yao-${VERSION}-${P}${EXT}"
if ! aws s3 ls "s3://${R2_BUCKET}/${KEY}" --endpoint-url "$R2_ENDPOINTS" >/dev/null 2>&1; then
MISSING=$((MISSING+1))
fi
if ! aws s3 ls "s3://${R2_BUCKET}/${KEY}.sha256" --endpoint-url "$R2_ENDPOINTS" >/dev/null 2>&1; then
MISSING=$((MISSING+1))
fi
done
if [ "$MISSING" -eq 0 ]; then
echo "All ${#PLATFORMS[@]} platform assets verified on R2."
exit 0
fi
echo "Attempt $ATTEMPT: $MISSING asset(s) still missing, waiting 30s..."
sleep 30
done
echo "::error::Timed out waiting for all platform assets on R2."
exit 1
- name: Trigger CDN latest.json update
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ needs.resolve.outputs.version }}"
gh workflow run update-cdn-latest.yml \
-f version="${VERSION}" \
-f mark_latest="true"
echo "Triggered update-cdn-latest.yml for ${VERSION}"