Build MCP Server Containers #26
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: Build MCP Server Containers | |
| on: | |
| push: | |
| branches: [ main ] | |
| paths: | |
| - 'npx/**/*.yaml' | |
| - 'uvx/**/*.yaml' | |
| - 'go/**/*.yaml' | |
| - 'main.go' | |
| - 'go.mod' | |
| - 'go.sum' | |
| pull_request: | |
| branches: [ main ] | |
| paths: | |
| - 'npx/**/*.yaml' | |
| - 'uvx/**/*.yaml' | |
| - 'go/**/*.yaml' | |
| - 'main.go' | |
| - 'go.mod' | |
| - 'go.sum' | |
| workflow_dispatch: | |
| env: | |
| REGISTRY: ghcr.io | |
| IMAGE_NAME: ${{ github.repository }} | |
| jobs: | |
| discover-configs: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| configs: ${{ steps.find-configs.outputs.configs }} | |
| changed-configs: ${{ steps.find-configs.outputs.changed-configs }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 | |
| with: | |
| fetch-depth: 0 # Need full history for change detection | |
| - name: Find configuration files to build | |
| id: find-configs | |
| run: | | |
| # Find all spec.yaml files in protocol directories | |
| all_configs=$(find npx uvx go -name "spec.yaml" -type f 2>/dev/null | sort) | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| # For manual triggers, build all configs | |
| configs_to_build="$all_configs" | |
| echo "Manual trigger - building all configurations" | |
| elif [ "${{ github.event_name }}" == "pull_request" ]; then | |
| # For PRs, build configs that changed compared to target branch | |
| changed_files=$(git diff --name-only origin/${{ github.base_ref }}...HEAD) | |
| configs_to_build="" | |
| # Check if main.go, go.mod, or go.sum changed (rebuild all) | |
| if echo "$changed_files" | grep -E "(main\.go|go\.mod|go\.sum)"; then | |
| echo "Core files changed - building all configurations" | |
| configs_to_build="$all_configs" | |
| else | |
| # Only build configs whose spec.yaml files changed or their parent directories changed | |
| for config in $all_configs; do | |
| config_dir=$(dirname "$config") | |
| # Check if spec.yaml itself changed or any file in its directory changed | |
| if echo "$changed_files" | grep -q "^$config$" || echo "$changed_files" | grep -q "^$config_dir/"; then | |
| configs_to_build="$configs_to_build$config"$'\n' | |
| fi | |
| done | |
| fi | |
| else | |
| # For pushes to main, build configs that changed in this push | |
| changed_files=$(git diff --name-only HEAD~1..HEAD) | |
| configs_to_build="" | |
| # Check if main.go, go.mod, or go.sum changed (rebuild all) | |
| if echo "$changed_files" | grep -E "(main\.go|go\.mod|go\.sum)"; then | |
| echo "Core files changed - building all configurations" | |
| configs_to_build="$all_configs" | |
| else | |
| # Only build configs whose spec.yaml files changed or their parent directories changed | |
| for config in $all_configs; do | |
| config_dir=$(dirname "$config") | |
| # Check if spec.yaml itself changed or any file in its directory changed | |
| if echo "$changed_files" | grep -q "^$config$" || echo "$changed_files" | grep -q "^$config_dir/"; then | |
| configs_to_build="$configs_to_build$config"$'\n' | |
| fi | |
| done | |
| fi | |
| fi | |
| # Convert to JSON array, filtering out empty lines | |
| configs_json=$(echo "$configs_to_build" | grep -v '^$' | jq -R -s -c 'split("\n")[:-1]') | |
| all_configs_json=$(echo "$all_configs" | jq -R -s -c 'split("\n")[:-1]') | |
| echo "configs=$all_configs_json" >> $GITHUB_OUTPUT | |
| echo "changed-configs=$configs_json" >> $GITHUB_OUTPUT | |
| echo "All configurations: $all_configs_json" | |
| echo "Configurations to build: $configs_json" | |
| mcp-security-scan: | |
| needs: discover-configs | |
| runs-on: ubuntu-latest | |
| if: ${{ needs.discover-configs.outputs.changed-configs != '[]' }} | |
| strategy: | |
| matrix: | |
| config: ${{ fromJson(needs.discover-configs.outputs.changed-configs) }} | |
| fail-fast: false | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Set up uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| - name: Install dependencies | |
| run: | | |
| # Install mcp-scan | |
| uv tool install mcp-scan | |
| # Install Python dependencies for scripts | |
| uv pip install --system pyyaml | |
| # Verify installation (mcp-scan doesn't have --version, so we use help) | |
| uv tool run mcp-scan --help || true | |
| - name: Extract metadata from config | |
| id: meta | |
| run: | | |
| config_file="${{ matrix.config }}" | |
| # Extract protocol and server name from path like "npx/context7/spec.yaml" | |
| protocol=$(echo "$config_file" | cut -d'/' -f1) | |
| server_name=$(echo "$config_file" | cut -d'/' -f2) | |
| echo "config_file=$config_file" >> $GITHUB_OUTPUT | |
| echo "protocol=$protocol" >> $GITHUB_OUTPUT | |
| echo "server_name=$server_name" >> $GITHUB_OUTPUT | |
| - name: Run MCP Security Scan | |
| id: scan | |
| run: | | |
| echo "🔍 Scanning MCP server: ${{ steps.meta.outputs.server_name }}" | |
| # Generate MCP configuration | |
| python3 scripts/mcp-scan/generate_mcp_config.py \ | |
| "${{ matrix.config }}" \ | |
| "${{ steps.meta.outputs.protocol }}" \ | |
| "${{ steps.meta.outputs.server_name }}" \ | |
| > "/tmp/${{ steps.meta.outputs.server_name }}-mcp-config.json" | |
| # Run mcp-scan | |
| scan_output="/tmp/mcp-scan-${{ steps.meta.outputs.server_name }}.json" | |
| if uv tool run mcp-scan scan "/tmp/${{ steps.meta.outputs.server_name }}-mcp-config.json" \ | |
| --json \ | |
| --storage-file "/tmp/mcp-scan-storage" \ | |
| --server-timeout 30 \ | |
| --suppress-mcpserver-io true \ | |
| > "$scan_output" 2>&1; then | |
| echo "scan_passed=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "scan_passed=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Process results (pass the config file to check for allowed issues) | |
| python3 scripts/mcp-scan/process_scan_results.py \ | |
| "$scan_output" \ | |
| "${{ steps.meta.outputs.server_name }}" \ | |
| "${{ matrix.config }}" \ | |
| > scan-summary.json | |
| # Copy scan output to current directory for artifact upload | |
| cp "$scan_output" "mcp-scan-${{ steps.meta.outputs.server_name }}.json" | |
| - name: Upload scan results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: mcp-scan-${{ steps.meta.outputs.server_name }} | |
| path: | | |
| mcp-scan-${{ steps.meta.outputs.server_name }}.json | |
| scan-summary.json | |
| retention-days: 30 | |
| build-containers: | |
| needs: [discover-configs, mcp-security-scan] | |
| runs-on: ubuntu-latest | |
| if: ${{ needs.discover-configs.outputs.changed-configs != '[]' }} | |
| strategy: | |
| matrix: | |
| config: ${{ fromJson(needs.discover-configs.outputs.changed-configs) }} | |
| fail-fast: false | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write # Needed for OIDC token (sigstore) | |
| attestations: write # Needed for attestations | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 | |
| - name: Set up Go | |
| uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 | |
| with: | |
| go-version: '1.24' | |
| - name: Set up QEMU | |
| uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3 | |
| - name: Install Cosign | |
| uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 | |
| - name: Log in to Container Registry | |
| if: github.event_name != 'pull_request' | |
| uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Extract metadata from config | |
| id: meta | |
| run: | | |
| config_file="${{ matrix.config }}" | |
| echo "config_file=$config_file" >> $GITHUB_OUTPUT | |
| # Extract protocol from directory name | |
| protocol=$(dirname "$config_file") | |
| echo "protocol=$protocol" >> $GITHUB_OUTPUT | |
| # Extract server name from directory name (parent of spec.yaml) | |
| server_name=$(echo "$config_file" | cut -d'/' -f2) | |
| echo "server_name=$server_name" >> $GITHUB_OUTPUT | |
| # Extract version from YAML file (prefer spec.version, fallback to metadata.version) | |
| spec_version=$(grep -A1 "^spec:" "$config_file" | grep "version:" | sed 's/.*version:[ ]*"\(.*\)".*/\1/' | head -1) | |
| metadata_version=$(grep -A10 "^metadata:" "$config_file" | grep "version:" | sed 's/.*version:[ ]*"\(.*\)".*/\1/' | head -1) | |
| # Use spec.version if available, otherwise metadata.version, otherwise "latest" | |
| if [ -n "$spec_version" ]; then | |
| version="$spec_version" | |
| elif [ -n "$metadata_version" ]; then | |
| version="$metadata_version" | |
| else | |
| version="latest" | |
| fi | |
| echo "version=$version" >> $GITHUB_OUTPUT | |
| # Generate image name | |
| image_name="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}/${protocol}/${server_name}" | |
| echo "image_name=$image_name" >> $GITHUB_OUTPUT | |
| - name: Generate Dockerfile | |
| id: dockerfile | |
| run: | | |
| echo "Generating Dockerfile for ${{ steps.meta.outputs.config_file }}" | |
| # Create a temporary directory for the Dockerfile | |
| dockerfile_dir=$(mktemp -d) | |
| dockerfile_path="${dockerfile_dir}/Dockerfile" | |
| # Generate the Dockerfile using our tool | |
| go run main.go -config "${{ steps.meta.outputs.config_file }}" -output "${dockerfile_path}" | |
| echo "dockerfile_dir=$dockerfile_dir" >> $GITHUB_OUTPUT | |
| echo "dockerfile_path=$dockerfile_path" >> $GITHUB_OUTPUT | |
| # Display the generated Dockerfile for debugging | |
| echo "Generated Dockerfile:" | |
| cat "${dockerfile_path}" | |
| - name: Build and push Docker image | |
| id: build | |
| uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6 | |
| with: | |
| context: ${{ steps.dockerfile.outputs.dockerfile_dir }} | |
| file: ${{ steps.dockerfile.outputs.dockerfile_path }} | |
| platforms: linux/amd64,linux/arm64 | |
| push: ${{ github.event_name != 'pull_request' }} | |
| tags: | | |
| ${{ steps.meta.outputs.image_name }}:${{ steps.meta.outputs.version }} | |
| ${{ steps.meta.outputs.image_name }}:latest | |
| labels: | | |
| org.opencontainers.image.title=${{ steps.meta.outputs.server_name }} | |
| org.opencontainers.image.description=MCP server for ${{ steps.meta.outputs.server_name }} | |
| org.opencontainers.image.vendor=Stacklok | |
| org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} | |
| org.opencontainers.image.revision=${{ github.sha }} | |
| org.opencontainers.image.version=${{ steps.meta.outputs.version }} | |
| cache-from: type=gha | |
| cache-to: type=gha,mode=max | |
| sbom: true | |
| provenance: true | |
| - name: Sign container images with Cosign | |
| if: github.event_name != 'pull_request' | |
| env: | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| echo "Signing image ${{ steps.meta.outputs.image_name }}@${DIGEST}" | |
| cosign sign --yes ${{ steps.meta.outputs.image_name }}@${DIGEST} | |
| # Also sign the tagged versions for better UX | |
| cosign sign --yes ${{ steps.meta.outputs.image_name }}:${{ steps.meta.outputs.version }} | |
| cosign sign --yes ${{ steps.meta.outputs.image_name }}:latest | |
| echo "✅ Images signed with Sigstore/Cosign" >> $GITHUB_STEP_SUMMARY | |
| - name: Download MCP security scan results | |
| if: github.event_name != 'pull_request' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: mcp-scan-${{ steps.meta.outputs.server_name }} | |
| path: /tmp/scan-results | |
| - name: Create security scan attestation | |
| if: github.event_name != 'pull_request' | |
| env: | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| echo "Creating security scan attestation for ${{ steps.meta.outputs.image_name }}@${DIGEST}" | |
| # Read the scan summary | |
| SCAN_SUMMARY=$(cat /tmp/scan-results/scan-summary.json) | |
| SCAN_STATUS=$(echo "$SCAN_SUMMARY" | jq -r '.status') | |
| # Create custom attestation for security scan | |
| cat > /tmp/security-attestation.json <<EOF | |
| { | |
| "_type": "https://in-toto.io/Statement/v0.1", | |
| "predicateType": "https://github.com/stacklok/dockyard/mcp-security-scan/v1", | |
| "subject": [ | |
| { | |
| "name": "${{ steps.meta.outputs.image_name }}", | |
| "digest": { | |
| "sha256": "${DIGEST#sha256:}" | |
| } | |
| } | |
| ], | |
| "predicate": { | |
| "scanTool": "mcp-scan", | |
| "scanDate": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", | |
| "scanResult": $SCAN_SUMMARY, | |
| "repository": "${{ github.repository }}", | |
| "commitSha": "${{ github.sha }}", | |
| "workflowRun": "${{ github.run_id }}", | |
| "configFile": "${{ matrix.config }}" | |
| } | |
| } | |
| EOF | |
| # Attest the security scan results | |
| cosign attest --yes \ | |
| --predicate /tmp/security-attestation.json \ | |
| --type https://github.com/stacklok/dockyard/mcp-security-scan/v1 \ | |
| ${{ steps.meta.outputs.image_name }}@${DIGEST} | |
| echo "✅ Security scan attestation created" >> $GITHUB_STEP_SUMMARY | |
| # Clean up | |
| rm -f /tmp/security-attestation.json | |
| - name: Generate image summary | |
| run: | | |
| echo "## Container Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Config**: ${{ steps.meta.outputs.config_file }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Protocol**: ${{ steps.meta.outputs.protocol }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Server**: ${{ steps.meta.outputs.server_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Image**: ${{ steps.meta.outputs.image_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: ${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Platforms**: linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY | |
| echo "- **SBOM**: ✅ Attested" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Build Provenance**: ✅ Attested" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Security Scan**: ✅ Attested" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Signatures**: ✅ Signed with Sigstore/Cosign" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ github.event_name }}" != "pull_request" ]; then | |
| echo "- **Status**: ✅ Built, pushed, and signed" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Tags**:" >> $GITHUB_STEP_SUMMARY | |
| echo " - ${{ steps.meta.outputs.image_name }}:${{ steps.meta.outputs.version }}" >> $GITHUB_STEP_SUMMARY | |
| echo " - ${{ steps.meta.outputs.image_name }}:latest" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Digest**: ${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- **Status**: ✅ Built (not pushed - PR)" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| mcp-scan-report: | |
| needs: mcp-security-scan | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' && always() | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - name: Download all scan results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: mcp-scan-* | |
| path: scan-artifacts | |
| - name: Comment PR with scan results | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| let comment = '## 🔒 MCP Security Scan Results\n\n'; | |
| let hasAnyIssues = false; | |
| let totalServersScanned = 0; | |
| let totalVulnerabilities = 0; | |
| // Find all scan summary files in the scan-artifacts directory | |
| const summaryFiles = []; | |
| const artifactsDir = 'scan-artifacts'; | |
| // Debug: List all files in current directory | |
| console.log('Current directory contents:', fs.readdirSync('.')); | |
| if (fs.existsSync(artifactsDir)) { | |
| console.log('Artifacts directory exists'); | |
| const artifactDirs = fs.readdirSync(artifactsDir); | |
| console.log('Artifact directories:', artifactDirs); | |
| // Each artifact directory should contain the scan results | |
| for (const dir of artifactDirs) { | |
| const dirPath = path.join(artifactsDir, dir); | |
| if (fs.statSync(dirPath).isDirectory()) { | |
| // Look for scan-summary.json in each artifact directory | |
| const summaryFile = path.join(dirPath, 'scan-summary.json'); | |
| if (fs.existsSync(summaryFile)) { | |
| summaryFiles.push(summaryFile); | |
| console.log('Found summary file:', summaryFile); | |
| } else { | |
| console.log(`No scan-summary.json in ${dirPath}, contents:`, fs.readdirSync(dirPath)); | |
| } | |
| } | |
| } | |
| } else { | |
| console.log('Artifacts directory does not exist'); | |
| } | |
| console.log('Total summary files found:', summaryFiles.length); | |
| if (summaryFiles.length === 0) { | |
| comment += '⚠️ No MCP servers were scanned in this PR.\n'; | |
| } else { | |
| for (const file of summaryFiles) { | |
| try { | |
| const summary = JSON.parse(fs.readFileSync(file, 'utf8')); | |
| totalServersScanned++; | |
| if (summary.status === 'passed') { | |
| comment += `### ✅ ${summary.server}\n`; | |
| comment += `- **Status**: Passed\n`; | |
| comment += `- **Tools scanned**: ${summary.tools_scanned || 0}\n`; | |
| comment += `- **Result**: No security issues detected\n\n`; | |
| } else if (summary.status === 'failed') { | |
| hasAnyIssues = true; | |
| totalVulnerabilities += summary.vulnerability_count || 0; | |
| comment += `### ❌ ${summary.server}\n`; | |
| comment += `- **Status**: Failed\n`; | |
| comment += `- **Tools scanned**: ${summary.tools_scanned || 0}\n`; | |
| comment += `- **Vulnerabilities found**: ${summary.vulnerability_count || 0}\n`; | |
| comment += '\n**Security issues detected:**\n'; | |
| if (summary.vulnerabilities) { | |
| summary.vulnerabilities.forEach(vuln => { | |
| comment += `- **[${vuln.code}]** ${vuln.message}\n`; | |
| }); | |
| } | |
| comment += '\n'; | |
| } else if (summary.status === 'warning') { | |
| comment += `### ⚠️ ${summary.server}\n`; | |
| comment += `- **Status**: Warning\n`; | |
| comment += `- **Message**: ${summary.message}\n\n`; | |
| } else { | |
| comment += `### ⚠️ ${summary.server}\n`; | |
| comment += `- **Status**: Error\n`; | |
| comment += `- **Message**: ${summary.message || 'Unknown error'}\n\n`; | |
| } | |
| } catch (error) { | |
| console.error(`Error parsing ${file}:`, error); | |
| comment += `### ⚠️ Error parsing scan results\n`; | |
| comment += `Could not parse ${file}: ${error.message}\n\n`; | |
| } | |
| } | |
| // Add summary | |
| if (totalServersScanned > 0) { | |
| comment += '---\n'; | |
| comment += `**Summary**: Scanned ${totalServersScanned} MCP server(s)`; | |
| if (hasAnyIssues) { | |
| comment += `, found ${totalVulnerabilities} vulnerability issue(s).\n\n`; | |
| comment += '⚠️ **Action Required**: Security issues were detected. Please review and address them before merging.\n'; | |
| } else { | |
| comment += ', all passed security checks. ✅\n'; | |
| } | |
| } | |
| } | |
| // Find and update or create comment | |
| const { data: comments } = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const botComment = comments.find(comment => | |
| comment.user.type === 'Bot' && comment.body.includes('MCP Security Scan Results') | |
| ); | |
| if (botComment) { | |
| // Update existing comment | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: botComment.id, | |
| body: comment | |
| }); | |
| console.log(`Updated existing comment #${botComment.id}`); | |
| } else { | |
| // Create new comment | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body: comment | |
| }); | |
| console.log('Created new comment'); | |
| } | |
| summary: | |
| needs: [discover-configs, mcp-security-scan, build-containers] | |
| runs-on: ubuntu-latest | |
| if: always() | |
| steps: | |
| - name: Build Summary | |
| run: | | |
| echo "## Dockyard Build Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Trigger**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Ref**: ${{ github.ref }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Total Configs**: ${{ needs.discover-configs.outputs.configs }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Changed Configs**: ${{ needs.discover-configs.outputs.changed-configs }}" >> $GITHUB_STEP_SUMMARY | |
| if [ "${{ needs.mcp-security-scan.result }}" == "success" ]; then | |
| echo "- **Security Scan**: ✅ All MCP servers passed security scanning" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.mcp-security-scan.result }}" == "failure" ]; then | |
| echo "- **Security Scan**: ❌ Some MCP servers have security issues" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.mcp-security-scan.result }}" == "skipped" ]; then | |
| echo "- **Security Scan**: ⏭️ Skipped" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [ "${{ needs.build-containers.result }}" == "success" ]; then | |
| echo "- **Build Status**: ✅ All changed containers built successfully" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Features**:" >> $GITHUB_STEP_SUMMARY | |
| echo " - 🏗️ Multi-architecture support (amd64, arm64)" >> $GITHUB_STEP_SUMMARY | |
| echo " - 📦 SBOM (Software Bill of Materials) included" >> $GITHUB_STEP_SUMMARY | |
| echo " - 🔐 Provenance attestation for supply chain security" >> $GITHUB_STEP_SUMMARY | |
| echo " - ✍️ Sigstore/Cosign signatures for image verification" >> $GITHUB_STEP_SUMMARY | |
| echo " - 🚀 GitHub Actions cache for faster builds" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-containers.result }}" == "failure" ]; then | |
| echo "- **Build Status**: ❌ Some containers failed to build" >> $GITHUB_STEP_SUMMARY | |
| elif [ "${{ needs.build-containers.result }}" == "skipped" ]; then | |
| echo "- **Build Status**: ⏭️ No configuration changes detected" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "- **Build Status**: ⚠️ Build status unknown" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| # Add efficiency note | |
| changed_count=$(echo '${{ needs.discover-configs.outputs.changed-configs }}' | jq length) | |
| total_count=$(echo '${{ needs.discover-configs.outputs.configs }}' | jq length) | |
| echo "- **Efficiency**: Built $changed_count out of $total_count configurations" >> $GITHUB_STEP_SUMMARY |