From 42e7c61a1152b8ab14ce5fc263d753992c25efe7 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 8 Oct 2026 14:56:31 +0200 Subject: [PATCH 1/6] Rework Renovate config and drop Dependabot - Remove dependabot.yml; Renovate already manages GitHub Actions and the two were opening competing PRs - Group non-major action updates with a 7 day release age (replaces the Dependabot cooldown) - Drop rebaseWhen so the default applies (fewer redundant multi-arch runs) - Remove no-op rules (enabled: true, dependencyDashboardApproval: false) - Scope separateMinorPatch to composer/composer - Pin 2.2 to patch releases with allowedVersions - Merge the two identical custom managers, anchor/escape the path pattern, accept -RC versions - Add a PR note for a new Composer major on latest - Don't propose PHP major base image bumps for 1.10 and 2.2 - Add helpers:pinGitHubActionDigests Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 8 ---- .github/renovate.json | 83 ++++++++++++++++++++++++------------------ 2 files changed, 47 insertions(+), 44 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 177b067..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "monthly" - cooldown: - default-days: 7 diff --git a/.github/renovate.json b/.github/renovate.json index c78dd17..f3f0310 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,85 +1,96 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "rebaseWhen": "behind-base-branch", "labels": [ "dependencies" ], "extends": [ - "config:recommended" + "config:recommended", + "helpers:pinGitHubActionDigests" ], - "separateMinorPatch": true, "packageRules": [ { - "matchPackageNames": [ - "*" - ], - "dependencyDashboardApproval": false + "description": "Batch action updates (each one triggers full multi-arch builds) and add a cooldown", + "matchManagers": [ + "github-actions" + ], + "matchUpdateTypes": [ + "minor", + "patch", + "digest" + ], + "groupName": "GitHub Actions", + "minimumReleaseAge": "7 days" }, { "matchPackageNames": [ "composer/composer" ], - "enabled": true + "separateMinorPatch": true }, { - "description": "Disables major and minor updates for composer/composer in 2.2", + "description": "2.2 is LTS: patch releases only", "matchFileNames": [ "2.2/Dockerfile" ], "matchPackageNames": [ "composer/composer" ], - "matchUpdateTypes": [ - "major", - "minor" + "allowedVersions": "<2.3.0", + "commitMessageSuffix": " (2.2)", + "additionalBranchPrefix": "2.2-" + }, + { + "matchFileNames": [ + "latest/Dockerfile" ], - "enabled": false + "matchPackageNames": [ + "composer/composer" + ], + "commitMessageSuffix": " (Latest)", + "additionalBranchPrefix": "latest-" }, { - "description": "Enables patch updates for composer/composer in 2.2", + "description": "A new Composer major needs manual follow-up", "matchFileNames": [ - "2.2/Dockerfile" + "latest/Dockerfile" ], "matchPackageNames": [ "composer/composer" ], "matchUpdateTypes": [ - "patch" + "major" ], - "commitMessageSuffix": " (2.2)", - "additionalBranchPrefix": "2.2-", - "enabled": true + "prBodyNotes": [ + "⚠️ New major: update aliases in `generate-stackbrew-library.sh` and consider creating an LTS directory for the previous major." + ] }, { + "description": "Legacy/LTS images stay on PHP 8", "matchFileNames": [ - "latest/Dockerfile" + "1.10/Dockerfile", + "2.2/Dockerfile" + ], + "matchDatasources": [ + "docker" ], "matchPackageNames": [ - "composer/composer" + "php" ], - "commitMessageSuffix": " (Latest)", - "additionalBranchPrefix": "latest-" + "matchUpdateTypes": [ + "major" + ], + "enabled": false } ], "customManagers": [ { "customType": "regex", + "description": "Composer version in maintained Dockerfiles (1.10 is frozen)", "managerFilePatterns": [ - "/latest/Dockerfile$/" - ], - "matchStrings": [ - "ENV COMPOSER_VERSION=[\"']?(?\\d+\\.\\d+\\.\\d+)[\"']?\\s+" - ], - "datasourceTemplate": "github-releases", - "depNameTemplate": "composer/composer" - }, - { - "customType": "regex", - "managerFilePatterns": [ - "/2.2/Dockerfile$/" + "/^(latest|2\\.2)/Dockerfile$/" ], "matchStrings": [ - "ENV COMPOSER_VERSION=[\"']?(?\\d+\\.\\d+\\.\\d+)[\"']?\\s+" + "ENV COMPOSER_VERSION=[\"']?(?\\d+\\.\\d+\\.\\d+(?:-RC\\d+)?)[\"']?\\s" ], "datasourceTemplate": "github-releases", "depNameTemplate": "composer/composer" From b61b22c288e2beab75724392feea5816b85e5f9b Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 8 Oct 2026 14:58:51 +0200 Subject: [PATCH 2/6] Build bz2 and zip with docker-php-ext-install Both extensions are bundled with php-src, so the base image can build them itself. This drops the third-party docker-php-extension-installer download (and its manually maintained sha512). Co-Authored-By: Claude Opus 5.5 --- 1.10/Dockerfile | 26 ++++++++++++++------------ 2.2/Dockerfile | 26 ++++++++++++++------------ latest/Dockerfile | 26 ++++++++++++++------------ 3 files changed, 42 insertions(+), 36 deletions(-) diff --git a/1.10/Dockerfile b/1.10/Dockerfile index ad19ca6..1dc29a2 100644 --- a/1.10/Dockerfile +++ b/1.10/Dockerfile @@ -25,22 +25,24 @@ ENV COMPOSER_HOME=/tmp ENV COMPOSER_VERSION=1.10.28 RUN set -eux ; \ - # install https://github.com/mlocati/docker-php-extension-installer - curl \ - --silent \ - --fail \ - --location \ - --retry 3 \ - --output /usr/local/bin/install-php-extensions \ - --url https://github.com/mlocati/docker-php-extension-installer/releases/download/2.11.12/install-php-extensions \ - ; \ - echo 0c3594c9865bf1e2372cfd3da355cf5115c56fdcc9956218e06c130d99d7754d806088d8d0771f6e84f01e93cd65928df2579d50d7d66811010552eae6fe671a /usr/local/bin/install-php-extensions | sha512sum --strict --check ; \ - chmod +x /usr/local/bin/install-php-extensions ; \ # install necessary/useful extensions not included in base image - install-php-extensions \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + bzip2-dev \ + libzip-dev \ + ; \ + docker-php-ext-install -j "$(nproc)" \ bz2 \ zip \ ; \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )" ; \ + apk add --no-cache --virtual .composer-phpext-rundeps $runDeps ; \ + apk del --no-network .build-deps ; \ # install public keys for snapshot and tag validation, see https://composer.github.io/pubkeys.html curl \ --silent \ diff --git a/2.2/Dockerfile b/2.2/Dockerfile index d1d6b58..cd56d84 100644 --- a/2.2/Dockerfile +++ b/2.2/Dockerfile @@ -25,22 +25,24 @@ ENV COMPOSER_HOME=/tmp ENV COMPOSER_VERSION=2.2.30 RUN set -eux ; \ - # install https://github.com/mlocati/docker-php-extension-installer - curl \ - --silent \ - --fail \ - --location \ - --retry 3 \ - --output /usr/local/bin/install-php-extensions \ - --url https://github.com/mlocati/docker-php-extension-installer/releases/download/2.11.12/install-php-extensions \ - ; \ - echo 0c3594c9865bf1e2372cfd3da355cf5115c56fdcc9956218e06c130d99d7754d806088d8d0771f6e84f01e93cd65928df2579d50d7d66811010552eae6fe671a /usr/local/bin/install-php-extensions | sha512sum --strict --check ; \ - chmod +x /usr/local/bin/install-php-extensions ; \ # install necessary/useful extensions not included in base image - install-php-extensions \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + bzip2-dev \ + libzip-dev \ + ; \ + docker-php-ext-install -j "$(nproc)" \ bz2 \ zip \ ; \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )" ; \ + apk add --no-cache --virtual .composer-phpext-rundeps $runDeps ; \ + apk del --no-network .build-deps ; \ # install public keys for snapshot and tag validation, see https://composer.github.io/pubkeys.html curl \ --silent \ diff --git a/latest/Dockerfile b/latest/Dockerfile index 4f808e4..fb56055 100644 --- a/latest/Dockerfile +++ b/latest/Dockerfile @@ -25,22 +25,24 @@ ENV COMPOSER_HOME=/tmp ENV COMPOSER_VERSION=2.10.3 RUN set -eux ; \ - # install https://github.com/mlocati/docker-php-extension-installer - curl \ - --silent \ - --fail \ - --location \ - --retry 3 \ - --output /usr/local/bin/install-php-extensions \ - --url https://github.com/mlocati/docker-php-extension-installer/releases/download/2.11.12/install-php-extensions \ - ; \ - echo 0c3594c9865bf1e2372cfd3da355cf5115c56fdcc9956218e06c130d99d7754d806088d8d0771f6e84f01e93cd65928df2579d50d7d66811010552eae6fe671a /usr/local/bin/install-php-extensions | sha512sum --strict --check ; \ - chmod +x /usr/local/bin/install-php-extensions ; \ # install necessary/useful extensions not included in base image - install-php-extensions \ + apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + bzip2-dev \ + libzip-dev \ + ; \ + docker-php-ext-install -j "$(nproc)" \ bz2 \ zip \ ; \ + runDeps="$( \ + scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions \ + | tr ',' '\n' \ + | sort -u \ + | awk 'system("[ -e /usr/local/lib/" $1 " ]") == 0 { next } { print "so:" $1 }' \ + )" ; \ + apk add --no-cache --virtual .composer-phpext-rundeps $runDeps ; \ + apk del --no-network .build-deps ; \ # install public keys for snapshot and tag validation, see https://composer.github.io/pubkeys.html curl \ --silent \ From 587b675bc4f3e6f28344fe4ba1b2f9d1a707a43d Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 8 Oct 2026 14:59:36 +0200 Subject: [PATCH 3/6] Fix zizmor coverage and tidy build workflows - zizmor only triggered on .github/**.yml, so the .yaml build workflows and composite action were never scanned; rename to zizmor.yaml and match both extensions - Pass the metadata-action labels to build-push-action (they were computed but unused) - Rewrite manifest creation with arrays instead of unquoted command substitution / variable printf formats (SC2046, SC2059) - Document the $/ self-repository action reference Co-Authored-By: Claude Opus 5.5 --- .github/workflows/1.10.yaml | 31 ++++++++++++------- .github/workflows/2.2.yaml | 31 ++++++++++++------- .github/workflows/latest.yaml | 31 ++++++++++++------- .github/workflows/{zizmor.yml => zizmor.yaml} | 2 ++ 4 files changed, 59 insertions(+), 36 deletions(-) rename .github/workflows/{zizmor.yml => zizmor.yaml} (93%) diff --git a/.github/workflows/1.10.yaml b/.github/workflows/1.10.yaml index 0c75e35..9a2fe64 100644 --- a/.github/workflows/1.10.yaml +++ b/.github/workflows/1.10.yaml @@ -51,6 +51,7 @@ jobs: - name: Determine Composer version from Dockerfile id: composer-version + # "$/" = this repository at the running commit (no checkout needed), see https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax uses: $/.github/actions/determine-composer-version with: working-directory: ${{ env.DIRECTORY }} @@ -172,6 +173,7 @@ jobs: context: ${{ env.DIRECTORY }} target: binary-with-runtime annotations: ${{ needs.prepare.outputs.full-annotations }} + labels: ${{ needs.prepare.outputs.full-labels }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.ECR_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} @@ -182,6 +184,7 @@ jobs: context: ${{ env.DIRECTORY }} target: standalone-binary annotations: ${{ needs.prepare.outputs.bin-annotations }} + labels: ${{ needs.prepare.outputs.bin-labels }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.ECR_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} @@ -256,21 +259,25 @@ jobs: env: JSON: ${{ needs.prepare.outputs.full-json }} run: | - docker buildx imagetools create $(jq -cr --arg slug "$DOCKERHUB_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$DOCKERHUB_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$ECR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$ECR_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$GHCR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$GHCR_SLUG@sha256:%s " *) + for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done - name: Create manifest list for binary-only image and push working-directory: /tmp/digests/bin env: JSON: ${{ needs.prepare.outputs.bin-json }} run: | - docker buildx imagetools create $(jq -cr --arg slug "$DOCKERHUB_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$DOCKERHUB_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$ECR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$ECR_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$GHCR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$GHCR_SLUG@sha256:%s " *) + for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done diff --git a/.github/workflows/2.2.yaml b/.github/workflows/2.2.yaml index 62ec9b0..b69dfde 100644 --- a/.github/workflows/2.2.yaml +++ b/.github/workflows/2.2.yaml @@ -51,6 +51,7 @@ jobs: - name: Determine Composer version from Dockerfile id: composer-version + # "$/" = this repository at the running commit (no checkout needed), see https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax uses: $/.github/actions/determine-composer-version with: working-directory: ${{ env.DIRECTORY }} @@ -172,6 +173,7 @@ jobs: context: ${{ env.DIRECTORY }} target: binary-with-runtime annotations: ${{ needs.prepare.outputs.full-annotations }} + labels: ${{ needs.prepare.outputs.full-labels }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.ECR_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} @@ -182,6 +184,7 @@ jobs: context: ${{ env.DIRECTORY }} target: standalone-binary annotations: ${{ needs.prepare.outputs.bin-annotations }} + labels: ${{ needs.prepare.outputs.bin-labels }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.ECR_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} @@ -256,21 +259,25 @@ jobs: env: JSON: ${{ needs.prepare.outputs.full-json }} run: | - docker buildx imagetools create $(jq -cr --arg slug "$DOCKERHUB_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$DOCKERHUB_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$ECR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$ECR_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$GHCR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$GHCR_SLUG@sha256:%s " *) + for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done - name: Create manifest list for binary-only image and push working-directory: /tmp/digests/bin env: JSON: ${{ needs.prepare.outputs.bin-json }} run: | - docker buildx imagetools create $(jq -cr --arg slug "$DOCKERHUB_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$DOCKERHUB_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$ECR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$ECR_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$GHCR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$GHCR_SLUG@sha256:%s " *) + for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done diff --git a/.github/workflows/latest.yaml b/.github/workflows/latest.yaml index 6dbbcab..ce00c9e 100644 --- a/.github/workflows/latest.yaml +++ b/.github/workflows/latest.yaml @@ -51,6 +51,7 @@ jobs: - name: Determine Composer version from Dockerfile id: composer-version + # "$/" = this repository at the running commit (no checkout needed), see https://github.blog/changelog/2026-07-30-reference-same-repository-actions-with-self-repository-syntax uses: $/.github/actions/determine-composer-version with: working-directory: ${{ env.DIRECTORY }} @@ -176,6 +177,7 @@ jobs: context: ${{ env.DIRECTORY }} target: binary-with-runtime annotations: ${{ needs.prepare.outputs.full-annotations }} + labels: ${{ needs.prepare.outputs.full-labels }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.ECR_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} @@ -186,6 +188,7 @@ jobs: context: ${{ env.DIRECTORY }} target: standalone-binary annotations: ${{ needs.prepare.outputs.bin-annotations }} + labels: ${{ needs.prepare.outputs.bin-labels }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ env.DOCKERHUB_SLUG }},${{ env.ECR_SLUG }},${{ env.GHCR_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.ref == 'refs/heads/main' && github.event_name != 'pull_request' }} @@ -260,21 +263,25 @@ jobs: env: JSON: ${{ needs.prepare.outputs.full-json }} run: | - docker buildx imagetools create $(jq -cr --arg slug "$DOCKERHUB_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$DOCKERHUB_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$ECR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$ECR_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$GHCR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$GHCR_SLUG@sha256:%s " *) + for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done - name: Create manifest list for binary-only image and push working-directory: /tmp/digests/bin env: JSON: ${{ needs.prepare.outputs.bin-json }} run: | - docker buildx imagetools create $(jq -cr --arg slug "$DOCKERHUB_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$DOCKERHUB_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$ECR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$ECR_SLUG@sha256:%s " *) - docker buildx imagetools create $(jq -cr --arg slug "$GHCR_SLUG" '.tags | map(select(startswith($slug)) | "--tag " + .) | join(" ")' <<< "$JSON") \ - $(printf "$GHCR_SLUG@sha256:%s " *) + for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yaml similarity index 93% rename from .github/workflows/zizmor.yml rename to .github/workflows/zizmor.yaml index ba903e4..683ce6c 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yaml @@ -6,9 +6,11 @@ on: - main paths: - '.github/**.yml' + - '.github/**.yaml' pull_request: paths: - '.github/**.yml' + - '.github/**.yaml' concurrency: group: ${{ github.workflow }}-${{ github.ref }} From 583c248a384c9cabb4a0c5a720c3381ff1340cd2 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 8 Oct 2026 14:59:41 +0200 Subject: [PATCH 4/6] Describe the Renovate-driven release flow in README Co-Authored-By: Claude Opus 5.5 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5c22cb4..e8df5be 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Docker Hub documentation can be found at https://github.com/docker-library/docs/ The "official" image release workflow is as follows: - :robot: a new tag is pushed to [Composer repository] -- :robot: [release workflow] on [Composer repository] creates an issue regarding new tag on [Docker repository] -- :writing_hand: modification to relevant `Dockerfile`s is pushed/merged +- :robot: [Renovate] opens a pull request bumping the relevant `Dockerfile`s +- :writing_hand: pull request is reviewed and merged - :writing_hand: a pull request is submitted to the [official images repository] - :writing_hand: pull request is merged, resulting in new release being added to [Docker Hub (official)] @@ -26,8 +26,8 @@ The "official" image release workflow is as follows: The "community" image release workflow is as follows: - :robot: a new tag is pushed to [Composer repository] -- :robot: [release workflow] on [Composer repository] creates an issue regarding new tag on [Docker repository] -- :writing_hand: modification to relevant `Dockerfile`s is pushed/merged +- :robot: [Renovate] opens a pull request bumping the relevant `Dockerfile`s +- :writing_hand: pull request is reviewed and merged - :robot: [docker workflows] builds and pushes new release to [Docker Hub (community)] - :robot: [docker workflows] builds and pushes new release to [Amazon Public ECR] - :robot: [docker workflows] builds and pushes new release to [GHCR] @@ -35,7 +35,7 @@ The "community" image release workflow is as follows: [composer repository]: https://github.com/composer/composer [docker repository]: https://github.com/composer/docker [official images repository]: https://github.com/docker-library/official-images/ -[release workflow]: https://github.com/composer/composer/blob/main/.github/workflows/release.yml +[renovate]: https://github.com/composer/docker/blob/main/.github/renovate.json [docker workflows]: https://github.com/composer/docker/tree/main/.github/workflows [Amazon Public ECR]: https://gallery.ecr.aws/composer/composer [GHCR]: https://github.com/composer/docker/pkgs/container/docker From 8019172e020e1a1035fb4f14c76584b45d10bace Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 8 Oct 2026 15:00:05 +0200 Subject: [PATCH 5/6] Make generate-stackbrew-library.sh ShellCheck-clean Replace implicit word-splitting with explicit arrays (mapfile/read -a) and stop excluding the script from the ShellCheck workflow. Generated output is unchanged. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/shellcheck.yaml | 2 +- generate-stackbrew-library.sh | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/shellcheck.yaml b/.github/workflows/shellcheck.yaml index ad330b2..142d4b9 100644 --- a/.github/workflows/shellcheck.yaml +++ b/.github/workflows/shellcheck.yaml @@ -24,4 +24,4 @@ jobs: with: persist-credentials: false - name: ShellCheck - run: find . -type f -name '*.sh' | grep -v generate-stackbrew-library | xargs shellcheck + run: find . -type f -name '*.sh' -print0 | xargs -0 shellcheck diff --git a/generate-stackbrew-library.sh b/generate-stackbrew-library.sh index b1d9853..5e05b2f 100755 --- a/generate-stackbrew-library.sh +++ b/generate-stackbrew-library.sh @@ -71,7 +71,7 @@ for dir in */ ; do done # sort directories descending -IFS=$'\n'; directories=( $(echo "${directories[*]}" | sort -rV) ); unset IFS +mapfile -t directories < <(printf '%s\n' "${directories[@]}" | sort -rV) # manifest header cat <<-HEADER @@ -86,14 +86,15 @@ HEADER for directory in "${directories[@]}"; do commit="$(dirCommit "$directory")" version="$(extractVersion "$commit" "$directory")" - tags=("$version" "${version%.*}" "${aliases[$directory]:-}") + read -ra extraTags <<< "${aliases[$directory]:-}" + tags=("$version" "${version%.*}" "${extraTags[@]}") parent="$(awk 'toupper($1) == "FROM" { print $2; exit }' "$directory/Dockerfile")" - arches="${parentRepoToArches[$parent]}" + read -ra arches <<< "${parentRepoToArches[$parent]}" cat <<-METADATA - Tags: $(join ', ' ${tags[*]}) - Architectures: $(join ', ' $arches) + Tags: $(join ', ' "${tags[@]}") + Architectures: $(join ', ' "${arches[@]}") GitFetch: refs/heads/main GitCommit: $commit Directory: $directory From 27c494c0defcd2944dd7fe85baa33e97a1520581 Mon Sep 17 00:00:00 2001 From: Rob Bast Date: Thu, 8 Oct 2026 16:29:55 +0200 Subject: [PATCH 6/6] Extract manifest list creation into a composite action The same loop was repeated for the full and binary-only images in all three build workflows. Move it to .github/actions/create-manifest-list and call it via the $/ self-repository reference, which works without a checkout in the merge job. Co-Authored-By: Claude Opus 5.5 --- .../actions/create-manifest-list/action.yaml | 40 +++++++++++++++++++ .github/workflows/1.10.yaml | 40 ++++++++----------- .github/workflows/2.2.yaml | 40 ++++++++----------- .github/workflows/latest.yaml | 40 ++++++++----------- 4 files changed, 88 insertions(+), 72 deletions(-) create mode 100644 .github/actions/create-manifest-list/action.yaml diff --git a/.github/actions/create-manifest-list/action.yaml b/.github/actions/create-manifest-list/action.yaml new file mode 100644 index 0000000..d2701f8 --- /dev/null +++ b/.github/actions/create-manifest-list/action.yaml @@ -0,0 +1,40 @@ +# https://docs.github.com/en/actions/creating-actions/creating-a-composite-action +# https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#inputs +# https://docs.github.com/en/actions/creating-actions/metadata-syntax-for-github-actions#runs-for-composite-run-steps-actions + +name: "Create and push manifest lists" + +description: "Creates a multi-platform manifest list per image from pushed digests and tags it using docker/metadata-action JSON output." + +inputs: + metadata-json: + description: "JSON output of docker/metadata-action" + required: true + digests-directory: + description: "Directory containing one empty file per pushed digest, named after its sha256 hex" + required: true + images: + description: "Newline-separated list of image names to create manifest lists for" + required: true + +runs: + using: "composite" + + steps: + - name: "Create and push manifest lists" + run: | + mapfile -t slugs <<< "$IMAGES" + for slug in "${slugs[@]}"; do + [ -n "$slug" ] || continue + mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") + sources=() + for digest in *; do + sources+=("$slug@sha256:$digest") + done + docker buildx imagetools create "${tags[@]}" "${sources[@]}" + done + shell: "bash" + working-directory: "${{ inputs.digests-directory }}" + env: + IMAGES: ${{ inputs.images }} + JSON: ${{ inputs.metadata-json }} diff --git a/.github/workflows/1.10.yaml b/.github/workflows/1.10.yaml index 9a2fe64..2d60ed8 100644 --- a/.github/workflows/1.10.yaml +++ b/.github/workflows/1.10.yaml @@ -255,29 +255,21 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list for full image and push - working-directory: /tmp/digests/full - env: - JSON: ${{ needs.prepare.outputs.full-json }} - run: | - for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do - mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") - sources=() - for digest in *; do - sources+=("$slug@sha256:$digest") - done - docker buildx imagetools create "${tags[@]}" "${sources[@]}" - done + uses: $/.github/actions/create-manifest-list + with: + metadata-json: ${{ needs.prepare.outputs.full-json }} + digests-directory: /tmp/digests/full + images: | + ${{ env.DOCKERHUB_SLUG }} + ${{ env.ECR_SLUG }} + ${{ env.GHCR_SLUG }} - name: Create manifest list for binary-only image and push - working-directory: /tmp/digests/bin - env: - JSON: ${{ needs.prepare.outputs.bin-json }} - run: | - for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do - mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") - sources=() - for digest in *; do - sources+=("$slug@sha256:$digest") - done - docker buildx imagetools create "${tags[@]}" "${sources[@]}" - done + uses: $/.github/actions/create-manifest-list + with: + metadata-json: ${{ needs.prepare.outputs.bin-json }} + digests-directory: /tmp/digests/bin + images: | + ${{ env.DOCKERHUB_SLUG }} + ${{ env.ECR_SLUG }} + ${{ env.GHCR_SLUG }} diff --git a/.github/workflows/2.2.yaml b/.github/workflows/2.2.yaml index b69dfde..e8b2e09 100644 --- a/.github/workflows/2.2.yaml +++ b/.github/workflows/2.2.yaml @@ -255,29 +255,21 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list for full image and push - working-directory: /tmp/digests/full - env: - JSON: ${{ needs.prepare.outputs.full-json }} - run: | - for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do - mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") - sources=() - for digest in *; do - sources+=("$slug@sha256:$digest") - done - docker buildx imagetools create "${tags[@]}" "${sources[@]}" - done + uses: $/.github/actions/create-manifest-list + with: + metadata-json: ${{ needs.prepare.outputs.full-json }} + digests-directory: /tmp/digests/full + images: | + ${{ env.DOCKERHUB_SLUG }} + ${{ env.ECR_SLUG }} + ${{ env.GHCR_SLUG }} - name: Create manifest list for binary-only image and push - working-directory: /tmp/digests/bin - env: - JSON: ${{ needs.prepare.outputs.bin-json }} - run: | - for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do - mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") - sources=() - for digest in *; do - sources+=("$slug@sha256:$digest") - done - docker buildx imagetools create "${tags[@]}" "${sources[@]}" - done + uses: $/.github/actions/create-manifest-list + with: + metadata-json: ${{ needs.prepare.outputs.bin-json }} + digests-directory: /tmp/digests/bin + images: | + ${{ env.DOCKERHUB_SLUG }} + ${{ env.ECR_SLUG }} + ${{ env.GHCR_SLUG }} diff --git a/.github/workflows/latest.yaml b/.github/workflows/latest.yaml index ce00c9e..353648d 100644 --- a/.github/workflows/latest.yaml +++ b/.github/workflows/latest.yaml @@ -259,29 +259,21 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create manifest list for full image and push - working-directory: /tmp/digests/full - env: - JSON: ${{ needs.prepare.outputs.full-json }} - run: | - for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do - mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") - sources=() - for digest in *; do - sources+=("$slug@sha256:$digest") - done - docker buildx imagetools create "${tags[@]}" "${sources[@]}" - done + uses: $/.github/actions/create-manifest-list + with: + metadata-json: ${{ needs.prepare.outputs.full-json }} + digests-directory: /tmp/digests/full + images: | + ${{ env.DOCKERHUB_SLUG }} + ${{ env.ECR_SLUG }} + ${{ env.GHCR_SLUG }} - name: Create manifest list for binary-only image and push - working-directory: /tmp/digests/bin - env: - JSON: ${{ needs.prepare.outputs.bin-json }} - run: | - for slug in "$DOCKERHUB_SLUG" "$ECR_SLUG" "$GHCR_SLUG"; do - mapfile -t tags < <(jq -r --arg slug "$slug" '.tags[] | select(startswith($slug)) | "--tag=" + .' <<< "$JSON") - sources=() - for digest in *; do - sources+=("$slug@sha256:$digest") - done - docker buildx imagetools create "${tags[@]}" "${sources[@]}" - done + uses: $/.github/actions/create-manifest-list + with: + metadata-json: ${{ needs.prepare.outputs.bin-json }} + digests-directory: /tmp/digests/bin + images: | + ${{ env.DOCKERHUB_SLUG }} + ${{ env.ECR_SLUG }} + ${{ env.GHCR_SLUG }}