github-actions-maintainer 0.7.0

General-purpose GitHub Actions maintenance toolkit with secure workflow pinning
Documentation
name: Docker

on:
  push:
    branches: [main]
    tags:
      - "v*"
  pull_request:
    branches: [main]
    paths:
      - ".github/workflows/docker.yml"
      - "Dockerfile"
      - ".dockerignore"
      - "Cargo.toml"
      - "Cargo.lock"
      - "src/**"
      - "crates/**"
  schedule:
    - cron: "0 4 * * 1"
  workflow_dispatch:

permissions:
  contents: read
  packages: read

env:
  REGISTRY: ghcr.io
  DOCKERHUB_REGISTRY: docker.io
  DOCKERHUB_NAMESPACE: ${{ vars.RUST_TEMPLATE_DOCKERHUB_NAMESPACE || 'threatflux' }}
  PUBLISH_DOCKERHUB: ${{ vars.RUST_TEMPLATE_PUBLISH_DOCKERHUB || 'false' }}
  DOCKER_BUILDKIT: 1
  BINARY_NAME: ${{ vars.RUST_TEMPLATE_BINARY_NAME || 'github-actions-maintainer' }}
  BINARY_PACKAGE: ${{ vars.RUST_TEMPLATE_BINARY_PACKAGE || '' }}
  SBOM_MANIFEST_PATH: ${{ vars.RUST_TEMPLATE_SBOM_MANIFEST_PATH || 'Cargo.toml' }}

jobs:
  build:
    name: Build Docker Image
    runs-on: ${{ vars.RUST_TEMPLATE_RUNNER_UBUNTU || 'ubuntu-latest' }}
    permissions:
      contents: read
      packages: write
      security-events: write
    outputs:
      digest: ${{ steps.build.outputs.digest }}
      image_name: ${{ steps.lowercase.outputs.image_name }}
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Lowercase image name
        id: lowercase
        run: echo "image_name=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"

      - name: Lowercase repo name
        id: dockerhub_repo
        run: |
          REPO_NAME="${GITHUB_REPOSITORY#*/}"
          echo "name=${REPO_NAME,,}" >> "$GITHUB_OUTPUT"

      - name: Detect Rust toolchain version
        id: rust_toolchain
        run: |
          TOOLCHAIN_VERSION="$(awk -F '"' '/^channel[[:space:]]*=/ { print $2; exit }' rust-toolchain.toml)"
          if [[ -z "${TOOLCHAIN_VERSION}" ]]; then
            echo "Unable to detect Rust toolchain version from rust-toolchain.toml" >&2
            exit 1
          fi
          echo "version=${TOOLCHAIN_VERSION}" >> "$GITHUB_OUTPUT"

      - name: Compose image list
        id: images
        run: |
          GHCR_IMAGE="${REGISTRY}/${{ steps.lowercase.outputs.image_name }}"
          DOCKERHUB_IMAGE="${DOCKERHUB_REGISTRY}/${DOCKERHUB_NAMESPACE}/${{ steps.dockerhub_repo.outputs.name }}"
          {
            echo "images<<EOF"
            echo "${GHCR_IMAGE}"
            if [[ "${PUBLISH_DOCKERHUB}" == "true" ]]; then
              echo "${DOCKERHUB_IMAGE}"
            fi
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Select build platforms
        id: platforms
        run: |
          echo "value=linux/amd64,linux/arm64" >> "$GITHUB_OUTPUT"
          echo "needs_qemu=true" >> "$GITHUB_OUTPUT"

      - name: Set up QEMU
        if: steps.platforms.outputs.needs_qemu == 'true'
        uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

      - name: Log in to GitHub Container Registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Log in to Docker Hub
        if: github.event_name != 'pull_request' && env.PUBLISH_DOCKERHUB == 'true'
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ${{ env.DOCKERHUB_REGISTRY }}
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Extract runtime metadata
        id: meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: ${{ steps.images.outputs.images }}
          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=
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Extract base image metadata
        id: base_meta
        uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
        with:
          images: ${{ steps.images.outputs.images }}
          tags: |
            type=raw,value=base-rust-${{ steps.rust_toolchain.outputs.version }}
            type=raw,value=base-rust-latest,enable={{is_default_branch}}

      - name: Build base image
        if: github.event_name != 'pull_request'
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: .
          target: rust-base
          platforms: ${{ steps.platforms.outputs.value }}
          push: true
          tags: ${{ steps.base_meta.outputs.tags }}
          labels: ${{ steps.base_meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ github.ref_name }}
            BUILD_DATE=${{ github.event.repository.updated_at }}
            VCS_REF=${{ github.sha }}
            BINARY_NAME=${{ env.BINARY_NAME }}
            BINARY_PACKAGE=${{ env.BINARY_PACKAGE }}
            SBOM_MANIFEST_PATH=${{ env.SBOM_MANIFEST_PATH }}

      - name: Build and export (PRs)
        if: github.event_name == 'pull_request'
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: .
          platforms: linux/amd64
          tags: ${{ steps.lowercase.outputs.image_name }}:scan
          labels: ${{ steps.meta.outputs.labels }}
          outputs: type=docker,dest=/tmp/image.tar
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ github.ref_name }}
            BUILD_DATE=${{ github.event.repository.updated_at }}
            VCS_REF=${{ github.sha }}
            BINARY_NAME=${{ env.BINARY_NAME }}
            BINARY_PACKAGE=${{ env.BINARY_PACKAGE }}
            SBOM_MANIFEST_PATH=${{ env.SBOM_MANIFEST_PATH }}

      - name: Build and push runtime image (non-PR)
        if: github.event_name != 'pull_request'
        id: build
        uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
        with:
          context: .
          platforms: ${{ steps.platforms.outputs.value }}
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ github.ref_name }}
            BUILD_DATE=${{ github.event.repository.updated_at }}
            VCS_REF=${{ github.sha }}
            BINARY_NAME=${{ env.BINARY_NAME }}
            BINARY_PACKAGE=${{ env.BINARY_PACKAGE }}
            SBOM_MANIFEST_PATH=${{ env.SBOM_MANIFEST_PATH }}

      - name: Upload image artifact
        if: github.event_name == 'pull_request'
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        with:
          name: docker-image
          path: /tmp/image.tar
          retention-days: 1

  scan:
    name: Security Scan
    needs: build
    runs-on: ${{ vars.RUST_TEMPLATE_RUNNER_UBUNTU || 'ubuntu-latest' }}
    permissions:
      contents: read
      packages: read
      security-events: write
    steps:
      - name: Checkout
        uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          persist-credentials: false

      - name: Download image (PRs)
        if: github.event_name == 'pull_request'
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        with:
          name: docker-image
          path: /tmp

      - name: Load image (PRs)
        if: github.event_name == 'pull_request'
        run: docker load -i /tmp/image.tar

      - name: Get short SHA
        id: sha
        run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

      - name: Log in to registry
        if: github.event_name != 'pull_request'
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Pull image
        if: github.event_name != 'pull_request'
        run: docker pull ${{ env.REGISTRY }}/${{ needs.build.outputs.image_name }}:${{ steps.sha.outputs.short }}

      - name: Run Trivy scanner
        uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
        with:
          image-ref: ${{ github.event_name == 'pull_request' && format('{0}:scan', needs.build.outputs.image_name) || format('{0}/{1}:{2}', env.REGISTRY, needs.build.outputs.image_name, steps.sha.outputs.short) }}
          format: sarif
          output: trivy-results.sarif
          severity: CRITICAL,HIGH
          vuln-type: os,library
          ignore-unfixed: true
          limit-severities-for-sarif: true
          trivyignores: .trivyignore.yaml
        continue-on-error: true

      - name: Upload Trivy results
        uses: github/codeql-action/upload-sarif@3d8036cf7fe7433e4a725cf513a6ea56c7fd0f14 # codeql-bundle-v2.25.0
        with:
          sarif_file: trivy-results.sarif
        if: always() && hashFiles('trivy-results.sarif') != ''

  sign:
    name: Sign Container
    needs: [build, scan]
    runs-on: ${{ vars.RUST_TEMPLATE_RUNNER_UBUNTU || 'ubuntu-latest' }}
    if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
    permissions:
      contents: read
      packages: write
      id-token: write
    steps:
      - name: Install cosign
        uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2

      - name: Log in to registry
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Sign container image
        run: cosign sign --yes ${{ env.REGISTRY }}/${{ needs.build.outputs.image_name }}@${{ needs.build.outputs.digest }}
        env:
          COSIGN_EXPERIMENTAL: 1

  sbom:
    name: Generate SBOM
    needs: build
    runs-on: ${{ vars.RUST_TEMPLATE_RUNNER_UBUNTU || 'ubuntu-latest' }}
    if: github.event_name != 'pull_request'
    steps:
      - name: Get short SHA
        id: sha
        run: echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"

      - name: Log in to registry
        uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Generate SBOM
        uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
        with:
          image: ${{ env.REGISTRY }}/${{ needs.build.outputs.image_name }}:${{ steps.sha.outputs.short }}
          artifact-name: sbom.spdx.json
          format: spdx-json
        continue-on-error: true