name: Docker Image on: workflow_dispatch: inputs: version: description: "Image version/tag to publish, for example v1.0.0-beta" required: false type: string image_name: description: "Image name without registry. Defaults to owner/repo." required: false type: string push: tags: ["v*"] env: REGISTRY: ghcr.io DEFAULT_IMAGE_NAME: ${{ github.repository }} DOCKERFILE: ./docker/Dockerfile BUILD_CONTEXT: . CACHE_SCOPE: docker-image STABLE_FLOATING_TAG: latest PRERELEASE_FLOATING_TAG: beta PRERELEASE_PATTERN: (alpha|beta|rc) permissions: contents: read packages: write attestations: write id-token: write jobs: build: name: Build (${{ matrix.arch }}) runs-on: ubuntu-latest strategy: fail-fast: false matrix: include: - arch: amd64 platform: linux/amd64 - arch: arm64 platform: linux/arm64 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true persist-credentials: false - name: Set build metadata shell: bash env: INPUT_VERSION: ${{ github.event.inputs.version }} INPUT_IMAGE_NAME: ${{ github.event.inputs.image_name }} run: | set -euo pipefail pointed_tag="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)" input_version="${INPUT_VERSION//[[:space:]]/}" image_name="${INPUT_IMAGE_NAME:-$DEFAULT_IMAGE_NAME}" if [[ "$GITHUB_REF" == refs/tags/* ]]; then version="$GITHUB_REF_NAME" elif [[ -n "$input_version" ]]; then version="$input_version" elif [[ -n "$pointed_tag" ]]; then version="$pointed_tag" else echo "workflow_dispatch requires a version input when HEAD is not tagged" >&2 exit 1 fi if [[ ! -f "$DOCKERFILE" ]]; then echo "Dockerfile not found: $DOCKERFILE" >&2 exit 1 fi if [[ ! -d "$BUILD_CONTEXT" ]]; then echo "Docker context not found: $BUILD_CONTEXT" >&2 exit 1 fi { echo "IMAGE=${REGISTRY}/${image_name,,}" echo "VERSION=$version" echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" echo "VCS_REF=$GITHUB_SHA" } >> "$GITHUB_ENV" - name: Set up QEMU uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push digest id: build uses: docker/build-push-action@v6 with: context: ${{ env.BUILD_CONTEXT }} file: ${{ env.DOCKERFILE }} platforms: ${{ matrix.platform }} outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true build-args: | VERSION=${{ env.VERSION }} BUILD_DATE=${{ env.BUILD_DATE }} VCS_REF=${{ env.VCS_REF }} labels: | org.opencontainers.image.title=${{ github.event.repository.name }} org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.version=${{ env.VERSION }} org.opencontainers.image.created=${{ env.BUILD_DATE }} cache-from: type=gha,scope=${{ env.CACHE_SCOPE }}-${{ matrix.arch }} cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=${{ env.CACHE_SCOPE }}-${{ matrix.arch }} - name: Export digest shell: bash env: DIGEST: ${{ steps.build.outputs.digest }} run: | set -euo pipefail mkdir -p /tmp/image-digests touch "/tmp/image-digests/${DIGEST#sha256:}" - name: Upload digest uses: actions/upload-artifact@v4 with: name: image-digests-${{ matrix.arch }} path: /tmp/image-digests/* if-no-files-found: error retention-days: 1 - name: Generate artifact attestation uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true merge: name: Merge multi-arch manifest runs-on: ubuntu-latest needs: build steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 fetch-tags: true persist-credentials: false - name: Set build metadata shell: bash env: INPUT_VERSION: ${{ github.event.inputs.version }} INPUT_IMAGE_NAME: ${{ github.event.inputs.image_name }} run: | set -euo pipefail pointed_tag="$(git tag --points-at HEAD --list 'v*' | sort -V | tail -n1)" input_version="${INPUT_VERSION//[[:space:]]/}" image_name="${INPUT_IMAGE_NAME:-$DEFAULT_IMAGE_NAME}" if [[ "$GITHUB_REF" == refs/tags/* ]]; then version="$GITHUB_REF_NAME" elif [[ -n "$input_version" ]]; then version="$input_version" elif [[ -n "$pointed_tag" ]]; then version="$pointed_tag" else echo "workflow_dispatch requires a version input when HEAD is not tagged" >&2 exit 1 fi if [[ ! -f "$DOCKERFILE" ]]; then echo "Dockerfile not found: $DOCKERFILE" >&2 exit 1 fi if [[ ! -d "$BUILD_CONTEXT" ]]; then echo "Docker context not found: $BUILD_CONTEXT" >&2 exit 1 fi { echo "IMAGE=${REGISTRY}/${image_name,,}" echo "VERSION=$version" } >> "$GITHUB_ENV" - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/image-digests pattern: image-digests-* merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push manifest list working-directory: /tmp/image-digests shell: bash run: | set -euo pipefail shopt -s nullglob references=() for digest in *; do references+=("${IMAGE}@sha256:${digest}") done if [[ ${#references[@]} -eq 0 ]]; then echo "No digests found in /tmp/image-digests" >&2 exit 1 fi floating_tag="$STABLE_FLOATING_TAG" if [[ "$VERSION" =~ $PRERELEASE_PATTERN ]]; then floating_tag="$PRERELEASE_FLOATING_TAG" fi docker buildx imagetools create \ -t "${IMAGE}:${VERSION}" \ -t "${IMAGE}:${floating_tag}" \ "${references[@]}" - name: Inspect image run: docker buildx imagetools inspect "${IMAGE}:${VERSION}"