Add reusable actions for native BuildKit build in GHA#273
Add reusable actions for native BuildKit build in GHA#273
Conversation
This PR fundamentally changes how our images are built. The usage of the Builder container is dropped in favor of "native" build using BuildKit with docker/build-push-action. Dockerfiles are now the single source of truth for all labels and build arguments - the build metadata (version, date, architecture, repository) is passed via --build-arg and consumed directly in the Dockerfile's LABEL instruction, removing the need for external label injection. Build caching uses GitHub Actions cache as the primary backend, with inline cache metadata embedded in pushed images as a fallback for cache reuse across git refs (since GHA cache is scoped per branch/tag). Registry images are verified with cosign before being used as cache sources. Images are compressed with zstd (level 9) instead of gzip, reducing image size and improving pull times on registries and runtimes that support it. Multi-arch support is handled by building per-architecture images in parallel on native runners (amd64 on ubuntu-24.04, aarch64 on ubuntu-24.04-arm), then combining them into a single manifest list using docker buildx imagetools. Thanks to the caching, the builder workflow now also runs on push to the master branch, keeping the GHA cache warm for release builds without adding significant CI cost. A reference implementation is in home-assistant/docker-base#347.
The common convention are dashes, stick with that.
actions/build-image/action.yml
Outdated
| build_args+=("BUILD_VERSION=${VERSION}") | ||
| build_args+=("BUILD_ARCH=${ARCH}") | ||
| build_args+=("BUILD_DATE=${build_date}") | ||
| build_args+=("BUILD_REPOSITORY=https://github.com/${GITHUB_REPOSITORY}") |
There was a problem hiding this comment.
We should pass this one as a build arg as core is not using it.
I would recommend putting this directly in the Dockerfile, as it is static
There was a problem hiding this comment.
It's not static because IMHO the builds from forks shouldn't have it the same. I took inspiration here from Frenck's workflows.
I think it won't do any harm if we add it to the Core image, and when we start using this shared action for it, it will be filled out correctly.
actions/build-image/action.yml
Outdated
| } >> "$GITHUB_OUTPUT" | ||
|
|
||
| if [[ "${PUSH}" == "true" ]]; then | ||
| echo output="type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true" >> "$GITHUB_OUTPUT" |
There was a problem hiding this comment.
| echo output="type=image,push=true,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true" >> "$GITHUB_OUTPUT" | |
| echo output="type=image,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true" >> "$GITHUB_OUTPUT" |
Can be removed as the action has a dedicated attribute for it.
Why are we setting compression and co only when we are pushing an image?
There was a problem hiding this comment.
We can drop the push, right.
No strong reason why not to keep the remaining arguments when the image is not pushed, except maybe it will be a bit slower because the zstd level 9 compression will be slower than the default gzip.
I'm changing it to keep the oci-mediatypes shared but keeping the compression only for the pushed image. We need to conditionally set the type anyway, let me know if you think there's some reason to preserve the compression for both.
There was a problem hiding this comment.
In my opinion, we should keep non-push and push the same (except of pushing it) as so we make sure that we have tested everything. I don#t expect that there could be an error in compression but you never know, so I prefer that both do the same
If there are no steps following the build that require the image, the load is pointless and may just delay the build with no real benefit.
This PR completely drops usage of the builder action in favor of new actions introduced in home-assistant/builder#273. This results in faster builds with better caching options and simple local builds using Docker BuildKit. The image dependency chain currently still uses per-arch builds but once docker-base and docker repositories start publishing multi-arch images, we can simplify the action a bit further. The idea to use composite actions comes from #162245 and this PR fully predates it. There is minor difference that the files generated twice in per-arch builds are now generated and archived by the init job.
agners
left a comment
There was a problem hiding this comment.
I like the re-use of cosign-verify. Also that we sign the actual image digest now, it's what cosign recommends 👍 Looks quite good to me 💪. Some minor nits/comments.
.github/workflows/builder.yml
Outdated
|
|
||
| - name: Build image | ||
| id: build | ||
| uses: home-assistant/builder/actions/build-image@gha-builder |
There was a problem hiding this comment.
What is our strategy wrt to versioning this action?
There was a problem hiding this comment.
Short-term I wanted to keep making releases in the builder repo and using the tag as the reference. However, I'd like to split all our actions to separate repos with separate releases, sha-pinning and such. But it's a topic for another discussion.
There was a problem hiding this comment.
As discussed elsewhere, to avoid creating too many repos we can keep the builder related actions in this/a single git repository, and release them all at once. sha-pinning etc. should work.
There was a problem hiding this comment.
Yeah, this would be the way in the end - forgot to update the comment.
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
.github/workflows/builder.yml
Outdated
| image-tag: | ||
| description: Base image tag (e.g., "3.23") | ||
| required: true | ||
| type: string | ||
| image-extra-tags: | ||
| description: Additional tags, one per line | ||
| required: false | ||
| default: "" | ||
| type: string |
There was a problem hiding this comment.
Can we combine these two arguments into one like the builder action does?
Two ways to specify tags:
- A string value
- A list of values
actions/build-image/action.yml
Outdated
| build_args+=("BUILD_VERSION=${VERSION}") | ||
| build_args+=("BUILD_ARCH=${ARCH}") | ||
| build_args+=("BUILD_DATE=${build_date}") | ||
| build_args+=("BUILD_REPOSITORY=https://github.com/${GITHUB_REPOSITORY}") |
There was a problem hiding this comment.
After thinking this over, I suggest that we don't set any build args by default and that the user should specify them. This makes it clearer which one will be used.
Also, for example, the input arg version is only used to set a build arg, which I think is better if the caller specifies it directly. If the caller is specifying it, it has the advantage that he can also control the name and does not need to go through the builder to find out which ones are set automatically.
Also, in case of the input arg version, it is required, and so the caller cannot use it without. For example, if he uses it somewhere, where he doesn't want to specify the version (Intermediate version or version just used in build pipelines)
There was a problem hiding this comment.
I'm a bit lost, what exactly you want to remove from defaults? These in the action?
I meant to use sane defaults for all arguments, so it will be easy to reuse the action/workflow it without unnecessary boilerplate. If you want to remove it from the action, I'd at least prefer to keep it in the workflow to make reuse in plugin/apps build easy. And e.g. in the case of BUILD_DATE I think there's no benefit at all in requiring the user to fill it out. The action is meant to abstract common patterns of building our images which share common structure - otherwise we'd make it just a generic "docker/build-push-and-cosign" action which will require boilerplate in every repo.
Edit: Turns out that calling reusable workflow from another repo causes issues with Cosign signing, so this is not the way.
There was a problem hiding this comment.
This has been largely refactored after the discussion in the Core PR. The only build args passed now are BUILD_ARCH and BUILD_VERSION. The first one is used even in some of HA containers to handle different architectures, the other one is rather to keep the contract given previously: https://developers.home-assistant.io/docs/apps/configuration?_highlight=build_version#build-args
The BUILD_FROM will be removed here (the source of it was build.yaml), while Supervisor will likely keep passing it from the build.yaml for a while until we drop the support for it there too.
| description: Build, push, and optionally sign a single-arch container image | ||
|
|
||
| inputs: | ||
| arch: |
There was a problem hiding this comment.
Can we sort the arguments somehow?
Or alphabetically, or maybe all required one first and then the optional ones.
There was a problem hiding this comment.
I tried to sort them kinda "semantically" but it gets out of hands. I'll adjust the names of some to keep them in clusters and sort alphabetically.
|
|
||
| inputs: | ||
| image: | ||
| description: Container image reference to verify |
There was a problem hiding this comment.
Can we be consistent with specifying if something is required or not?
Other actions have always specified the required field and we should do it here too for consistency
There was a problem hiding this comment.
Will do. Missed that after I refactored Cosign into an action.
…ulti-arch-manifest actions
Unlike in reusable workflow, in action it's evaluated with the repository from where the action is called.
It's passed by the legacy builder and by Supervisor, keep it for backward compat.
actions/build-image/action.yml
Outdated
| @@ -0,0 +1,276 @@ | |||
| name: Build image | |||
| description: Build, push, and optionally sign a single-arch container image | |||
There was a problem hiding this comment.
Nit: Technically, push is optional too.
| description: Build, push, and optionally sign a single-arch container image | |
| description: Build, and optionally push and sign a single-arch container image |
This PR provides a set of reusable composite actions that replace the Builder container with "native" BuildKit builds using docker/build-push-action.
actions/build-imagebuilds and optionally pushes and signs a single-architecture image. Build metadata (BUILD_ARCH,BUILD_VERSION) is passed to the Dockerfile via--build-arg, while OCI and Home Assistant labels (io.hass.arch,io.hass.version,org.opencontainers.image.*) are applied directly by the action through docker/build-push-action's label support. Additional build args and labels can be passed through thebuild-argsandlabelsinputs. Images are compressed with zstd (level 9) instead of gzip, reducing image size and improving pull times on registries and runtimes that support it. Build caching uses GitHub Actions cache as the primary backend, with inline cache metadata embedded in pushed images as a fallback for cache reuse across git refs (since GHA cache is scoped per branch/tag). Pushed images are signed with Cosign, with retry and exponential backoff. Base and cache images can optionally be verified before the build starts.actions/cosign-verifyverifies the Cosign signature of a container image against a certificate identity and OIDC issuer, with retry logic and an optional allow-failure mode.actions/prepare-multi-arch-matrixvalidates the requested architectures (amd64, aarch64) and outputs a JSON matrix mapping each to a native runner (ubuntu-24.04, ubuntu-24.04-arm) and a registry image name, ready to be consumed by a build matrix job.actions/publish-multi-arch-manifestcombines per-architecture images into a single manifest list usingdocker buildx imagetools create, applies all requested tags, and signs the resulting manifest with Cosign.Together, these actions support a workflow where per-architecture images are built in parallel on native runners, then combined into a multi-arch manifest. Thanks to the caching, the build can also run on push to the master branch to keep the GHA cache warm for release builds without adding significant CI cost.
A reference implementation is in home-assistant/docker-base#347.