chore: git cache cleanup
This commit is contained in:
757
docs/plans/archive/archive_supply_chain_pr_implementation.md
Normal file
757
docs/plans/archive/archive_supply_chain_pr_implementation.md
Normal file
@@ -0,0 +1,757 @@
|
||||
# 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)
|
||||
|
||||
```yaml
|
||||
- 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/charon` → `wikid82/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)
|
||||
|
||||
```yaml
|
||||
- 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)
|
||||
|
||||
```yaml
|
||||
- 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:
|
||||
|
||||
```bash
|
||||
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):
|
||||
|
||||
```yaml
|
||||
- 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):
|
||||
|
||||
```yaml
|
||||
- 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):
|
||||
|
||||
```yaml
|
||||
- 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
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```yaml
|
||||
- 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**:
|
||||
|
||||
```yaml
|
||||
if: |
|
||||
(github.event_name != 'schedule' || github.ref == 'refs/heads/main') &&
|
||||
(github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success')
|
||||
```
|
||||
|
||||
**Updated**:
|
||||
|
||||
```yaml
|
||||
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
|
||||
|
||||
```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
|
||||
|
||||
```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**
|
||||
Reference in New Issue
Block a user