Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: build multiarch images natively #214

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 147 additions & 6 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ on:
push:
branches:
- main
- iainlane/build-multiarch-natively
paths:
- go.mod
- go.sum
Expand All @@ -21,29 +22,169 @@ 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
# for testing
type=ref,event=branch

- 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 }}
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading