From b0be017eff3fa711d6ee59a9962b96af09c8d8a3 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Sat, 15 Aug 2026 09:10:42 +0000 Subject: [PATCH 1/2] ci: reduce pull request build matrices Pull requests without the full-build-matrix label now build a reduced docker/static matrix (one bookworm variant per PHP version on linux/amd64), while pushes, schedules, releases, manual runs and labeled PRs keep the full matrix. Fixes #2580. --- .github/scripts/ci-build-matrix-test.sh | 54 ++++++++++++++++ .github/scripts/ci-build-matrix.sh | 61 +++++++++++++++++++ .../scripts/docker-compute-fingerprints.sh | 11 ++++ .github/workflows/docker.yaml | 15 ++--- .github/workflows/static.yaml | 10 +-- 5 files changed, 136 insertions(+), 15 deletions(-) create mode 100755 .github/scripts/ci-build-matrix-test.sh create mode 100755 .github/scripts/ci-build-matrix.sh diff --git a/.github/scripts/ci-build-matrix-test.sh b/.github/scripts/ci-build-matrix-test.sh new file mode 100755 index 0000000000..d6e82b781b --- /dev/null +++ b/.github/scripts/ci-build-matrix-test.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" + +assert_output() { + local kind="$1" + local full_matrix="$2" + local expected="$3" + local actual + + actual="$(METADATA="${TEST_METADATA}" REBUILD_VARIANTS="${REBUILD_VARIANTS:-}" "${SCRIPT_DIR}/ci-build-matrix.sh" "${kind}" "${full_matrix}")" + if [[ "${actual}" != "${expected}" ]]; then + printf 'expected:\n%s\nactual:\n%s\n' "${expected}" "${actual}" >&2 + return 1 + fi +} + +TEST_METADATA='{ + "group": { + "default": { + "targets": [ + "builder-php-8-2-bookworm", + "builder-php-8-2-trixie", + "builder-php-8-3-bookworm", + "builder-php-8-3-trixie", + "runner-php-8-2-bookworm", + "runner-php-8-2-trixie", + "runner-php-8-3-bookworm", + "runner-php-8-3-trixie" + ] + } + }, + "target": { + "builder-php-8-2-bookworm": { + "platforms": ["linux/amd64", "linux/arm64"] + }, + "static-builder-musl": { + "platforms": ["linux/amd64", "linux/arm64"] + } + } +}' + +assert_output docker false $'variants=["php-8-2-bookworm","php-8-3-bookworm"]\nplatforms=["linux/amd64"]' +assert_output docker true $'variants=["php-8-2-bookworm","php-8-2-trixie","php-8-3-bookworm","php-8-3-trixie"]\nplatforms=["linux/amd64","linux/arm64"]' +assert_output static false 'platforms=["linux/amd64"]' +assert_output static true 'platforms=["linux/amd64","linux/arm64"]' + +# On scheduled rebuilds REBUILD_VARIANTS narrows the docker matrix to the changed bases only. +REBUILD_VARIANTS='["php-8-2-trixie","php-8-3-bookworm"]' \ + assert_output docker true $'variants=["php-8-2-trixie","php-8-3-bookworm"]\nplatforms=["linux/amd64","linux/arm64"]' +# The empty-array sentinel leaves the matrix untouched. +REBUILD_VARIANTS='[]' \ + assert_output docker true $'variants=["php-8-2-bookworm","php-8-2-trixie","php-8-3-bookworm","php-8-3-trixie"]\nplatforms=["linux/amd64","linux/arm64"]' diff --git a/.github/scripts/ci-build-matrix.sh b/.github/scripts/ci-build-matrix.sh new file mode 100755 index 0000000000..261f98d865 --- /dev/null +++ b/.github/scripts/ci-build-matrix.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Platforms built on reduced pull-request runs. +readonly REDUCED_PLATFORMS='["linux/amd64"]' +# Sentinel emitted by docker-compute-fingerprints.sh when no variant needs a rebuild. +readonly EMPTY_JSON_ARRAY='[]' + +write_output() { + if [[ -n "${GITHUB_OUTPUT:-}" ]]; then + echo "$1" >>"${GITHUB_OUTPUT}" + else + echo "$1" + fi +} + +kind="${1:?matrix kind is required}" +full_matrix="${2:?full matrix flag is required}" + +case "${kind}" in +docker) + if [[ "${full_matrix}" == "true" ]]; then + variants="$( + jq -c '.group.default.targets | map(sub("runner-|builder-"; "")) | unique' <<<"${METADATA}" + )" + platforms="$(jq -c 'first(.target[]) | .platforms' <<<"${METADATA}")" + else + variants="$( + jq -c '.group.default.targets + | map(sub("runner-|builder-"; "")) + | unique + | map(select(endswith("-bookworm")))' <<<"${METADATA}" + )" + platforms="${REDUCED_PLATFORMS}" + fi + + # On scheduled rebuilds, only build the variants whose base images changed. + if [[ -n "${REBUILD_VARIANTS:-}" && "${REBUILD_VARIANTS}" != "${EMPTY_JSON_ARRAY}" ]]; then + variants="$( + jq -c --argjson rebuild "${REBUILD_VARIANTS}" \ + 'map(select(. as $v | $rebuild | index($v)))' <<<"${variants}" + )" + fi + + write_output "variants=${variants}" + write_output "platforms=${platforms}" + ;; +static) + if [[ "${full_matrix}" == "true" ]]; then + platforms="$(jq -c 'first(.target[]) | .platforms' <<<"${METADATA}")" + else + platforms="${REDUCED_PLATFORMS}" + fi + + write_output "platforms=${platforms}" + ;; +*) + echo "unknown matrix kind: ${kind}" >&2 + exit 1 + ;; +esac diff --git a/.github/scripts/docker-compute-fingerprints.sh b/.github/scripts/docker-compute-fingerprints.sh index 259d01b993..e3f97dda28 100755 --- a/.github/scripts/docker-compute-fingerprints.sh +++ b/.github/scripts/docker-compute-fingerprints.sh @@ -71,6 +71,17 @@ main() { local METADATA METADATA="$(PHP_VERSION="${PHP_VERSION}" docker buildx bake --print | jq -c)" + # On reduced pull-request runs only the Bookworm variants are built, so restrict the + # metadata to them and avoid fingerprinting (and failing on) base images we never build. + if [[ "${FULL_BUILD_MATRIX:-true}" != "true" ]]; then + METADATA="$( + jq -c ' + .group.default.targets |= map(select(endswith("-bookworm"))) + | .target |= with_entries(select(.key | endswith("-bookworm"))) + ' <<<"${METADATA}" + )" + fi + # Collect the base images (docker-image:// contexts) of each variant. The variant key # is derived from the php-base ref (e.g. "php:8.4.23-zts-trixie" -> "8.4.23-trixie") and # matches the "${php-version}-${os}" keys expected by docker-bake.hcl for BASE_FINGERPRINTS. diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 3c285d677f..987b89bc01 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -9,6 +9,7 @@ on: - main paths: - "docker-bake.hcl" + - ".github/scripts/ci-build-matrix.sh" - ".github/workflows/docker.yaml" - "**cgo.go" - "**Dockerfile" @@ -73,6 +74,7 @@ jobs: id: check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-build-matrix') }} run: ./.github/scripts/docker-compute-fingerprints.sh - name: Create variants matrix if: ${{ !fromJson(steps.check.outputs.skip) }} @@ -81,21 +83,14 @@ jobs: run: | set -e METADATA="$(docker buildx bake --print | jq -c)" - variants="$(jq -c '.group.default.targets|map(sub("runner-|builder-"; ""))|unique' <<< "${METADATA}")" - # On scheduled rebuilds, only build the variants whose base images changed - if [[ -n "${REBUILD_VARIANTS}" && "${REBUILD_VARIANTS}" != "[]" ]]; then - variants="$(jq -c --argjson rebuild "${REBUILD_VARIANTS}" 'map(select(. as $v | $rebuild | index($v)))' <<< "${variants}")" - fi - { - echo metadata="${METADATA}" - echo variants="${variants}" - echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")" - } >> "${GITHUB_OUTPUT}" + echo metadata="${METADATA}" >> "${GITHUB_OUTPUT}" + METADATA="${METADATA}" ./.github/scripts/ci-build-matrix.sh docker "${FULL_BUILD_MATRIX}" env: SHA: ${{ github.sha }} VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || 'dev' }} PHP_VERSION: ${{ steps.check.outputs.php_version }} REBUILD_VARIANTS: ${{ steps.check.outputs.rebuild_variants }} + FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-build-matrix') }} build: runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} needs: diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml index f454ea9123..b315617120 100644 --- a/.github/workflows/static.yaml +++ b/.github/workflows/static.yaml @@ -10,6 +10,7 @@ on: - main paths: - "docker-bake.hcl" + - ".github/scripts/ci-build-matrix.sh" - ".github/workflows/static.yaml" - "**cgo.go" - "**Dockerfile" @@ -74,14 +75,13 @@ jobs: run: | METADATA="$(docker buildx bake --print static-builder-musl | jq -c)" GNU_METADATA="$(docker buildx bake --print static-builder-gnu | jq -c)" - { - echo metadata="${METADATA}" - echo platforms="$(jq -c 'first(.target[]) | .platforms' <<< "${METADATA}")" - echo gnu_metadata="${GNU_METADATA}" - } >> "${GITHUB_OUTPUT}" + echo metadata="${METADATA}" >> "${GITHUB_OUTPUT}" + METADATA="${METADATA}" ./.github/scripts/ci-build-matrix.sh static "${FULL_BUILD_MATRIX}" + echo gnu_metadata="${GNU_METADATA}" >> "${GITHUB_OUTPUT}" env: SHA: ${{ github.sha }} VERSION: ${{ steps.check.outputs.ref || 'dev' }} + FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-build-matrix') }} build-linux-musl: permissions: From ec1570d18534eeede4e7d2056be7fee63b3f8223 Mon Sep 17 00:00:00 2001 From: Ben Younes <2910651+ousamabenyounes@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:52:38 +0000 Subject: [PATCH 2/2] fix(ci): preserve matrix helper on schedules --- .github/scripts/ci-build-matrix-test.sh | 62 +++++++++++++++++++++++++ .github/workflows/docker.yaml | 9 ++-- .github/workflows/static.yaml | 12 ++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/.github/scripts/ci-build-matrix-test.sh b/.github/scripts/ci-build-matrix-test.sh index d6e82b781b..5713eeb494 100755 --- a/.github/scripts/ci-build-matrix-test.sh +++ b/.github/scripts/ci-build-matrix-test.sh @@ -2,6 +2,65 @@ set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" +REPOSITORY_ROOT="$(cd -- "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd)" +readonly REPOSITORY_ROOT +readonly FULL_MATRIX_LABEL='ready to test' +readonly LEGACY_FULL_MATRIX_LABEL='full-build-matrix' +readonly PULL_REQUEST_ACTIVITY_TYPES='types: [opened, synchronize, reopened, labeled]' +readonly PRESERVED_MATRIX_SCRIPT="\${RUNNER_TEMP}/ci-build-matrix.sh" +readonly MATRIX_SCRIPT_COPY="cp .github/scripts/ci-build-matrix.sh \"\${RUNNER_TEMP}/ci-build-matrix.sh\"" +readonly STATIC_CHECKOUT_REF=" ref: \${{ steps.check.outputs.ref }}" +readonly MATRIX_WORKFLOWS=( + "${REPOSITORY_ROOT}/.github/workflows/docker.yaml" + "${REPOSITORY_ROOT}/.github/workflows/static.yaml" +) + +assert_workflow_label() { + local label_count workflow + + for workflow in "${MATRIX_WORKFLOWS[@]}"; do + label_count="$(grep -Fc "'${FULL_MATRIX_LABEL}'" "${workflow}" || true)" + if [[ "${label_count}" -ne 1 ]]; then + printf '%s must define the issue-specified full matrix label %q exactly once; found %d\n' \ + "${workflow}" "${FULL_MATRIX_LABEL}" "${label_count}" >&2 + return 1 + fi + if grep -Fq "'${LEGACY_FULL_MATRIX_LABEL}'" "${workflow}"; then + printf '%s still uses the unsupported full matrix label %q\n' "${workflow}" "${LEGACY_FULL_MATRIX_LABEL}" >&2 + return 1 + fi + if ! grep -Fq "${PULL_REQUEST_ACTIVITY_TYPES}" "${workflow}"; then + printf '%s does not run when the full matrix label is applied\n' "${workflow}" >&2 + return 1 + fi + done +} + +assert_preserved_matrix_script() { + local copy_line ref_switch_line workflow + + for workflow in "${MATRIX_WORKFLOWS[@]}"; do + copy_line="$(grep -nF "${MATRIX_SCRIPT_COPY}" "${workflow}" | cut -d: -f1 || true)" + if [[ -z "${copy_line}" ]]; then + printf '%s does not preserve the matrix script outside the mutable checkout\n' "${workflow}" >&2 + return 1 + fi + if ! grep -Fq "${PRESERVED_MATRIX_SCRIPT}" "${workflow}"; then + printf '%s does not execute the preserved matrix script\n' "${workflow}" >&2 + return 1 + fi + if grep -Fq './.github/scripts/ci-build-matrix.sh' "${workflow}"; then + printf '%s executes the matrix script from a checkout that may change refs\n' "${workflow}" >&2 + return 1 + fi + done + + ref_switch_line="$(grep -nF "${STATIC_CHECKOUT_REF}" "${REPOSITORY_ROOT}/.github/workflows/static.yaml" | cut -d: -f1 || true)" + if [[ -z "${ref_switch_line}" || "${copy_line}" -ge "${ref_switch_line}" ]]; then + printf 'static.yaml must preserve the matrix script before checking out a release\n' >&2 + return 1 + fi +} assert_output() { local kind="$1" @@ -52,3 +111,6 @@ REBUILD_VARIANTS='["php-8-2-trixie","php-8-3-bookworm"]' \ # The empty-array sentinel leaves the matrix untouched. REBUILD_VARIANTS='[]' \ assert_output docker true $'variants=["php-8-2-bookworm","php-8-2-trixie","php-8-3-bookworm","php-8-3-trixie"]\nplatforms=["linux/amd64","linux/arm64"]' + +assert_workflow_label +assert_preserved_matrix_script diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 987b89bc01..0673e2df1b 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -5,6 +5,7 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} on: pull_request: + types: [opened, synchronize, reopened, labeled] branches: - main paths: @@ -38,6 +39,8 @@ env: jobs: prepare: runs-on: ubuntu-24.04 + env: + FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ready to test') }} outputs: # Push if it's a scheduled job, a tag, or if we're committing to the main branch push: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false }} @@ -62,6 +65,8 @@ jobs: with: fetch-depth: 0 persist-credentials: false + - name: Preserve matrix script + run: cp .github/scripts/ci-build-matrix.sh "${RUNNER_TEMP}/ci-build-matrix.sh" - name: Set up Docker Buildx uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Login to DockerHub @@ -74,7 +79,6 @@ jobs: id: check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-build-matrix') }} run: ./.github/scripts/docker-compute-fingerprints.sh - name: Create variants matrix if: ${{ !fromJson(steps.check.outputs.skip) }} @@ -84,13 +88,12 @@ jobs: set -e METADATA="$(docker buildx bake --print | jq -c)" echo metadata="${METADATA}" >> "${GITHUB_OUTPUT}" - METADATA="${METADATA}" ./.github/scripts/ci-build-matrix.sh docker "${FULL_BUILD_MATRIX}" + METADATA="${METADATA}" "${RUNNER_TEMP}/ci-build-matrix.sh" docker "${FULL_BUILD_MATRIX}" env: SHA: ${{ github.sha }} VERSION: ${{ (github.ref_type == 'tag' && github.ref_name) || steps.check.outputs.ref || 'dev' }} PHP_VERSION: ${{ steps.check.outputs.php_version }} REBUILD_VARIANTS: ${{ steps.check.outputs.rebuild_variants }} - FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-build-matrix') }} build: runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} needs: diff --git a/.github/workflows/static.yaml b/.github/workflows/static.yaml index b315617120..626dad5766 100644 --- a/.github/workflows/static.yaml +++ b/.github/workflows/static.yaml @@ -6,6 +6,7 @@ concurrency: on: pull_request: + types: [opened, synchronize, reopened, labeled] branches: - main paths: @@ -44,6 +45,8 @@ env: jobs: prepare: runs-on: ubuntu-24.04 + env: + FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ready to test') }} outputs: push: ${{ toJson((steps.check.outputs.ref || (github.event_name == 'workflow_dispatch' && inputs.version) || startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main' && github.event_name != 'pull_request')) && true || false) }} platforms: ${{ steps.matrix.outputs.platforms }} @@ -65,6 +68,12 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REF: ${{ (github.ref_type == 'tag' && github.ref_name) || (github.event_name == 'workflow_dispatch' && inputs.version) || '' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Preserve matrix script + run: cp .github/scripts/ci-build-matrix.sh "${RUNNER_TEMP}/ci-build-matrix.sh" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: steps.check.outputs.ref != '' with: ref: ${{ steps.check.outputs.ref }} persist-credentials: false @@ -76,12 +85,11 @@ jobs: METADATA="$(docker buildx bake --print static-builder-musl | jq -c)" GNU_METADATA="$(docker buildx bake --print static-builder-gnu | jq -c)" echo metadata="${METADATA}" >> "${GITHUB_OUTPUT}" - METADATA="${METADATA}" ./.github/scripts/ci-build-matrix.sh static "${FULL_BUILD_MATRIX}" + METADATA="${METADATA}" "${RUNNER_TEMP}/ci-build-matrix.sh" static "${FULL_BUILD_MATRIX}" echo gnu_metadata="${GNU_METADATA}" >> "${GITHUB_OUTPUT}" env: SHA: ${{ github.sha }} VERSION: ${{ steps.check.outputs.ref || 'dev' }} - FULL_BUILD_MATRIX: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'full-build-matrix') }} build-linux-musl: permissions: