fix(docker): wrap docker CLI invocations in user shell to inherit PAT… #192
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |