Files
Charon/.github/workflows/docker-build.yml
2025-11-19 16:47:27 +00:00

275 lines
12 KiB
YAML

name: Build and Push Docker Images
on:
push:
branches:
- main # Pushes to main → tags as "latest"
- development # Pushes to development → tags as "dev"
tags:
- 'v*.*.*' # Version tags (v1.0.0, v1.2.3, etc.) → tags as version number
workflow_dispatch: # Allows manual trigger from GitHub UI
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/${{ github.event.repository.name }}
jobs:
build-and-push:
name: Build and Push Docker Image
runs-on: ubuntu-latest
concurrency:
group: docker-build-${{ github.ref }}
cancel-in-progress: true
outputs:
skip_build: ${{ steps.skip.outputs.skip_build }}
permissions:
contents: read
packages: write
security-events: write
steps:
# Step 1: Download the code
- name: 📥 Checkout code
uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4
# Normalize IMAGE_NAME to lowercase to satisfy container registry format
- name: 🔤 Normalize image name
run: |
raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
- name: 🧪 Determine skip condition
id: skip
run: |
should_skip=false
actor='${{ github.actor }}'
event='${{ github.event_name }}'
head_msg='${{ github.event.head_commit.message }}'
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
echo "skip_build=$should_skip" >> $GITHUB_OUTPUT
if [ "$should_skip" = true ]; then
echo "Skipping heavy docker build for actor=$actor event=$event (message/title matched)"
else
echo "Proceeding with full docker build"
fi
# Step 2: Set up QEMU for multi-platform builds (ARM, AMD64, etc.)
- name: 🔧 Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
# Step 3: Set up Docker Buildx (advanced Docker builder)
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3
# Resolve immutable digest for Caddy base
- name: 📦 Resolve Caddy base digest
id: caddy
run: |
docker pull caddy:2-alpine
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' caddy:2-alpine)
echo "image=$DIGEST" >> $GITHUB_OUTPUT
# Step 4: Log in to GitHub Container Registry
- name: 🔐 Log in to GitHub Container Registry
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.PROJECT_TOKEN }}
# Step 5: Figure out what tags to use
- name: 🏷️ Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Tag "latest" for main branch
type=raw,value=latest,enable={{is_default_branch}}
# Tag "dev" for development branch
type=raw,value=dev,enable=${{ github.ref == 'refs/heads/development' }}
# Tag version numbers from git tags (v1.0.0 → 1.0.0)
type=semver,pattern={{version}}
# Tag major.minor from git tags (v1.2.3 → 1.2)
type=semver,pattern={{major}}.{{minor}}
# Tag major from git tags (v1.2.3 → 1)
type=semver,pattern={{major}}
# Ephemeral tag for pull requests (derive number from GITHUB_REF if available)
type=raw,value=pr-${{ github.ref_name }},enable=${{ github.event_name == 'pull_request' }}
# Short SHA tag as fallback (for non-default non-dev push events)
type=sha,format=short,enable=${{ github.event_name != 'pull_request' }}
# Step 6: Build the frontend first
- name: 🎨 Build frontend
if: steps.skip.outputs.skip_build != 'true'
run: |
cd frontend
npm ci
npm run build
# Step 7: Build and push Docker image
- name: 🐳 Build and push Docker image
if: steps.skip.outputs.skip_build != 'true'
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # v5
id: build
with:
context: .
file: ./Dockerfile
platforms: ${{ github.event_name == 'pull_request' && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
CADDY_IMAGE=${{ steps.caddy.outputs.image }}
# Step 8: Run Trivy scan (table output first for visibility)
- name: 📋 Run Trivy scan (table output)
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
id: trivy_table
continue-on-error: true
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
format: 'table'
severity: 'CRITICAL,HIGH'
exit-code: '0'
# Step 9: Run Trivy security scan (SARIF)
- name: 🔍 Run Trivy vulnerability scanner
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true'
id: trivy
continue-on-error: true
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '0'
severity: 'CRITICAL,HIGH'
# Step 10: Upload Trivy results to GitHub Security tab
- name: 📤 Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@c3d42c5d08633d8b33635fbd94b000a0e2585b3c # v3
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && (steps.trivy.outcome == 'success' || steps.trivy.outcome == 'failure')
with:
sarif_file: 'trivy-results.sarif'
# Step 11: Fail if vulnerabilities found
- name: ❌ Check for vulnerabilities
if: github.event_name != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy_table.outcome == 'failure'
run: |
echo "::error::CRITICAL or HIGH vulnerabilities found in image"
exit 1
# Step 11: Create a summary
- 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 "- **Registry**: GitHub Container Registry (ghcr.io)" >> $GITHUB_STEP_SUMMARY
echo "- **Repository**: ${{ env.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
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🚀 How to Use" >> $GITHUB_STEP_SUMMARY
echo '```bash' >> $GITHUB_STEP_SUMMARY
echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
echo "docker run -d -p 8080:8080 -v caddy_data:/app/data ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY
echo '```' >> $GITHUB_STEP_SUMMARY
- name: 📋 Create skip summary
if: steps.skip.outputs.skip_build == 'true'
run: |
echo "## 🚫 Docker Build Skipped" >> $GITHUB_STEP_SUMMARY
echo "Reason: renovate bot or chore(deps)/chore commit/PR title." >> $GITHUB_STEP_SUMMARY
echo "Actor: ${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
echo "Event: ${{ github.event_name }}" >> $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'
steps:
# Ensure normalized lowercase IMAGE_NAME for this job as well
- name: 🔤 Normalize image name
run: |
raw="${{ github.repository_owner }}/${{ github.event.repository.name }}"
echo "IMAGE_NAME=$(echo "$raw" | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV
# Step 1: Figure out which tag to test
- 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
# Step 2: Pull the image we just built
- name: 📥 Pull Docker image
run: docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
# Step 3: Start the container
- name: 🚀 Run container
run: |
docker run -d \
--name test-container \
-p 8080:8080 \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}
# Step 4/5: Wait and check health with retries
- name: 🏥 Test health endpoint (retries)
run: |
set +e
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health || echo "000")
if [ "$code" = "200" ]; then
echo "✅ Health check passed on attempt $i"
exit 0
fi
echo "Attempt $i/30: health not ready (code=$code); waiting..."
sleep 2
done
echo "❌ Health check failed after retries"
docker logs test-container || true
exit 1
# Step 6: Check the logs for errors
- name: 📋 Check container logs
if: always()
run: docker logs test-container
# Step 7: Clean up
- name: 🧹 Stop container
if: always()
run: docker stop test-container && docker rm test-container
# Step 8: Summary
- name: 📋 Create test summary
if: always()
run: |
echo "## 🧪 Docker Image Test Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Image**: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.tag.outputs.tag }}" >> $GITHUB_STEP_SUMMARY
echo "- **Health Check**: ${{ job.status == 'success' && '✅ Passed' || '❌ Failed' }}" >> $GITHUB_STEP_SUMMARY