diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c225f7ac..f459cf0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,10 @@ on: type: boolean jobs: - build-macos: - runs-on: macos-latest + # Intel build on Intel runner for native compilation + # Note: macos-15-intel is the last Intel runner, supported until Fall 2027 + build-macos-intel: + runs-on: macos-15-intel steps: - uses: actions/checkout@v4 @@ -35,8 +37,8 @@ jobs: - uses: actions/cache@v4 with: path: ${{ steps.pnpm-cache.outputs.dir }} - key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: ${{ runner.os }}-pnpm- + key: ${{ runner.os }}-x64-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-x64-pnpm- - name: Install dependencies run: cd auto-claude-ui && pnpm install --frozen-lockfile @@ -51,14 +53,7 @@ jobs: CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} - - name: Package macOS (Apple Silicon) - run: cd auto-claude-ui && pnpm run package:mac -- --arch=arm64 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} - CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} - - - name: Notarize macOS apps + - name: Notarize macOS Intel app env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} @@ -83,7 +78,76 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: macos-builds + name: macos-intel-builds + path: | + auto-claude-ui/dist/*.dmg + auto-claude-ui/dist/*.zip + + # Apple Silicon build on ARM64 runner for native compilation + build-macos-arm64: + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Get pnpm store directory + id: pnpm-cache + run: echo "dir=$(pnpm store path)" >> $GITHUB_OUTPUT + + - uses: actions/cache@v4 + with: + path: ${{ steps.pnpm-cache.outputs.dir }} + key: ${{ runner.os }}-arm64-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: ${{ runner.os }}-arm64-pnpm- + + - name: Install dependencies + run: cd auto-claude-ui && pnpm install --frozen-lockfile + + - name: Build application + run: cd auto-claude-ui && pnpm run build + + - name: Package macOS (Apple Silicon) + run: cd auto-claude-ui && pnpm run package:mac -- --arch=arm64 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CSC_LINK: ${{ secrets.MAC_CERTIFICATE }} + CSC_KEY_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + + - name: Notarize macOS ARM64 app + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + if [ -z "$APPLE_ID" ]; then + echo "Skipping notarization: APPLE_ID not configured" + exit 0 + fi + cd auto-claude-ui + for dmg in dist/*.dmg; do + echo "Notarizing $dmg..." + xcrun notarytool submit "$dmg" \ + --apple-id "$APPLE_ID" \ + --password "$APPLE_APP_SPECIFIC_PASSWORD" \ + --team-id "$APPLE_TEAM_ID" \ + --wait + xcrun stapler staple "$dmg" + echo "Successfully notarized and stapled $dmg" + done + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: macos-arm64-builds path: | auto-claude-ui/dist/*.dmg auto-claude-ui/dist/*.zip @@ -179,7 +243,7 @@ jobs: auto-claude-ui/dist/*.deb create-release: - needs: [build-macos, build-windows, build-linux] + needs: [build-macos-intel, build-macos-arm64, build-windows, build-linux] runs-on: ubuntu-latest permissions: contents: write @@ -234,14 +298,51 @@ jobs: for file in release-assets/*.{exe,dmg,AppImage,deb}; do [ -f "$file" ] || continue filename=$(basename "$file") - echo "Scanning $filename..." + filesize=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file") + echo "Scanning $filename (${filesize} bytes)..." + + # For files > 32MB, get a special upload URL first + if [ "$filesize" -gt 33554432 ]; then + echo " Large file detected, requesting upload URL..." + upload_url_response=$(curl -s --request GET \ + --url "https://www.virustotal.com/api/v3/files/upload_url" \ + --header "x-apikey: $VT_API_KEY") + + upload_url=$(echo "$upload_url_response" | jq -r '.data // empty') + if [ -z "$upload_url" ]; then + echo "::error::Failed to get upload URL for large file $filename" + echo "Response: $upload_url_response" + scan_failed=true + continue + fi + api_url="$upload_url" + else + api_url="https://www.virustotal.com/api/v3/files" + fi # Upload file to VirusTotal response=$(curl -s --request POST \ - --url "https://www.virustotal.com/api/v3/files" \ + --url "$api_url" \ --header "x-apikey: $VT_API_KEY" \ --form "file=@$file") + # Check if response is valid JSON before parsing + if ! echo "$response" | jq -e . >/dev/null 2>&1; then + echo "::error::VirusTotal returned invalid JSON for $filename" + echo "Response (first 500 chars): ${response:0:500}" + scan_failed=true + continue + fi + + # Check for API error response + error_code=$(echo "$response" | jq -r '.error.code // empty') + if [ -n "$error_code" ]; then + error_msg=$(echo "$response" | jq -r '.error.message // "Unknown error"') + echo "::error::VirusTotal API error for $filename: $error_code - $error_msg" + scan_failed=true + continue + fi + # Extract analysis ID analysis_id=$(echo "$response" | jq -r '.data.id // empty') @@ -255,13 +356,20 @@ jobs: echo "Uploaded $filename, analysis ID: $analysis_id" # Wait for analysis to complete (max 5 minutes per file) + analysis="" for i in {1..30}; do sleep 10 analysis=$(curl -s --request GET \ --url "https://www.virustotal.com/api/v3/analyses/$analysis_id" \ --header "x-apikey: $VT_API_KEY") - status=$(echo "$analysis" | jq -r '.data.attributes.status') + # Validate JSON response + if ! echo "$analysis" | jq -e . >/dev/null 2>&1; then + echo " Warning: Invalid JSON response on attempt $i, retrying..." + continue + fi + + status=$(echo "$analysis" | jq -r '.data.attributes.status // "unknown"') echo " Status: $status (attempt $i/30)" if [ "$status" = "completed" ]; then @@ -269,6 +377,14 @@ jobs: fi done + # Final validation that we have valid analysis data + if ! echo "$analysis" | jq -e '.data.attributes.stats' >/dev/null 2>&1; then + echo "::warning::Could not get complete analysis for $filename, using local hash" + file_hash=$(sha256sum "$file" | cut -d' ' -f1) + echo "- [$filename](https://www.virustotal.com/gui/file/$file_hash) - ⚠️ Analysis incomplete" >> vt_results.md + continue + fi + # Get file hash for permanent URL file_hash=$(echo "$analysis" | jq -r '.meta.file_info.sha256 // empty')