From da190ce0e93e4a119c59d3265dac1e1cf41fd683 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Sun, 15 Feb 2026 11:06:30 +0800 Subject: [PATCH] feat: add Homebrew tap support and ad-hoc macOS signing - Fix macOS certificate import to skip gracefully when no Apple Developer ID is configured, resolving build failures on macOS runners - Add ad-hoc signing fallback (codesign -s -) when APPLE_SIGNING_IDENTITY secret is not set, enabling macOS builds without paid Developer ID - Add update-homebrew CI job that automatically updates the ion-ash/homebrew-mcpmux cask with correct SHA256 hashes on each release - Extend setup-release.ps1 with -ShowSecrets and -SetupHomebrew flags Co-Authored-By: Claude Opus 4.6 Signed-off-by: Mohammod Al Amin Ashik --- .github/workflows/release.yml | 138 +++++++++++++++++++++++++++++++--- scripts/setup-release.ps1 | 116 +++++++++++++++++++++++++++- 2 files changed, 240 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9e9f06f..80d6c231 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,7 +89,7 @@ jobs: - run: pnpm install --frozen-lockfile - # Import Apple certificate for macOS signing + # Import Apple certificate for macOS signing (only when Developer ID is available) - name: Import Apple certificate if: matrix.os == 'macos-latest' || matrix.os == 'macos-15' env: @@ -97,14 +97,31 @@ jobs: APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} run: | - if [ -n "$APPLE_CERTIFICATE" ]; then - echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 - security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain - security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain - rm certificate.p12 + if [ -z "$APPLE_CERTIFICATE" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then + echo "No Apple certificate configured — skipping import" + exit 0 + fi + echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + rm certificate.p12 + + # Determine macOS signing identity: Developer ID if available, ad-hoc (-) otherwise + - name: Resolve macOS signing identity + if: matrix.os == 'macos-latest' || matrix.os == 'macos-15' + id: macos-signing + env: + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + run: | + if [ -n "$APPLE_SIGNING_IDENTITY" ]; then + echo "identity=$APPLE_SIGNING_IDENTITY" >> "$GITHUB_OUTPUT" + echo "Using Developer ID signing" + else + echo "identity=-" >> "$GITHUB_OUTPUT" + echo "Using ad-hoc signing (no Apple Developer ID)" fi - name: Build Tauri app @@ -114,11 +131,11 @@ jobs: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig - # macOS signing - APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + # macOS signing — falls back to ad-hoc (-) when no Apple Developer cert + APPLE_SIGNING_IDENTITY: ${{ steps.macos-signing.outputs.identity }} APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - # macOS notarization + # macOS notarization (only works with Developer ID) APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} @@ -131,3 +148,100 @@ jobs: releaseDraft: false prerelease: false updaterJsonKeepUniversal: true + + # ───────────────────────────────────────────────────────────── + # Update Homebrew Tap: Push new version to homebrew-mcpmux + # ───────────────────────────────────────────────────────────── + update-homebrew: + needs: [release-please, build-release] + if: needs.release-please.outputs.release_created == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Wait for release assets + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ needs.release-please.outputs.version }}" + TAG="${{ needs.release-please.outputs.tag_name }}" + echo "Checking release assets for $TAG..." + + # Wait up to 5 minutes for macOS DMGs to appear + for i in $(seq 1 30); do + ASSETS=$(gh api repos/${{ github.repository }}/releases/tags/$TAG --jq '.assets[].name' 2>/dev/null || echo "") + if echo "$ASSETS" | grep -q "aarch64.dmg" && echo "$ASSETS" | grep -q "x64.dmg"; then + echo "Both macOS DMGs found" + break + fi + echo "Waiting for macOS DMGs... (attempt $i/30)" + sleep 10 + done + + - name: Compute SHA256 and update cask + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + VERSION="${{ needs.release-please.outputs.version }}" + BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}" + + # Download DMGs and compute SHA256 + echo "Downloading macOS DMGs..." + curl -fSL "${BASE_URL}/McpMux_${VERSION}_aarch64.dmg" -o arm64.dmg || { echo "ARM64 DMG not found — skipping Homebrew update"; exit 0; } + curl -fSL "${BASE_URL}/McpMux_${VERSION}_x64.dmg" -o x64.dmg || { echo "x64 DMG not found — skipping Homebrew update"; exit 0; } + + SHA_ARM64=$(shasum -a 256 arm64.dmg | cut -d' ' -f1) + SHA_X64=$(shasum -a 256 x64.dmg | cut -d' ' -f1) + echo "ARM64 SHA256: $SHA_ARM64" + echo "x64 SHA256: $SHA_X64" + + # Clone the tap repo and update the cask + git clone https://x-access-token:${GH_TOKEN}@github.com/ion-ash/homebrew-mcpmux.git tap + + # Generate cask file (Ruby heredoc content with shell variable expansion) + CASK_FILE="tap/Casks/mcpmux.rb" + { + echo 'cask "mcpmux" do' + echo ' arch arm: "aarch64", intel: "x64"' + echo '' + echo " version \"${VERSION}\"" + echo " sha256 arm: \"${SHA_ARM64}\"," + echo " intel: \"${SHA_X64}\"" + echo '' + echo ' url "https://github.com/ion-ash/mcp-mux/releases/download/v#{version}/McpMux_#{version}_#{arch}.dmg",' + echo ' verified: "github.com/ion-ash/mcp-mux/"' + echo '' + echo ' name "McpMux"' + echo ' desc "Unified MCP gateway and manager for AI clients"' + echo ' homepage "https://mcpmux.com"' + echo '' + echo ' depends_on macos: ">= :high_sierra"' + echo '' + echo ' livecheck do' + echo ' url "https://github.com/ion-ash/mcp-mux/releases/latest"' + echo ' strategy :github_latest' + echo ' end' + echo '' + echo ' app "McpMux.app"' + echo '' + echo ' # Remove quarantine for ad-hoc signed app (no Apple Developer ID)' + echo ' postflight do' + echo ' system_command "/usr/bin/xattr",' + echo ' args: ["-cr", "#{appdir}/McpMux.app"]' + echo ' end' + echo '' + echo ' zap trash: [' + echo ' "~/Library/Application Support/com.mcpmux.desktop",' + echo ' "~/Library/Preferences/com.mcpmux.desktop.plist",' + echo ' "~/Library/Caches/com.mcpmux.desktop",' + echo ' "~/Library/Saved Application State/com.mcpmux.desktop.savedState",' + echo ' ]' + echo 'end' + } > "$CASK_FILE" + + cd tap + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add Casks/mcpmux.rb + git commit -m "Update mcpmux to ${VERSION}" + git push diff --git a/scripts/setup-release.ps1 b/scripts/setup-release.ps1 index f6c75bbd..a108b111 100644 --- a/scripts/setup-release.ps1 +++ b/scripts/setup-release.ps1 @@ -4,15 +4,127 @@ # Run once to generate signing keys for Tauri auto-updater. # # Usage: -# .\setup-release.ps1 # Generate new signing key +# .\setup-release.ps1 # Generate new Tauri updater signing key # .\setup-release.ps1 -ShowPubkey # Show public key for tauri.conf.json +# .\setup-release.ps1 -SetupHomebrew # Set up Homebrew tap token in GitHub +# .\setup-release.ps1 -ShowSecrets # Show all required GitHub secrets and their status param( - [switch]$ShowPubkey + [switch]$ShowPubkey, + [switch]$SetupHomebrew, + [switch]$ShowSecrets ) $keyPath = "$env:USERPROFILE\.tauri\mcpmux.key" $pubkeyPath = "$env:USERPROFILE\.tauri\mcpmux.key.pub" +$repo = "ion-ash/mcp-mux" +$tapRepo = "ion-ash/homebrew-mcpmux" + +# ── Show all secrets status ────────────────────────────────── +if ($ShowSecrets) { + Write-Host "" + Write-Host "McpMux GitHub Secrets Status" -ForegroundColor Cyan + Write-Host "============================" -ForegroundColor Cyan + Write-Host "" + Write-Host "Repository: https://github.com/$repo/settings/secrets/actions" -ForegroundColor Gray + Write-Host "" + + # Check which secrets exist via gh CLI + $existingSecrets = @() + try { + $existingSecrets = gh secret list --repo $repo --json name --jq '.[].name' 2>$null | ForEach-Object { $_.Trim() } + } catch {} + + $allSecrets = @( + @{ Name = "TAURI_SIGNING_PRIVATE_KEY"; Category = "Updater (required)"; Note = "Generated by: .\setup-release.ps1" } + @{ Name = "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"; Category = "Updater (optional)"; Note = "Password for the signing key" } + @{ Name = "HOMEBREW_TAP_TOKEN"; Category = "Homebrew (required)"; Note = "Generated by: .\setup-release.ps1 -SetupHomebrew" } + @{ Name = "APPLE_SIGNING_IDENTITY"; Category = "macOS (optional)"; Note = "Leave empty for ad-hoc signing (-)" } + @{ Name = "APPLE_CERTIFICATE"; Category = "macOS (optional)"; Note = "Base64 .p12 — only with Apple Developer ID" } + @{ Name = "APPLE_CERTIFICATE_PASSWORD"; Category = "macOS (optional)"; Note = ".p12 password" } + @{ Name = "APPLE_ID"; Category = "macOS notarize (optional)"; Note = "Apple ID email" } + @{ Name = "APPLE_PASSWORD"; Category = "macOS notarize (optional)"; Note = "App-specific password" } + @{ Name = "APPLE_TEAM_ID"; Category = "macOS notarize (optional)"; Note = "10-char team ID" } + @{ Name = "KEYCHAIN_PASSWORD"; Category = "macOS (optional)"; Note = "Temp keychain password for CI" } + ) + + foreach ($secret in $allSecrets) { + $status = if ($existingSecrets -contains $secret.Name) { "SET" } else { "NOT SET" } + $color = if ($status -eq "SET") { "Green" } else { + if ($secret.Category -match "optional") { "DarkGray" } else { "Yellow" } + } + $icon = if ($status -eq "SET") { "[OK]" } else { "[ ]" } + Write-Host " $icon " -NoNewline -ForegroundColor $color + Write-Host "$($secret.Name)" -NoNewline -ForegroundColor White + Write-Host " — $($secret.Note)" -ForegroundColor DarkGray + } + + Write-Host "" + Write-Host "macOS ad-hoc signing:" -ForegroundColor Cyan + Write-Host " No secrets needed! The release workflow defaults to ad-hoc signing (-)" -ForegroundColor Gray + Write-Host " when APPLE_SIGNING_IDENTITY is not set. This is sufficient for" -ForegroundColor Gray + Write-Host " distribution via Homebrew tap (postflight removes quarantine)." -ForegroundColor Gray + Write-Host "" + exit 0 +} + +# ── Setup Homebrew tap token ───────────────────────────────── +if ($SetupHomebrew) { + Write-Host "" + Write-Host "Homebrew Tap Token Setup" -ForegroundColor Cyan + Write-Host "========================" -ForegroundColor Cyan + Write-Host "" + Write-Host "This creates a fine-grained GitHub token for the release workflow" -ForegroundColor Gray + Write-Host "to push cask updates to $tapRepo." -ForegroundColor Gray + Write-Host "" + + # Check if gh is available + if (-not (Get-Command gh -ErrorAction SilentlyContinue)) { + Write-Host "GitHub CLI (gh) is required. Install: https://cli.github.com" -ForegroundColor Red + exit 1 + } + + # Check auth + $authStatus = gh auth status 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "Not authenticated with GitHub CLI. Run: gh auth login" -ForegroundColor Red + exit 1 + } + + Write-Host "Creating fine-grained PAT for $tapRepo..." -ForegroundColor Gray + Write-Host "" + Write-Host "Since 'gh' cannot create fine-grained tokens via CLI yet," -ForegroundColor Yellow + Write-Host "please create one manually:" -ForegroundColor Yellow + Write-Host "" + Write-Host " 1. Go to: https://github.com/settings/personal-access-tokens/new" -ForegroundColor White + Write-Host " 2. Token name: mcpmux-homebrew-tap" -ForegroundColor White + Write-Host " 3. Expiration: 90 days (or custom)" -ForegroundColor White + Write-Host " 4. Repository: $tapRepo (only)" -ForegroundColor White + Write-Host " 5. Permissions: Contents — Read and write" -ForegroundColor White + Write-Host " 6. Click 'Generate token' and copy it" -ForegroundColor White + Write-Host "" + + $token = Read-Host "Paste the token here (or press Enter to skip)" + if ([string]::IsNullOrWhiteSpace($token)) { + Write-Host "Skipped. Set it manually:" -ForegroundColor Yellow + Write-Host " gh secret set HOMEBREW_TAP_TOKEN --repo $repo" -ForegroundColor White + Write-Host "" + exit 0 + } + + # Set the secret + Write-Host "" + Write-Host "Setting HOMEBREW_TAP_TOKEN secret on $repo..." -ForegroundColor Gray + $token | gh secret set HOMEBREW_TAP_TOKEN --repo $repo + if ($LASTEXITCODE -eq 0) { + Write-Host "Done! HOMEBREW_TAP_TOKEN is now set." -ForegroundColor Green + } else { + Write-Host "Failed to set secret. Try manually:" -ForegroundColor Red + Write-Host " gh secret set HOMEBREW_TAP_TOKEN --repo $repo" -ForegroundColor White + } + Write-Host "" + exit 0 +} if ($ShowPubkey) { if (Test-Path $pubkeyPath) {