Skip to content

feat(rotation): Phase 1 — multi-key keyring plumbing (#72) #240

feat(rotation): Phase 1 — multi-key keyring plumbing (#72)

feat(rotation): Phase 1 — multi-key keyring plumbing (#72) #240

Workflow file for this run

name: Docker Build and Push
on:
push:
branches: [main]
tags:
- 'v*'
pull_request:
branches: [main]
# Cancel superseded PR runs; let main/tag builds finish (they push images).
concurrency:
group: docker-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions: {}
env:
REGISTRY: ghcr.io
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
apps: ${{ steps.apps.outputs.apps }}
steps:
- name: Harden runner
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Filter changed paths
id: filter
# Tag pushes have no base ref to diff against, so always-build is the
# safe default (a tag means we want to release both images).
if: github.event_name != 'push' || github.ref_type != 'tag'
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
with:
filters: |
gearbox:
- 'gearbox/**'
- '.github/workflows/docker.yml'
gearbox-agent:
- 'gearbox-agent/**'
- '.github/workflows/docker.yml'
- name: Build matrix from filter results
id: apps
env:
IS_TAG: ${{ github.event_name == 'push' && github.ref_type == 'tag' }}
GEARBOX: ${{ steps.filter.outputs.gearbox }}
AGENT: ${{ steps.filter.outputs.gearbox-agent }}
run: |
if [ "$IS_TAG" = "true" ]; then
echo 'apps=["gearbox","gearbox-agent"]' >> "$GITHUB_OUTPUT"
exit 0
fi
apps=()
[ "$GEARBOX" = "true" ] && apps+=("gearbox")
[ "$AGENT" = "true" ] && apps+=("gearbox-agent")
if [ ${#apps[@]} -eq 0 ]; then
echo "apps=[]" >> "$GITHUB_OUTPUT"
else
printf -v joined '"%s",' "${apps[@]}"
echo "apps=[${joined%,}]" >> "$GITHUB_OUTPUT"
fi
build-and-push:
name: Build and Push (${{ matrix.app }})
needs: changes
if: needs.changes.outputs.apps != '[]'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
security-events: write
strategy:
fail-fast: false
matrix:
app: ${{ fromJSON(needs.changes.outputs.apps) }}
env:
IMAGE_NAME: ${{ github.repository }}/${{ matrix.app }}
steps:
- name: Harden runner
uses: step-security/harden-runner@a5ad31d6a139d249332a2605b85202e8c0b78450 # v2
with:
egress-policy: audit
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
- name: Set up QEMU
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Extract metadata
id: meta
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
# Build amd64-only locally so we can scan it before deciding to push.
# Cache is shared with the multi-platform push step below, so the
# incremental cost of the second build is essentially zero.
- name: Build image for scan
id: scan-build
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v6
with:
context: ./${{ matrix.app }}
push: false
load: true
tags: ${{ matrix.app }}:scan
cache-from: type=gha,scope=${{ matrix.app }}
cache-to: type=gha,mode=max,scope=${{ matrix.app }}
build-args: |
VERSION=${{ github.ref_name }}
COMMIT_SHA=${{ github.sha }}
BUILD_DATE=${{ github.event.repository.updated_at }}
# Gating scan: fail the build on CRITICAL/HIGH unfixed-or-fixed in
# production releases, but only warn on PRs (so authors can iterate
# without being blocked by upstream-only CVEs).
- name: Run Trivy image scan (gating)
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: ${{ matrix.app }}:scan
format: 'sarif'
output: 'trivy-image-results.sarif'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
exit-code: ${{ github.event_name == 'pull_request' && '0' || '1' }}
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4
if: always()
with:
sarif_file: 'trivy-image-results.sarif'
category: trivy-docker-${{ matrix.app }}
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Multi-platform build + push, only after the scan has passed. Reuses
# the cache from the scan build so amd64 layers are essentially free.
- name: Build and push (multi-platform)
id: push
if: github.event_name != 'pull_request'
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v6
with:
context: ./${{ matrix.app }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.app }}
cache-to: type=gha,mode=max,scope=${{ matrix.app }}
build-args: |
VERSION=${{ github.ref_name }}
COMMIT_SHA=${{ github.sha }}
BUILD_DATE=${{ github.event.repository.updated_at }}
platforms: linux/amd64,linux/arm64
- name: Generate artifact attestation
if: github.event_name != 'pull_request' && !github.event.repository.private
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
docker-status:
name: Docker Status
needs: [changes, build-and-push]
if: always()
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Verify required jobs succeeded or skipped intentionally
env:
CHANGES: ${{ needs.changes.result }}
BUILD: ${{ needs.build-and-push.result }}
run: |
fail=0
for job in CHANGES BUILD; do
result="${!job}"
case "$result" in
success|skipped) ;;
*) echo "::error::Job $job ended with result: $result"; fail=1 ;;
esac
done
exit $fail