Build and Publish Containers #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |