Skip to content

Build and Publish Containers #26

Build and Publish Containers

Build and Publish Containers #26

name: Build and Publish Containers
on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
# Run weekly on Sundays at 00:00 UTC to catch new vulnerabilities
- cron: "0 0 * * 0"
workflow_dispatch:
# Allow manual triggering
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest
labels: |
org.opencontainers.image.authors=${{ github.actor }}
- name: Process metadata
run: |
# Convert newline-separated labels to multiple --label arguments
LABEL_ARGS=$(echo "${{ steps.meta.outputs.labels }}" | while IFS= read -r line; do
[ -n "$line" ] && echo -n "--label \"$line\" "
done)
echo "LABEL_ARGS=$LABEL_ARGS" >> $GITHUB_ENV
- name: Install Devcontainer CLI
run: npm install -g @devcontainers/cli
# Step 1: Build and publish base-ubuntu container first
- name: Build and publish base-ubuntu container
run: |
cd src/base-ubuntu
devcontainer build --workspace-folder . \
--image-name ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:base-ubuntu \
--platform linux/arm64,linux/amd64 \
--output type=registry \
${{ env.LABEL_ARGS }}
# Step 2: Extract the digest of the freshly built base-ubuntu image
- name: Get base-ubuntu image digest
id: base-digest
run: |
# Get the digest of the multi-platform manifest from the registry
DIGEST=$(docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:base-ubuntu --format '{{json .Manifest.Digest}}' | tr -d '"')
echo "digest=$DIGEST" >> $GITHUB_OUTPUT
echo "full_ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" >> $GITHUB_OUTPUT
echo "::notice::Base image digest: $DIGEST"
# Step 3: Update node devcontainer.json to use the exact base-ubuntu digest
- name: Update node devcontainer.json with base image digest
run: |
BASE_IMAGE="${{ steps.base-digest.outputs.full_ref }}"
echo "Updating node base image to: $BASE_IMAGE"
jq --arg img "$BASE_IMAGE" '.image = $img' src/node/.devcontainer/devcontainer.json > tmp.json
mv tmp.json src/node/.devcontainer/devcontainer.json
cat src/node/.devcontainer/devcontainer.json
# Step 4: Build and publish node container using the pinned base image
- name: Build and publish node container
run: |
cd src/node
devcontainer build --workspace-folder . \
--image-name ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:node \
--platform linux/arm64,linux/amd64 \
--output type=registry \
${{ env.LABEL_ARGS }}
# Step 5: Update python devcontainer.json to use the exact base-ubuntu digest
- name: Update python devcontainer.json with base image digest
run: |
BASE_IMAGE="${{ steps.base-digest.outputs.full_ref }}"
echo "Updating python base image to: $BASE_IMAGE"
jq --arg img "$BASE_IMAGE" '.image = $img' src/python/.devcontainer/devcontainer.json > tmp.json
mv tmp.json src/python/.devcontainer/devcontainer.json
cat src/python/.devcontainer/devcontainer.json
# Step 6: Build and publish python container using the pinned base image
- name: Build and publish python container
run: |
cd src/python
devcontainer build --workspace-folder . \
--image-name ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:python \
--platform linux/arm64,linux/amd64 \
--output type=registry \
${{ env.LABEL_ARGS }}
# Security scanning job - runs after successful build
security-scan:
needs: build-and-publish
runs-on: ubuntu-latest
# Only run on push to main or scheduled runs (not on PRs to avoid duplicate scans)
if: github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
permissions:
contents: read
packages: read
security-events: write
strategy:
fail-fast: false
matrix:
image: [base-ubuntu, node, python]
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pull container image
run: docker pull ghcr.io/${{ github.repository }}:${{ matrix.image }}
- name: Generate SBOM with Trivy
uses: aquasecurity/trivy-action@0.31.0
with:
image-ref: "ghcr.io/${{ github.repository }}:${{ matrix.image }}"
format: "cyclonedx"
output: "sbom-${{ matrix.image }}.json"
- name: Upload SBOM as artifact
uses: actions/upload-artifact@v4
with:
name: sbom-${{ matrix.image }}
path: sbom-${{ matrix.image }}.json
retention-days: 90
- name: Scan for vulnerabilities with Trivy
uses: aquasecurity/trivy-action@0.31.0
with:
image-ref: "ghcr.io/${{ github.repository }}:${{ matrix.image }}"
format: "sarif"
output: "trivy-${{ matrix.image }}.sarif"
severity: "CRITICAL,HIGH,MEDIUM"
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: "trivy-${{ matrix.image }}.sarif"
category: "container-${{ matrix.image }}"
# Cleanup old container images from GHCR
cleanup-old-images:
needs: [build-and-publish, security-scan]
runs-on: ubuntu-latest
# Only run on main branch to prevent accidental deletions from PR builds
# Runs on push, schedule (weekly scans), and manual dispatch
if: github.ref == 'refs/heads/main'
permissions:
packages: write
# Don't fail the workflow if cleanup fails
continue-on-error: true
steps:
- name: Log cleanup start
run: |
echo "=== GHCR Cleanup Job ==="
echo "Package: ghcr.io/${{ github.repository }}"
echo "Trigger: ${{ github.event_name }}"
echo ""
echo "Retention Policy:"
echo " - Keep current tags: base-ubuntu, node, python"
echo " - Keep 10 most recent tagged versions"
echo " - Delete ALL untagged versions"
echo " - Delete ghost/partial multi-arch images"
echo " - Delete orphaned referrer images"
echo ""
# Single cleanup step for the devcontainer package
# All image variants (base-ubuntu, node, python) are tags within this single package
- name: Cleanup old container image versions
uses: dataaxiom/ghcr-cleanup-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
package: devcontainer
owner: ${{ github.repository_owner }}
# Delete all untagged images - we use specific tags for production
delete-untagged: true
keep-n-untagged: 0
# Keep recent tagged versions for rollback capability
keep-n-tagged: 10
# Protect the current production tags from deletion
exclude-tags: base-ubuntu,node,python
# Clean up corrupted multi-architecture images
delete-ghost-images: true
delete-partial-images: true
# Clean up orphaned attestation/referrer images
delete-orphaned-images: true
# Set to true for testing, false for actual cleanup
dry-run: false
- name: Log cleanup complete
run: |
echo "=== Cleanup Complete ==="
echo "View packages: https://github.com/${{ github.repository_owner }}?tab=packages&repo_name=devcontainer"