Skip to content

Commit b924871

Browse files
committed
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
1 parent 1477ddb commit b924871

File tree

2 files changed

+182
-6
lines changed

2 files changed

+182
-6
lines changed

.github/workflows/build.yml

Lines changed: 144 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,29 +21,167 @@ on:
2121
merge_group:
2222

2323
jobs:
24-
main:
24+
build:
2525
permissions:
26+
attestations: write
2627
contents: read
2728
id-token: write
2829

29-
runs-on: ubuntu-latest
30+
strategy:
31+
matrix:
32+
runner:
33+
- ubuntu-24.04
34+
- ubuntu-24.04-arm
35+
36+
name: Build and push Docker image for ${{ matrix.runner }}
37+
38+
runs-on: ${{ matrix.runner }}
39+
40+
outputs:
41+
digest: ${{ steps.build.outputs.digest }}
42+
3043
steps:
3144
- name: Checkout
3245
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
3346
with:
3447
persist-credentials: false
3548

49+
- name: Login to DockerHub
50+
if: github.event_name == 'push'
51+
uses: grafana/shared-workflows/actions/dockerhub-login@13fb504e3bfe323c1188bf244970d94b2d336e86 # dockerhub-login-v1.0.1
52+
3653
- name: Set Docker Buildx up
3754
uses: docker/setup-buildx-action@f7ce87c1d6bead3e36075b2ce75da1f6cc28aaca # v3.9.0
3855

39-
- name: Build Docker image
40-
uses: grafana/shared-workflows/actions/build-push-to-dockerhub@402975d84dd3fac9ba690f994f412d0ee2f51cf4 # build-push-to-dockerhub-v0.1.1
56+
# No tags
57+
- name: Build and push Docker image
58+
id: build
59+
uses: docker/build-push-action@ca877d9245402d1537745e0e356eab47c3520991 # v6.13.0
4160
with:
42-
platforms: linux/amd64,linux/arm64
61+
cache-from: type=gha
62+
cache-to: type=gha,mode=max
63+
outputs: type=image,"name=grafana/wait-for-github",push-by-digest=true,name-canonical=true
64+
provenance: true
4365
push: ${{ github.event_name == 'push' }}
66+
sbom: false
67+
68+
- name: Export digests
69+
if: github.event_name == 'push'
70+
id: export-digests
71+
env:
72+
DIGEST: ${{ steps.build.outputs.digest }}
73+
RUNNER_TEMP: ${{ runner.temp }}
74+
run: |
75+
# The digest of the _index_ - this is what we ultimately push, and
76+
# what we need to refer to in the multi-arch manifest.
77+
mkdir -pv "${RUNNER_TEMP}"/artifact/digests
78+
touch "${RUNNER_TEMP}/artifact/digests/${DIGEST#sha256:}"
79+
80+
# The digest of the _manifest_ referred to by the index. When `docker
81+
# buildx imagetools create` processes its inputs, it creates a new
82+
# combines these manifest references into a new index. So we should
83+
# attest this digest, then clients can find it given the multiarch
84+
# index, by dereferncing to the per-arch manifests and looking at the
85+
# referrers on them.
86+
docker buildx imagetools inspect "grafana/wait-for-github@${DIGEST}" --raw | \
87+
jq \
88+
--raw-output \
89+
'.manifests[] |
90+
select (
91+
.mediaType == "application/vnd.oci.image.manifest.v1+json" and .annotations["vnd.docker.reference.type"] == null
92+
) |
93+
.digest |
94+
@sh' | \
95+
( echo -n 'digest=' && cat ) | \
96+
97+
- name: Generate SBOM
98+
if: github.event_name == 'push'
99+
uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.18.0
100+
with:
101+
format: cyclonedx-json
102+
image: grafana/wait-for-github@${{ steps.export-digests.outputs.digest }}
103+
output-file: ${{ runner.temp }}/sbom-${{ matrix.runner }}.json
104+
105+
- name: Generate SBOM attestation
106+
if: github.event_name == 'push'
107+
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
108+
with:
109+
push-to-registry: true
110+
subject-digest: ${{ steps.export-digests.outputs.digest }}
111+
subject-name: index.docker.io/grafana/wait-for-github
112+
sbom-path: ${{ runner.temp }}/sbom-${{ matrix.runner }}.json
113+
114+
- name: Upload artifact
115+
if: github.event_name == 'push'
116+
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
117+
with:
118+
name: artifacts-${{ matrix.runner }}
119+
path: ${{ runner.temp }}/artifact/
120+
if-no-files-found: error
121+
retention-days: 1
122+
123+
manifest:
124+
if: github.event_name == 'push'
125+
126+
needs:
127+
- build
128+
129+
permissions:
130+
attestations: write
131+
id-token: write
132+
133+
name: Generate multi-arch manifest list and build provenance attestation
134+
135+
runs-on: ubuntu-24.04
136+
137+
outputs:
138+
digest: ${{ steps.inspect.outputs.digest }}
139+
140+
steps:
141+
- name: Download artifacts
142+
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
143+
with:
144+
merge-multiple: true
145+
path: ${{ runner.temp }}/artifacts
146+
pattern: artifacts-*
147+
148+
- name: Extract metadata (tags, labels) for Docker
149+
id: meta
150+
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
151+
with:
152+
images: grafana/wait-for-github
153+
sep-tags: ' '
44154
tags: |
45155
# tag with branch name for `main`
46156
type=ref,event=branch,enable={{is_default_branch}}
47157
# tag with semver, and `latest`
48158
type=ref,event=tag
49-
repository: grafana/wait-for-github
159+
160+
- name: Login to DockerHub
161+
uses: grafana/shared-workflows/actions/dockerhub-login@13fb504e3bfe323c1188bf244970d94b2d336e86 # dockerhub-login-v1.0.1
162+
163+
- name: Create manifest list and push
164+
working-directory: ${{ runner.temp }}/artifacts/digests
165+
run: |
166+
docker buildx imagetools create $(jq --compact-output --raw-output '.tags | map("-t " + .) | join(" ")' <<< "${DOCKER_METADATA_OUTPUT_JSON}") \
167+
$(printf 'grafana/wait-for-github@sha256:%s ' *)
168+
169+
- name: Inspect image
170+
id: inspect
171+
env:
172+
VERSION: ${{ steps.meta.outputs.version }}
173+
run: |
174+
docker buildx imagetools inspect "grafana/wait-for-github:${VERSION}"
175+
176+
# Output image digest as github output
177+
docker buildx imagetools inspect "grafana/wait-for-github:${VERSION}" --format "{{json .Manifest.Digest}}" | \
178+
xargs | \
179+
( echo -n 'digest=' && cat ) | \
180+
tee -a "${GITHUB_OUTPUT}"
181+
182+
- name: Generate build provenance attestation
183+
uses: actions/attest-build-provenance@520d128f165991a6c774bcb264f323e3d70747f4 # v2.2.0
184+
with:
185+
push-to-registry: true
186+
subject-name: index.docker.io/grafana/wait-for-github
187+
subject-digest: ${{ steps.inspect.outputs.digest }}

README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,44 @@ jobs:
221221
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
222222
```
223223
224+
## Verifying the image
225+
226+
Container images pushed by this repository can be verified to have been built
227+
using our CI workflows, meaning that you can take one of our images and trace it
228+
back to the source commit and workflow run from which it was built. This uses
229+
[GitHub's artefact attestation][attestation] support. [This page][attestation]
230+
contains instructions for how to verify the attestations, including in
231+
Kubernetes clusters and offline environments.
232+
233+
As a brief example, the `gh` CLI can be used in an to verify the attestation
234+
_online_:
235+
236+
```console
237+
$ gh attestation verify --bundle-from-oci --repo grafana/wait-for-github oci://grafana/wait-for-github:main
238+
Loaded digest sha256:83af77d5e81326dee6593937688a27916a2bb5da7886cec095b8de75cb9744e1 for oci://grafana/wait-for-github:main
239+
Loaded 1 attestation from GitHub API
240+
241+
[...]
242+
243+
✓ Verification succeeded!
244+
245+
The following 1 attestation matched the policy criteria
246+
247+
- Attestation #1
248+
- Build repo:..... grafana/wait-for-github
249+
- Build workflow:. .github/workflows/build.yml@refs/heads/main
250+
- Signer repo:.... grafana/wait-for-github
251+
- Signer workflow: .github/workflows/build.yml@refs/heads/main
252+
```
253+
254+
What this shows is that the image `grafana/wait-for-github:main` was built from
255+
the `grafana/wait-for-github` repository using the workflow given in the
256+
command's output. Re-run the command with `--format=json` to see all of the
257+
information contained within the attestation, for example a link to the commit
258+
and the build themselves.
259+
260+
[attestation]: https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations
261+
224262
## Contributing
225263

226264
Contributions via issues and GitHub PRs are very welcome. We'll try to be

0 commit comments

Comments
 (0)