Skip to content

fix(docker): wrap docker CLI invocations in user shell to inherit PAT… #192

fix(docker): wrap docker CLI invocations in user shell to inherit PAT…

fix(docker): wrap docker CLI invocations in user shell to inherit PAT… #192

Workflow file for this run

name: Release
on:
push:
tags: ["v*"]
workflow_dispatch:
permissions:
contents: write
id-token: write
env:
GITHUB_ENVIRONMENT: production
jobs:
# Generate AI-powered release notes using Claude API
generate-notes:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
outputs:
notes: ${{ steps.generate.outputs.notes }}
notes_file: ${{ steps.generate.outputs.notes_file }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for git log
- name: Get previous tag
id: prev_tag
run: |
# Get the tag before the current one
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
echo "tag=$PREV_TAG" >> $GITHUB_OUTPUT
if [ -n "$PREV_TAG" ]; then
echo "Previous tag: $PREV_TAG"
else
echo "No previous tag found (first release)"
fi
- name: Collect commits
id: commits
run: |
PREV_TAG="${{ steps.prev_tag.outputs.tag }}"
MAX_COMMITS=200
if [ -z "$PREV_TAG" ]; then
# First release - get all commits
echo "Collecting all commits (first release)"
COMMITS=$(git log --pretty=format:"- %s" --no-merges | head -n $MAX_COMMITS)
TOTAL=$(git rev-list --count --no-merges HEAD)
else
# Get commits since previous tag
echo "Collecting commits from $PREV_TAG to HEAD"
COMMITS=$(git log "${PREV_TAG}..HEAD" --pretty=format:"- %s" --no-merges | head -n $MAX_COMMITS)
TOTAL=$(git rev-list --count --no-merges "${PREV_TAG}..HEAD")
fi
# Filter out noise (merge commits, version bumps)
COMMITS=$(echo "$COMMITS" | grep -v -E "^- (Merge branch|Merge pull request|Bump version|chore\(deps\)|chore\(release\))" || echo "$COMMITS")
# Add truncation note if needed
if [ "$TOTAL" -gt "$MAX_COMMITS" ]; then
COMMITS="$COMMITS
(Showing $MAX_COMMITS most recent of $TOTAL commits)"
echo "::warning::Truncated to $MAX_COMMITS commits (total: $TOTAL)"
fi
# Output commits using heredoc for multiline
echo "commits<<EOF" >> $GITHUB_OUTPUT
echo "$COMMITS" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
echo "Collected $(echo "$COMMITS" | wc -l) commits"
- name: Generate release notes with Claude
id: generate
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
VERSION="${{ github.ref_name }}"
COMMITS="${{ steps.commits.outputs.commits }}"
# Check if API key is configured
if [ -z "$ANTHROPIC_API_KEY" ]; then
echo "::warning::ANTHROPIC_API_KEY not configured, using fallback message"
NOTES="Release notes could not be generated automatically (API key not configured). See commit history for changes."
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
fi
# Build the prompt
PROMPT="Generate concise release notes for version ${VERSION} of MCPProxy (Smart MCP Proxy).
MCPProxy is a smart proxy for AI agents using the Model Context Protocol (MCP). It provides intelligent tool discovery, token savings, and security quarantine for MCP servers. MCPProxy has two editions: Personal (desktop app) and Teams (multi-user server). Note which changes affect which edition if applicable.
Commits since last release:
${COMMITS}
Requirements:
- Maximum 400 words
- Use markdown format
- DO NOT include a title or header like 'MCPProxy vX.X.X Release Notes' - GitHub already shows the version
- Start directly with a brief 1-2 sentence summary of this release
- Group changes into sections (use only sections that have content):
- **New Features** - New functionality (feat: commits)
- **Bug Fixes** - Fixed issues (fix: commits)
- **Breaking Changes** - Changes requiring user action
- **Improvements** - Enhancements to existing features
- Skip internal changes (chore:, docs:, test:, ci: commits) unless significant
- Use bullet points for each change
- Be specific but brief
- If there are no meaningful changes, say 'Minor internal improvements and maintenance updates.'"
# Call Claude API using jq for proper JSON escaping
PAYLOAD=$(jq -n \
--arg model "claude-sonnet-4-5-20250929" \
--argjson max_tokens 1024 \
--arg prompt "$PROMPT" \
'{
model: $model,
max_tokens: $max_tokens,
messages: [{
role: "user",
content: $prompt
}]
}')
echo "Calling Claude API..."
RESPONSE=$(curl -s --max-time 30 \
https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "content-type: application/json" \
-H "anthropic-version: 2023-06-01" \
-d "$PAYLOAD" 2>&1) || {
echo "::warning::Claude API request failed (timeout or network error)"
NOTES="Release notes could not be generated automatically. See commit history for changes."
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
}
# Check for API errors
ERROR_MSG=$(echo "$RESPONSE" | jq -r '.error.message // empty' 2>/dev/null || echo "")
if [ -n "$ERROR_MSG" ]; then
echo "::warning::Claude API error: $ERROR_MSG"
NOTES="Release notes could not be generated automatically. See commit history for changes."
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
exit 0
fi
# Extract the generated notes
NOTES=$(echo "$RESPONSE" | jq -r '.content[0].text // empty' 2>/dev/null || echo "")
if [ -z "$NOTES" ]; then
echo "::warning::Failed to extract release notes from API response"
NOTES="Release notes could not be generated automatically. See commit history for changes."
else
echo "✅ Release notes generated successfully"
fi
# Output notes
echo "notes<<EOF" >> $GITHUB_OUTPUT
echo "$NOTES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Save to file for artifact
NOTES_FILE="RELEASE_NOTES-${VERSION}.md"
echo "$NOTES" > "$NOTES_FILE"
echo "notes_file=$NOTES_FILE" >> $GITHUB_OUTPUT
- name: Upload release notes artifact
uses: actions/upload-artifact@v4
with:
name: release-notes
path: RELEASE_NOTES-${{ github.ref_name }}.md
if-no-files-found: ignore
build:
environment: production
# Only run on version tags
if: startsWith(github.ref, 'refs/tags/v')
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cgo: "0"
name: mcpproxy-linux-amd64
archive_format: tar.gz
- os: ubuntu-latest
goos: linux
goarch: arm64
cgo: "0"
name: mcpproxy-linux-arm64
archive_format: tar.gz
- os: windows-latest
goos: windows
goarch: amd64
cgo: "1"
name: mcpproxy-windows-amd64.exe
archive_format: zip
- os: windows-latest
goos: windows
goarch: arm64
cgo: "1"
name: mcpproxy-windows-arm64.exe
archive_format: zip
- os: macos-15
goos: darwin
goarch: amd64
cgo: "1"
name: mcpproxy-darwin-amd64
archive_format: tar.gz
- os: macos-15
goos: darwin
goarch: arm64
cgo: "1"
name: mcpproxy-darwin-arm64
archive_format: tar.gz
# Server edition (Linux only) — uncomment when server MVP is ready
# - os: ubuntu-latest
# goos: linux
# goarch: amd64
# cgo: "0"
# name: mcpproxy-server-linux-amd64
# archive_format: tar.gz
# edition: server
# - os: ubuntu-latest
# goos: linux
# goarch: arm64
# cgo: "0"
# name: mcpproxy-server-linux-arm64
# archive_format: tar.gz
# edition: server
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download release notes artifact
uses: actions/download-artifact@v4
with:
name: release-notes
path: .
continue-on-error: true # Don't fail if notes not yet available
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
- name: Cache Go modules and build
uses: actions/cache@v4
with:
path: |
~/.cache/go-build
~/go/pkg/mod
~/AppData/Local/go-build
key: ${{ runner.os }}-go-1.25-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-1.25-
- name: Download dependencies
run: go mod download
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install frontend dependencies
run: cd frontend && npm ci
- name: Build frontend
run: cd frontend && npm run build
- name: Copy frontend dist to embed location
shell: bash
run: |
rm -rf web/frontend
mkdir -p web/frontend
cp -r frontend/dist web/frontend/
- name: Import Code-Signing Certificates (macOS)
if: matrix.goos == 'darwin'
run: |
set -euo pipefail
echo "📦 Preparing isolated keychain for code signing"
UNIQUE_ID="${{ matrix.goos }}-${{ matrix.goarch }}-$$-$(date +%s)"
TEMP_KEYCHAIN="mcpproxy-build-${UNIQUE_ID}.keychain"
security create-keychain -p "temp123" "$TEMP_KEYCHAIN"
security list-keychains -s "$TEMP_KEYCHAIN" ~/Library/Keychains/login.keychain-db /Library/Keychains/System.keychain
security unlock-keychain -p "temp123" "$TEMP_KEYCHAIN"
security set-keychain-settings -t 3600 -l "$TEMP_KEYCHAIN"
if [ -z "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" ] || [ -z "${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" ]; then
echo "❌ APPLE_DEVELOPER_ID_CERT and APPLE_DEVELOPER_ID_CERT_PASSWORD secrets are required"
exit 1
fi
echo "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" | base64 -d > developer-id.p12
security import developer-id.p12 \
-k "$TEMP_KEYCHAIN" \
-P "${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" \
-T /usr/bin/codesign \
-T /usr/bin/productbuild \
-T /usr/bin/productsign \
-T /usr/bin/security
rm -f developer-id.p12
echo "🔍 Checking for separate Developer ID Installer certificate"
INSTALLER_ID=$(security find-identity -v -p basic "$TEMP_KEYCHAIN" | grep "Developer ID Installer" || true)
if [ -z "$INSTALLER_ID" ]; then
if [ -z "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }}" ] || [ -z "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD }}" ]; then
echo "❌ Developer ID Installer identity not found in APPLE_DEVELOPER_ID_CERT"
echo " Provide APPLE_DEVELOPER_ID_INSTALLER_CERT and password secrets"
exit 1
fi
echo "Importing dedicated Developer ID Installer certificate"
echo "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT }}" | base64 -d > developer-id-installer.p12
security import developer-id-installer.p12 \
-k "$TEMP_KEYCHAIN" \
-P "${{ secrets.APPLE_DEVELOPER_ID_INSTALLER_CERT_PASSWORD }}" \
-T /usr/bin/productsign \
-T /usr/bin/productbuild \
-T /usr/bin/codesign \
-T /usr/bin/security
rm -f developer-id-installer.p12
fi
security set-key-partition-list -S apple-tool:,apple: -s -k "temp123" "$TEMP_KEYCHAIN"
APP_CERT_IDENTITY=$(security find-identity -v -p codesigning "$TEMP_KEYCHAIN" | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
PKG_CERT_IDENTITY=$(security find-identity -v -p basic "$TEMP_KEYCHAIN" | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"')
if [ -z "$APP_CERT_IDENTITY" ]; then
echo "❌ Developer ID Application identity not found after import"
exit 1
fi
if [ -z "$PKG_CERT_IDENTITY" ]; then
echo "❌ Developer ID Installer identity not found after import"
exit 1
fi
echo "✅ Using Developer ID Application: $APP_CERT_IDENTITY"
echo "✅ Using Developer ID Installer: $PKG_CERT_IDENTITY"
echo "APP_CERT_IDENTITY=$APP_CERT_IDENTITY" >> "$GITHUB_ENV"
echo "PKG_CERT_IDENTITY=$PKG_CERT_IDENTITY" >> "$GITHUB_ENV"
echo "$TEMP_KEYCHAIN" > .keychain_name
echo "=== Available signing identities in temporary keychain ==="
security find-identity -v "$TEMP_KEYCHAIN"
echo "✅ Certificate import completed"
- name: Build binary and create archives
shell: bash
env:
CGO_ENABLED: ${{ matrix.cgo }}
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
# ✅ Force minimum supported macOS version for compatibility (13.0 for Swift tray app)
MACOSX_DEPLOYMENT_TARGET: "13.0"
# Defensive CGO flags to ensure proper deployment target
CGO_CFLAGS: "-mmacosx-version-min=13.0"
CGO_LDFLAGS: "-mmacosx-version-min=13.0"
run: |
VERSION=${GITHUB_REF#refs/tags/}
LDFLAGS="-s -w -X main.version=${VERSION} -X github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi.buildVersion=${VERSION}"
# Determine clean binary name and build flags
EDITION="${{ matrix.edition }}"
BUILD_TAGS=""
if [ "$EDITION" = "server" ]; then
CLEAN_BINARY="mcpproxy-server"
BUILD_TAGS="-tags server"
elif [ "${{ matrix.goos }}" = "windows" ]; then
CLEAN_BINARY="mcpproxy.exe"
else
CLEAN_BINARY="mcpproxy"
fi
# Create clean core binary for archive
go build ${BUILD_TAGS} -ldflags "${LDFLAGS}" -o ${CLEAN_BINARY} ./cmd/mcpproxy
# Build tray binary for platforms with GUI support (macOS and Windows, personal only)
if [ "$EDITION" != "server" ] && { [ "${{ matrix.goos }}" = "darwin" ] || [ "${{ matrix.goos }}" = "windows" ]; }; then
echo "Building mcpproxy-tray for ${{ matrix.goos }}..."
# Determine tray binary name
if [ "${{ matrix.goos }}" = "windows" ]; then
TRAY_BINARY="mcpproxy-tray.exe"
else
TRAY_BINARY="mcpproxy-tray"
fi
go build -ldflags "${LDFLAGS}" -o ${TRAY_BINARY} ./cmd/mcpproxy-tray
fi
# Build Swift tray app (macOS only — replaces Go tray in .app bundle for DMG/PKG)
if [ "$EDITION" != "server" ] && [ "${{ matrix.goos }}" = "darwin" ]; then
chmod +x scripts/build-swift-app.sh
OUTPUT_DIR="$(pwd)/swift-build"
mkdir -p "$OUTPUT_DIR"
./scripts/build-swift-app.sh "${VERSION}" "${{ matrix.goarch }}" "$OUTPUT_DIR"
SWIFT_APP="$OUTPUT_DIR/MCPProxy.app"
if [ -d "$SWIFT_APP" ]; then
echo "SWIFT_APP_PATH=$SWIFT_APP" >> $GITHUB_ENV
else
echo "❌ Swift app build failed"
exit 1
fi
fi
# Code sign macOS binaries
if [ "${{ matrix.goos }}" = "darwin" ]; then
echo "Code signing macOS binary..."
# Debug: List all available certificates
echo "Available certificates:"
security find-identity -v -p codesigning
# Find the Developer ID certificate identity
CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
# Verify we found a valid certificate
if [ -n "${CERT_IDENTITY}" ]; then
echo "✅ Found Developer ID certificate: ${CERT_IDENTITY}"
else
echo "❌ No Developer ID certificate found, using team ID as fallback"
CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}"
echo "⚠️ Using fallback identity: ${CERT_IDENTITY}"
fi
# Validate entitlements file formatting (Apple's recommendation)
echo "=== Validating entitlements file ==="
if [ -f "scripts/entitlements.plist" ]; then
echo "Validating entitlements formatting with plutil..."
if plutil -lint scripts/entitlements.plist; then
echo "✅ Entitlements file is properly formatted"
else
echo "❌ Entitlements file has formatting issues"
exit 1
fi
# Convert to XML format if needed (Apple's recommendation)
plutil -convert xml1 scripts/entitlements.plist
echo "✅ Entitlements converted to XML format"
else
echo "⚠️ No entitlements file found"
fi
# Sign both binaries with proper Developer ID certificate, hardened runtime, and timestamp
echo "=== Signing binaries with hardened runtime ==="
# Install GNU coreutils for timeout command (macOS compatibility)
if ! command -v timeout &> /dev/null; then
echo "Installing GNU coreutils for timeout command..."
brew install coreutils
# Use gtimeout from coreutils
TIMEOUT_CMD="gtimeout"
else
TIMEOUT_CMD="timeout"
fi
# Sign core binary
echo "Signing core binary: ${CLEAN_BINARY}"
SIGN_SUCCESS=false
for attempt in 1 2 3; do
echo "Core binary signing attempt $attempt/3..."
# Use timeout command to prevent hanging (max 5 minutes per attempt)
if $TIMEOUT_CMD 300 codesign --force \
--options runtime \
--entitlements scripts/entitlements.plist \
--sign "${CERT_IDENTITY}" \
--timestamp \
${CLEAN_BINARY}; then
SIGN_SUCCESS=true
echo "✅ Core binary signing succeeded on attempt $attempt"
break
else
echo "❌ Core binary signing attempt $attempt failed or timed out"
if [ $attempt -lt 3 ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ "$SIGN_SUCCESS" != "true" ]; then
echo "❌ All core binary signing attempts failed"
exit 1
fi
# Sign tray binary
echo "Signing tray binary: mcpproxy-tray"
TRAY_SIGN_SUCCESS=false
for attempt in 1 2 3; do
echo "Tray binary signing attempt $attempt/3..."
# Use timeout command to prevent hanging (max 5 minutes per attempt)
if $TIMEOUT_CMD 300 codesign --force \
--options runtime \
--entitlements scripts/entitlements.plist \
--sign "${CERT_IDENTITY}" \
--timestamp \
mcpproxy-tray; then
TRAY_SIGN_SUCCESS=true
echo "✅ Tray binary signing succeeded on attempt $attempt"
break
else
echo "❌ Tray binary signing attempt $attempt failed or timed out"
if [ $attempt -lt 3 ]; then
echo "Retrying in 10 seconds..."
sleep 10
fi
fi
done
if [ "$TRAY_SIGN_SUCCESS" != "true" ]; then
echo "❌ All tray binary signing attempts failed"
exit 1
fi
# Verify signing, hardened runtime, and timestamp using Apple's recommended methods
echo "=== Verifying binary signatures (Apple's recommended verification) ==="
# Verify core binary
echo "=== Core binary verification ==="
codesign --verify --verbose ${CLEAN_BINARY}
echo "Core binary basic verification: $?"
# Apple's recommended strict verification for notarization
echo "=== Core binary strict verification (matches notarization requirements) ==="
if codesign -vvv --deep --strict ${CLEAN_BINARY}; then
echo "✅ Core binary strict verification PASSED - ready for notarization"
else
echo "❌ Core binary strict verification FAILED - will not pass notarization"
exit 1
fi
# Verify tray binary
echo "=== Tray binary verification ==="
codesign --verify --verbose mcpproxy-tray
echo "Tray binary basic verification: $?"
# Apple's recommended strict verification for notarization
echo "=== Tray binary strict verification (matches notarization requirements) ==="
if codesign -vvv --deep --strict mcpproxy-tray; then
echo "✅ Tray binary strict verification PASSED - ready for notarization"
else
echo "❌ Strict verification FAILED - will not pass notarization"
exit 1
fi
# Check for secure timestamp (Apple's recommended check)
echo "=== Checking for secure timestamp ==="
TIMESTAMP_CHECK=$(codesign -dvv ${CLEAN_BINARY} 2>&1)
if echo "$TIMESTAMP_CHECK" | grep -q "Timestamp="; then
echo "✅ Secure timestamp present:"
echo "$TIMESTAMP_CHECK" | grep "Timestamp="
else
echo "❌ No secure timestamp found"
echo "Full output:"
echo "$TIMESTAMP_CHECK"
fi
# Display detailed signature info
codesign --display --verbose=4 ${CLEAN_BINARY}
# Check entitlements formatting (Apple's recommendation)
echo "=== Checking entitlements formatting ==="
codesign --display --entitlements - ${CLEAN_BINARY} | head -10
# Verify with spctl (Gatekeeper assessment) - expected to fail before notarization
echo "=== Gatekeeper assessment (expected to fail before notarization) ==="
if spctl --assess --verbose ${CLEAN_BINARY}; then
echo "✅ Gatekeeper assessment: PASSED (unexpected but good!)"
else
echo "⚠️ Gatekeeper assessment: REJECTED (expected - binary needs notarization)"
echo "This is normal - the binary will pass after Apple completes notarization"
fi
echo "✅ Binary signed successfully with hardened runtime and timestamp"
fi
# Create archive with version info
ARCHIVE_BASE="mcpproxy-${VERSION#v}-${{ matrix.goos }}-${{ matrix.goarch }}"
LATEST_ARCHIVE_BASE="mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}"
# Determine files to include in archive
FILES_TO_ARCHIVE="${CLEAN_BINARY}"
# Add tray binary if it exists (Windows and macOS)
if [ "${{ matrix.goos }}" = "windows" ] && [ -f "mcpproxy-tray.exe" ]; then
FILES_TO_ARCHIVE="${FILES_TO_ARCHIVE} mcpproxy-tray.exe"
echo "Including mcpproxy-tray.exe in archive"
elif [ "${{ matrix.goos }}" = "darwin" ] && [ -f "mcpproxy-tray" ]; then
FILES_TO_ARCHIVE="${FILES_TO_ARCHIVE} mcpproxy-tray"
echo "Including mcpproxy-tray in archive"
fi
if [ "${{ matrix.archive_format }}" = "zip" ]; then
# Create ZIP archive (Windows)
# Use PowerShell Compress-Archive on Windows since zip command isn't available
if [ "${{ matrix.goos }}" = "windows" ]; then
# Convert space-separated list to comma-separated for PowerShell
PS_FILES=$(echo ${FILES_TO_ARCHIVE} | sed 's/ /,/g')
powershell -Command "Compress-Archive -Path ${PS_FILES} -DestinationPath '${ARCHIVE_BASE}.zip'"
powershell -Command "Compress-Archive -Path ${PS_FILES} -DestinationPath '${LATEST_ARCHIVE_BASE}.zip'"
else
# Create versioned archive
zip "${ARCHIVE_BASE}.zip" ${FILES_TO_ARCHIVE}
# Create latest archive
zip "${LATEST_ARCHIVE_BASE}.zip" ${FILES_TO_ARCHIVE}
fi
else
# Create versioned archive
tar -czf "${ARCHIVE_BASE}.tar.gz" ${FILES_TO_ARCHIVE}
# Create latest archive
tar -czf "${LATEST_ARCHIVE_BASE}.tar.gz" ${FILES_TO_ARCHIVE}
fi
- name: Install Inno Setup (Windows)
if: matrix.goos == 'windows'
shell: pwsh
run: |
choco install innosetup -y
# Verify installation
if (Test-Path "C:\Program Files (x86)\Inno Setup 6\ISCC.exe") {
Write-Host "✅ Inno Setup installed successfully"
} else {
Write-Host "❌ Inno Setup installation failed"
exit 1
}
- name: Create Windows installer (Windows)
if: matrix.goos == 'windows'
shell: pwsh
run: |
$VERSION = "${{ github.ref_name }}"
$ARCH = "${{ matrix.goarch }}"
Write-Host "Building Windows installer for version ${VERSION} (${ARCH})"
# Run the build script
.\scripts\build-windows-installer.ps1 -Version $VERSION -Arch $ARCH
# Verify installer was created
$INSTALLER_NAME = "mcpproxy-setup-${VERSION}-${ARCH}.exe"
$INSTALLER_PATH = "dist\${INSTALLER_NAME}"
if (Test-Path $INSTALLER_PATH) {
Write-Host "✅ Windows installer created: ${INSTALLER_NAME}"
$size = (Get-Item $INSTALLER_PATH).Length / 1MB
Write-Host " Size: $([math]::Round($size, 2)) MB"
} else {
Write-Host "❌ Windows installer not found at ${INSTALLER_PATH}"
exit 1
}
- name: Upload unsigned Windows installer for signing
if: matrix.goos == 'windows'
uses: actions/upload-artifact@v4
id: upload-unsigned-installer
with:
name: unsigned-installer-windows-${{ matrix.goarch }}
# Upload exe directly - GitHub Actions will ZIP it automatically
path: dist/mcpproxy-setup-${{ github.ref_name }}-${{ matrix.goarch }}.exe
- name: Create .icns icon (macOS)
if: matrix.goos == 'darwin'
run: |
chmod +x scripts/create-icns.sh
./scripts/create-icns.sh
- name: Create DMG installer (macOS)
if: matrix.goos == 'darwin'
env:
# Ensure DMG creation also uses correct deployment target
MACOSX_DEPLOYMENT_TARGET: "13.0"
CGO_CFLAGS: "-mmacosx-version-min=13.0"
CGO_LDFLAGS: "-mmacosx-version-min=13.0"
run: |
VERSION=${GITHUB_REF#refs/tags/}
chmod +x scripts/create-dmg.sh
# Determine binary names
CORE_BINARY="mcpproxy"
# Use Swift app for DMG when available, fall back to Go tray binary
if [ -n "$SWIFT_APP_PATH" ] && [ -d "$SWIFT_APP_PATH" ]; then
echo "Using Swift app bundle for DMG: $SWIFT_APP_PATH"
./scripts/create-dmg.sh "$SWIFT_APP_PATH" ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
else
echo "Using Go tray binary for DMG"
TRAY_BINARY="mcpproxy-tray"
./scripts/create-dmg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
fi
# Sign DMG
DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.dmg"
echo "Signing DMG: ${DMG_NAME}"
# Find the Developer ID certificate identity
CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
# Verify we found a valid certificate
if [ -n "${CERT_IDENTITY}" ]; then
echo "✅ Found Developer ID certificate for DMG: ${CERT_IDENTITY}"
else
echo "❌ No Developer ID certificate found for DMG, using team ID as fallback"
CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}"
echo "⚠️ Using fallback identity for DMG: ${CERT_IDENTITY}"
fi
# Sign DMG with proper certificate and timestamp
codesign --force \
--sign "${CERT_IDENTITY}" \
--timestamp \
"${DMG_NAME}"
# Verify DMG signing
echo "=== Verifying DMG signature ==="
codesign --verify --verbose "${DMG_NAME}"
echo "DMG verification: $?"
codesign --display --verbose=4 "${DMG_NAME}"
echo "✅ DMG created and signed successfully: ${DMG_NAME}"
- name: Create PKG installer (macOS)
if: matrix.goos == 'darwin'
env:
# Ensure PKG creation also uses correct deployment target
MACOSX_DEPLOYMENT_TARGET: "13.0"
CGO_CFLAGS: "-mmacosx-version-min=13.0"
CGO_LDFLAGS: "-mmacosx-version-min=13.0"
run: |
VERSION=${GITHUB_REF#refs/tags/}
chmod +x scripts/create-pkg.sh
chmod +x scripts/create-installer-dmg.sh
# Set up certificate environment for PKG creation (reuse from binary signing)
echo "=== Setting up certificate environment for PKG creation ==="
# Debug: List all available certificates for PKG creation
echo "=== Available certificates for PKG creation ==="
echo "Codesigning certificates:"
security find-identity -v -p codesigning || echo "No codesigning certificates found"
echo "Basic certificates:"
security find-identity -v -p basic || echo "No basic certificates found"
echo "All certificates:"
security find-identity -v || echo "No certificates found"
# Prefer identities exported during certificate import, fallback to keychain lookup
APP_CERT_IDENTITY="${APP_CERT_IDENTITY:-}"
PKG_CERT_IDENTITY="${PKG_CERT_IDENTITY:-}"
if [ -z "${APP_CERT_IDENTITY}" ]; then
APP_CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"')
fi
if [ -z "${PKG_CERT_IDENTITY}" ]; then
PKG_CERT_IDENTITY=$(security find-identity -v -p basic | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"')
fi
if [ -z "${APP_CERT_IDENTITY}" ]; then
echo "❌ Developer ID Application certificate not available in the build keychain"
exit 1
fi
if [ -z "${PKG_CERT_IDENTITY}" ]; then
echo "❌ Developer ID Installer certificate not available in the isolated keychain"
echo " Embed the 'Developer ID Installer' identity in APPLE_DEVELOPER_ID_CERT"
exit 1
fi
echo "✅ Using Developer ID Application certificate: ${APP_CERT_IDENTITY}"
echo "✅ Using Developer ID Installer certificate: ${PKG_CERT_IDENTITY}"
export APP_CERT_IDENTITY
export PKG_CERT_IDENTITY
# Determine binary names
CORE_BINARY="mcpproxy"
# Use Swift app for PKG when available, fall back to Go tray binary
echo "Creating signed PKG installer with certificate: ${PKG_CERT_IDENTITY}"
if [ -n "$SWIFT_APP_PATH" ] && [ -d "$SWIFT_APP_PATH" ]; then
echo "Using Swift app bundle for PKG: $SWIFT_APP_PATH"
./scripts/create-pkg.sh "$SWIFT_APP_PATH" ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
else
echo "Using Go tray binary for PKG"
TRAY_BINARY="mcpproxy-tray"
./scripts/create-pkg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }}
fi
# Create installer DMG containing the PKG
PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg"
./scripts/create-installer-dmg.sh ${PKG_NAME} ${VERSION} ${{ matrix.goarch }}
echo "✅ PKG installer and installer DMG created successfully"
- name: Submit for notarization (macOS)
if: matrix.goos == 'darwin'
run: |
set -euo pipefail
VERSION=${GITHUB_REF#refs/tags/}
PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg"
INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg"
notarize_and_staple() {
local FILE_NAME="$1"
local FILE_LABEL="$2"
if [ ! -f "${FILE_NAME}" ]; then
echo "❌ ${FILE_LABEL} (${FILE_NAME}) not found"
return 1
fi
echo "Submitting ${FILE_LABEL} for notarization: ${FILE_NAME}"
local SUBMISSION_OUTPUT
if ! SUBMISSION_OUTPUT=$(xcrun notarytool submit "${FILE_NAME}" \
--apple-id "${{ secrets.APPLE_ID_USERNAME }}" \
--password "${{ secrets.APPLE_ID_APP_PASSWORD }}" \
--team-id "${{ secrets.APPLE_TEAM_ID }}" \
--wait \
--output-format json 2>&1); then
echo "❌ ${FILE_LABEL} notarization failed"
echo "Output: ${SUBMISSION_OUTPUT}"
return 1
fi
local SUBMISSION_ID
SUBMISSION_ID=$(echo "${SUBMISSION_OUTPUT}" | jq -r '.id // empty')
local STATUS
STATUS=$(echo "${SUBMISSION_OUTPUT}" | jq -r '.status // empty')
if [ -z "${SUBMISSION_ID}" ] || [ "${SUBMISSION_ID}" = "null" ] || [ "${STATUS}" != "Accepted" ]; then
echo "❌ ${FILE_LABEL} notarization did not succeed"
echo "Response: ${SUBMISSION_OUTPUT}"
return 1
fi
echo "✅ ${FILE_LABEL} notarization accepted (ID: ${SUBMISSION_ID})"
echo "${SUBMISSION_ID}" > "${FILE_NAME}.submission_id"
echo "Stapling notarization ticket to ${FILE_LABEL}"
xcrun stapler staple "${FILE_NAME}"
xcrun stapler validate "${FILE_NAME}"
}
notarize_and_staple "${PKG_NAME}" "PKG installer"
notarize_and_staple "${INSTALLER_DMG_NAME}" "Installer DMG"
echo "✅ Notarization and stapling complete"
- name: Cleanup isolated keychain (macOS)
if: matrix.goos == 'darwin' && always()
run: |
# Clean up the isolated keychain we created for this worker
if [ -f .keychain_name ]; then
TEMP_KEYCHAIN=$(cat .keychain_name)
echo "Cleaning up keychain: ${TEMP_KEYCHAIN}"
# Remove from search list and delete
security delete-keychain "$TEMP_KEYCHAIN" 2>/dev/null || echo "Keychain already cleaned up"
rm -f .keychain_name
echo "✅ Keychain cleanup completed"
else
echo "No keychain to clean up"
fi
- name: Upload versioned archive artifact
uses: actions/upload-artifact@v4
with:
name: versioned-${{ matrix.edition || 'personal' }}-${{ matrix.goos }}-${{ matrix.goarch }}
path: mcpproxy-*-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}
- name: Upload latest archive artifact
uses: actions/upload-artifact@v4
with:
name: latest-${{ matrix.edition || 'personal' }}-${{ matrix.goos }}-${{ matrix.goarch }}
path: mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}
- name: Upload macOS installer DMG
if: matrix.goos == 'darwin'
run: |
set -euo pipefail
VERSION=${GITHUB_REF#refs/tags/}
INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg"
echo "Looking for files:"
echo " Installer DMG: ${INSTALLER_DMG_NAME}"
if [ ! -f "${INSTALLER_DMG_NAME}" ]; then
echo "❌ Installer DMG not found: ${INSTALLER_DMG_NAME}"
exit 1
fi
mkdir -p installers-artifact
cp "${INSTALLER_DMG_NAME}" installers-artifact/
SUBMISSION_ID_FILE="${INSTALLER_DMG_NAME}.submission_id"
if [ -f "${SUBMISSION_ID_FILE}" ]; then
echo "✅ Found submission ID file: ${SUBMISSION_ID_FILE}"
cp "${SUBMISSION_ID_FILE}" installers-artifact/
else
echo "⚠️ No submission ID file found: ${SUBMISSION_ID_FILE}"
fi
echo "Files to upload:"
ls -la installers-artifact/
- name: Upload macOS installers artifact
if: matrix.goos == 'darwin'
uses: actions/upload-artifact@v4
with:
name: installers-${{ matrix.goos }}-${{ matrix.goarch }}
path: installers-artifact/*
# Sign Windows installers with SignPath
sign-windows:
needs: build
runs-on: ubuntu-latest
environment: production
if: startsWith(github.ref, 'refs/tags/v')
strategy:
matrix:
arch: [amd64, arm64]
steps:
- name: Download unsigned installer
uses: actions/download-artifact@v4
with:
name: unsigned-installer-windows-${{ matrix.arch }}
path: unsigned
- name: Re-upload for SignPath
id: reupload
uses: actions/upload-artifact@v4
with:
name: signpath-input-windows-${{ matrix.arch }}
# Upload exe - GitHub Actions will ZIP it for SignPath
path: unsigned/*.exe
- name: Submit to SignPath for signing
uses: signpath/github-action-submit-signing-request@v1
with:
api-token: '${{ secrets.SIGNPATH_API_TOKEN }}'
organization-id: '84efd51b-c11c-4a85-82e6-7c3b1157d7ca'
project-slug: 'mcpproxy-go'
signing-policy-slug: 'release-signing'
artifact-configuration-slug: 'initial'
github-artifact-id: '${{ steps.reupload.outputs.artifact-id }}'
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 3600
output-artifact-directory: signed
- name: Prepare signed installer
run: |
VERSION=${GITHUB_REF#refs/tags/}
ARCH="${{ matrix.arch }}"
TARGET="mcpproxy-setup-${VERSION}-${ARCH}.exe"
echo "Preparing signed installer..."
cd signed
ls -la
# SignPath returns signed exe - verify it exists
if [ -f "$TARGET" ]; then
echo "✅ Signed installer already named correctly: $TARGET"
else
# Find and rename if needed
SIGNED_EXE=$(find . -name "*.exe" -type f | head -1)
if [ -n "$SIGNED_EXE" ]; then
mv "$SIGNED_EXE" "$TARGET"
echo "✅ Renamed to: $TARGET"
else
echo "❌ No signed .exe found in SignPath output"
exit 1
fi
fi
- name: Upload signed Windows installer
uses: actions/upload-artifact@v4
with:
name: installer-windows-${{ matrix.arch }}
path: signed/mcpproxy-setup-${{ github.ref_name }}-${{ matrix.arch }}.exe
build-docker:
runs-on: ubuntu-latest
needs: [build]
# Disabled until server MVP is ready — uncomment to enable
if: false && startsWith(github.ref, 'refs/tags/v')
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
build-args: |
VERSION=${{ github.ref_name }}
COMMIT=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
tags: |
ghcr.io/smart-mcp-proxy/mcpproxy-server:${{ github.ref_name }}
ghcr.io/smart-mcp-proxy/mcpproxy-server:latest
release:
needs: [build, sign-windows, generate-notes] # add build-docker when server MVP is ready
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Reorganize files
run: |
VERSION=${GITHUB_REF#refs/tags/}
# Create a flat structure to avoid duplicates
mkdir -p release-files
# Copy archives (tar.gz and zip files)
find dist -name "*.tar.gz" -o -name "*.zip" | while read file; do
filename=$(basename "$file")
cp "$file" "release-files/$filename"
done
# Copy Windows installer files (.exe)
find dist -path "*/installer-windows-*" -name "*.exe" | while read installer_file; do
filename=$(basename "$installer_file")
echo "Found Windows installer: $filename"
cp "$installer_file" "release-files/$filename"
done
# Handle installer files (DMG and PKG) and notarization submissions
mkdir -p pending-notarizations
# Process installer artifacts (only notarized installer DMGs)
find dist -path "*/installers-*" -name "*.dmg" | while read installer_file; do
filename=$(basename "$installer_file")
submission_id_file="${installer_file}.submission_id"
if [ -f "$submission_id_file" ]; then
# File has pending notarization
SUBMISSION_ID=$(cat "$submission_id_file")
# Validate submission ID before creating pending file
if [ -n "$SUBMISSION_ID" ] && [ "$SUBMISSION_ID" != "null" ] && [ ${#SUBMISSION_ID} -gt 10 ]; then
echo "Found valid pending notarization for $filename (ID: $SUBMISSION_ID)"
cp "$installer_file" "release-files/$filename"
# Create pending notarization record
cat > "pending-notarizations/${filename}.pending" << EOF
{
"submission_id": "$SUBMISSION_ID",
"file_name": "$filename",
"version": "$VERSION",
"submitted_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
else
echo "❌ Invalid submission ID for $filename: '$SUBMISSION_ID'"
echo "Copying installer file without notarization tracking"
cp "$installer_file" "release-files/$filename"
fi
else
# No notarization submission (shouldn't happen, but handle it)
echo "No submission ID for $filename, copying as-is"
cp "$installer_file" "release-files/$filename"
fi
done
- name: List files for upload
run: |
echo "Files to upload:"
ls -la release-files/
echo "Pending notarizations:"
ls -la pending-notarizations/ || echo "No pending notarizations"
- name: Set version variable
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "CLEAN_VERSION=${VERSION}" >> $GITHUB_ENV
- name: Create release with binaries
uses: softprops/action-gh-release@v2
with:
files: release-files/*
body: |
${{ needs.generate-notes.outputs.notes }}
---
## Download Installers
| Platform | Download | Notes |
|----------|----------|-------|
| **macOS (Apple Silicon)** | [**Download DMG**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-arm64-installer.dmg) | Signed & Notarized - Recommended for M1/M2/M3/M4 |
| **macOS (Intel)** | [**Download DMG**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-amd64-installer.dmg) | Signed & Notarized |
| **Windows (64-bit)** | [**Download Setup**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-setup-${{ github.ref_name }}-amd64.exe) | Setup wizard |
| **Windows (ARM64)** | [**Download Setup**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-setup-${{ github.ref_name }}-arm64.exe) | For ARM Windows devices |
| **Linux (AMD64)** | [**Download tar.gz**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-linux-amd64.tar.gz) | Binary package |
| **Linux (ARM64)** | [**Download tar.gz**](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-linux-arm64.tar.gz) | For ARM Linux |
**Homebrew (macOS/Linux):**
```bash
brew install smart-mcp-proxy/mcpproxy/mcpproxy
```
<details>
<summary>Other download options (auto-update URLs, archives)</summary>
**Auto-update URLs** (always points to latest):
- [Linux AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-linux-amd64.tar.gz) | [Linux ARM64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-linux-arm64.tar.gz)
- [Windows AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-windows-amd64.zip) | [Windows ARM64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-windows-arm64.zip)
- [macOS AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-darwin-amd64.tar.gz) | [macOS ARM64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-darwin-arm64.tar.gz)
**Binary archives (this version):**
- [macOS AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-amd64.tar.gz) | [macOS ARM64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-arm64.tar.gz)
- [Windows AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-windows-amd64.zip) | [Windows ARM64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-windows-arm64.zip)
</details>
---
<details>
<summary>Installation Instructions</summary>
### Windows
1. Download the installer for your architecture
2. Run `mcpproxy-setup-*.exe`
3. Follow the installation wizard (requires Administrator privileges)
4. Launch "MCPProxy" from Start Menu
### macOS
1. Download the signed DMG for your Mac
2. Double-click the DMG to mount it
3. Double-click the PKG installer inside
4. Follow the installation wizard
5. Launch mcpproxy.app from Applications folder
### Linux / Manual Installation
1. Download the appropriate archive
2. Extract: `tar -xzf mcpproxy-*.tar.gz`
3. Make executable: `chmod +x mcpproxy`
4. Run: `./mcpproxy serve`
</details>
<details>
<summary>Platform Support & Usage</summary>
### Platform Support
- **macOS**: Full system tray support with menu and icons
- **Windows**: Full system tray support with menu and icons
- **Linux**: Headless mode only (CLI)
### Usage
**GUI (Recommended):**
- Launch mcpproxy.app from Applications (auto-starts core server)
- Manages server via system tray menu
**CLI:**
```bash
mcpproxy serve # Start server
mcpproxy serve --listen 127.0.0.1:8081 # Custom port
export MCPPROXY_API_KEY=your-secret-key # Set API key
```
</details>
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload pending notarizations
if: hashFiles('pending-notarizations/*.pending') != ''
run: |
# Upload pending notarization files as release assets
for pending_file in pending-notarizations/*.pending; do
if [ -f "$pending_file" ]; then
echo "Uploading pending notarization: $(basename "$pending_file")"
gh release upload "${{ github.ref_name }}" "$pending_file" --clobber
fi
done
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update-homebrew:
needs: release
runs-on: ubuntu-latest
environment: production
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout tap repository
uses: actions/checkout@v4
with:
repository: smart-mcp-proxy/homebrew-mcpproxy
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
path: tap
- name: Download platform binaries and calculate SHA256
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "Processing version: ${VERSION}"
# Wait a bit for GitHub release assets to be available
sleep 15
# Define platforms
PLATFORMS=("darwin-arm64" "darwin-amd64" "linux-arm64" "linux-amd64")
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
# Download each platform tarball and calculate SHA256
for PLATFORM in "${PLATFORMS[@]}"; do
TARBALL="mcpproxy-${VERSION}-${PLATFORM}.tar.gz"
URL="${BASE_URL}/${TARBALL}"
echo "Downloading ${PLATFORM}: ${URL}"
# Try downloading with retries
for i in {1..5}; do
echo " Attempt ${i}/5..."
if curl -fsSL "${URL}" -o "${TARBALL}"; then
echo " Download successful"
break
else
echo " Download failed, retrying in 10 seconds..."
sleep 10
fi
if [ $i -eq 5 ]; then
echo "All download attempts failed for ${PLATFORM}"
curl -I "${URL}" || true
exit 1
fi
done
# Calculate SHA256 and export as environment variable
SHA=$(sha256sum "${TARBALL}" | cut -d' ' -f1)
VAR_NAME="SHA256_$(echo ${PLATFORM} | tr '-' '_' | tr '[:lower:]' '[:upper:]')"
echo "${VAR_NAME}=${SHA}" >> $GITHUB_ENV
echo " SHA256: ${SHA}"
# Clean up tarball
rm -f "${TARBALL}"
done
echo "VERSION=${VERSION}" >> $GITHUB_ENV
- name: Generate Homebrew formula
run: |
cd tap
mkdir -p Formula
cat > Formula/mcpproxy.rb << 'FORMULA_EOF'
class Mcpproxy < Formula
desc "Smart MCP Proxy - Intelligent tool discovery and proxying for MCP servers"
homepage "https://github.com/smart-mcp-proxy/mcpproxy-go"
version "${VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-darwin-arm64.tar.gz"
sha256 "${SHA256_DARWIN_ARM64}"
else
url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-darwin-amd64.tar.gz"
sha256 "${SHA256_DARWIN_AMD64}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-linux-arm64.tar.gz"
sha256 "${SHA256_LINUX_ARM64}"
else
url "https://github.com/smart-mcp-proxy/mcpproxy-go/releases/download/v${VERSION}/mcpproxy-${VERSION}-linux-amd64.tar.gz"
sha256 "${SHA256_LINUX_AMD64}"
end
end
def install
bin.install "mcpproxy"
bin.install "mcpproxy-tray" if OS.mac? && File.exist?("mcpproxy-tray")
end
test do
assert_match version.to_s, shell_output("#{bin}/mcpproxy --version")
end
end
FORMULA_EOF
# Remove the 10-space YAML indentation from the formula
sed -i 's/^ //' Formula/mcpproxy.rb
# Substitute environment variables
sed -i "s/\${VERSION}/${VERSION}/g" Formula/mcpproxy.rb
sed -i "s/\${SHA256_DARWIN_ARM64}/${SHA256_DARWIN_ARM64}/g" Formula/mcpproxy.rb
sed -i "s/\${SHA256_DARWIN_AMD64}/${SHA256_DARWIN_AMD64}/g" Formula/mcpproxy.rb
sed -i "s/\${SHA256_LINUX_ARM64}/${SHA256_LINUX_ARM64}/g" Formula/mcpproxy.rb
sed -i "s/\${SHA256_LINUX_AMD64}/${SHA256_LINUX_AMD64}/g" Formula/mcpproxy.rb
echo "Formula created successfully:"
cat Formula/mcpproxy.rb
- name: Download installer DMGs and calculate SHA256 for cask
run: |
VERSION=${GITHUB_REF#refs/tags/v}
BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
for ARCH in arm64 amd64; do
DMG="mcpproxy-${VERSION}-darwin-${ARCH}-installer.dmg"
URL="${BASE_URL}/${DMG}"
echo "Downloading ${DMG}..."
for i in {1..5}; do
echo " Attempt ${i}/5..."
if curl -fsSL "${URL}" -o "${DMG}"; then
echo " Download successful"
break
else
echo " Download failed, retrying in 10 seconds..."
sleep 10
fi
if [ $i -eq 5 ]; then
echo "All download attempts failed for ${DMG}"
curl -I "${URL}" || true
exit 1
fi
done
SHA=$(sha256sum "${DMG}" | cut -d' ' -f1)
VAR_NAME="SHA256_CASK_$(echo ${ARCH} | tr '[:lower:]' '[:upper:]')"
echo "${VAR_NAME}=${SHA}" >> $GITHUB_ENV
echo " SHA256: ${SHA}"
rm -f "${DMG}"
done
- name: Update Homebrew cask
run: |
cd tap
mkdir -p Casks
CASK_FILE="Casks/mcpproxy.rb"
if [ ! -f "${CASK_FILE}" ]; then
echo "Cask file not found at ${CASK_FILE}, skipping cask update"
exit 0
fi
# Bump version and both sha256 values in place
sed -i "s/version \"[^\"]*\"/version \"${VERSION}\"/" "${CASK_FILE}"
# arm64 sha256 is the first occurrence, amd64 is the second
sed -i "0,/sha256 \"[^\"]*\"/{s/sha256 \"[^\"]*\"/sha256 \"${SHA256_CASK_ARM64}\"/}" "${CASK_FILE}"
sed -i "0,/sha256 \"${SHA256_CASK_ARM64}\"/{b}; s/sha256 \"[^\"]*\"/sha256 \"${SHA256_CASK_AMD64}\"/" "${CASK_FILE}"
echo "Cask updated:"
cat "${CASK_FILE}"
- name: Commit and push changes
run: |
cd tap
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/mcpproxy.rb Casks/mcpproxy.rb
# Check if there are changes to commit
if git diff --staged --quiet; then
echo "No changes to commit"
else
git commit -m "Update mcpproxy to ${VERSION} (pre-built binaries)"
git push
echo "Changes committed and pushed successfully"
fi
# Deploy documentation to Cloudflare Pages
deploy-docs:
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'smart-mcp-proxy/mcpproxy-go'
# Non-blocking: docs failure doesn't block release
continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: website/package-lock.json
- name: Set version for docs
run: |
VERSION="${{ github.ref_name }}"
# Extract minor version: v0.11.2 → 0.11
MINOR_VERSION=$(echo "$VERSION" | sed 's/^v//' | cut -d. -f1,2)
echo "MCPPROXY_VERSION=$MINOR_VERSION" >> $GITHUB_ENV
echo "Documentation version: $MINOR_VERSION"
- name: Install dependencies
working-directory: website
run: npm ci
- name: Build docs with version
working-directory: website
run: |
# Inject version into config
sed -i "s/__VERSION__/$MCPPROXY_VERSION/g" docusaurus.config.js
npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/wrangler-action@v3
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: pages deploy website/build --project-name=mcpproxy-docs
# Trigger marketing site to update download links
trigger-marketing-update:
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v') && github.repository == 'smart-mcp-proxy/mcpproxy-go'
# Non-blocking: marketing update failure doesn't block release
continue-on-error: true
steps:
- name: Trigger marketing site update
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.MARKETING_SITE_DISPATCH_TOKEN }}
repository: smart-mcp-proxy/mcpproxy.app-website
event-type: update-version
client-payload: '{"version": "${{ github.ref_name }}"}'
mcp-registry:
name: Publish to MCP Registry
needs: release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
continue-on-error: true
steps:
- uses: actions/checkout@v4
- name: Extract version from tag
id: version
run: echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
- name: Install mcp-publisher
run: |
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" | tar xz mcp-publisher
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Update version in server.json
run: |
jq --arg v "${{ steps.version.outputs.VERSION }}" '.version = $v' server.json > server.tmp && mv server.tmp server.json
- name: Publish to MCP Registry
run: ./mcp-publisher publish