From 49a00fd04a8f27f0e5243b59753cc2a6055af0a2 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Wed, 5 Aug 2026 16:45:07 +0200 Subject: [PATCH 1/5] Build/Test Tools: Allow the reusable PHPUnit workflow to test another repository. A reusable workflow checks out the repository that calls it, so a caller that is not wordpress-develop cannot use this workflow and ends up maintaining a copy of it. Add optional `repository` and `ref` inputs so the checkout can point elsewhere, and an `overlay-artifact` input that unpacks a same-run artifact over the checkout, for callers whose test files are not part of the repository being tested. Derive the prepared Gutenberg artifact name from `ref` as well. Every branch in a run shares one artifact namespace, so a caller testing several branches needs one build per branch. The producer composes the same name from its own `ref`, so the name is never passed between them and `gutenberg-artifact` stays the boolean it became in [63162]. All inputs default to empty, so existing callers are unaffected: `repository` falls back to the calling repository, an empty `ref` is already checkout's own default and keeps the original `gutenberg-build` name, and the overlay step is skipped. --- .../workflows/reusable-phpunit-tests-v3.yml | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index 4f7addcd4e60e..d4bebc3153fa1 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -13,6 +13,16 @@ on: required: false type: 'string' default: 'ubuntu-24.04' + repository: + description: 'The repository to check out and test. Defaults to the repository calling this workflow.' + required: false + type: 'string' + default: '' + ref: + description: 'The branch, tag, or SHA to check out. Defaults to the ref that triggered the calling workflow.' + required: false + type: 'string' + default: '' php: description: 'The version of PHP to use, in the format of X.Y' required: true @@ -82,6 +92,11 @@ on: required: false type: string default: '' + overlay-artifact: + description: 'The name of a same-workflow artifact whose contents are unpacked over the checkout. Optional: for callers whose test files are not part of the repository being tested.' + required: false + type: string + default: '' secrets: CODECOV_TOKEN: description: 'The Codecov token required for uploading reports.' @@ -111,6 +126,7 @@ jobs: # Performs the following steps: # - Sets environment variables. # - Checks out the repository. + # - Unpacks the caller-provided overlay artifact over the checkout, if any. # - Downloads the prepared Gutenberg build provided by the calling workflow. # - Sets up Node.js. # - Sets up PHP. @@ -143,15 +159,30 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: + repository: ${{ inputs.repository || github.repository }} + ref: ${{ inputs.ref }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false + # If the caller specifies an overlay artifact then unpack it over the checkout. + - name: Download overlay artifact + if: inputs.overlay-artifact != '' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + # Without a run ID, this action reads only from the caller's current workflow run. + name: ${{ inputs.overlay-artifact }} + path: . + digest-mismatch: error + # Only WordPress 7.0+ include Gutenberg-maintained assets from a built zip file. - name: Download prepared Gutenberg build if: ${{ inputs.gutenberg-artifact }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: gutenberg-build + # Every branch in a run shares one artifact namespace, so the producer names each + # build after the ref it came from. Compose the same name here rather than taking + # it as an input: the two sides already agree on `ref`. + name: gutenberg-build${{ inputs.ref && format( '-{0}', inputs.ref ) || '' }} path: gutenberg digest-mismatch: error From ea2e26527c18ac6349bf68ecd0eeb4574919f996 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Thu, 6 Aug 2026 15:44:46 +0200 Subject: [PATCH 2/5] Build/Test Tools: Allow the Gutenberg producer to prepare another repository. The producer checks out the commit that started the calling workflow run, so a caller preparing a build for a branch of another repository always got the wrong one. Add optional `repository` and `ref` inputs. Name the uploaded artifact after the ref it was built from. Every branch in a run shares one artifact namespace, so a caller preparing several builds would otherwise collide on a single name. The PHPUnit consumer composes the same name from its own `ref`, so the name is never passed between them. Retry the download as well. On branches whose download.js predates the in-script retry the blob is streamed straight into tar in a single attempt, so an interrupted stream fails the job outright. Both inputs default to the current behaviour, so existing callers are unaffected: an empty `ref` keeps the original `gutenberg-build` name. --- .../workflows/reusable-prepare-gutenberg.yml | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/reusable-prepare-gutenberg.yml b/.github/workflows/reusable-prepare-gutenberg.yml index c5e1f904a9680..c150b456b4201 100644 --- a/.github/workflows/reusable-prepare-gutenberg.yml +++ b/.github/workflows/reusable-prepare-gutenberg.yml @@ -6,6 +6,17 @@ name: Prepare Gutenberg build on: workflow_call: + inputs: + repository: + description: 'The repository to check out. Defaults to the repository calling this workflow.' + required: false + type: 'string' + default: '' + ref: + description: 'The branch, tag, or SHA to check out. Defaults to the commit that started the calling workflow run.' + required: false + type: 'string' + default: '' outputs: gutenberg-sha: description: 'The immutable Gutenberg source SHA verified by this workflow.' @@ -29,8 +40,10 @@ jobs: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - # Resolve the Gutenberg ref from the exact commit that started this workflow run. - ref: ${{ github.sha }} + repository: ${{ inputs.repository || github.repository }} + # Resolve the Gutenberg ref from the exact commit that started this workflow run, + # unless the caller is preparing a build for a repository or branch of its own. + ref: ${{ inputs.ref || github.sha }} show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} persist-credentials: false @@ -42,7 +55,17 @@ jobs: - name: Download and verify Gutenberg build id: download run: | - node tools/gutenberg/download.js + # Branches whose download.js predates the in-script retry stream the blob straight + # into tar in a single attempt, so an interrupted stream fails the run outright. + for attempt in 1 2 3; do + node tools/gutenberg/download.js && break + if [ "$attempt" = 3 ]; then + echo "Gutenberg download failed after $attempt attempts." >&2 + exit 1 + fi + echo "Gutenberg download failed; retrying in 5 seconds..." + sleep 5 + done gutenberg_sha="$(tr -d '\n' < gutenberg/.gutenberg-hash)" if [[ ! "$gutenberg_sha" =~ ^[a-fA-F0-9]{40}$ ]]; then echo "Expected a 40-character Gutenberg SHA, received: $gutenberg_sha" >&2 @@ -53,7 +76,10 @@ jobs: - name: Upload Gutenberg build uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: gutenberg-build + # Every branch in a run shares one artifact namespace, so name the build after the + # ref it came from. The PHPUnit consumer composes the same name from its own `ref`, + # so the name is never passed between them. + name: gutenberg-build${{ inputs.ref && format( '-{0}', inputs.ref ) || '' }} path: gutenberg/ if-no-files-found: error include-hidden-files: true From f0c36d3056b0cb6b640221f391cda3f55494fd89 Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Mon, 31 Aug 2026 16:40:33 +0200 Subject: [PATCH 3/5] Build/Test Tools: Document the ref values the artifact name allows. Both workflows compose the Gutenberg artifact name from `ref`, and upload-artifact rejects a name holding a slash or any of : < > | * ? or a double quote. A `refs/heads/trunk` or `feature/x` value fails the upload, after the download has already run. Workflow expressions have no string replacement, so the two sides cannot sanitise the value independently and still agree on a name. State the constraint on both inputs instead. `reusable-test-core-build-process` avoids the problem the other way, naming its ZIP after a pull request number or a SHA. Claude-Session: https://claude.ai/code/session_011piBrz5J1ZWLqwuCF2YYgG --- .github/workflows/reusable-phpunit-tests-v3.yml | 2 ++ .github/workflows/reusable-prepare-gutenberg.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index d4bebc3153fa1..3988f75fa900a 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -18,6 +18,8 @@ on: required: false type: 'string' default: '' + # Also composes the Gutenberg build name, under the producer's constraint: a valid + # artifact name, so no slashes and none of : < > | * ? " ref: description: 'The branch, tag, or SHA to check out. Defaults to the ref that triggered the calling workflow.' required: false diff --git a/.github/workflows/reusable-prepare-gutenberg.yml b/.github/workflows/reusable-prepare-gutenberg.yml index c150b456b4201..7564c40e83416 100644 --- a/.github/workflows/reusable-prepare-gutenberg.yml +++ b/.github/workflows/reusable-prepare-gutenberg.yml @@ -12,6 +12,8 @@ on: required: false type: 'string' default: '' + # Names the uploaded build too, so it must also be a valid artifact name: no slashes, + # and none of : < > | * ? " — a `refs/heads/trunk` or `feature/x` value fails the upload. ref: description: 'The branch, tag, or SHA to check out. Defaults to the commit that started the calling workflow run.' required: false From 974e393cc1c68c46b84b17aca026c490df4ec28e Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Mon, 31 Aug 2026 16:43:54 +0200 Subject: [PATCH 4/5] Build/Test Tools: Require a ref when pointing at another repository. The new `repository` input is only half a location. Without a `ref` the PHPUnit workflow checks out that repository's default branch, so a job labelled for one branch tests another and still reports green. The Gutenberg producer fails instead, but on a commit SHA that says nothing about the missing input. Fail both up front, before the checkout, rather than leaving the two workflows to diverge on how they mishandle the same gap. Claude-Session: https://claude.ai/code/session_011piBrz5J1ZWLqwuCF2YYgG --- .github/workflows/reusable-phpunit-tests-v3.yml | 9 +++++++++ .github/workflows/reusable-prepare-gutenberg.yml | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml index 3988f75fa900a..02dd3e46a1dc8 100644 --- a/.github/workflows/reusable-phpunit-tests-v3.yml +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -126,6 +126,7 @@ jobs: # Runs the PHPUnit tests for WordPress. # # Performs the following steps: + # - Requires a ref when the caller points at another repository. # - Sets environment variables. # - Checks out the repository. # - Unpacks the caller-provided overlay artifact over the checkout, if any. @@ -153,6 +154,14 @@ jobs: contents: read steps: + # `ref` otherwise falls back to checkout's own default, which for another repository is + # its default branch: a "WordPress 6.2" job would test trunk and report green. + - name: Require a ref when testing another repository + if: ${{ inputs.repository != '' && inputs.ref == '' }} + run: | + echo 'Testing another repository needs an explicit ref.' >&2 + exit 1 + - name: Configure environment variables run: | echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" diff --git a/.github/workflows/reusable-prepare-gutenberg.yml b/.github/workflows/reusable-prepare-gutenberg.yml index 7564c40e83416..a305d42bbfc27 100644 --- a/.github/workflows/reusable-prepare-gutenberg.yml +++ b/.github/workflows/reusable-prepare-gutenberg.yml @@ -39,6 +39,13 @@ jobs: contents: read steps: + # `ref` otherwise falls back to this run's commit, which another repository does not have. + - name: Require a ref when preparing another repository + if: ${{ inputs.repository != '' && inputs.ref == '' }} + run: | + echo 'Preparing another repository needs an explicit ref.' >&2 + exit 1 + - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: From 58e8a8f840fbfae9977c58c34e15ec6e03d0a1fa Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Mon, 31 Aug 2026 16:44:42 +0200 Subject: [PATCH 5/5] Build/Test Tools: Keep the download retry inside the job timeout. Three outer attempts around the three that download.js now makes itself is nine downloads of up to two minutes, well past the ten minutes the job is allowed. A persistently failing download had the job killed on the timeout instead of reporting why it gave up. Retry once, and raise the timeout to cover the two attempts that remain. Branches whose download.js predates the in-script retry still get the second try they were given this for. Claude-Session: https://claude.ai/code/session_011piBrz5J1ZWLqwuCF2YYgG --- .../workflows/reusable-prepare-gutenberg.yml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/reusable-prepare-gutenberg.yml b/.github/workflows/reusable-prepare-gutenberg.yml index a305d42bbfc27..1e95f09fd2a05 100644 --- a/.github/workflows/reusable-prepare-gutenberg.yml +++ b/.github/workflows/reusable-prepare-gutenberg.yml @@ -32,7 +32,8 @@ jobs: prepare-gutenberg: name: Gutenberg runs-on: ubuntu-24.04 - timeout-minutes: 10 + # Two download attempts of up to six minutes each, where download.js retries internally. + timeout-minutes: 15 outputs: gutenberg-sha: ${{ steps.download.outputs.gutenberg-sha }} permissions: @@ -66,15 +67,15 @@ jobs: run: | # Branches whose download.js predates the in-script retry stream the blob straight # into tar in a single attempt, so an interrupted stream fails the run outright. - for attempt in 1 2 3; do - node tools/gutenberg/download.js && break - if [ "$attempt" = 3 ]; then - echo "Gutenberg download failed after $attempt attempts." >&2 - exit 1 - fi - echo "Gutenberg download failed; retrying in 5 seconds..." + # One retry, not two: where download.js does retry, it already allows three tries of + # two minutes each, and a third attempt here would outrun `timeout-minutes` and have + # the job killed rather than reported. + if ! node tools/gutenberg/download.js; then + echo 'Gutenberg download failed; retrying in 5 seconds...' sleep 5 - done + node tools/gutenberg/download.js + fi + gutenberg_sha="$(tr -d '\n' < gutenberg/.gutenberg-hash)" if [[ ! "$gutenberg_sha" =~ ^[a-fA-F0-9]{40}$ ]]; then echo "Expected a 40-character Gutenberg SHA, received: $gutenberg_sha" >&2