From 2e43872dc860e4f5e805d922559f919bf8150877 Mon Sep 17 00:00:00 2001 From: Iain Lane Date: Tue, 18 Feb 2025 17:32:10 +0000 Subject: [PATCH 1/2] ci: build multiarch images natively, generate SBOMs and provenance We currently build multiarch images using QEMU. This is slow. These days GitHub offers native arm64 builders for free for public repositories like ours, so let's use it. This is following [the recipe outlined on Docker's website][method], with a couple of adjustments to generate SBOMs and provenance attestations so that our images can be verified. [method]: https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners --- .github/workflows/build.yml | 150 ++++++++++++++++++++++++++++++++++-- README.md | 38 +++++++++ 2 files changed, 182 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index afefc53..a4398d6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,29 +21,167 @@ on: merge_group: jobs: - main: + build: permissions: + attestations: write contents: read id-token: write - runs-on: ubuntu-latest + strategy: + matrix: + runner: + - ubuntu-24.04 + - ubuntu-24.04-arm + + name: Build and push Docker image for ${{ matrix.runner }} + + runs-on: ${{ matrix.runner }} + + outputs: + digest: ${{ steps.build.outputs.digest }} + steps: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: persist-credentials: false + - name: Login to DockerHub + if: github.event_name == 'push' + uses: grafana/shared-workflows/actions/dockerhub-login@13fb504e3bfe323c1188bf244970d94b2d336e86 # dockerhub-login-v1.0.1 + - name: Set Docker Buildx up uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0 - - name: Build Docker image - uses: grafana/shared-workflows/actions/build-push-to-dockerhub@402975d84dd3fac9ba690f994f412d0ee2f51cf4 # build-push-to-dockerhub-v0.1.1 + # No tags + - name: Build and push Docker image + id: build + uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0 with: - platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max + outputs: type=image,"name=grafana/wait-for-github",push-by-digest=true,name-canonical=true + provenance: true push: ${{ github.event_name == 'push' }} + sbom: false + + - name: Export digests + if: github.event_name == 'push' + id: export-digests + env: + DIGEST: ${{ steps.build.outputs.digest }} + RUNNER_TEMP: ${{ runner.temp }} + run: | + # The digest of the _index_ - this is what we ultimately push, and + # what we need to refer to in the multi-arch manifest. + mkdir -pv "${RUNNER_TEMP}"/artifact/digests + touch "${RUNNER_TEMP}/artifact/digests/${DIGEST#sha256:}" + + # The digest of the _manifest_ referred to by the index. When `docker + # buildx imagetools create` processes its inputs, it creates a new + # combines these manifest references into a new index. So we should + # attest this digest, then clients can find it given the multiarch + # index, by dereferncing to the per-arch manifests and looking at the + # referrers on them. + docker buildx imagetools inspect "grafana/wait-for-github@${DIGEST}" --raw | \ + jq \ + --raw-output \ + '.manifests[] | + select ( + .mediaType == "application/vnd.oci.image.manifest.v1+json" and .annotations["vnd.docker.reference.type"] == null + ) | + .digest' | \ + ( echo -n 'digest=' && cat ) | \ + tee -a "${GITHUB_OUTPUT}" + + - name: Generate SBOM + if: github.event_name == 'push' + uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0 + with: + format: cyclonedx-json + image: grafana/wait-for-github@${{ steps.export-digests.outputs.digest }} + output-file: ${{ runner.temp }}/sbom-${{ matrix.runner }}.json + + - name: Generate SBOM attestation + if: github.event_name == 'push' + uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0 + with: + push-to-registry: true + subject-digest: ${{ steps.export-digests.outputs.digest }} + subject-name: index.docker.io/grafana/wait-for-github + sbom-path: ${{ runner.temp }}/sbom-${{ matrix.runner }}.json + + - name: Upload artifact + if: github.event_name == 'push' + uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + with: + name: artifacts-${{ matrix.runner }} + path: ${{ runner.temp }}/artifact/ + if-no-files-found: error + retention-days: 1 + + manifest: + if: github.event_name == 'push' + + needs: + - build + + permissions: + attestations: write + id-token: write + + name: Generate multi-arch manifest list and build provenance attestation + + runs-on: ubuntu-24.04 + + outputs: + digest: ${{ steps.inspect.outputs.digest }} + + steps: + - name: Download artifacts + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + merge-multiple: true + path: ${{ runner.temp }}/artifacts + pattern: artifacts-* + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1 + with: + images: grafana/wait-for-github + sep-tags: ' ' tags: | # tag with branch name for `main` type=ref,event=branch,enable={{is_default_branch}} # tag with semver, and `latest` type=ref,event=tag - repository: grafana/wait-for-github + + - name: Login to DockerHub + uses: grafana/shared-workflows/actions/dockerhub-login@13fb504e3bfe323c1188bf244970d94b2d336e86 # dockerhub-login-v1.0.1 + + - name: Create manifest list and push + working-directory: ${{ runner.temp }}/artifacts/digests + run: | + docker buildx imagetools create $(jq --compact-output --raw-output '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}") \ + $(printf 'grafana/wait-for-github@sha256:%s ' *) + + - name: Inspect image + id: inspect + env: + VERSION: ${{ steps.meta.outputs.version }} + run: | + docker buildx imagetools inspect "grafana/wait-for-github:${VERSION}" + + # Output image digest as github output + docker buildx imagetools inspect "grafana/wait-for-github:${VERSION}" --format "{{json .Manifest.Digest}}" | \ + xargs | \ + ( echo -n 'digest=' && cat ) | \ + tee -a "${GITHUB_OUTPUT}" + + - name: Generate build provenance attestation + uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0 + with: + push-to-registry: true + subject-name: index.docker.io/grafana/wait-for-github + subject-digest: ${{ steps.inspect.outputs.digest }} diff --git a/README.md b/README.md index 236c7e7..4fba7f8 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,44 @@ jobs: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} ``` +## Verifying the image + +Container images pushed by this repository can be verified to have been built +using our CI workflows, meaning that you can take one of our images and trace it +back to the source commit and workflow run from which it was built. This uses +[GitHub's artefact attestation][attestation] support. [This page][attestation] +contains instructions for how to verify the attestations, including in +Kubernetes clusters and offline environments. + +As a brief example, the `gh` CLI can be used in an to verify the attestation +_online_: + +```console +$ gh attestation verify --bundle-from-oci --repo grafana/wait-for-github oci://grafana/wait-for-github:main +Loaded digest sha256:83af77d5e81326dee6593937688a27916a2bb5da7886cec095b8de75cb9744e1 for oci://grafana/wait-for-github:main +Loaded 1 attestation from GitHub API + +[...] + +✓ Verification succeeded! + +The following 1 attestation matched the policy criteria + +- Attestation #1 + - Build repo:..... grafana/wait-for-github + - Build workflow:. .github/workflows/build.yml@refs/heads/main + - Signer repo:.... grafana/wait-for-github + - Signer workflow: .github/workflows/build.yml@refs/heads/main +``` + +What this shows is that the image `grafana/wait-for-github:main` was built from +the `grafana/wait-for-github` repository using the workflow given in the +command's output. Re-run the command with `--format=json` to see all of the +information contained within the attestation, for example a link to the commit +and the build themselves. + +[attestation]: https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations + ## Contributing Contributions via issues and GitHub PRs are very welcome. We'll try to be From 1ee603c4777c6cce3ccfaf0740eee59596966fd6 Mon Sep 17 00:00:00 2001 From: Iain Lane Date: Tue, 18 Feb 2025 17:43:44 +0000 Subject: [PATCH 2/2] run on push for testing --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4398d6..ffbe83b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,6 +11,7 @@ on: push: branches: - main + - iainlane/build-multiarch-natively paths: - go.mod - go.sum @@ -156,6 +157,8 @@ jobs: type=ref,event=branch,enable={{is_default_branch}} # tag with semver, and `latest` type=ref,event=tag + # for testing + type=ref,event=branch - name: Login to DockerHub uses: grafana/shared-workflows/actions/dockerhub-login@13fb504e3bfe323c1188bf244970d94b2d336e86 # dockerhub-login-v1.0.1