ci: adds dedicated release token #13
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
| # Builds the multi-arch image and publishes to GHCR (+ DockerHub if configured). | |
| # | |
| # Triggered by git tag creation, which publishes to the same docker tag. | |
| # Each platform is built on its own runner in parallel and pushed by digest; | |
| # a final merge job stitches the digests into one manifest list per registry, | |
| # then attaches provenance + SBOM attestations. | |
| # | |
| # Platforms: amd64 (native), arm64 (native arm runner), and arm/v7, 386, | |
| # ppc64le, s390x (glibc, cross-compiled on amd64). No emulation is used, so | |
| # QEMU is not needed. | |
| name: π³ Docker | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Docker tag to publish. Leave empty to update latest' | |
| required: false | |
| type: string | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - '*' | |
| concurrency: | |
| group: docker-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| env: | |
| IMAGE_NAME: adguardian | |
| DOCKER_USER: lissy93 | |
| GHCR_REGISTRY: ghcr.io | |
| DOCKERHUB_REGISTRY: docker.io | |
| jobs: | |
| # Resolve the tag + target registries once, shared by every build leg + merge | |
| prepare: | |
| name: π§ Prepare | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| outputs: | |
| tag: ${{ steps.meta.outputs.tag }} | |
| ghcr_image: ${{ steps.meta.outputs.ghcr_image }} | |
| publish_images: ${{ steps.meta.outputs.publish_images }} | |
| dockerhub_enabled: ${{ steps.meta.outputs.dockerhub_enabled }} | |
| steps: | |
| - name: Compute image metadata | |
| id: meta | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| MANUAL_TAG: ${{ inputs.tag }} | |
| DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then | |
| if [[ -n "$MANUAL_TAG" ]]; then TAG="$MANUAL_TAG"; else TAG="latest"; fi | |
| elif [[ "$GITHUB_REF" == refs/tags/* ]]; then | |
| TAG="${GITHUB_REF#refs/tags/}" | |
| else | |
| TAG="latest" | |
| fi | |
| TAG="$(echo "$TAG" | sed -E 's/[^A-Za-z0-9_.-]+/-/g; s/^[.-]+//' | cut -c1-128)" | |
| if [[ -z "$TAG" ]]; then | |
| echo "::error::Computed Docker tag is empty" | |
| exit 1 | |
| fi | |
| GHCR_IMAGE="${GHCR_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}" | |
| PUBLISH_IMAGES="$GHCR_IMAGE" | |
| DOCKERHUB_ENABLED="false" | |
| if [[ -n "$DOCKERHUB_PASSWORD" ]]; then | |
| PUBLISH_IMAGES="${PUBLISH_IMAGES},${DOCKERHUB_REGISTRY}/${DOCKER_USER}/${IMAGE_NAME}" | |
| DOCKERHUB_ENABLED="true" | |
| fi | |
| { | |
| echo "tag=$TAG" | |
| echo "ghcr_image=$GHCR_IMAGE" | |
| echo "publish_images=$PUBLISH_IMAGES" | |
| echo "dockerhub_enabled=$DOCKERHUB_ENABLED" | |
| } >> "$GITHUB_OUTPUT" | |
| # Build each platform on its own runner, in parallel, pushing by digest | |
| build: | |
| name: ποΈ Build ${{ matrix.platform }} | |
| needs: prepare | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 60 | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # 64-bit: ultra-light scratch/musl image (Dockerfile), built natively | |
| - platform: linux/amd64 | |
| runner: ubuntu-latest | |
| dockerfile: ./Dockerfile | |
| - platform: linux/arm64 | |
| runner: ubuntu-24.04-arm | |
| dockerfile: ./Dockerfile | |
| # glibc/Debian image (Dockerfile.full), cross-compiled on amd64 | |
| - platform: linux/arm/v7 | |
| runner: ubuntu-latest | |
| dockerfile: ./Dockerfile.full | |
| - platform: linux/386 | |
| runner: ubuntu-latest | |
| dockerfile: ./Dockerfile.full | |
| - platform: linux/ppc64le | |
| runner: ubuntu-latest | |
| dockerfile: ./Dockerfile.full | |
| - platform: linux/s390x | |
| runner: ubuntu-latest | |
| dockerfile: ./Dockerfile.full | |
| steps: | |
| - name: ποΈ Checkout | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: π·οΈ Prepare platform name | |
| env: | |
| PLATFORM: ${{ matrix.platform }} | |
| run: echo "PLATFORM_PAIR=${PLATFORM//\//-}" >> "$GITHUB_ENV" | |
| - name: π§± Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: π Login to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: π Login to DockerHub | |
| if: needs.prepare.outputs.dockerhub_enabled == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ env.DOCKER_USER }} | |
| password: ${{ secrets.DOCKERHUB_PASSWORD }} | |
| - name: ποΈ Build and push by digest | |
| id: build | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| platforms: ${{ matrix.platform }} | |
| provenance: false | |
| sbom: false | |
| cache-from: type=gha,scope=${{ env.PLATFORM_PAIR }} | |
| cache-to: type=gha,mode=max,scope=${{ env.PLATFORM_PAIR }} | |
| outputs: type=image,"name=${{ needs.prepare.outputs.publish_images }}",push-by-digest=true,name-canonical=true,push=true | |
| - name: π€ Export digest | |
| env: | |
| DIGEST: ${{ steps.build.outputs.digest }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p /tmp/digests | |
| touch "/tmp/digests/${DIGEST#sha256:}" | |
| - name: β¬οΈ Upload digest | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: digests-${{ env.PLATFORM_PAIR }} | |
| path: /tmp/digests/* | |
| if-no-files-found: error | |
| retention-days: 1 | |
| # Combine the per-platform digests into one multi-arch manifest per registry | |
| merge: | |
| name: π§© Merge manifest | |
| needs: [prepare, build] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| permissions: | |
| contents: read | |
| packages: write | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: π₯ Download digests | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: /tmp/digests | |
| pattern: digests-* | |
| merge-multiple: true | |
| - name: π§± Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: π Login to GitHub Container Registry | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.GHCR_REGISTRY }} | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: π Login to DockerHub | |
| if: needs.prepare.outputs.dockerhub_enabled == 'true' | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ${{ env.DOCKERHUB_REGISTRY }} | |
| username: ${{ env.DOCKER_USER }} | |
| password: ${{ secrets.DOCKERHUB_PASSWORD }} | |
| - name: π§© Create combined manifest list | |
| shell: bash | |
| env: | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| PUBLISH_IMAGES: ${{ needs.prepare.outputs.publish_images }} | |
| run: | | |
| set -euo pipefail | |
| cd /tmp/digests | |
| shopt -s nullglob | |
| DIGEST_FILES=(*) | |
| if [ ${#DIGEST_FILES[@]} -eq 0 ]; then | |
| echo "::error::No digests found to merge" | |
| exit 1 | |
| fi | |
| IFS=',' read -ra IMAGES <<< "$PUBLISH_IMAGES" | |
| for IMG in "${IMAGES[@]}"; do | |
| REFS=() | |
| for f in "${DIGEST_FILES[@]}"; do | |
| REFS+=("${IMG}@sha256:${f}") | |
| done | |
| docker buildx imagetools create -t "${IMG}:${TAG}" "${REFS[@]}" | |
| done | |
| - name: π Inspect and capture GHCR digest | |
| id: digest | |
| shell: bash | |
| env: | |
| GHCR_IMAGE: ${{ needs.prepare.outputs.ghcr_image }} | |
| TAG: ${{ needs.prepare.outputs.tag }} | |
| run: | | |
| set -euo pipefail | |
| GHCR_DIGEST=$(docker buildx imagetools inspect "${GHCR_IMAGE}:${TAG}" --format '{{.Manifest.Digest}}') | |
| echo "ghcr_digest=${GHCR_DIGEST}" >> "$GITHUB_OUTPUT" | |
| docker buildx imagetools inspect "${GHCR_IMAGE}:${TAG}" | |
| - name: πͺͺ Attest build provenance | |
| uses: actions/attest-build-provenance@v4 | |
| continue-on-error: true | |
| with: | |
| subject-name: ${{ needs.prepare.outputs.ghcr_image }} | |
| subject-digest: ${{ steps.digest.outputs.ghcr_digest }} | |
| push-to-registry: true | |
| - name: π Generate SBOM | |
| uses: anchore/sbom-action@v0.24.0 | |
| continue-on-error: true | |
| with: | |
| image: ${{ needs.prepare.outputs.ghcr_image }}@${{ steps.digest.outputs.ghcr_digest }} | |
| format: spdx-json | |
| output-file: sbom.spdx.json | |
| - name: πͺͺ Attest SBOM | |
| uses: actions/attest@v4 | |
| continue-on-error: true | |
| with: | |
| subject-name: ${{ needs.prepare.outputs.ghcr_image }} | |
| subject-digest: ${{ steps.digest.outputs.ghcr_digest }} | |
| predicate-type: https://spdx.dev/Document | |
| predicate-path: sbom.spdx.json | |
| push-to-registry: true |