Skip to content

Add reusable actions for native BuildKit build in GHA#273

Open
sairon wants to merge 23 commits intomasterfrom
gha-builder
Open

Add reusable actions for native BuildKit build in GHA#273
sairon wants to merge 23 commits intomasterfrom
gha-builder

Conversation

@sairon
Copy link
Member

@sairon sairon commented Mar 4, 2026

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-image builds 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 the build-args and labels inputs. 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-verify verifies 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-matrix validates 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-manifest combines per-architecture images into a single manifest list using docker 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.

sairon added 2 commits March 4, 2026 09:40
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.
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}")
Copy link
Member

@edenhaus edenhaus Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

} >> "$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"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

sairon added a commit to home-assistant/core that referenced this pull request Mar 4, 2026
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.
@sairon sairon requested a review from edenhaus March 4, 2026 17:37
Copy link
Member

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


- name: Build image
id: build
uses: home-assistant/builder/actions/build-image@gha-builder
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is our strategy wrt to versioning this action?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this would be the way in the end - forgot to update the comment.

@home-assistant
Copy link

home-assistant bot commented Mar 5, 2026

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@home-assistant home-assistant bot marked this pull request as draft March 5, 2026 09:33
Comment on lines +23 to +31
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +170 to +173
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}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Member Author

@sairon sairon Mar 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we sort the arguments somehow?
Or alphabetically, or maybe all required one first and then the optional ones.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. Missed that after I refactored Cosign into an action.

It's passed by the legacy builder and by Supervisor, keep it for backward
compat.
@@ -0,0 +1,276 @@
name: Build image
description: Build, push, and optionally sign a single-arch container image
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Technically, push is optional too.

Suggested change
description: Build, push, and optionally sign a single-arch container image
description: Build, and optionally push and sign a single-arch container image

Copy link
Member

@agners agners left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@sairon sairon marked this pull request as ready for review March 10, 2026 16:29
@home-assistant home-assistant bot requested review from agners and edenhaus March 10, 2026 16:29
@sairon sairon changed the title Add reusable workflow for native BuildKit build in GHA Add reusable actions for native BuildKit build in GHA Mar 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants