Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
24bbd9f
Add reusable workflow for native BuildKit build in GHA
sairon Mar 4, 2026
75583a2
Use dashes instead of underscores in inputs
sairon Mar 4, 2026
677f4d3
Move actions to actions folder
sairon Mar 4, 2026
7a7fab5
Improve (multi-)arch validation, fix cache-from tag
sairon Mar 4, 2026
f1b88d0
Address review comments - remove platform, adjust output argument
sairon Mar 4, 2026
89b3247
Make gha cache configurable
sairon Mar 4, 2026
2335ceb
Add 'load' input and default to false
sairon Mar 4, 2026
28a77db
Don't set any output if image is not pushed nor loaded
sairon Mar 4, 2026
433b20d
Change builder workflow name
sairon Mar 4, 2026
78b43ea
Specify required and defaults in cosign-verify
sairon Mar 5, 2026
e93d246
Docker registry -> container registry, context description update
sairon Mar 5, 2026
88a0820
Sort inputs alphabetically, rename to keep semantic clustering
sairon Mar 5, 2026
ea75d5e
Merge image tags into a single argument
sairon Mar 5, 2026
8d2380f
Fix container-(username|password) -> container-registry-(username|pas…
sairon Mar 5, 2026
a5d42ec
Drop builder workflow, create prepare-multi-arch-matrix and publish-m…
sairon Mar 5, 2026
52b5620
Set default registry-prefix
sairon Mar 5, 2026
e5d272b
Print matrix at the end of prepare-multi-arch-matrix
sairon Mar 5, 2026
c711632
Set dynamic arch/repo/version labels in build-image action, add label…
sairon Mar 10, 2026
48c5edc
Reintroduce BUILD_VERSION
sairon Mar 10, 2026
4a7d1db
Bump to docker/login-action v4.0.0 (as in #274)
sairon Mar 10, 2026
69243fc
Use imagetools metadata-file as source for digest when signing manifest
sairon Mar 10, 2026
eee43c8
Adjust build-image description
sairon Mar 10, 2026
2426beb
Set same params for output type=image and type=docker
sairon Mar 10, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
name: Set tag envronment variable

- name: Login to GitHub Container Registry
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down
276 changes: 276 additions & 0 deletions actions/build-image/action.yml
Original file line number Diff line number Diff line change
@@ -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


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.

description: Architecture to build (e.g., "amd64")
required: true
build-args:
description: Additional build arguments (key=value format, one per line)
required: false
default: ""
cache-gha:
description: Whether to use GitHub Actions cache for build caching
required: false
default: "true"
cache-gha-scope:
description: Scope for build cache sharing (defaults to architecture, set if building multiple images from a single repo)
required: false
default: ""
cache-image-tag:
description: Tag of the image containing BuildKit inline cache metadata
required: false
default: "latest"
container-registry-password:
description: Password for container registry (use secrets.GITHUB_TOKEN for GHCR)
required: true
container-registry:
description: Container registry (e.g., "ghcr.io")
required: false
default: "ghcr.io"
container-registry-username:
description: Username for container registry (defaults to repository owner)
required: false
default: ${{ github.repository_owner }}
context:
description: Build context path (usually the directory with Dockerfile)
required: true
cosign:
description: Whether to sign images with Cosign
required: false
default: "true"
cosign-base-identity:
description: Certificate identity regexp for verifying the base (FROM) image
required: false
default: ""
cosign-base-issuer:
description: Certificate OIDC issuer regexp for base image verification (defaults to cosign-issuer)
required: false
default: ""
cosign-base-verify:
description: Base image reference to verify with cosign before building
required: false
default: ""
cosign-identity:
description: Certificate identity regexp for verifying cache images (defaults to current repo pattern)
required: false
default: ""
cosign-issuer:
description: Certificate OIDC issuer regexp for all cosign verification
required: false
default: "https://token.actions.githubusercontent.com"
file:
description: Dockerfile path (defaults to "Dockerfile" in the context directory)
required: false
default: ""
image:
description: Full image name without tag (e.g., "ghcr.io/home-assistant/amd64-base")
required: true
image-tags:
description: Image tags, one per line
required: true
labels:
description: Additional OCI labels (key=value format, one per line)
required: false
default: ""
load:
description: Whether to load the built image into the Docker engine
required: false
default: "false"
push:
description: Whether to push images to registry
required: false
default: "false"
version:
description: Image version label
required: true

outputs:
digest:
description: Image digest from the build
value: ${{ steps.build.outputs.digest }}

runs:
using: composite
steps:
- name: Login to container registry
if: inputs.push == 'true'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with:
registry: ${{ inputs.container-registry }}
username: ${{ inputs.container-registry-username }}
password: ${{ inputs.container-registry-password }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Install Cosign
if: inputs.cosign == 'true' || inputs.cosign-base-verify != ''
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
with:
cosign-release: "v2.5.3"

- name: Verify cache image ${{ inputs.image }}:${{ inputs.cache-image-tag }}
if: inputs.cosign == 'true'
id: verify_cache
uses: home-assistant/builder/actions/cosign-verify@gha-builder
with:
image: ${{ inputs.image }}:${{ inputs.cache-image-tag }}
cosign-identity: ${{ inputs.cosign-identity || env.DEFAULT_COSIGN_IDENTITY }}
cosign-issuer: ${{ inputs.cosign-issuer }}
allow-failure: true
env:
DEFAULT_COSIGN_IDENTITY: https://github.com/${{ github.repository }}/.*

- name: Verify base image
if: inputs.push == 'true' && inputs.cosign-base-verify != '' && inputs.cosign-base-identity != ''
uses: home-assistant/builder/actions/cosign-verify@gha-builder
with:
image: ${{ inputs.cosign-base-verify }}
cosign-identity: ${{ inputs.cosign-base-identity }}
cosign-issuer: ${{ inputs.cosign-base-issuer || inputs.cosign-issuer }}
allow-failure: false

- name: Set build options
id: options
shell: bash
env:
IMAGE: ${{ inputs.image }}
IMAGE_TAGS: ${{ inputs.image-tags }}
PUSH: ${{ inputs.push }}
LOAD: ${{ inputs.load }}
VERSION: ${{ inputs.version }}
ARCH: ${{ inputs.arch }}
GITHUB_REPOSITORY: ${{ github.repository }}
BUILD_ARGS_INPUT: ${{ inputs.build-args }}
LABELS_INPUT: ${{ inputs.labels }}
COSIGN_ENABLED: ${{ inputs.cosign }}
CACHE_GHA: ${{ inputs.cache-gha }}
CACHE_SCOPE: ${{ inputs.cache-gha-scope }}
CACHE_IMAGE_TAG: ${{ inputs.cache-image-tag }}
CACHE_VERIFIED: ${{ steps.verify_cache.outputs.verified }}
FILE_INPUT: ${{ inputs.file }}
CONTEXT: ${{ inputs.context }}
run: |
tags=()
while IFS= read -r tag; do
[[ -n "$tag" ]] && tags+=("${IMAGE}:${tag}")
done <<< "${IMAGE_TAGS}"

{
echo "tags<<EOF"
printf '%s\n' "${tags[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

build_date="$(date --rfc-3339=seconds --utc)"

build_args=()
build_args+=("BUILD_ARCH=${ARCH}")
build_args+=("BUILD_VERSION=${VERSION}")
while IFS= read -r line; do
[[ -n "$line" ]] && build_args+=("$line")
done <<< "${BUILD_ARGS_INPUT}"
{
echo "build_args<<EOF"
printf '%s\n' "${build_args[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

labels=()
labels+=("io.hass.arch=${ARCH}")
labels+=("io.hass.version=${VERSION}")
labels+=("org.opencontainers.image.created=${build_date}")
labels+=("org.opencontainers.image.source=https://github.com/${GITHUB_REPOSITORY}")
labels+=("org.opencontainers.image.version=${VERSION}")
while IFS= read -r line; do
[[ -n "$line" ]] && labels+=("$line")
done <<< "${LABELS_INPUT}"
{
echo "labels<<EOF"
printf '%s\n' "${labels[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

if [[ "${PUSH}" == "true" ]]; then
echo "output=type=image,compression=zstd,compression-level=9,force-compression=true,oci-mediatypes=true" >> "$GITHUB_OUTPUT"
elif [[ "${LOAD}" == "true" ]]; then
echo "output=type=docker,oci-mediatypes=true" >> "$GITHUB_OUTPUT"
fi

if [[ -z "${CACHE_SCOPE}" ]]; then
cache_scope="${ARCH}"
else
cache_scope="${ARCH}-${CACHE_SCOPE}"
fi
echo cache_scope="${cache_scope}" >> "$GITHUB_OUTPUT"

cache_from=()
if [[ "${CACHE_GHA}" == "true" ]]; then
cache_from+=("type=gha,scope=${cache_scope}")
fi
if [[ "${COSIGN_ENABLED}" != "true" ]] || [[ "${CACHE_VERIFIED}" == "true" ]]; then
cache_from+=("type=registry,ref=${IMAGE}:${CACHE_IMAGE_TAG}")
fi
{
echo "cache_from<<EOF"
printf '%s\n' "${cache_from[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

cache_to=("type=inline")
if [[ "${CACHE_GHA}" == "true" ]]; then
cache_to+=("type=gha,mode=max,scope=${cache_scope}")
fi
{
echo "cache_to<<EOF"
printf '%s\n' "${cache_to[@]}"
echo "EOF"
} >> "$GITHUB_OUTPUT"

if [ -z "${FILE_INPUT}" ]; then
echo file="${CONTEXT}/Dockerfile" >> "$GITHUB_OUTPUT"
else
echo file="${FILE_INPUT}" >> "$GITHUB_OUTPUT"
fi

- name: Build image
id: build
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: ${{ env.CONTEXT }} # zizmor: ignore[template-injection]
file: ${{ steps.options.outputs.file }}
pull: true
push: ${{ inputs.push == 'true' }}
load: ${{ inputs.load == 'true' }}
build-args: ${{ steps.options.outputs.build_args }}
labels: ${{ steps.options.outputs.labels }}
tags: ${{ steps.options.outputs.tags }}
outputs: ${{ steps.options.outputs.output }}
cache-to: ${{ steps.options.outputs.cache_to }}
cache-from: ${{ steps.options.outputs.cache_from }}

env:
CONTEXT: ${{ inputs.context }}

- name: Sign per-arch image
if: inputs.push == 'true' && inputs.cosign == 'true'
shell: bash
env:
IMAGE_REF: ${{ inputs.image }}@${{ steps.build.outputs.digest }}
run: |
echo "::group::Signing image: ${IMAGE_REF}"

for i in {1..5}; do
if cosign sign --yes "${IMAGE_REF}"; then
echo "Signed: ${IMAGE_REF}"
exit 0
fi
echo "Signing attempt ${i} failed, retrying..."
sleep $((2 ** i))
done

echo "::endgroup::"

echo "::error::Failed to sign image ${IMAGE_REF}"
exit 1
64 changes: 64 additions & 0 deletions actions/cosign-verify/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Verify a signature on the supplied container image
description: |
Verify Cosign signature of a container image.
Requires Cosign to be installed in the runner environment.

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.

required: true
cosign-identity:
description: Certificate identity regexp for verifying cache images (defaults to current repo pattern)
required: false
default: ""
cosign-issuer:
description: Certificate OIDC issuer regexp for all cosign verification
required: false
default: "https://token.actions.githubusercontent.com"
allow-failure:
description: Whether to allow failure of this step (defaults to false). Only shows a warning if verification fails.
required: false
default: "false"

outputs:
verified:
description: Whether the image was successfully verified with Cosign
value: ${{ steps.verify.outputs.verified }}

runs:
using: composite
steps:
- name: Verify the image
id: verify
shell: bash
env:
IMAGE_REF: ${{ inputs.image }}
COSIGN_IDENTITY: ${{ inputs.cosign-identity }}
COSIGN_ISSUER: ${{ inputs.cosign-issuer }}
ALLOW_FAILURE: ${{ inputs.allow-failure }}
run: |
echo "::group::Verifying image: ${IMAGE_REF}"

for i in {1..5}; do
if cosign verify \
--certificate-identity-regexp "${COSIGN_IDENTITY}" \
--certificate-oidc-issuer-regexp "${COSIGN_ISSUER}" \
"${IMAGE_REF}"; then
echo "Image verified: ${IMAGE_REF}"
echo "verified=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Verification attempt ${i} failed, retrying..."
sleep $((2 ** i))
done

echo "::endgroup::"

echo "verified=false" >> "$GITHUB_OUTPUT"

if [[ "${ALLOW_FAILURE}" == "true" ]]; then
echo "::warning::Image verification failed for ${IMAGE_REF}, ignoring"
else
echo "::error::Image verification failed for ${IMAGE_REF}"
exit 1
fi
Loading