Files
Charon/docs/plans/archive_supply_chain_pr_implementation.md

29 KiB

CI Docker Build Failure Analysis & Fix Plan

Issue: Docker Build workflow failing on PR builds during image artifact save Workflow: .github/workflows/docker-build.yml Error: Error response from daemon: reference does not exist Date: 2026-01-12 Status: Analysis Complete - Ready for Implementation


Executive Summary

The docker-build.yml workflow is failing at the "Save Docker Image as Artifact" step (line 135-142) for PR builds. The root cause is a mismatch between the image name/tag format used by docker/build-push-action with load: true and the image reference used in the docker save command.

Impact: All PR builds fail at the artifact save step, preventing the verify-supply-chain-pr job from running.

Fix Complexity: Low - Single line change to use correct image reference format.


Root Cause Analysis

The Failing Step (Lines 135-142)

- name: Save Docker Image as Artifact
  if: github.event_name == 'pull_request'
  run: |
    IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]')
    docker save ghcr.io/${IMAGE_NAME}:pr-${{ github.event.pull_request.number }} -o /tmp/charon-pr-image.tar
    ls -lh /tmp/charon-pr-image.tar

Line 140: Normalizes the image name to lowercase (e.g., Wikid82/charonwikid82/charon) Line 141: Attempts to save the image with the full registry path: ghcr.io/wikid82/charon:pr-123

The Build Step (Lines 111-123)

- name: Build and push Docker image
  if: steps.skip.outputs.skip_build != 'true'
  id: build-and-push
  uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6
  with:
    context: .
    platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
    push: ${{ github.event_name != 'pull_request' }}
    load: ${{ github.event_name == 'pull_request' }}
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
    no-cache: true
    pull: true
    build-args: |
      VERSION=${{ steps.meta.outputs.version }}
      BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}
      VCS_REF=${{ github.sha }}
      CADDY_IMAGE=${{ steps.caddy.outputs.image }}

Key Parameters for PR Builds:

  • push: false (line 117)
  • load: true (line 118) - This loads the image into the local Docker daemon
  • tags: ${{ steps.meta.outputs.tags }} (line 119)

The Metadata Step (Lines 105-113)

- name: Extract metadata (tags, labels)
  if: steps.skip.outputs.skip_build != 'true'
  id: meta
  uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
  with:
    images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
    tags: |
      type=raw,value=latest,enable={{is_default_branch}}
      type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
      type=raw,value=beta,enable=${{ github.ref == 'refs/heads/feature/beta-release' }}
      type=raw,value=pr-${{ github.event.pull_request.number }},enable=${{ github.event_name == 'pull_request' }}
      type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}

For PR builds, only this tag is enabled (line 111):

  • type=raw,value=pr-${{ github.event.pull_request.number }}

This generates the tag: ghcr.io/${IMAGE_NAME}:pr-${PR_NUMBER}

Example: For PR #123 with owner "Wikid82", the tag would be:

  • Input to metadata-action: ghcr.io/wikid82/charon (already normalized at line 56-57)
  • Generated tag: ghcr.io/wikid82/charon:pr-123

The Critical Issue

When docker/build-push-action uses load: true, the behavior depends on the Docker Buildx backend:

  1. Expected Behavior: Image is loaded into local Docker daemon with the tags specified in tags:
  2. Actual Behavior: The image might be loaded with tags but without guaranteed registry prefix OR the tags might not all be applied to the local image

Evidence from Docker Build-Push-Action Documentation:

When using load: true, the image is loaded into the local Docker daemon. However, multi-platform builds cannot be loaded (they require push: true), so only single-platform builds work with load: true.

The Problem: The docker save command at line 141 references:

ghcr.io/${IMAGE_NAME}:pr-${{ github.event.pull_request.number }}

But the image loaded locally might be tagged as:

  • ghcr.io/wikid82/charon:pr-123 (correct - what we expect)
  • wikid82/charon:pr-123 (missing registry prefix)
  • Or the image might exist but with a different tag format

Why This Matters

The docker save command requires an exact match of the image name and tag as it exists in the local Docker daemon. If the image is loaded as wikid82/charon:pr-123 but we're trying to save ghcr.io/wikid82/charon:pr-123, Docker will throw:

Error response from daemon: reference does not exist

This is exactly the error we're seeing.

Job Dependencies Analysis

Looking at the complete workflow structure:

build-and-push (lines 34-234)
├── Outputs: skip_build, digest
├── Steps include:
│   ├── Build image (load=true for PRs)
│   ├── Save image artifact (FAILS HERE) ❌
│   └── Upload artifact (never reached)
│
test-image (lines 354-463)
├── needs: build-and-push
├── if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request'
└── (Not relevant for PRs)
│
trivy-pr-app-only (lines 465-493)
├── if: github.event_name == 'pull_request'
└── (Independent - builds its own image)
│
verify-supply-chain-pr (lines 495-722)
├── needs: build-and-push
├── if: github.event_name == 'pull_request' && needs.build-and-push.result == 'success'
├── Steps include:
│   ├── Download artifact (NEVER RUNS - artifact doesn't exist) ❌
│   ├── Load image
│   └── Scan image
└── (Currently skipped due to build-and-push failure)
│
verify-supply-chain-pr-skipped (lines 724-754)
├── needs: build-and-push
└── if: github.event_name == 'pull_request' && needs.build-and-push.outputs.skip_build == 'true'

Dependency Chain Impact:

  1. build-and-push fails at line 141 (docker save)
  2. Artifact is never uploaded (lines 144-150)
  3. verify-supply-chain-pr cannot download artifact (line 517) - job is marked as "skipped" or "failed"
  4. Supply chain verification never runs for PRs

Verification of the Issue

Looking at similar patterns in the file that work correctly:

Line 376 (in test-image job):

- name: Normalize image name
  run: |
    raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
    IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')
    echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV

This job doesn't load images locally - it pulls from the registry (line 395):

- name: Pull Docker image
  run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}

So this pattern works because it's pulling from a pushed image, not a locally loaded one.

Line 516 (in verify-supply-chain-pr job):

- name: Normalize image name
  run: |
    IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]')
    echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV

This step expects to load the image from an artifact (lines 511-520), so it doesn't directly reference a registry image. The image is loaded from the tar file uploaded by build-and-push.

Key Difference: The verify-supply-chain-pr job expects the artifact to exist, but since build-and-push fails at the docker save step, the artifact is never created.


Technical Design

Workflow-Level Configuration

Tool Versions (extracted as environment variables):

  • SYFT_VERSION: v1.17.0
  • GRYPE_VERSION: v0.85.0

These will be defined at the workflow level to ensure consistency and easier updates.

Job Definitions

Job 1: Image Artifact Upload (modification to existing build-and-push job) Trigger: Only for pull_request events Purpose: Save and upload the built Docker image as an artifact

Job 2: verify-supply-chain-pr Trigger: Only for pull_request events Dependency: needs: build-and-push Purpose: Download image artifact, perform SBOM generation and vulnerability scanning Skip Conditions:

  • If build-and-push output skip_build == 'true'
  • If build-and-push did not succeed

Job 3: verify-supply-chain-pr-skipped Trigger: Only for pull_request events Dependency: needs: build-and-push Purpose: Provide user feedback when build is skipped Run Condition: If build-and-push output skip_build == 'true'

Key Technical Decisions

Decision 1: Image Sharing Strategy

Chosen Approach: Save image as tar archive and share via GitHub Actions artifacts Why:

  • Jobs run in isolated environments; local Docker images are not shared by default
  • Artifacts provide reliable cross-job data sharing
  • Avoids registry push for PR builds (maintains current security model)
  • 1-day retention minimizes storage costs Alternative Considered: Push to registry with ephemeral tags (rejected: requires registry permissions, security concerns, cleanup complexity)

Decision 2: Tool Versions

Syft: v1.17.0 (matches existing security-verify-sbom skill) Grype: v0.85.0 (matches existing security-verify-sbom skill) Why: Consistent with existing workflows, tested versions

Decision 3: Failure Behavior

Critical Vulnerabilities: Fail the job (exit code 1) High Vulnerabilities: Warn but don't fail Why: Aligns with project standards (see security-verify-sbom.SKILL.md)

Decision 4: SARIF Category Strategy

Category Format: supply-chain-pr-${{ github.event.pull_request.number }}-${{ github.sha }} Why: Including SHA prevents conflicts when multiple commits are pushed to the same PR concurrently Without SHA: Concurrent uploads to the same category would overwrite each other

Decision 5: Null Safety in Outputs

Approach: Add explicit null checks and fallback values for all step outputs Why:

  • Step outputs may be undefined if steps are skipped or fail
  • Prevents workflow failures in reporting steps
  • Ensures graceful degradation of user feedback

Decision 6: Workflow Conflict Resolution

Issue: supply-chain-verify.yml currently handles PR workflow_run events, creating duplicate verification Solution: Update supply-chain-verify.yml to exclude PR builds from workflow_run triggers Why: Inline verification in docker-build.yml provides faster feedback; workflow_run is unnecessary for PRs


Implementation Steps

Step 1: Update Workflow Environment Variables

File: .github/workflows/docker-build.yml Location: After line 22 (after existing env: section start) Action: Add tool version variables

env:
  # ... existing variables ...
  SYFT_VERSION: v1.17.0
  GRYPE_VERSION: v0.85.0

Step 2: Add Artifact Upload to build-and-push Job

File: .github/workflows/docker-build.yml Location: After the "Build and push Docker image" step (after line 113) Action: Insert two new steps for image artifact handling

    - name: Save Docker Image as Artifact
      if: github.event_name == 'pull_request'
      run: |
        IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]')
        docker save ghcr.io/${IMAGE_NAME}:pr-${{ github.event.pull_request.number }} -o /tmp/charon-pr-image.tar
        ls -lh /tmp/charon-pr-image.tar

    - name: Upload Image Artifact
      if: github.event_name == 'pull_request'
      uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
      with:
        name: pr-image-${{ github.event.pull_request.number }}
        path: /tmp/charon-pr-image.tar
        retention-days: 1

Rationale: These steps execute only for PRs and share the built image with downstream jobs.

Step 3: Add verify-supply-chain-pr Job

File: .github/workflows/docker-build.yml Location: After line 229 (end of trivy-pr-app-only job) Action: Insert complete job definition

See complete YAML in Appendix A.

Step 4: Add verify-supply-chain-pr-skipped Job

File: .github/workflows/docker-build.yml Location: After the verify-supply-chain-pr job Action: Insert complete job definition

See complete YAML in Appendix B.

Step 5: Update supply-chain-verify.yml to Avoid PR Conflicts

File: .github/workflows/supply-chain-verify.yml Location: Update the verify-sbom job condition (around line 68) Current:

if: |
  (github.event_name != 'schedule' || github.ref == 'refs/heads/main') &&
  (github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success')

Updated:

if: |
  (github.event_name != 'schedule' || github.ref == 'refs/heads/main') &&
  (github.event_name != 'workflow_run' ||
    (github.event.workflow_run.conclusion == 'success' &&
     github.event.workflow_run.event != 'pull_request'))

Rationale: Prevents duplicate supply chain verification for PRs. The inline job in docker-build.yml now handles PR verification.


Generate:

  • SBOM file (CycloneDX JSON)
  • Vulnerability scan results (JSON)
  • GitHub SARIF report (for Security tab integration)

Upload: All as workflow artifacts with 30-day retention


Detailed Implementation

This implementation includes 3 main components:

  1. Workflow-level environment variables for tool versions
  2. Modifications to build-and-push job to upload image artifact
  3. Two new jobs: verify-supply-chain-pr (main verification) and verify-supply-chain-pr-skipped (feedback)
  4. Update to supply-chain-verify.yml to prevent duplicate verification

See complete YAML job definitions in Appendix A and B.

Insertion Instructions

Location in docker-build.yml:

  • Environment variables: After line 22
  • Image artifact upload: After line 113 (in build-and-push job)
  • New jobs: After line 229 (end of trivy-pr-app-only job)

No modifications needed to other existing jobs. The build-and-push job already outputs everything we need.


Testing Plan

Phase 1: Basic Validation

  1. Create test PR on feature/beta-release
  2. Verify artifact upload/download works correctly
  3. Verify image loads successfully in verification job
  4. Check image reference is correct (no "image not found")
  5. Validate SBOM generation (component count >0)
  6. Validate vulnerability scanning
  7. Check PR comment is posted with status/table (including commit SHA)
  8. Verify SARIF upload to Security tab with unique category
  9. Verify job summary is created with all null checks working

Phase 2: Critical Fixes Validation

  1. Image Access: Verify artifact contains image tar, verify download succeeds, verify docker load works
  2. Conditionals: Test that job skips when build-and-push fails or is skipped
  3. SARIF Category: Push multiple commits to same PR, verify no SARIF conflicts in Security tab
  4. Null Checks: Force step failure, verify job summary and PR comment still generate gracefully
  5. Workflow Conflict: Verify supply-chain-verify.yml does NOT trigger for PR builds
  6. Skipped Feedback: Create chore commit, verify skipped feedback job posts comment

Phase 3: Edge Cases

  1. Test with intentionally vulnerable dependency
  2. Test with build skip (chore commit)
  3. Test concurrent PRs (verify artifacts don't collide)
  4. Test rapid successive commits to same PR

Phase 4: Performance Validation

  1. Measure baseline PR build time (without feature)
  2. Measure new PR build time (with feature)
  3. Verify increase is within expected 50-60% range
  4. Monitor artifact storage usage

Phase 5: Rollback

If issues arise, revert the commit. No impact on main/tag builds.


Success Criteria

Functional

  • Artifacts are uploaded/downloaded correctly for all PR builds
  • Image loads successfully in verification job
  • Job runs for all PR builds (when not skipped)
  • Job correctly skips when build-and-push fails or is skipped
  • Generates valid SBOM
  • Performs vulnerability scan
  • Uploads artifacts with appropriate retention
  • Comments on PR with commit SHA and vulnerability table
  • Fails on critical vulnerabilities
  • Uploads SARIF with unique category (no conflicts)
  • Skipped build feedback is posted when build is skipped
  • No duplicate verification from supply-chain-verify.yml

Performance

  • ⏱️ Completes in <15 minutes
  • 📦 Artifact size <250MB
  • 📈 Total PR build time increase: 50-60% (acceptable)

Reliability

  • 🔒 All null checks in place (no undefined variable errors)
  • 🔄 Handles concurrent PR commits without conflicts
  • Graceful degradation if steps fail

Appendix A: Complete verify-supply-chain-pr Job YAML

  # ============================================================================
  # Supply Chain Verification for PR Builds
  # ============================================================================
  # This job performs SBOM generation and vulnerability scanning for PR builds.
  # It depends on the build-and-push job completing successfully and uses the
  # Docker image artifact uploaded by that job.
  #
  # Dependency Chain: build-and-push (builds & uploads) → verify-supply-chain-pr (downloads & scans)
  # ============================================================================
  verify-supply-chain-pr:
    name: Supply Chain Verification (PR)
    needs: build-and-push
    runs-on: ubuntu-latest
    timeout-minutes: 15
    # Critical Fix #2: Enhanced conditional with result check
    if: |
      github.event_name == 'pull_request' &&
      needs.build-and-push.outputs.skip_build != 'true' &&
      needs.build-and-push.result == 'success'
    permissions:
      contents: read
      pull-requests: write
      security-events: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6

      # Critical Fix #1: Download image artifact
      - name: Download Image Artifact
        uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
        with:
          name: pr-image-${{ github.event.pull_request.number }}

      # Critical Fix #1: Load Docker image
      - name: Load Docker Image
        run: |
          docker load -i charon-pr-image.tar
          docker images
          echo "✅ Image loaded successfully"

      - name: Normalize image name
        run: |
          IMAGE_NAME=$(echo "${{ github.repository_owner }}/charon" | tr '[:upper:]' '[:lower:]')
          echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV

      - name: Set PR image reference
        id: image
        run: |
          IMAGE_REF="ghcr.io/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}"
          echo "ref=${IMAGE_REF}" >> $GITHUB_OUTPUT
          echo "📦 Will verify: ${IMAGE_REF}"

      - name: Install Verification Tools
        run: |
          # Use workflow-level environment variables for versions
          curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin ${{ env.SYFT_VERSION }}
          curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin ${{ env.GRYPE_VERSION }}
          syft version
          grype version

      - name: Generate SBOM
        id: sbom
        run: |
          echo "🔍 Generating SBOM for ${{ steps.image.outputs.ref }}..."
          if ! syft ${{ steps.image.outputs.ref }} -o cyclonedx-json > sbom-pr.cyclonedx.json; then
            echo "❌ SBOM generation failed"
            exit 1
          fi
          COMPONENT_COUNT=$(jq '.components | length' sbom-pr.cyclonedx.json 2>/dev/null || echo "0")
          echo "📦 SBOM contains ${COMPONENT_COUNT} components"
          if [[ ${COMPONENT_COUNT} -eq 0 ]]; then
            echo "⚠️ WARNING: SBOM contains no components"
            exit 1
          fi
          echo "component_count=${COMPONENT_COUNT}" >> $GITHUB_OUTPUT

      - name: Scan for Vulnerabilities
        id: scan
        run: |
          echo "🔍 Scanning for vulnerabilities..."
          grype db update
          if ! grype sbom:./sbom-pr.cyclonedx.json --output json --file vuln-scan.json; then
            echo "❌ Vulnerability scan failed"
            exit 1
          fi
          echo ""
          echo "=== Vulnerability Summary ==="
          grype sbom:./sbom-pr.cyclonedx.json --output table || true
          CRITICAL=$(jq '[.matches[] | select(.vulnerability.severity == "Critical")] | length' vuln-scan.json 2>/dev/null || echo "0")
          HIGH=$(jq '[.matches[] | select(.vulnerability.severity == "High")] | length' vuln-scan.json 2>/dev/null || echo "0")
          MEDIUM=$(jq '[.matches[] | select(.vulnerability.severity == "Medium")] | length' vuln-scan.json 2>/dev/null || echo "0")
          LOW=$(jq '[.matches[] | select(.vulnerability.severity == "Low")] | length' vuln-scan.json 2>/dev/null || echo "0")
          echo ""
          echo "📊 Vulnerability Breakdown:"
          echo "  🔴 Critical: ${CRITICAL}"
          echo "  🟠 High: ${HIGH}"
          echo "  🟡 Medium: ${MEDIUM}"
          echo "  🟢 Low: ${LOW}"
          echo "critical=${CRITICAL}" >> $GITHUB_OUTPUT
          echo "high=${HIGH}" >> $GITHUB_OUTPUT
          echo "medium=${MEDIUM}" >> $GITHUB_OUTPUT
          echo "low=${LOW}" >> $GITHUB_OUTPUT
          if [[ ${CRITICAL} -gt 0 ]]; then
            echo "::error::${CRITICAL} CRITICAL vulnerabilities found - BLOCKING"
          fi
          if [[ ${HIGH} -gt 0 ]]; then
            echo "::warning::${HIGH} HIGH vulnerabilities found"
          fi

      - name: Generate SARIF Report
        if: always()
        run: |
          echo "📋 Generating SARIF report..."
          grype sbom:./sbom-pr.cyclonedx.json --output sarif --file grype-results.sarif || true

      # Critical Fix #3: SARIF category includes SHA to prevent conflicts
      - name: Upload SARIF to GitHub Security
        if: always()
        uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
        with:
          sarif_file: grype-results.sarif
          category: supply-chain-pr-${{ github.event.pull_request.number }}-${{ github.sha }}
        continue-on-error: true

      - name: Upload Artifacts
        if: always()
        uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
        with:
          name: supply-chain-pr-${{ github.event.pull_request.number }}
          path: |
            sbom-pr.cyclonedx.json
            vuln-scan.json
            grype-results.sarif
          retention-days: 30

      # Critical Fix #4: Null checks in PR comment
      - name: Comment on PR
        if: always()
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
        with:
          script: |
            const critical = '${{ steps.scan.outputs.critical }}' || '0';
            const high = '${{ steps.scan.outputs.high }}' || '0';
            const medium = '${{ steps.scan.outputs.medium }}' || '0';
            const low = '${{ steps.scan.outputs.low }}' || '0';
            const components = '${{ steps.sbom.outputs.component_count }}' || 'N/A';
            const commitSha = '${{ github.sha }}'.substring(0, 7);

            let status = '✅ **PASSED**';
            let statusEmoji = '✅';

            if (parseInt(critical) > 0) {
              status = '❌ **BLOCKED** - Critical vulnerabilities found';
              statusEmoji = '❌';
            } else if (parseInt(high) > 0) {
              status = '⚠️ **WARNING** - High vulnerabilities found';
              statusEmoji = '⚠️';
            }

            const body = `## ${statusEmoji} Supply Chain Verification (PR Build)

            **Status**: ${status}
            **Commit**: \`${commitSha}\`
            **Image**: \`${{ steps.image.outputs.ref }}\`
            **Components Scanned**: ${components}

            ### 📊 Vulnerability Summary

            | Severity | Count |
            |----------|-------|
            | 🔴 Critical | ${critical} |
            | 🟠 High | ${high} |
            | 🟡 Medium | ${medium} |
            | 🟢 Low | ${low} |

            ${parseInt(critical) > 0 ? '### ❌ Critical Vulnerabilities Detected\n\n**Action Required**: This PR cannot be merged until critical vulnerabilities are resolved.\n\n' : ''}
            ${parseInt(high) > 0 ? '### ⚠️ High Vulnerabilities Detected\n\n**Recommendation**: Review and address high-severity vulnerabilities before merging.\n\n' : ''}
            📋 [View Full Report](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})
            📦 [Download Artifacts](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}#artifacts)
            `;

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body
            });

      - name: Fail on Critical Vulnerabilities
        if: steps.scan.outputs.critical != '0'
        run: |
          echo "❌ CRITICAL: ${{ steps.scan.outputs.critical }} critical vulnerabilities found"
          echo "This PR is blocked from merging until critical vulnerabilities are resolved."
          exit 1

      # Critical Fix #4: Null checks in job summary
      - name: Create Job Summary
        if: always()
        run: |
          # Use default values if outputs are not set
          COMPONENT_COUNT="${{ steps.sbom.outputs.component_count }}"
          CRITICAL="${{ steps.scan.outputs.critical }}"
          HIGH="${{ steps.scan.outputs.high }}"
          MEDIUM="${{ steps.scan.outputs.medium }}"
          LOW="${{ steps.scan.outputs.low }}"

          # Apply defaults
          COMPONENT_COUNT="${COMPONENT_COUNT:-N/A}"
          CRITICAL="${CRITICAL:-0}"
          HIGH="${HIGH:-0}"
          MEDIUM="${MEDIUM:-0}"
          LOW="${LOW:-0}"

          echo "## 🔒 Supply Chain Verification - PR #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "**Image**: \`${{ steps.image.outputs.ref }}\`" >> $GITHUB_STEP_SUMMARY
          echo "**Components**: ${COMPONENT_COUNT}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### Vulnerability Breakdown" >> $GITHUB_STEP_SUMMARY
          echo "- 🔴 Critical: ${CRITICAL}" >> $GITHUB_STEP_SUMMARY
          echo "- 🟠 High: ${HIGH}" >> $GITHUB_STEP_SUMMARY
          echo "- 🟡 Medium: ${MEDIUM}" >> $GITHUB_STEP_SUMMARY
          echo "- 🟢 Low: ${LOW}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [[ ${CRITICAL} -gt 0 ]]; then
            echo "❌ **BLOCKED**: Critical vulnerabilities must be resolved" >> $GITHUB_STEP_SUMMARY
          elif [[ ${HIGH} -gt 0 ]]; then
            echo "⚠️ **WARNING**: High vulnerabilities detected" >> $GITHUB_STEP_SUMMARY
          else
            echo "✅ **PASSED**: No critical or high vulnerabilities" >> $GITHUB_STEP_SUMMARY
          fi

Appendix B: verify-supply-chain-pr-skipped Job YAML

  # ============================================================================
  # Supply Chain Verification - Skipped Feedback
  # ============================================================================
  # This job provides user feedback when the build is skipped (e.g., chore commits).
  # Critical Fix #7: User feedback for skipped builds
  # ============================================================================
  verify-supply-chain-pr-skipped:
    name: Supply Chain Verification (Skipped)
    needs: build-and-push
    runs-on: ubuntu-latest
    if: |
      github.event_name == 'pull_request' &&
      needs.build-and-push.outputs.skip_build == 'true'
    permissions:
      pull-requests: write

    steps:
      - name: Comment on PR - Build Skipped
        uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
        with:
          script: |
            const commitSha = '${{ github.sha }}'.substring(0, 7);
            const body = `## ⏭️ Supply Chain Verification (Skipped)

            **Commit**: \`${commitSha}\`
            **Reason**: Build was skipped (likely a documentation-only or chore commit)

            Supply chain verification is not performed for skipped builds. If this commit should trigger a build, ensure it includes changes to application code or dependencies.
            `;

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: body
            });

END OF IMPLEMENTATION PLAN