Skip to content

Enhance macOS code signing process in GitHub Actions workflow by addi… #46

Enhance macOS code signing process in GitHub Actions workflow by addi…

Enhance macOS code signing process in GitHub Actions workflow by addi… #46

Workflow file for this run

name: Release
on:
push:
tags: ["v*"]
permissions:
contents: write
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
goos: linux
goarch: amd64
cgo: "0"
name: mcpproxy-linux-amd64
archive_format: tar.gz
- os: ubuntu-latest
goos: windows
goarch: amd64
cgo: "0"
name: mcpproxy-windows-amd64.exe
archive_format: zip
- os: macos-latest
goos: darwin
goarch: amd64
cgo: "1"
name: mcpproxy-darwin-amd64
archive_format: tar.gz
- os: macos-latest
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: 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 }}
run: |
VERSION=${GITHUB_REF#refs/tags/}
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 binary for archive
go build -ldflags "${LDFLAGS}" -o ${CLEAN_BINARY} ./cmd/mcpproxy
# 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 with proper Developer ID certificate, hardened runtime, and timestamp
echo "=== Signing binary 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
# Add timeout and retry logic for signing to prevent hanging
SIGN_SUCCESS=false
for attempt in 1 2 3; do
echo "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 "✅ Signing succeeded on attempt $attempt"
break
else
echo "❌ 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 signing attempts failed"
# Try signing without timestamp as fallback
echo "Attempting fallback signing without timestamp..."
if $TIMEOUT_CMD 120 codesign --force \
--options runtime \
--entitlements scripts/entitlements.plist \
--sign "${CERT_IDENTITY}" \
${CLEAN_BINARY}; then
echo "⚠️ Fallback signing succeeded (without timestamp)"
echo "NOTE: This binary may not pass notarization without timestamp"
else
echo "❌ Even fallback signing failed - cannot proceed"
exit 1
fi
fi
# Verify signing, hardened runtime, and timestamp using Apple's recommended methods
echo "=== Verifying binary signature (Apple's recommended verification) ==="
# Basic verification
codesign --verify --verbose ${CLEAN_BINARY}
echo "Basic verification: $?"
# Apple's recommended strict verification for notarization
echo "=== Strict verification (matches notarization requirements) ==="
if codesign -vvv --deep --strict ${CLEAN_BINARY}; then
echo "✅ 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 }}"
if [ "${{ matrix.archive_format }}" = "zip" ]; then
# Create versioned archive
zip "${ARCHIVE_BASE}.zip" ${CLEAN_BINARY}
# Create latest archive
zip "${LATEST_ARCHIVE_BASE}.zip" ${CLEAN_BINARY}
else
# Create versioned archive
tar -czf "${ARCHIVE_BASE}.tar.gz" ${CLEAN_BINARY}
# Create latest archive
tar -czf "${LATEST_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'
run: |
VERSION=${GITHUB_REF#refs/tags/}
chmod +x scripts/create-dmg.sh
# Determine binary name
CLEAN_BINARY="mcpproxy"
# Create DMG
./scripts/create-dmg.sh ${CLEAN_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: Submit for notarization (macOS)
if: matrix.goos == 'darwin'
run: |
VERSION=${GITHUB_REF#refs/tags/}
DMG_NAME="mcpproxy-${VERSION#v}-darwin-${{ matrix.goarch }}.dmg"
echo "Submitting DMG for notarization..."
SUBMISSION_ID=$(xcrun notarytool submit "${DMG_NAME}" --apple-id "${{ secrets.APPLE_ID_USERNAME }}" --password "${{ secrets.APPLE_ID_APP_PASSWORD }}" --team-id "${{ secrets.APPLE_TEAM_ID }}" --output-format json | jq -r '.id')
echo "Submission ID: ${SUBMISSION_ID}"
echo "${SUBMISSION_ID}" > "${DMG_NAME}.submission_id"
echo "DMG submitted for notarization (ID: ${SUBMISSION_ID})"
- 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 latest archive artifact
uses: actions/upload-artifact@v4
with:
name: latest-${{ matrix.goos }}-${{ matrix.goarch }}
path: mcpproxy-latest-${{ matrix.goos }}-${{ matrix.goarch }}.${{ matrix.archive_format }}
- name: Upload DMG installer (macOS)
if: matrix.goos == 'darwin'
uses: actions/upload-artifact@v4
with:
name: dmg-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
mcpproxy-*-darwin-${{ matrix.goarch }}.dmg
mcpproxy-*-darwin-${{ matrix.goarch }}.dmg.submission_id
release:
needs: build
runs-on: ubuntu-latest
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
# Handle DMG files and notarization submissions
mkdir -p pending-notarizations
find dist -path "*/dmg-*" -name "*.dmg" | while read dmg_file; do
filename=$(basename "$dmg_file")
submission_id_file="${dmg_file}.submission_id"
if [ -f "$submission_id_file" ]; then
# DMG has pending notarization
echo "Found pending notarization for $filename"
cp "$dmg_file" "release-files/$filename"
# Create pending notarization record
SUBMISSION_ID=$(cat "$submission_id_file")
cat > "pending-notarizations/${filename}.pending" << EOF
{
"submission_id": "$SUBMISSION_ID",
"dmg_name": "$filename",
"version": "$VERSION",
"submitted_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
else
# No notarization submission (shouldn't happen, but handle it)
echo "No submission ID for $filename, copying as-is"
cp "$dmg_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: Create release with binaries
uses: softprops/action-gh-release@v2
with:
files: release-files/*
body: |
## mcpproxy ${{ github.ref_name }}
Smart MCP Proxy - Intelligent tool discovery and proxying for Model Context Protocol servers.
### Download Links
**Latest Version (auto-updates):**
- [Linux AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-linux-amd64.tar.gz)
- [Windows AMD64](https://github.com/${{ github.repository }}/releases/latest/download/mcpproxy-latest-windows-amd64.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)
**macOS Installers (Recommended):**
- [macOS Universal DMG](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ github.ref_name }}-darwin-arm64.dmg) (Apple Silicon)
- [macOS Intel DMG](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ github.ref_name }}-darwin-amd64.dmg) (Intel)
**This Version (${{ github.ref_name }}):**
- [Linux AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ github.ref_name }}-linux-amd64.tar.gz)
- [Windows AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ github.ref_name }}-windows-amd64.zip)
- [macOS AMD64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ github.ref_name }}-darwin-amd64.tar.gz)
- [macOS ARM64](https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}/mcpproxy-${{ github.ref_name }}-darwin-arm64.tar.gz)
### Installation
**macOS (Recommended - DMG Installer):**
1. Download the appropriate DMG file for your Mac (Apple Silicon or Intel)
2. Double-click the DMG to mount it
3. Drag mcpproxy.app to Applications folder
4. Launch from Applications or Launchpad
5. The app will appear in your system tray with autostart capability
**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` (default)
- Headless: `./mcpproxy --tray=false`
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
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: Update Homebrew formula
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "Processing version: ${VERSION}"
cd tap
# Add debugging and retry logic
ARCHIVE_URL="https://github.com/${{ github.repository }}/archive/refs/tags/v${VERSION}.tar.gz"
echo "Downloading from: ${ARCHIVE_URL}"
# Wait a bit for GitHub to generate the archive
sleep 10
# Try downloading with retries
for i in {1..5}; do
echo "Download attempt ${i}/5..."
if wget -q "${ARCHIVE_URL}" -O "v${VERSION}.tar.gz"; 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"
echo "Checking if file exists at URL..."
curl -I "${ARCHIVE_URL}" || true
exit 1
fi
done
# Verify file was downloaded
if [ ! -f "v${VERSION}.tar.gz" ]; then
echo "Archive file not found after download"
ls -la
exit 1
fi
# Calculate SHA256
SOURCE_SHA=$(sha256sum "v${VERSION}.tar.gz" | cut -d' ' -f1)
echo "Calculated SHA256: ${SOURCE_SHA}"
# Create formula directory
mkdir -p Formula
# Create formula file
printf 'class Mcpproxy < Formula\n desc "Smart MCP Proxy - Intelligent tool discovery and proxying for Model Context Protocol servers"\n homepage "https://github.com/smart-mcp-proxy/mcpproxy-go"\n url "https://github.com/smart-mcp-proxy/mcpproxy-go/archive/refs/tags/v%s.tar.gz"\n sha256 "%s"\n license "MIT"\n head "https://github.com/smart-mcp-proxy/mcpproxy-go.git"\n\n depends_on "go" => :build\n\n def install\n system "go", "build", "-ldflags", "-s -w -X mcpproxy-go/cmd/mcpproxy.version=v%s -X main.version=v%s", "-o", "mcpproxy", "./cmd/mcpproxy"\n bin.install "mcpproxy"\n end\n\n test do\n assert_match version.to_s, shell_output("#{bin}/mcpproxy --version")\n end\nend\n' "${VERSION}" "${SOURCE_SHA}" "${VERSION}" "${VERSION}" > Formula/mcpproxy.rb
echo "Formula created successfully"
cat Formula/mcpproxy.rb
# Clean up
rm -f *.tar.gz
- 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
# 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 ${GITHUB_REF#refs/tags/v}"
git push
echo "Changes committed and pushed successfully"
fi