# =============================================================================
# REUSABLE WORKFLOW: Docker Build and Security Scanning
# PURPOSE: Build Docker images and scan for vulnerabilities with Trivy
# USAGE: Called by PR and main workflows for container validation
# OUTPUTS: Security findings uploaded to GitHub Security tab, Docker image artifact
# =============================================================================
name: Reusable Docker
on:
workflow_call:
inputs:
platforms:
description: 'Docker platforms to build (e.g., linux/amd64,linux/arm64)'
type: string
default: 'linux/amd64' # Single platform for PRs, multi for main
push-image:
description: 'Whether to push image to registry (always false for this workflow)'
type: boolean
default: false
save-artifact:
description: 'Whether to save Docker image as artifact for later use'
type: boolean
default: false
artifact-name:
description: 'Name for the Docker image artifact'
type: string
default: 'docker-image'
version:
description: 'Version tag for the Docker image'
type: string
default: ''
image-name:
description: 'Docker image name (without registry)'
type: string
default: 'deepsource-mcp-server'
tag_sha:
description: 'SHA of the version tag for consistent naming'
type: string
default: ''
build_artifact:
description: 'Name of the pre-built TypeScript artifact to use'
type: string
default: ''
outputs:
image-digest:
description: 'Docker image digest'
value: ${{ jobs.docker.outputs.digest }}
artifact-name:
description: 'Name of the saved artifact'
value: ${{ jobs.docker.outputs.artifact-name }}
# SECURITY: Required permissions for Docker operations
# Note: packages: write is only needed if pushing to GitHub Container Registry
# Calling workflows can omit it if not pushing images
permissions:
contents: read # Read source code
security-events: write # Upload Trivy scan results
packages: write # Push Docker images to GitHub Container Registry
jobs:
docker:
runs-on: ubuntu-latest
outputs:
digest: ${{ steps.build.outputs.digest }}
artifact-name: ${{ inputs.artifact-name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download build artifact
# Download pre-built TypeScript if artifact name provided
if: inputs.build_artifact != ''
uses: actions/download-artifact@v4
with:
name: ${{ inputs.build_artifact }}
# =============================================================================
# DOCKER SETUP
# Configure build environment for single or multi-platform builds
# =============================================================================
- name: Set up QEMU
# Required for multi-platform builds (arm64)
if: contains(inputs.platforms, 'arm64')
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
# Advanced Docker builder with cache support
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
# Login to GHCR for multi-platform builds that need to be pushed to registry
# Single-platform builds for PRs don't need registry push
if: inputs.save-artifact && contains(inputs.platforms, ',')
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# =============================================================================
# DOCKER BUILD
# Build image with layer caching for efficiency
# =============================================================================
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
# Use GHCR for multi-platform artifact builds, local name otherwise
images: |
${{ (inputs.save-artifact && contains(inputs.platforms, ',')) && format('ghcr.io/{0}/{1}', github.repository_owner, inputs.image-name) || inputs.image-name }}
tags: |
type=raw,value=${{ inputs.version }},enable=${{ inputs.version != '' }}
type=raw,value=latest,enable=${{ inputs.version != '' }}
type=ref,event=pr
type=sha,format=short
- name: Determine build configuration
# Set clear variables for build mode to improve readability
id: build-config
run: |
# Determine if we're building for multiple platforms
IS_MULTI_PLATFORM="false"
if echo "${{ inputs.platforms }}" | grep -q ','; then
IS_MULTI_PLATFORM="true"
fi
# For multi-platform builds with save-artifact, push to GHCR
# For single-platform builds or PR builds, load locally or save to tar
SAVE_ARTIFACT="${{ inputs.save-artifact }}"
SHOULD_PUSH="false"
CAN_LOAD="false"
OUTPUT_TYPE=""
if [ "$SAVE_ARTIFACT" = "true" ] && [ "$IS_MULTI_PLATFORM" = "true" ]; then
# Multi-platform artifact build: push to GHCR
SHOULD_PUSH="true"
CAN_LOAD="false"
elif [ "$SAVE_ARTIFACT" != "true" ] && [ "$IS_MULTI_PLATFORM" = "false" ]; then
# Single-platform PR build: load locally
CAN_LOAD="true"
else
# Single-platform artifact build: save to tar
CAN_LOAD="false"
SHA_TO_USE="${{ inputs.tag_sha || github.sha }}"
OUTPUT_TYPE="type=docker,dest=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar"
fi
{
echo "is_multi_platform=$IS_MULTI_PLATFORM"
echo "should_push=$SHOULD_PUSH"
echo "can_load=$CAN_LOAD"
echo "output_type=$OUTPUT_TYPE"
} >> $GITHUB_OUTPUT
echo "📋 Build configuration:"
echo " Multi-platform: $IS_MULTI_PLATFORM"
echo " Save artifact: $SAVE_ARTIFACT"
echo " Should push: $SHOULD_PUSH"
echo " Can load: $CAN_LOAD"
echo " Output type: $OUTPUT_TYPE"
- name: Build Docker image
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ inputs.platforms }}
push: ${{ steps.build-config.outputs.should_push == 'true' }}
load: ${{ steps.build-config.outputs.can_load == 'true' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # Use GitHub Actions cache
cache-to: type=gha,mode=max # Maximum cache retention
build-args: |
VERSION=${{ inputs.version || github.sha }}
outputs: ${{ steps.build-config.outputs.output_type }}
# =============================================================================
# SECURITY SCANNING
# Trivy vulnerability scanning with configurable severity
# =============================================================================
- name: Determine Trivy scan configuration
# Set clear variables for scan inputs to improve readability
id: scan-config
run: |
# Determine scanning mode based on build configuration
CAN_LOAD="${{ steps.build-config.outputs.can_load }}"
if [ "$CAN_LOAD" = "true" ]; then
# For loaded single-platform images, scan by image reference
FIRST_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n1)
{
echo "scan_input="
echo "scan_image_ref=$FIRST_TAG"
} >> $GITHUB_OUTPUT
echo "Using image reference for scanning: $FIRST_TAG"
else
# For multi-platform or artifact builds, scan the tar file
SHA_TO_USE="${{ inputs.tag_sha || github.sha }}"
{
echo "scan_input=${{ inputs.artifact-name }}-${SHA_TO_USE}.tar"
echo "scan_image_ref="
} >> $GITHUB_OUTPUT
echo "Using tar file for scanning: ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar"
fi
- name: Run Trivy vulnerability scanner
# SECURITY: Scan image for vulnerabilities before any distribution
# NOTE: Multi-platform OCI exports cannot be scanned from tar files
# Scans for vulnerabilities, secrets, misconfigurations, and licenses
# License findings are informational only (see LICENSES.md)
if: steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ',')
uses: aquasecurity/trivy-action@0.28.0
with:
input: ${{ steps.scan-config.outputs.scan_input }}
image-ref: ${{ steps.scan-config.outputs.scan_image_ref }}
exit-code: '1'
format: 'sarif'
hide-progress: false
output: 'trivy-results.sarif'
severity: 'HIGH,CRITICAL'
scanners: 'vuln,secret,misconfig'
trivyignores: '.trivyignore'
version: 'latest'
env:
TRIVY_DEBUG: 'true'
- name: Check Trivy results for vulnerabilities
# Fail build if non-license security issues are found
# License findings are informational and don't fail the build
if: steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ',')
run: |
if [ -f trivy-results.sarif ]; then
# Check for vulnerabilities, secrets, or misconfigurations (not licenses)
SECURITY_ISSUES=$(jq -r '.runs[0].results[] | select(.ruleId | startswith("CVE-") or startswith("SECRET-") or startswith("CONFIG-")) | .level' trivy-results.sarif 2>/dev/null | wc -l || echo "0")
if [ "$SECURITY_ISSUES" -gt 0 ]; then
echo "::error::Found $SECURITY_ISSUES security issue(s) in container image"
echo "Review the scan results in the Security tab after SARIF upload"
exit 1
fi
echo "No security vulnerabilities found (license findings are informational)"
fi
- name: Upload Trivy results to GitHub Security
# Always upload results, even if scan fails
# Results viewable at: Security > Code scanning alerts
if: always() && (steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ','))
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
category: 'container-scan-${{ github.event_name }}'
- name: Upload Trivy SARIF as artifact
# Upload SARIF file as artifact for debugging and inspection
if: always() && (steps.build-config.outputs.can_load == 'true' || !contains(inputs.platforms, ','))
uses: actions/upload-artifact@v4
with:
name: trivy-${{ github.sha }}
path: trivy-results.sarif
retention-days: 7
# =============================================================================
# ARTIFACT STORAGE
# Save Docker image tar files for single-platform builds
# Multi-platform builds are pushed to GHCR instead
# =============================================================================
- name: Compress Docker image artifact
# Compress the tar file to reduce storage costs
# Only for single-platform builds (multi-platform builds pushed to GHCR)
if: inputs.save-artifact && !contains(inputs.platforms, ',')
run: |
SHA_TO_USE="${{ inputs.tag_sha || github.sha }}"
echo "Compressing Docker image artifact..."
gzip -9 ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar
ls -lh ${{ inputs.artifact-name }}-${SHA_TO_USE}.tar.gz
- name: Upload Docker image artifact
# Store single-platform image tar for deterministic publishing
# Multi-platform images are stored in GHCR registry
if: inputs.save-artifact && !contains(inputs.platforms, ',')
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}
path: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}.tar.gz
retention-days: 7 # Keep for a week (enough for release cycle)
compression-level: 0 # Already compressed with gzip
# =============================================================================
# SUPPLY CHAIN SECURITY
# Generate attestations for build provenance (main builds only)
# =============================================================================
- name: Generate attestations for GHCR images
# Creates cryptographic proof of build provenance for multi-platform images
# Multi-platform images are stored in GHCR registry
if: inputs.save-artifact && contains(inputs.platforms, ',') && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != ''
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/${{ github.repository_owner }}/${{ inputs.image-name }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Generate attestations for tar artifacts
# Creates cryptographic proof of build provenance for single-platform tar files
if: inputs.save-artifact && !contains(inputs.platforms, ',') && inputs.version != '' && env.ACTIONS_ID_TOKEN_REQUEST_URL != ''
uses: actions/attest-build-provenance@v2
with:
subject-path: ${{ inputs.artifact-name }}-${{ inputs.tag_sha || github.sha }}.tar.gz