Skip to content
docktionary

Snippets / CI/CD

GitHub Actions: build, scan, push

Builds a container image, scans it for vulnerabilities, and pushes it to GHCR on every push to main.

github-actionsciregistry
deploy.yml
name: build-and-push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    # Default token permissions are read-only -- "packages: write" is what
    # actually lets this job push to GHCR.
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4

      # Needed for cache-from/cache-to below; the default builder docker
      # already has doesn't support the gha cache backend.
      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          # GITHUB_TOKEN is minted fresh per run -- no PAT to create or
          # rotate for pushing to GHCR.
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          push: true
          # Tagged by commit SHA, not "latest" -- every build is traceable
          # back to the exact source it was built from.
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          # mode=max also caches intermediate layers from earlier build
          # stages, not just the final image -- bigger cache, faster builds.
          cache-to: type=gha,mode=max

      - name: Scan image
        # Pinned to a commit, not a version tag -- most of trivy-action's
        # version tags were hijacked to credential-stealing malware in a
        # real 2026 supply-chain attack (GHSA-69fq-xp46-6x23). This SHA is
        # v0.36.0, a GitHub immutable release.
        uses: aquasecurity/trivy-action@a9c7b0f06e461e9d4b4d1711f154ee024b8d7ab8 # v0.36.0
        with:
          image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
          severity: CRITICAL,HIGH
          # Non-zero exit on a finding fails the job -- the image is
          # already pushed by this point, so this gates later deploy
          # steps rather than the push itself.
          exit-code: "1"