Publish Docker images to both Docker Hub (docker.io/wikid82/charon) and GitHub Container Registry (ghcr.io/wikid82/charon) for maximum reach. Add Docker Hub login with secret existence check for graceful fallback Update docker/metadata-action to generate tags for both registries Add Cosign keyless signing for both GHCR and Docker Hub images Attach SBOM to Docker Hub via cosign attach sbom Add Docker Hub signature verification to supply-chain-verify workflow Update README with Docker Hub badges and dual registry examples Update getting-started.md with both registry options Supply chain security maintained: identical tags, signatures, and SBOMs on both registries. PR images remain GHCR-only.
549 lines
25 KiB
YAML
549 lines
25 KiB
YAML
name: Docker Build, Publish & Test
|
|
|
|
# This workflow replaced .github/workflows/docker-publish.yml (deleted in commit f640524b on Dec 21, 2025)
|
|
# Enhancements over the previous workflow:
|
|
# - SBOM generation and attestation for supply chain security
|
|
# - CVE-2025-68156 verification for Caddy security patches
|
|
# - Enhanced PR handling with dedicated scanning
|
|
# - Improved workflow orchestration with supply-chain-verify.yml
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- development
|
|
- 'feature/**'
|
|
# Note: Tags are handled by release-goreleaser.yml to avoid duplicate builds
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- development
|
|
- 'feature/**'
|
|
workflow_dispatch:
|
|
workflow_call:
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
GHCR_REGISTRY: ghcr.io
|
|
DOCKERHUB_REGISTRY: docker.io
|
|
IMAGE_NAME: wikid82/charon
|
|
SYFT_VERSION: v1.17.0
|
|
GRYPE_VERSION: v0.85.0
|
|
|
|
jobs:
|
|
build-and-push:
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
security-events: write
|
|
id-token: write # Required for SBOM attestation
|
|
attestations: write # Required for SBOM attestation
|
|
|
|
outputs:
|
|
skip_build: ${{ steps.skip.outputs.skip_build }}
|
|
digest: ${{ steps.build-and-push.outputs.digest }}
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
|
|
- name: Normalize image name
|
|
run: |
|
|
IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')
|
|
echo "IMAGE_NAME=${IMAGE_NAME}" >> $GITHUB_ENV
|
|
- name: Determine skip condition
|
|
id: skip
|
|
env:
|
|
ACTOR: ${{ github.actor }}
|
|
EVENT: ${{ github.event_name }}
|
|
HEAD_MSG: ${{ github.event.head_commit.message }}
|
|
REF: ${{ github.ref }}
|
|
HEAD_REF: ${{ github.head_ref }}
|
|
run: |
|
|
should_skip=false
|
|
pr_title=""
|
|
if [ "$EVENT" = "pull_request" ]; then
|
|
pr_title=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH" 2>/dev/null || echo '')
|
|
fi
|
|
if [ "$ACTOR" = "renovate[bot]" ]; then should_skip=true; fi
|
|
if echo "$HEAD_MSG" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
|
if echo "$HEAD_MSG" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
|
if echo "$pr_title" | grep -Ei '^chore\(deps' >/dev/null 2>&1; then should_skip=true; fi
|
|
if echo "$pr_title" | grep -Ei '^chore:' >/dev/null 2>&1; then should_skip=true; fi
|
|
# Always build on feature branches to ensure artifacts for testing
|
|
# For PRs: github.ref is refs/pull/N/merge, so check github.head_ref instead
|
|
# For pushes: github.ref is refs/heads/branch-name
|
|
is_feature_push=false
|
|
if [[ "$REF" == refs/heads/feature/* ]]; then
|
|
should_skip=false
|
|
is_feature_push=true
|
|
echo "Force building on feature branch (push)"
|
|
elif [[ "$HEAD_REF" == feature/* ]]; then
|
|
should_skip=false
|
|
echo "Force building on feature branch (PR)"
|
|
fi
|
|
|
|
echo "skip_build=$should_skip" >> $GITHUB_OUTPUT
|
|
echo "is_feature_push=$is_feature_push" >> $GITHUB_OUTPUT
|
|
|
|
- name: Set up QEMU
|
|
if: steps.skip.outputs.skip_build != 'true'
|
|
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
|
- name: Set up Docker Buildx
|
|
if: steps.skip.outputs.skip_build != 'true'
|
|
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
|
- name: Resolve Debian base image digest
|
|
if: steps.skip.outputs.skip_build != 'true'
|
|
id: caddy
|
|
run: |
|
|
docker pull debian:trixie-slim
|
|
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' debian:trixie-slim)
|
|
echo "image=$DIGEST" >> $GITHUB_OUTPUT
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
|
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
with:
|
|
registry: ${{ env.GHCR_REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Log in to Docker Hub
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && secrets.DOCKERHUB_TOKEN != ''
|
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
with:
|
|
registry: docker.io
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- 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.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
tags: |
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
type=semver,pattern={{major}}
|
|
type=raw,value=latest,enable={{is_default_branch}}
|
|
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
|
|
type=ref,event=branch,enable=${{ startsWith(github.ref, 'refs/heads/feature/') }}
|
|
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' }}
|
|
flavor: |
|
|
latest=false
|
|
# For feature branch pushes: build single-platform so we can load locally for artifact
|
|
# For main/development pushes: build multi-platform for production
|
|
# For PRs: build single-platform and load locally
|
|
- 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' || steps.skip.outputs.is_feature_push == 'true') && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
load: ${{ github.event_name == 'pull_request' || steps.skip.outputs.is_feature_push == 'true' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
no-cache: true # Prevent false positive vulnerabilities from cached layers
|
|
pull: true # Always pull fresh base images to get latest security patches
|
|
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 }}
|
|
|
|
# Critical Fix: Use exact tag from metadata instead of manual reconstruction
|
|
# WHY: docker/build-push-action with load:true applies the exact tags from
|
|
# docker/metadata-action. Manual reconstruction can cause mismatches due to:
|
|
# - Case sensitivity variations (owner name normalization)
|
|
# - Tag format differences in Buildx internal behavior
|
|
# - Registry prefix inconsistencies
|
|
#
|
|
# SOLUTION: Extract the first tag from metadata output (which is the PR tag)
|
|
# and use it directly with docker save. This guarantees we reference the
|
|
# exact image that was loaded into the local Docker daemon.
|
|
#
|
|
# VALIDATION: Added defensive checks to fail fast with diagnostics if:
|
|
# 1. No tag found in metadata output
|
|
# 2. Image doesn't exist locally after build
|
|
# 3. Artifact creation fails
|
|
- name: Save Docker Image as Artifact
|
|
if: github.event_name == 'pull_request' || steps.skip.outputs.is_feature_push == 'true'
|
|
run: |
|
|
# Extract the first tag from metadata action (PR tag)
|
|
IMAGE_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n 1)
|
|
|
|
if [[ -z "${IMAGE_TAG}" ]]; then
|
|
echo "❌ ERROR: No image tag found in metadata output"
|
|
echo "Metadata tags output:"
|
|
echo "${{ steps.meta.outputs.tags }}"
|
|
exit 1
|
|
fi
|
|
|
|
echo "🔍 Detected image tag: ${IMAGE_TAG}"
|
|
|
|
# Verify the image exists locally
|
|
if ! docker image inspect "${IMAGE_TAG}" >/dev/null 2>&1; then
|
|
echo "❌ ERROR: Image ${IMAGE_TAG} not found locally"
|
|
echo "📋 Available images:"
|
|
docker images
|
|
exit 1
|
|
fi
|
|
|
|
# Save the image using the exact tag from metadata
|
|
echo "💾 Saving image: ${IMAGE_TAG}"
|
|
docker save "${IMAGE_TAG}" -o /tmp/charon-pr-image.tar
|
|
|
|
# Verify the artifact was created
|
|
echo "✅ Artifact created:"
|
|
ls -lh /tmp/charon-pr-image.tar
|
|
|
|
- name: Upload Image Artifact
|
|
if: github.event_name == 'pull_request' || steps.skip.outputs.is_feature_push == 'true'
|
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
|
with:
|
|
name: ${{ github.event_name == 'pull_request' && format('pr-image-{0}', github.event.pull_request.number) || 'push-image' }}
|
|
path: /tmp/charon-pr-image.tar
|
|
retention-days: 1 # Only needed for workflow duration
|
|
|
|
- name: Verify Caddy Security Patches (CVE-2025-68156)
|
|
if: steps.skip.outputs.skip_build != 'true'
|
|
timeout-minutes: 2
|
|
continue-on-error: true
|
|
run: |
|
|
echo "🔍 Verifying Caddy binary contains patched expr-lang/expr@v1.17.7..."
|
|
echo ""
|
|
|
|
# Determine the image reference based on event type
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}"
|
|
echo "Using PR image: $IMAGE_REF"
|
|
else
|
|
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}"
|
|
echo "Using digest: $IMAGE_REF"
|
|
fi
|
|
|
|
echo ""
|
|
echo "==> Caddy version:"
|
|
timeout 30s docker run --rm --pull=never $IMAGE_REF caddy version || echo "⚠️ Caddy version check timed out or failed"
|
|
|
|
echo ""
|
|
echo "==> Extracting Caddy binary for inspection..."
|
|
CONTAINER_ID=$(docker create --pull=never $IMAGE_REF)
|
|
docker cp ${CONTAINER_ID}:/usr/bin/caddy ./caddy_binary
|
|
docker rm ${CONTAINER_ID}
|
|
|
|
echo ""
|
|
echo "==> Checking if Go toolchain is available locally..."
|
|
if command -v go >/dev/null 2>&1; then
|
|
echo "✅ Go found locally, inspecting binary dependencies..."
|
|
go version -m ./caddy_binary > caddy_deps.txt
|
|
|
|
echo ""
|
|
echo "==> Searching for expr-lang/expr dependency:"
|
|
if grep -i "expr-lang/expr" caddy_deps.txt; then
|
|
EXPR_VERSION=$(grep "expr-lang/expr" caddy_deps.txt | awk '{print $3}')
|
|
echo ""
|
|
echo "✅ Found expr-lang/expr: $EXPR_VERSION"
|
|
|
|
# Check if version is v1.17.7 or higher (vulnerable version is v1.16.9)
|
|
if echo "$EXPR_VERSION" | grep -E "^v1\.(1[7-9]|[2-9][0-9])\.[0-9]+$" >/dev/null; then
|
|
echo "✅ PASS: expr-lang version $EXPR_VERSION is patched (>= v1.17.7)"
|
|
else
|
|
echo "⚠️ WARNING: expr-lang version $EXPR_VERSION may be vulnerable (< v1.17.7)"
|
|
echo "Expected: v1.17.7 or higher to mitigate CVE-2025-68156"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "⚠️ expr-lang/expr not found in binary dependencies"
|
|
echo "This could mean:"
|
|
echo " 1. The dependency was stripped/optimized out"
|
|
echo " 2. Caddy was built without the expression evaluator"
|
|
echo " 3. Binary inspection failed"
|
|
echo ""
|
|
echo "Displaying all dependencies for review:"
|
|
cat caddy_deps.txt
|
|
fi
|
|
else
|
|
echo "⚠️ Go toolchain not available in CI environment"
|
|
echo "Cannot inspect binary modules - skipping dependency verification"
|
|
echo "Note: Runtime image does not require Go as Caddy is a standalone binary"
|
|
fi
|
|
|
|
# Cleanup
|
|
rm -f ./caddy_binary caddy_deps.txt
|
|
|
|
echo ""
|
|
echo "==> Verification complete"
|
|
|
|
- name: Verify CrowdSec Security Patches (CVE-2025-68156)
|
|
if: success()
|
|
continue-on-error: true
|
|
run: |
|
|
echo "🔍 Verifying CrowdSec binaries contain patched expr-lang/expr@v1.17.7..."
|
|
echo ""
|
|
|
|
# Determine the image reference based on event type
|
|
if [ "${{ github.event_name }}" = "pull_request" ]; then
|
|
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:pr-${{ github.event.pull_request.number }}"
|
|
echo "Using PR image: $IMAGE_REF"
|
|
else
|
|
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}"
|
|
echo "Using digest: $IMAGE_REF"
|
|
fi
|
|
|
|
echo ""
|
|
echo "==> CrowdSec cscli version:"
|
|
timeout 30s docker run --rm --pull=never $IMAGE_REF cscli version || echo "⚠️ CrowdSec version check timed out or failed (may not be installed for this architecture)"
|
|
|
|
echo ""
|
|
echo "==> Extracting cscli binary for inspection..."
|
|
CONTAINER_ID=$(docker create --pull=never $IMAGE_REF)
|
|
docker cp ${CONTAINER_ID}:/usr/local/bin/cscli ./cscli_binary 2>/dev/null || {
|
|
echo "⚠️ cscli binary not found - CrowdSec may not be available for this architecture"
|
|
docker rm ${CONTAINER_ID}
|
|
exit 0
|
|
}
|
|
docker rm ${CONTAINER_ID}
|
|
|
|
echo ""
|
|
echo "==> Checking if Go toolchain is available locally..."
|
|
if command -v go >/dev/null 2>&1; then
|
|
echo "✅ Go found locally, inspecting binary dependencies..."
|
|
go version -m ./cscli_binary > cscli_deps.txt
|
|
|
|
echo ""
|
|
echo "==> Searching for expr-lang/expr dependency:"
|
|
if grep -i "expr-lang/expr" cscli_deps.txt; then
|
|
EXPR_VERSION=$(grep "expr-lang/expr" cscli_deps.txt | awk '{print $3}')
|
|
echo ""
|
|
echo "✅ Found expr-lang/expr: $EXPR_VERSION"
|
|
|
|
# Check if version is v1.17.7 or higher (vulnerable version is v1.17.2)
|
|
if echo "$EXPR_VERSION" | grep -E "^v1\.(1[7-9]|[2-9][0-9])\.[7-9][0-9]*$|^v1\.17\.([7-9]|[1-9][0-9]+)$" >/dev/null; then
|
|
echo "✅ PASS: expr-lang version $EXPR_VERSION is patched (>= v1.17.7)"
|
|
else
|
|
echo "❌ FAIL: expr-lang version $EXPR_VERSION is vulnerable (< v1.17.7)"
|
|
echo "⚠️ WARNING: expr-lang version $EXPR_VERSION may be vulnerable (< v1.17.7)"
|
|
echo "Expected: v1.17.7 or higher to mitigate CVE-2025-68156"
|
|
exit 1
|
|
fi
|
|
else
|
|
echo "⚠️ expr-lang/expr not found in binary dependencies"
|
|
echo "This could mean:"
|
|
echo " 1. The dependency was stripped/optimized out"
|
|
echo " 2. CrowdSec was built without the expression evaluator"
|
|
echo " 3. Binary inspection failed"
|
|
echo ""
|
|
echo "Displaying all dependencies for review:"
|
|
cat cscli_deps.txt
|
|
fi
|
|
else
|
|
echo "⚠️ Go toolchain not available in CI environment"
|
|
echo "Cannot inspect binary modules - skipping dependency verification"
|
|
echo "Note: Runtime image does not require Go as CrowdSec is a standalone binary"
|
|
fi
|
|
|
|
# Cleanup
|
|
rm -f ./cscli_binary cscli_deps.txt
|
|
|
|
echo ""
|
|
echo "==> CrowdSec verification complete"
|
|
|
|
- name: Run Trivy scan (table output)
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
|
with:
|
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
|
format: 'table'
|
|
severity: 'CRITICAL,HIGH'
|
|
exit-code: '0'
|
|
continue-on-error: true
|
|
|
|
- name: Run Trivy vulnerability scanner (SARIF)
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
id: trivy
|
|
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # 0.33.1
|
|
with:
|
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
|
format: 'sarif'
|
|
output: 'trivy-results.sarif'
|
|
severity: 'CRITICAL,HIGH'
|
|
continue-on-error: true
|
|
|
|
- name: Check Trivy SARIF exists
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
id: trivy-check
|
|
run: |
|
|
if [ -f trivy-results.sarif ]; then
|
|
echo "exists=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "exists=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Upload Trivy results
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
|
|
uses: github/codeql-action/upload-sarif@19b2f06db2b6f5108140aeb04014ef02b648f789 # v4.31.11
|
|
with:
|
|
sarif_file: 'trivy-results.sarif'
|
|
token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Generate SBOM (Software Bill of Materials) for supply chain security
|
|
# Only for production builds (main/development) - feature branches use downstream supply-chain-pr.yml
|
|
- name: Generate SBOM
|
|
uses: anchore/sbom-action@62ad5284b8ced813296287a0b63906cb364b73ee # v0.22.0
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
with:
|
|
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
|
format: cyclonedx-json
|
|
output-file: sbom.cyclonedx.json
|
|
|
|
# Create verifiable attestation for the SBOM
|
|
- name: Attest SBOM
|
|
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
with:
|
|
subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
subject-digest: ${{ steps.build-and-push.outputs.digest }}
|
|
sbom-path: sbom.cyclonedx.json
|
|
push-to-registry: true
|
|
|
|
# Install Cosign for keyless signing
|
|
- name: Install Cosign
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
uses: sigstore/cosign-installer@d7d6bc7722e3daa8354c50bcb52f4837da5e9b6a # v3.8.1
|
|
|
|
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
|
- name: Sign GHCR Image
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
|
run: |
|
|
echo "Signing GHCR image with keyless signing..."
|
|
cosign sign --yes ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
|
echo "✅ GHCR image signed successfully"
|
|
|
|
# Sign Docker Hub image with keyless signing (Sigstore/Fulcio)
|
|
- name: Sign Docker Hub Image
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true' && secrets.DOCKERHUB_TOKEN != ''
|
|
run: |
|
|
echo "Signing Docker Hub image with keyless signing..."
|
|
cosign sign --yes ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
|
echo "✅ Docker Hub image signed successfully"
|
|
|
|
# Attach SBOM to Docker Hub image
|
|
- name: Attach SBOM to Docker Hub
|
|
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true' && secrets.DOCKERHUB_TOKEN != ''
|
|
run: |
|
|
echo "Attaching SBOM to Docker Hub image..."
|
|
cosign attach sbom --sbom sbom.cyclonedx.json ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
|
echo "✅ SBOM attached to Docker Hub image"
|
|
|
|
- name: Create summary
|
|
if: steps.skip.outputs.skip_build != 'true'
|
|
run: |
|
|
echo "## 🎉 Docker Image Built Successfully!" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "### 📦 Image Details" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **GHCR**: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Docker Hub**: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Tags**: " >> $GITHUB_STEP_SUMMARY
|
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
|
|
|
test-image:
|
|
name: Test Docker Image
|
|
needs: build-and-push
|
|
runs-on: ubuntu-latest
|
|
if: needs.build-and-push.outputs.skip_build != 'true' && github.event_name != 'pull_request'
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
|
|
|
- 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
|
|
- name: Determine image tag
|
|
id: tag
|
|
run: |
|
|
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
|
echo "tag=latest" >> $GITHUB_OUTPUT
|
|
elif [[ "${{ github.ref }}" == "refs/heads/development" ]]; then
|
|
echo "tag=dev" >> $GITHUB_OUTPUT
|
|
elif [[ "${{ github.ref }}" == refs/tags/v* ]]; then
|
|
echo "tag=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "tag=sha-$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- name: Log in to GitHub Container Registry
|
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Pull Docker image
|
|
run: docker pull ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
|
- name: Create Docker Network
|
|
run: docker network create charon-test-net
|
|
|
|
- name: Run Upstream Service (whoami)
|
|
run: |
|
|
docker run -d \
|
|
--name whoami \
|
|
--network charon-test-net \
|
|
traefik/whoami
|
|
|
|
- name: Run Charon Container
|
|
timeout-minutes: 3
|
|
run: |
|
|
docker run -d \
|
|
--name test-container \
|
|
--network charon-test-net \
|
|
-p 8080:8080 \
|
|
-p 80:80 \
|
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
|
|
|
|
# Wait for container to be healthy (max 3 minutes - Debian needs more startup time)
|
|
echo "Waiting for container to start..."
|
|
timeout 180s bash -c 'until docker exec test-container curl -sf http://localhost:8080/api/v1/health 2>/dev/null | grep -q "status"; do echo "Waiting..."; sleep 2; done' || {
|
|
echo "❌ Container failed to become healthy"
|
|
docker logs test-container
|
|
exit 1
|
|
}
|
|
echo "✅ Container is healthy"
|
|
- name: Run Integration Test
|
|
timeout-minutes: 5
|
|
run: ./scripts/integration-test.sh
|
|
|
|
- name: Check container logs
|
|
if: always()
|
|
run: docker logs test-container
|
|
|
|
- name: Stop container
|
|
if: always()
|
|
run: |
|
|
docker stop test-container whoami || true
|
|
docker rm test-container whoami || true
|
|
docker network rm charon-test-net || true
|
|
|
|
- name: Create test summary
|
|
if: always()
|
|
run: |
|
|
echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Image**: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
|
|
echo "- **Integration Test**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY
|