Files
Aleksei Sviridkin 9a7094427e feat(build): stitch amd64 and arm64 builds into multi-arch indexes
The arm64 images are built on native arm64 runners, apart from the
amd64 build, so each build pushes a single-platform image. The stitch
joins every first-party image with its -arm64 twin into one index,
moves every tag this build pushed for it to that index, and repins
the tree on the index digest, so one ref serves both architectures.

Most refs in the tree name only the build's IMAGE_TAG, while under
PUBLISH_VERSIONED=1 the build also pushes the component version, so
the refs alone cannot say which tags to move. The image-tags macro
therefore appends every tag it pushes to PUSHED_TAGS_LOG when that is
set, and the stitch moves exactly those, plus IMAGE_TAG and the tag a
ref carries. It reads no other tag: another build's tag that sits on
the same amd64 bytes is not this build's to move, and scanning every
tag of every repository would cost thousands of registry reads per
release.

It reads refs through hack/lib/image-refs.sh like every other
consumer, rewrites only the files that enumeration names, and then
searches the whole tree outside charts/, gzip payloads included, for
any old digest it re-pointed. A ref whose digest is also pinned where
the rewrite cannot reach is skipped instead of half rewritten.

The kamaji control-plane provider's chart ships its ref only inside
files/components.gz, built from a plain copy with gzip -n, which no
text search sees. The stitch rewrites both copies, recompressing with
gzip -n so the result is what the package's image target would have
written, rather than leaving that image amd64 only.

An image with no arm64 twin (talos, the e2e sandbox), or a pair that
is not one amd64 and one arm64 image, is skipped and named. A
registry read answered with a rate limit or a 5xx is retried a
bounded number of times; any other registry error fails the run at
once rather than reading as a missing arm64 image.

On a rerun after a stitch that failed partway, the build tag can
already sit on an index of the pinned image and an arm64 image, not
necessarily this run's if the arm64 leg rebuilt it. The stitch repins
on that index instead of skipping the image, and copies it to any of
the build's other tags still left on the amd64 image. A tag moved to
anything else fails the run, since the tree then no longer matches
the registry.

MATRIX_ARCH=arm64 makes hack/build-matrix.sh leave out the e2e
sandbox, which pins linux/amd64: an arm64 leg building it would push
a second amd64 image under its -arm64 tag.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-27 07:57:30 +03:00

94 lines
3.7 KiB
Bash
Executable File

#!/bin/sh
# Emit the JSON build matrix (the list of package dirs to build) for CI.
#
# Single source of truth: the `make -C packages/<dir> image` lines in the root
# Makefile `build:` target. Parsing them here keeps the CI matrix from drifting
# away from what `make build` actually builds.
#
# Usage: hack/build-matrix.sh [<changed-files>]
# no arg / empty / "FULL" -> every build unit (full matrix)
# <changed-files> -> only the units whose package dir has a changed
# file, UNLESS a shared-dependency path changed, in
# which case the full matrix is emitted (a change to
# cozy-lib / the build macros / go.mod / the build
# workflows can affect any image, so rebuild all).
#
# MATRIX_ARCH=arm64 also drops packages/core/testing, which pins linux/amd64
# (an x86 KVM sandbox): an arm64 leg building it would push a second amd64
# image under its -arm64 tag.
#
# Output: a JSON array of package dirs on stdout, e.g.
# ["packages/apps/mariadb","packages/system/dashboard"]
# An empty selection prints `[]` -> the matrix produces zero build jobs.
set -eu
MAKEFILE="${MAKEFILE:-Makefile}"
# Paths that force a full rebuild when touched. Kept deliberately broad — a
# false full-rebuild only costs time, a missed dependent ships a stale image.
full_rebuild_pattern='^(packages/library/|api/|cmd/|internal/|pkg/|hack/common-envs\.mk|hack/package\.mk|Makefile$|go\.mod$|go\.sum$|\.github/workflows/(pull-requests|build-main)\.yaml$|hack/build-matrix\.sh$)'
# The build units, parsed from the `build:` recipe (from `build:` to the next
# line that starts in column 0).
#
# packages/core/talos and packages/core/installer are deliberately excluded from
# the parallel matrix and handled by dedicated jobs instead:
# - talos: the installer tarball and Talos image are heavy; the dedicated
# leg runs when its direct or shared build inputs changed (and
# remains unconditional for forks). The container e2e lane does
# not consume a per-PR nocloud disk.
# - installer: its `flux push artifact --path=packages` bundles the ENTIRE,
# digest-patched packages tree into the OCI artifact the operator
# pulls, so it must run in the finalize step AFTER every other
# unit's digest edits are merged — never concurrently with them.
all_units() {
sed -n '/^build:/,/^[^[:space:]]/p' "$MAKEFILE" \
| grep -oE 'make -C packages/[A-Za-z0-9._/-]+ image' \
| sed -E 's/^make -C (packages[^ ]+) image$/\1/' \
| grep -vxE 'packages/core/(talos|installer)' \
| if [ "${MATRIX_ARCH:-}" = arm64 ]; then grep -vxF packages/core/testing; else cat; fi
}
emit_json() {
printf '['
_first=1
for _u in $1; do
if [ "$_first" -eq 1 ]; then _first=0; else printf ','; fi
printf '"%s"' "$_u"
done
printf ']\n'
}
units=$(all_units)
CHANGED="${1:-}"
if [ -z "$CHANGED" ] || [ "$CHANGED" = "FULL" ]; then
emit_json "$units"
exit 0
fi
if grep -qE "$full_rebuild_pattern" "$CHANGED"; then
emit_json "$units"
exit 0
fi
selected=""
for u in $units; do
if grep -qE "^$u/" "$CHANGED"; then
selected="$selected $u"
fi
done
# Targeted cross-package fan-out that dir-scoping cannot see:
# objectstorage-controller derives its COSI sidecar tag from
# packages/system/seaweedfs/values.yaml, so a seaweedfs-only diff must still
# rebuild objectstorage-controller to regenerate that tag.
if grep -qE '^packages/system/seaweedfs/' "$CHANGED"; then
case " $selected " in
*" packages/system/objectstorage-controller "*) : ;;
*) selected="$selected packages/system/objectstorage-controller" ;;
esac
fi
emit_json "$selected"