Notarize macOS #32
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: 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}" |