fix: Use component PKG to avoid Gatekeeper warnings #56
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: Prerelease | |
| on: | |
| push: | |
| branches: [next] | |
| tags: ["v*-rc.*", "v*-next.*"] | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| env: | |
| GITHUB_ENVIRONMENT: staging | |
| jobs: | |
| build: | |
| environment: staging | |
| # Run on pushes to next branch or prerelease tags | |
| if: github.ref == 'refs/heads/next' || startsWith(github.ref, 'refs/tags/v') && (contains(github.ref, '-rc.') || contains(github.ref, '-next.')) | |
| strategy: | |
| 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: ubuntu-latest | |
| goos: windows | |
| goarch: amd64 | |
| cgo: "0" | |
| name: mcpproxy-windows-amd64.exe | |
| archive_format: zip | |
| - os: ubuntu-latest | |
| goos: windows | |
| goarch: arm64 | |
| cgo: "0" | |
| name: mcpproxy-windows-arm64.exe | |
| archive_format: zip | |
| - os: macos-14 | |
| goos: darwin | |
| goarch: amd64 | |
| cgo: "1" | |
| name: mcpproxy-darwin-amd64 | |
| archive_format: tar.gz | |
| - os: macos-14 | |
| goos: darwin | |
| goarch: arm64 | |
| cgo: "1" | |
| name: mcpproxy-darwin-arm64 | |
| archive_format: tar.gz | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.23.10" | |
| - name: Cache Go modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go- | |
| - 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 | |
| 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: | | |
| echo "Converting and importing legacy P12 certificate..." | |
| # Decode the base64 P12 file | |
| echo "${{ secrets.APPLE_DEVELOPER_ID_CERT }}" | base64 -d > legacy_cert.p12 | |
| # Try extracting with standard OpenSSL first | |
| echo "Attempting extraction with standard OpenSSL..." | |
| if openssl pkcs12 -in legacy_cert.p12 \ | |
| -passin pass:"${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" \ | |
| -nodes \ | |
| -out temp_cert_and_key.pem 2>/dev/null; then | |
| echo "✅ Standard OpenSSL extraction succeeded" | |
| else | |
| echo "❌ Standard OpenSSL failed, installing OpenSSL 3.x with legacy provider..." | |
| # Install OpenSSL 3.x with legacy provider support | |
| brew install openssl@3 | |
| export PATH="/opt/homebrew/bin:$PATH" | |
| # Verify OpenSSL version | |
| openssl version | |
| # Extract with legacy provider support | |
| openssl pkcs12 -in legacy_cert.p12 \ | |
| -passin pass:"${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}" \ | |
| -provider legacy -provider default \ | |
| -nodes \ | |
| -out temp_cert_and_key.pem | |
| echo "✅ Legacy provider extraction succeeded" | |
| fi | |
| # Verify extraction worked | |
| if [ ! -f temp_cert_and_key.pem ]; then | |
| echo "❌ Certificate extraction failed" | |
| exit 1 | |
| fi | |
| echo "Certificates found: $(grep -c "BEGIN CERTIFICATE" temp_cert_and_key.pem)" | |
| echo "Private keys found: $(grep -c "BEGIN PRIVATE KEY" temp_cert_and_key.pem)" | |
| # Import certificate and key separately to avoid P12 compatibility issues | |
| openssl x509 -in temp_cert_and_key.pem -out cert_only.pem | |
| openssl rsa -in temp_cert_and_key.pem -out key_only.pem | |
| # Try importing to login keychain first, fallback to temp keychain | |
| echo "=== Importing certificates ===" | |
| # Skip login keychain in CI to avoid conflicts between parallel workers | |
| # Go directly to isolated temporary keychain | |
| echo "Creating isolated temporary keychain for this worker..." | |
| # Create unique keychain name for this matrix job to prevent conflicts | |
| UNIQUE_ID="${{ matrix.goos }}-${{ matrix.goarch }}-$$-$(date +%s)" | |
| TEMP_KEYCHAIN="mcpproxy-build-${UNIQUE_ID}.keychain" | |
| echo "Using keychain: ${TEMP_KEYCHAIN}" | |
| # Create isolated temporary keychain | |
| security create-keychain -p "temp123" "$TEMP_KEYCHAIN" | |
| # Add to search list WITHOUT setting as default (avoid conflicts) | |
| security list-keychains -s "$TEMP_KEYCHAIN" ~/Library/Keychains/login.keychain-db /Library/Keychains/System.keychain | |
| # Unlock and configure | |
| security unlock-keychain -p "temp123" "$TEMP_KEYCHAIN" | |
| security set-keychain-settings -t 3600 -l "$TEMP_KEYCHAIN" | |
| # Import to isolated keychain | |
| security import cert_only.pem -k "$TEMP_KEYCHAIN" -T /usr/bin/codesign | |
| security import key_only.pem -k "$TEMP_KEYCHAIN" -T /usr/bin/codesign | |
| # Set partition list for isolated keychain | |
| security set-key-partition-list -S apple-tool:,apple: -s -k "temp123" "$TEMP_KEYCHAIN" | |
| echo "✅ Imported to isolated temporary keychain: ${TEMP_KEYCHAIN}" | |
| # Store keychain name for cleanup | |
| echo "$TEMP_KEYCHAIN" > .keychain_name | |
| # Verify import | |
| echo "=== Available code signing identities ===" | |
| security find-identity -v -p codesigning | |
| # Clean up temporary files | |
| rm -f legacy_cert.p12 temp_cert_and_key.pem cert_only.pem key_only.pem | |
| echo "✅ Certificate import completed" | |
| - name: Build binary and create archives | |
| env: | |
| CGO_ENABLED: ${{ matrix.cgo }} | |
| GOOS: ${{ matrix.goos }} | |
| GOARCH: ${{ matrix.goarch }} | |
| # ✅ Force minimum supported macOS version for compatibility | |
| MACOSX_DEPLOYMENT_TARGET: "12.0" | |
| # Defensive CGO flags to ensure proper deployment target | |
| CGO_CFLAGS: "-mmacosx-version-min=12.0" | |
| CGO_LDFLAGS: "-mmacosx-version-min=12.0" | |
| run: | | |
| # For prerelease, determine version differently | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| else | |
| # Get last tag on any branch for base version | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| COMMIT_HASH=$(git rev-parse --short HEAD) | |
| VERSION="${LAST_TAG}-next.${COMMIT_HASH}" | |
| fi | |
| echo "Building version: ${VERSION}" | |
| LDFLAGS="-s -w -X mcpproxy-go/cmd/mcpproxy.version=${VERSION} -X main.version=${VERSION}" | |
| # Determine clean binary name | |
| if [ "${{ matrix.goos }}" = "windows" ]; then | |
| CLEAN_BINARY="mcpproxy.exe" | |
| else | |
| CLEAN_BINARY="mcpproxy" | |
| fi | |
| # Create clean core binary for archive | |
| go build -ldflags "${LDFLAGS}" -o ${CLEAN_BINARY} ./cmd/mcpproxy | |
| # Build tray binary for macOS | |
| if [ "${{ matrix.goos }}" = "darwin" ]; then | |
| echo "Building mcpproxy-tray for macOS..." | |
| go build -ldflags "${LDFLAGS}" -o mcpproxy-tray ./cmd/mcpproxy-tray | |
| 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 "❌ Tray binary strict verification FAILED - will not pass notarization" | |
| exit 1 | |
| fi | |
| # Check for secure timestamp (Apple's recommended check) | |
| echo "=== Checking for secure timestamps ===" | |
| CORE_TIMESTAMP_CHECK=$(codesign -dvv ${CLEAN_BINARY} 2>&1) | |
| if echo "$CORE_TIMESTAMP_CHECK" | grep -q "Timestamp="; then | |
| echo "✅ Core binary secure timestamp present:" | |
| echo "$CORE_TIMESTAMP_CHECK" | grep "Timestamp=" | |
| else | |
| echo "❌ No secure timestamp found for core binary" | |
| fi | |
| TRAY_TIMESTAMP_CHECK=$(codesign -dvv mcpproxy-tray 2>&1) | |
| if echo "$TRAY_TIMESTAMP_CHECK" | grep -q "Timestamp="; then | |
| echo "✅ Tray binary secure timestamp present:" | |
| echo "$TRAY_TIMESTAMP_CHECK" | grep "Timestamp=" | |
| else | |
| echo "❌ No secure timestamp found for tray binary" | |
| fi | |
| echo "✅ Both binaries signed successfully with hardened runtime and timestamp" | |
| fi | |
| # Create archive with version info - DO NOT create "latest" archives for prereleases | |
| ARCHIVE_BASE="mcpproxy-${VERSION#v}-${{ matrix.goos }}-${{ matrix.goarch }}" | |
| if [ "${{ matrix.archive_format }}" = "zip" ]; then | |
| # Create only versioned archive (no latest for prereleases) | |
| zip "${ARCHIVE_BASE}.zip" ${CLEAN_BINARY} | |
| else | |
| # Create only versioned archive (no latest for prereleases) | |
| tar -czf "${ARCHIVE_BASE}.tar.gz" ${CLEAN_BINARY} | |
| fi | |
| - 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: "12.0" | |
| CGO_CFLAGS: "-mmacosx-version-min=12.0" | |
| CGO_LDFLAGS: "-mmacosx-version-min=12.0" | |
| run: | | |
| # For prerelease, determine version differently (reuse from build step) | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| else | |
| # Get last tag on any branch for base version | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| COMMIT_HASH=$(git rev-parse --short HEAD) | |
| VERSION="${LAST_TAG}-next.${COMMIT_HASH}" | |
| fi | |
| chmod +x scripts/create-dmg.sh | |
| # Determine binary names | |
| TRAY_BINARY="mcpproxy-tray" | |
| CORE_BINARY="mcpproxy" | |
| # Create DMG with both tray and core binaries | |
| ./scripts/create-dmg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }} | |
| # 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: "12.0" | |
| CGO_CFLAGS: "-mmacosx-version-min=12.0" | |
| CGO_LDFLAGS: "-mmacosx-version-min=12.0" | |
| run: | | |
| # For prerelease, determine version differently (reuse from build step) | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| else | |
| # Get last tag on any branch for base version | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| COMMIT_HASH=$(git rev-parse --short HEAD) | |
| VERSION="${LAST_TAG}-next.${COMMIT_HASH}" | |
| fi | |
| 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" | |
| # Find the Developer ID Application certificate for app bundle signing | |
| APP_CERT_IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"') | |
| # Find the Developer ID Installer certificate for PKG signing | |
| PKG_CERT_IDENTITY=$(security find-identity -v -p basic | grep "Developer ID Installer" | head -1 | grep -o '"[^"]*"' | tr -d '"') | |
| if [ -n "${APP_CERT_IDENTITY}" ]; then | |
| echo "✅ Found Developer ID Application certificate: ${APP_CERT_IDENTITY}" | |
| else | |
| echo "❌ No Developer ID Application certificate found" | |
| APP_CERT_IDENTITY="${{ secrets.APPLE_TEAM_ID }}" | |
| fi | |
| if [ -n "${PKG_CERT_IDENTITY}" ]; then | |
| echo "✅ Found Developer ID Installer certificate: ${PKG_CERT_IDENTITY}" | |
| else | |
| echo "⚠️ No specific Developer ID Installer certificate found" | |
| echo "Note: PKG signing requires a separate 'Developer ID Installer' certificate" | |
| echo "App bundle will be signed with Developer ID Application, PKG will be unsigned" | |
| PKG_CERT_IDENTITY="" # Don't pass app cert to PKG signing | |
| fi | |
| # Export certificate identities for scripts to use | |
| export APP_CERT_IDENTITY="${APP_CERT_IDENTITY}" | |
| export PKG_CERT_IDENTITY="${PKG_CERT_IDENTITY}" | |
| # Determine binary names | |
| TRAY_BINARY="mcpproxy-tray" | |
| CORE_BINARY="mcpproxy" | |
| # Create PKG installer with both tray and core binaries | |
| if [ -n "${PKG_CERT_IDENTITY}" ]; then | |
| echo "Creating signed PKG installer with certificate: ${PKG_CERT_IDENTITY}" | |
| else | |
| echo "Creating PKG installer (unsigned - app bundle inside will be signed)" | |
| echo "Note: PKG signing requires a separate Developer ID Installer certificate" | |
| fi | |
| ./scripts/create-pkg.sh ${TRAY_BINARY} ${CORE_BINARY} ${VERSION} ${{ matrix.goarch }} | |
| # 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: | | |
| # For prerelease, determine version differently (reuse from build step) | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| else | |
| # Get last tag on any branch for base version | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| COMMIT_HASH=$(git rev-parse --short HEAD) | |
| VERSION="${LAST_TAG}-next.${COMMIT_HASH}" | |
| fi | |
| DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.dmg" | |
| PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg" | |
| INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg" | |
| # Function to submit file for notarization | |
| submit_for_notarization() { | |
| local FILE_NAME="$1" | |
| local FILE_TYPE="$2" | |
| echo "Submitting ${FILE_TYPE} for notarization: ${FILE_NAME}" | |
| # Submit with proper error handling and retries | |
| SUBMISSION_SUCCESS=false | |
| for attempt in 1 2 3; do | |
| echo "${FILE_TYPE} notarization attempt $attempt/3..." | |
| # Capture both stdout and stderr | |
| 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 }}" \ | |
| --output-format json 2>&1) | |
| SUBMISSION_EXIT_CODE=$? | |
| if [ $SUBMISSION_EXIT_CODE -eq 0 ]; then | |
| # Extract submission ID with validation | |
| SUBMISSION_ID=$(echo "$SUBMISSION_OUTPUT" | jq -r '.id // empty') | |
| if [ -n "$SUBMISSION_ID" ] && [ "$SUBMISSION_ID" != "null" ]; then | |
| echo "✅ ${FILE_TYPE} notarization submission successful on attempt $attempt" | |
| echo "Submission ID: ${SUBMISSION_ID}" | |
| # Save submission ID | |
| echo "${SUBMISSION_ID}" > "${FILE_NAME}.submission_id" | |
| SUBMISSION_SUCCESS=true | |
| break | |
| else | |
| echo "❌ Failed to extract valid submission ID from response" | |
| echo "Response: $SUBMISSION_OUTPUT" | |
| fi | |
| else | |
| echo "❌ ${FILE_TYPE} notarization submission failed with exit code: $SUBMISSION_EXIT_CODE" | |
| echo "Output: $SUBMISSION_OUTPUT" | |
| fi | |
| if [ $attempt -lt 3 ]; then | |
| echo "Retrying in 30 seconds..." | |
| sleep 30 | |
| fi | |
| done | |
| if [ "$SUBMISSION_SUCCESS" != "true" ]; then | |
| echo "❌ All ${FILE_TYPE} notarization submission attempts failed" | |
| echo "Skipping notarization tracking for ${FILE_NAME}" | |
| return 1 | |
| fi | |
| echo "✅ ${FILE_TYPE} submitted for notarization (ID: ${SUBMISSION_ID})" | |
| return 0 | |
| } | |
| # Submit all files for notarization | |
| submit_for_notarization "${DMG_NAME}" "App DMG" || true | |
| submit_for_notarization "${PKG_NAME}" "PKG installer" || true | |
| submit_for_notarization "${INSTALLER_DMG_NAME}" "Installer DMG" || true | |
| echo "✅ All files submitted for notarization" | |
| - 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.goos }}-${{ matrix.goarch }} | |
| path: mcpproxy-*-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }} | |
| - name: Upload macOS installers (DMG and PKG) | |
| if: matrix.goos == 'darwin' | |
| run: | | |
| # For prerelease, determine version to create exact file names (reuse from build step) | |
| if [[ "${{ github.ref }}" == refs/tags/* ]]; then | |
| VERSION=${GITHUB_REF#refs/tags/} | |
| else | |
| # Get last tag on any branch for base version | |
| LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") | |
| COMMIT_HASH=$(git rev-parse --short HEAD) | |
| VERSION="${LAST_TAG}-next.${COMMIT_HASH}" | |
| fi | |
| DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.dmg" | |
| PKG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.pkg" | |
| INSTALLER_DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}-installer.dmg" | |
| echo "Looking for files:" | |
| echo " App DMG: ${DMG_NAME}" | |
| echo " PKG installer: ${PKG_NAME}" | |
| echo " Installer DMG: ${INSTALLER_DMG_NAME}" | |
| # Prioritize installer DMG (contains PKG) as primary choice | |
| # Also include app DMG for users who prefer drag-and-drop | |
| FILES_TO_UPLOAD="" | |
| # Primary choice: Installer DMG (contains PKG installer) | |
| if [ -f "${INSTALLER_DMG_NAME}" ]; then | |
| echo "✅ Found Installer DMG (PRIMARY): ${INSTALLER_DMG_NAME}" | |
| FILES_TO_UPLOAD="${FILES_TO_UPLOAD} ${INSTALLER_DMG_NAME}" | |
| else | |
| echo "❌ Installer DMG not found: ${INSTALLER_DMG_NAME}" | |
| fi | |
| # Alternative choice: App DMG (drag-and-drop) | |
| if [ -f "${DMG_NAME}" ]; then | |
| echo "✅ Found App DMG (ALTERNATIVE): ${DMG_NAME}" | |
| FILES_TO_UPLOAD="${FILES_TO_UPLOAD} ${DMG_NAME}" | |
| else | |
| echo "❌ App DMG not found: ${DMG_NAME}" | |
| fi | |
| # Note: PKG file is included inside installer DMG, so we don't upload it separately | |
| if [ -z "${FILES_TO_UPLOAD}" ]; then | |
| echo "❌ No installer files found" | |
| exit 1 | |
| fi | |
| # Create artifact directory | |
| mkdir -p installers-artifact | |
| # Copy installer files (prioritize installer DMG) | |
| for file in ${FILES_TO_UPLOAD}; do | |
| cp "${file}" installers-artifact/ | |
| # Copy submission ID file if it exists | |
| SUBMISSION_ID_FILE="${file}.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 | |
| done | |
| 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/* | |
| release: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: staging | |
| # Only create releases for tag pushes, not branch pushes | |
| if: startsWith(github.ref, 'refs/tags/v') && (contains(github.ref, '-rc.') || contains(github.ref, '-next.')) | |
| 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) - only versioned, no latest for prereleases | |
| find dist -name "*.tar.gz" -o -name "*.zip" | while read file; do | |
| filename=$(basename "$file") | |
| cp "$file" "release-files/$filename" | |
| done | |
| # Handle installer files (DMG and PKG) and notarization submissions | |
| mkdir -p pending-notarizations | |
| # Process installer artifacts (contains DMG, PKG, and installer DMG files) | |
| find dist -path "*/installers-*" \( -name "*.dmg" -o -name "*.pkg" \) | 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 prerelease with binaries | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: release-files/* | |
| prerelease: true | |
| body: | | |
| ## mcpproxy ${{ github.ref_name }} (Prerelease) | |
| ⚠️ **This is a prerelease version** - Use at your own risk! | |
| Smart MCP Proxy - Intelligent tool discovery and proxying for Model Context Protocol servers. | |
| ### Download Links | |
| **This Prerelease (${{ github.ref_name }}):** | |
| - [Linux AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-linux-amd64.tar.gz) | |
| - [Linux ARM64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-linux-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) | |
| - [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) | |
| **macOS Installers (Recommended):** | |
| - [PKG Installer (Apple Silicon)](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-arm64-installer.dmg) - **Best Choice** | |
| - [PKG Installer (Intel)](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-amd64-installer.dmg) - **Best Choice** | |
| - [Drag-and-Drop DMG (Apple Silicon)](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-arm64.dmg) - Manual Setup | |
| - [Drag-and-Drop DMG (Intel)](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ env.CLEAN_VERSION }}-darwin-amd64.dmg) - Manual Setup | |
| ### Installation | |
| **macOS (Recommended - PKG Installer):** | |
| 1. Download the PKG installer DMG for your Mac (Apple Silicon or Intel) | |
| 2. Double-click the DMG to mount it | |
| 3. Double-click the PKG installer inside | |
| 4. Follow the installation wizard | |
| 5. CLI tool `mcpproxy` will be available in Terminal | |
| 6. Launch mcpproxy.app from Applications folder | |
| 7. The app will appear in your system tray | |
| **macOS (Alternative - Drag-and-Drop DMG):** | |
| 1. Download the drag-and-drop DMG file for your Mac | |
| 2. Double-click the DMG to mount it | |
| 3. Drag mcpproxy.app to Applications folder | |
| 4. Manually create CLI symlink: `sudo ln -sf /Applications/mcpproxy.app/Contents/Resources/bin/mcpproxy /usr/local/bin/mcpproxy` | |
| 5. Launch from Applications or Launchpad | |
| **Manual Installation (All Platforms):** | |
| 1. Download the appropriate archive for your platform using the links above | |
| 2. Extract the archive: `tar -xzf mcpproxy-*.tar.gz` (Linux/macOS) or unzip (Windows) | |
| 3. Make it executable: `chmod +x mcpproxy` (Linux/macOS) | |
| 4. Run `./mcpproxy` to start | |
| ### Platform Support | |
| - **macOS**: Full system tray support with menu and icons | |
| - **Windows**: Full system tray support with menu and icons | |
| - **Linux**: Headless mode only (no system tray due to compatibility) | |
| ### Usage | |
| - With tray: `./mcpproxy serve` (default) | |
| - Custom port (default: 8080): `./mcpproxy serve --listen :8081` | |
| - Headless: `./mcpproxy serve --tray=false` | |
| ### ⚠️ Prerelease Notes | |
| - This is a development version and may contain bugs | |
| - Not recommended for production use | |
| - Auto-updater will NOT automatically update to this version unless `MCPPROXY_ALLOW_PRERELEASE_UPDATES=true` is set | |
| 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 }} |