Skip to content

Commit da190ce

Browse files
its-mashclaude
andcommitted
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 <noreply@anthropic.com> Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 96795b0 commit da190ce

2 files changed

Lines changed: 240 additions & 14 deletions

File tree

.github/workflows/release.yml

Lines changed: 126 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,22 +89,39 @@ jobs:
8989

9090
- run: pnpm install --frozen-lockfile
9191

92-
# Import Apple certificate for macOS signing
92+
# Import Apple certificate for macOS signing (only when Developer ID is available)
9393
- name: Import Apple certificate
9494
if: matrix.os == 'macos-latest' || matrix.os == 'macos-15'
9595
env:
9696
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
9797
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
9898
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
9999
run: |
100-
if [ -n "$APPLE_CERTIFICATE" ]; then
101-
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
102-
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
103-
security default-keychain -s build.keychain
104-
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
105-
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
106-
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
107-
rm certificate.p12
100+
if [ -z "$APPLE_CERTIFICATE" ] || [ -z "$KEYCHAIN_PASSWORD" ]; then
101+
echo "No Apple certificate configured — skipping import"
102+
exit 0
103+
fi
104+
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
105+
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
106+
security default-keychain -s build.keychain
107+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
108+
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
109+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
110+
rm certificate.p12
111+
112+
# Determine macOS signing identity: Developer ID if available, ad-hoc (-) otherwise
113+
- name: Resolve macOS signing identity
114+
if: matrix.os == 'macos-latest' || matrix.os == 'macos-15'
115+
id: macos-signing
116+
env:
117+
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
118+
run: |
119+
if [ -n "$APPLE_SIGNING_IDENTITY" ]; then
120+
echo "identity=$APPLE_SIGNING_IDENTITY" >> "$GITHUB_OUTPUT"
121+
echo "Using Developer ID signing"
122+
else
123+
echo "identity=-" >> "$GITHUB_OUTPUT"
124+
echo "Using ad-hoc signing (no Apple Developer ID)"
108125
fi
109126
110127
- name: Build Tauri app
@@ -114,11 +131,11 @@ jobs:
114131
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
115132
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
116133
PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig
117-
# macOS signing
118-
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
134+
# macOS signing — falls back to ad-hoc (-) when no Apple Developer cert
135+
APPLE_SIGNING_IDENTITY: ${{ steps.macos-signing.outputs.identity }}
119136
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
120137
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
121-
# macOS notarization
138+
# macOS notarization (only works with Developer ID)
122139
APPLE_ID: ${{ secrets.APPLE_ID }}
123140
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
124141
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
@@ -131,3 +148,100 @@ jobs:
131148
releaseDraft: false
132149
prerelease: false
133150
updaterJsonKeepUniversal: true
151+
152+
# ─────────────────────────────────────────────────────────────
153+
# Update Homebrew Tap: Push new version to homebrew-mcpmux
154+
# ─────────────────────────────────────────────────────────────
155+
update-homebrew:
156+
needs: [release-please, build-release]
157+
if: needs.release-please.outputs.release_created == 'true'
158+
runs-on: ubuntu-latest
159+
permissions:
160+
contents: read
161+
steps:
162+
- name: Wait for release assets
163+
env:
164+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
165+
run: |
166+
VERSION="${{ needs.release-please.outputs.version }}"
167+
TAG="${{ needs.release-please.outputs.tag_name }}"
168+
echo "Checking release assets for $TAG..."
169+
170+
# Wait up to 5 minutes for macOS DMGs to appear
171+
for i in $(seq 1 30); do
172+
ASSETS=$(gh api repos/${{ github.repository }}/releases/tags/$TAG --jq '.assets[].name' 2>/dev/null || echo "")
173+
if echo "$ASSETS" | grep -q "aarch64.dmg" && echo "$ASSETS" | grep -q "x64.dmg"; then
174+
echo "Both macOS DMGs found"
175+
break
176+
fi
177+
echo "Waiting for macOS DMGs... (attempt $i/30)"
178+
sleep 10
179+
done
180+
181+
- name: Compute SHA256 and update cask
182+
env:
183+
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
184+
run: |
185+
VERSION="${{ needs.release-please.outputs.version }}"
186+
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
187+
188+
# Download DMGs and compute SHA256
189+
echo "Downloading macOS DMGs..."
190+
curl -fSL "${BASE_URL}/McpMux_${VERSION}_aarch64.dmg" -o arm64.dmg || { echo "ARM64 DMG not found — skipping Homebrew update"; exit 0; }
191+
curl -fSL "${BASE_URL}/McpMux_${VERSION}_x64.dmg" -o x64.dmg || { echo "x64 DMG not found — skipping Homebrew update"; exit 0; }
192+
193+
SHA_ARM64=$(shasum -a 256 arm64.dmg | cut -d' ' -f1)
194+
SHA_X64=$(shasum -a 256 x64.dmg | cut -d' ' -f1)
195+
echo "ARM64 SHA256: $SHA_ARM64"
196+
echo "x64 SHA256: $SHA_X64"
197+
198+
# Clone the tap repo and update the cask
199+
git clone https://x-access-token:${GH_TOKEN}@github.com/ion-ash/homebrew-mcpmux.git tap
200+
201+
# Generate cask file (Ruby heredoc content with shell variable expansion)
202+
CASK_FILE="tap/Casks/mcpmux.rb"
203+
{
204+
echo 'cask "mcpmux" do'
205+
echo ' arch arm: "aarch64", intel: "x64"'
206+
echo ''
207+
echo " version \"${VERSION}\""
208+
echo " sha256 arm: \"${SHA_ARM64}\","
209+
echo " intel: \"${SHA_X64}\""
210+
echo ''
211+
echo ' url "https://github.com/ion-ash/mcp-mux/releases/download/v#{version}/McpMux_#{version}_#{arch}.dmg",'
212+
echo ' verified: "github.com/ion-ash/mcp-mux/"'
213+
echo ''
214+
echo ' name "McpMux"'
215+
echo ' desc "Unified MCP gateway and manager for AI clients"'
216+
echo ' homepage "https://mcpmux.com"'
217+
echo ''
218+
echo ' depends_on macos: ">= :high_sierra"'
219+
echo ''
220+
echo ' livecheck do'
221+
echo ' url "https://github.com/ion-ash/mcp-mux/releases/latest"'
222+
echo ' strategy :github_latest'
223+
echo ' end'
224+
echo ''
225+
echo ' app "McpMux.app"'
226+
echo ''
227+
echo ' # Remove quarantine for ad-hoc signed app (no Apple Developer ID)'
228+
echo ' postflight do'
229+
echo ' system_command "/usr/bin/xattr",'
230+
echo ' args: ["-cr", "#{appdir}/McpMux.app"]'
231+
echo ' end'
232+
echo ''
233+
echo ' zap trash: ['
234+
echo ' "~/Library/Application Support/com.mcpmux.desktop",'
235+
echo ' "~/Library/Preferences/com.mcpmux.desktop.plist",'
236+
echo ' "~/Library/Caches/com.mcpmux.desktop",'
237+
echo ' "~/Library/Saved Application State/com.mcpmux.desktop.savedState",'
238+
echo ' ]'
239+
echo 'end'
240+
} > "$CASK_FILE"
241+
242+
cd tap
243+
git config user.name "github-actions[bot]"
244+
git config user.email "github-actions[bot]@users.noreply.github.com"
245+
git add Casks/mcpmux.rb
246+
git commit -m "Update mcpmux to ${VERSION}"
247+
git push

scripts/setup-release.ps1

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,127 @@
44
# Run once to generate signing keys for Tauri auto-updater.
55
#
66
# Usage:
7-
# .\setup-release.ps1 # Generate new signing key
7+
# .\setup-release.ps1 # Generate new Tauri updater signing key
88
# .\setup-release.ps1 -ShowPubkey # Show public key for tauri.conf.json
9+
# .\setup-release.ps1 -SetupHomebrew # Set up Homebrew tap token in GitHub
10+
# .\setup-release.ps1 -ShowSecrets # Show all required GitHub secrets and their status
911

1012
param(
11-
[switch]$ShowPubkey
13+
[switch]$ShowPubkey,
14+
[switch]$SetupHomebrew,
15+
[switch]$ShowSecrets
1216
)
1317

1418
$keyPath = "$env:USERPROFILE\.tauri\mcpmux.key"
1519
$pubkeyPath = "$env:USERPROFILE\.tauri\mcpmux.key.pub"
20+
$repo = "ion-ash/mcp-mux"
21+
$tapRepo = "ion-ash/homebrew-mcpmux"
22+
23+
# ── Show all secrets status ──────────────────────────────────
24+
if ($ShowSecrets) {
25+
Write-Host ""
26+
Write-Host "McpMux GitHub Secrets Status" -ForegroundColor Cyan
27+
Write-Host "============================" -ForegroundColor Cyan
28+
Write-Host ""
29+
Write-Host "Repository: https://github.com/$repo/settings/secrets/actions" -ForegroundColor Gray
30+
Write-Host ""
31+
32+
# Check which secrets exist via gh CLI
33+
$existingSecrets = @()
34+
try {
35+
$existingSecrets = gh secret list --repo $repo --json name --jq '.[].name' 2>$null | ForEach-Object { $_.Trim() }
36+
} catch {}
37+
38+
$allSecrets = @(
39+
@{ Name = "TAURI_SIGNING_PRIVATE_KEY"; Category = "Updater (required)"; Note = "Generated by: .\setup-release.ps1" }
40+
@{ Name = "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"; Category = "Updater (optional)"; Note = "Password for the signing key" }
41+
@{ Name = "HOMEBREW_TAP_TOKEN"; Category = "Homebrew (required)"; Note = "Generated by: .\setup-release.ps1 -SetupHomebrew" }
42+
@{ Name = "APPLE_SIGNING_IDENTITY"; Category = "macOS (optional)"; Note = "Leave empty for ad-hoc signing (-)" }
43+
@{ Name = "APPLE_CERTIFICATE"; Category = "macOS (optional)"; Note = "Base64 .p12 — only with Apple Developer ID" }
44+
@{ Name = "APPLE_CERTIFICATE_PASSWORD"; Category = "macOS (optional)"; Note = ".p12 password" }
45+
@{ Name = "APPLE_ID"; Category = "macOS notarize (optional)"; Note = "Apple ID email" }
46+
@{ Name = "APPLE_PASSWORD"; Category = "macOS notarize (optional)"; Note = "App-specific password" }
47+
@{ Name = "APPLE_TEAM_ID"; Category = "macOS notarize (optional)"; Note = "10-char team ID" }
48+
@{ Name = "KEYCHAIN_PASSWORD"; Category = "macOS (optional)"; Note = "Temp keychain password for CI" }
49+
)
50+
51+
foreach ($secret in $allSecrets) {
52+
$status = if ($existingSecrets -contains $secret.Name) { "SET" } else { "NOT SET" }
53+
$color = if ($status -eq "SET") { "Green" } else {
54+
if ($secret.Category -match "optional") { "DarkGray" } else { "Yellow" }
55+
}
56+
$icon = if ($status -eq "SET") { "[OK]" } else { "[ ]" }
57+
Write-Host " $icon " -NoNewline -ForegroundColor $color
58+
Write-Host "$($secret.Name)" -NoNewline -ForegroundColor White
59+
Write-Host "$($secret.Note)" -ForegroundColor DarkGray
60+
}
61+
62+
Write-Host ""
63+
Write-Host "macOS ad-hoc signing:" -ForegroundColor Cyan
64+
Write-Host " No secrets needed! The release workflow defaults to ad-hoc signing (-)" -ForegroundColor Gray
65+
Write-Host " when APPLE_SIGNING_IDENTITY is not set. This is sufficient for" -ForegroundColor Gray
66+
Write-Host " distribution via Homebrew tap (postflight removes quarantine)." -ForegroundColor Gray
67+
Write-Host ""
68+
exit 0
69+
}
70+
71+
# ── Setup Homebrew tap token ─────────────────────────────────
72+
if ($SetupHomebrew) {
73+
Write-Host ""
74+
Write-Host "Homebrew Tap Token Setup" -ForegroundColor Cyan
75+
Write-Host "========================" -ForegroundColor Cyan
76+
Write-Host ""
77+
Write-Host "This creates a fine-grained GitHub token for the release workflow" -ForegroundColor Gray
78+
Write-Host "to push cask updates to $tapRepo." -ForegroundColor Gray
79+
Write-Host ""
80+
81+
# Check if gh is available
82+
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
83+
Write-Host "GitHub CLI (gh) is required. Install: https://cli.github.com" -ForegroundColor Red
84+
exit 1
85+
}
86+
87+
# Check auth
88+
$authStatus = gh auth status 2>&1
89+
if ($LASTEXITCODE -ne 0) {
90+
Write-Host "Not authenticated with GitHub CLI. Run: gh auth login" -ForegroundColor Red
91+
exit 1
92+
}
93+
94+
Write-Host "Creating fine-grained PAT for $tapRepo..." -ForegroundColor Gray
95+
Write-Host ""
96+
Write-Host "Since 'gh' cannot create fine-grained tokens via CLI yet," -ForegroundColor Yellow
97+
Write-Host "please create one manually:" -ForegroundColor Yellow
98+
Write-Host ""
99+
Write-Host " 1. Go to: https://github.com/settings/personal-access-tokens/new" -ForegroundColor White
100+
Write-Host " 2. Token name: mcpmux-homebrew-tap" -ForegroundColor White
101+
Write-Host " 3. Expiration: 90 days (or custom)" -ForegroundColor White
102+
Write-Host " 4. Repository: $tapRepo (only)" -ForegroundColor White
103+
Write-Host " 5. Permissions: Contents — Read and write" -ForegroundColor White
104+
Write-Host " 6. Click 'Generate token' and copy it" -ForegroundColor White
105+
Write-Host ""
106+
107+
$token = Read-Host "Paste the token here (or press Enter to skip)"
108+
if ([string]::IsNullOrWhiteSpace($token)) {
109+
Write-Host "Skipped. Set it manually:" -ForegroundColor Yellow
110+
Write-Host " gh secret set HOMEBREW_TAP_TOKEN --repo $repo" -ForegroundColor White
111+
Write-Host ""
112+
exit 0
113+
}
114+
115+
# Set the secret
116+
Write-Host ""
117+
Write-Host "Setting HOMEBREW_TAP_TOKEN secret on $repo..." -ForegroundColor Gray
118+
$token | gh secret set HOMEBREW_TAP_TOKEN --repo $repo
119+
if ($LASTEXITCODE -eq 0) {
120+
Write-Host "Done! HOMEBREW_TAP_TOKEN is now set." -ForegroundColor Green
121+
} else {
122+
Write-Host "Failed to set secret. Try manually:" -ForegroundColor Red
123+
Write-Host " gh secret set HOMEBREW_TAP_TOKEN --repo $repo" -ForegroundColor White
124+
}
125+
Write-Host ""
126+
exit 0
127+
}
16128

17129
if ($ShowPubkey) {
18130
if (Test-Path $pubkeyPath) {

0 commit comments

Comments
 (0)