diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 1cd9d5e..cbadc92 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -1,294 +1,111 @@ name: Build macOS +# Release / on-demand packaging workflow. +# Per-commit compilation is covered by build-test.yml, so this only runs +# when explicitly invoked (release pipeline or manual dispatch). on: workflow_call: secrets: APPLE_CERTIFICATE: - required: true + required: false APPLE_CERTIFICATE_PASSWORD: - required: true + required: false KEYCHAIN_PASSWORD: - required: true + required: false APPLE_SIGNING_IDENTITY: - required: true + required: false APPLE_ID: - required: true + required: false APPLE_TEAM_ID: - required: true - APPLE_PASSWORD: - required: true - workflow_dispatch: - inputs: - skip_build: - description: 'Skip build and use artifacts from a previous run' required: false - default: false - type: boolean - run_id: - description: 'Run ID to download artifacts from (leave empty for latest)' + APPLE_PASSWORD: required: false - type: string - push: - branches: [main] + workflow_dispatch: jobs: build: - name: Build macOS ${{ matrix.target }} - if: ${{ !inputs.skip_build }} + name: Build macOS ${{ matrix.arch }} runs-on: ${{ matrix.os }} + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} strategy: + fail-fast: false matrix: include: - - os: macos-13 # Intel + - os: macos-13 # Intel target: x86_64-apple-darwin arch: x86_64 - - os: macos-14 # Apple Silicon + - os: macos-14 # Apple Silicon target: aarch64-apple-darwin arch: aarch64 - + steps: - uses: actions/checkout@v4 - + - name: Setup Rust uses: dtolnay/rust-toolchain@stable - + with: + targets: ${{ matrix.target }} + - name: Setup Rust cache uses: Swatinem/rust-cache@v2 with: workspaces: src-tauri - + - name: Setup Bun uses: oven-sh/setup-bun@v2 - + - name: Install dependencies run: bun install - - - name: Import Apple certificates - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - run: | - # Create variables - CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - - # Import certificate from secrets - echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH - - # Create temporary keychain - security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security set-keychain-settings -lut 21600 $KEYCHAIN_PATH - security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - - # Import certificate to keychain - security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - - - name: Build native - env: - CI: true - run: bun run tauri build - - - name: Upload architecture-specific artifacts - uses: actions/upload-artifact@v4 - with: - name: macos-${{ matrix.arch }} - path: | - src-tauri/target/release/bundle/macos/opcode.app - src-tauri/target/release/bundle/dmg/*.dmg - retention-days: 1 - - universal: - name: Create Universal Binary - needs: [build] - if: ${{ !cancelled() && (needs.build.result == 'success' || needs.build.result == 'skipped') }} - runs-on: macos-latest - steps: - - uses: actions/checkout@v4 - - - name: Download artifacts from current workflow - if: ${{ !inputs.skip_build }} - uses: actions/download-artifact@v4 - with: - pattern: macos-* - path: artifacts - - - name: Download artifacts from specific run - if: ${{ inputs.skip_build && inputs.run_id != '' }} - uses: dawidd6/action-download-artifact@v3 - with: - workflow: build-macos.yml - run_id: ${{ inputs.run_id }} - name: macos-* - path: artifacts - - - name: Download artifacts from latest run - if: ${{ inputs.skip_build && inputs.run_id == '' }} - uses: dawidd6/action-download-artifact@v3 - with: - workflow: build-macos.yml - workflow_conclusion: success - name: macos-* - path: artifacts - - - name: List downloaded artifacts - run: | - echo "๐Ÿ“ Artifact structure:" - find artifacts -type f -name "*.app" -o -name "*.dmg" | head -20 - echo "" - echo "๐Ÿ“ Full directory structure:" - ls -la artifacts/ - ls -la artifacts/macos-aarch64/ || echo "macos-aarch64 directory not found" - ls -la artifacts/macos-x86_64/ || echo "macos-x86_64 directory not found" - - - name: Import Apple certificates + + # Signing only happens when Apple Developer secrets are configured. + # Without them the build still succeeds, producing an unsigned bundle. + - name: Import Apple certificate + if: ${{ env.APPLE_CERTIFICATE != '' }} env: APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | - # Create variables - CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 - KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db - - # Import certificate from secrets - echo -n "$APPLE_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH - - # Create temporary keychain - security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security set-keychain-settings -lut 21600 $KEYCHAIN_PATH - security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - - # Import certificate to keychain - security import $CERTIFICATE_PATH -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH - security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH - security list-keychain -d user -s $KEYCHAIN_PATH - - - name: Create universal app - run: | - # Create temp directory - mkdir -p dmg_temp - - # Extract zip files if they exist - if [ -f "artifacts/macos-aarch64.zip" ]; then - echo "๐Ÿ“ฆ Extracting macos-aarch64.zip..." - unzip -q artifacts/macos-aarch64.zip -d artifacts/macos-aarch64/ - fi - - if [ -f "artifacts/macos-x86_64.zip" ]; then - echo "๐Ÿ“ฆ Extracting macos-x86_64.zip..." - unzip -q artifacts/macos-x86_64.zip -d artifacts/macos-x86_64/ - fi - - # Find the actual app paths - AARCH64_APP=$(find artifacts/macos-aarch64 -name "opcode.app" -type d | head -1) - X86_64_APP=$(find artifacts/macos-x86_64 -name "opcode.app" -type d | head -1) - - if [ -z "$AARCH64_APP" ] || [ -z "$X86_64_APP" ]; then - echo "โŒ Could not find app bundles" - echo "AARCH64_APP: $AARCH64_APP" - echo "X86_64_APP: $X86_64_APP" - exit 1 - fi - - echo "โœ… Found app bundles:" - echo " ARM64: $AARCH64_APP" - echo " x86_64: $X86_64_APP" - - # Copy ARM64 app as base - cp -R "$AARCH64_APP" dmg_temp/ - - # Create universal binary using lipo - lipo -create -output dmg_temp/opcode.app/Contents/MacOS/opcode \ - "$AARCH64_APP/Contents/MacOS/opcode" \ - "$X86_64_APP/Contents/MacOS/opcode" - - # Ensure executable permissions are set - chmod +x dmg_temp/opcode.app/Contents/MacOS/opcode - - echo "โœ… Universal binary created" - lipo -info dmg_temp/opcode.app/Contents/MacOS/opcode - - - name: Sign app bundle - env: - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - run: | - codesign --sign "$APPLE_SIGNING_IDENTITY" \ - --timestamp \ - --options runtime \ - --force \ - --deep \ - --entitlements src-tauri/entitlements.plist \ - dmg_temp/opcode.app - - - name: Create DMG - run: | - hdiutil create -volname "opcode Installer" \ - -srcfolder dmg_temp \ - -ov -format UDZO opcode.dmg - - - name: Sign DMG + CERTIFICATE_PATH="$RUNNER_TEMP/build_certificate.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/app-signing.keychain-db" + echo -n "$APPLE_CERTIFICATE" | base64 --decode -o "$CERTIFICATE_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 "$CERTIFICATE_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security list-keychain -d user -s "$KEYCHAIN_PATH" + + - name: Build Tauri app env: + # Tauri signs + notarizes when these are set; otherwise it builds unsigned. APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} - run: | - codesign --sign "$APPLE_SIGNING_IDENTITY" \ - --timestamp \ - --force opcode.dmg - - - name: Notarize DMG - env: APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: bun run tauri build --target ${{ matrix.target }} + + - name: Collect artifacts run: | - # Store notarization credentials - xcrun notarytool store-credentials "notarytool-profile" \ - --apple-id "$APPLE_ID" \ - --team-id "$APPLE_TEAM_ID" \ - --password "$APPLE_PASSWORD" - - # Submit for notarization - xcrun notarytool submit opcode.dmg \ - --keychain-profile "notarytool-profile" \ - --wait - - - name: Staple notarization - run: xcrun stapler staple opcode.dmg - - - name: Verify DMG - run: | - spctl -a -t open -vvv --context context:primary-signature opcode.dmg - echo "โœ… DMG verification complete" - - - name: Create artifacts directory - run: | - mkdir -p dist/macos-universal - cp opcode.dmg dist/macos-universal/ - - # Also save the app bundle using ditto to preserve permissions and signatures - ditto -c -k --sequesterRsrc --keepParent \ - dmg_temp/opcode.app dist/macos-universal/opcode.app.zip - - # Generate checksum - shasum -a 256 dist/macos-universal/* > dist/macos-universal/checksums.txt - + mkdir -p dist/macos-${{ matrix.arch }} + cp src-tauri/target/${{ matrix.target }}/release/bundle/dmg/*.dmg dist/macos-${{ matrix.arch }}/ || true + if ls dist/macos-${{ matrix.arch }}/*.dmg >/dev/null 2>&1; then + cd dist/macos-${{ matrix.arch }} + shasum -a 256 * > checksums.txt + fi + - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: macos-universal - path: dist/macos-universal/* - - - name: Cleanup - if: always() + name: macos-${{ matrix.arch }} + path: dist/macos-${{ matrix.arch }}/* + if-no-files-found: warn + retention-days: 7 + + - name: Cleanup keychain + if: ${{ always() && env.APPLE_CERTIFICATE != '' }} run: | - echo "๐Ÿงน Cleaning up temporary directories..." - rm -rf dmg_temp temp_x86 artifacts - - # Clean up keychain if [ -n "$RUNNER_TEMP" ] && [ -f "$RUNNER_TEMP/app-signing.keychain-db" ]; then security delete-keychain "$RUNNER_TEMP/app-signing.keychain-db" || true fi - - echo "โœ… Cleanup complete"