#!/bin/sh
# Emit the JSON build matrix (the list of package dirs to build) for CI.
#
# Single source of truth: the `make -C packages/
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 []
# no arg / empty / "FULL" -> every build unit (full matrix)
# -> 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"