From 7c2be04d142aca6e83b56d8cf6f9fc55f458fabd Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:15:56 +0000 Subject: [PATCH 1/4] CI: run the required checks for GitHub merge queue groups PHPUnit Feature, Shared Server fixtures, SonarQube and Public Boundary now also run on merge_group, so a merge queue can replace the require-up-to-date rerun loop. Base-revision checks use the queue group's base_sha, the boundary scan covers the group's exact base..head range, and the push trigger skips gh-readonly-queue/** branches that the merge_group run already scans. Shared Server fixtures has no path filter for merge_group, so every queued group gets the full profile. --- .github/workflows/phpunit-feature.yml | 9 +++++---- .github/workflows/public-boundary.yml | 6 ++++++ .github/workflows/server-parity.yml | 1 + .github/workflows/sonar.yml | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/phpunit-feature.yml b/.github/workflows/phpunit-feature.yml index 177f18693..72bd527cc 100644 --- a/.github/workflows/phpunit-feature.yml +++ b/.github/workflows/phpunit-feature.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + merge_group: workflow_dispatch: inputs: corpus_base_ref: @@ -91,7 +92,7 @@ jobs: - name: Require versioned worker OpenAPI evolution env: - OPENAPI_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || inputs.corpus_base_ref }} + OPENAPI_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event_name == 'push' && github.event.before || inputs.corpus_base_ref }} run: php scripts/ci/check-worker-openapi-evolution.php "$OPENAPI_BASE_REF" - name: Audit locked PHP dependencies @@ -99,7 +100,7 @@ jobs: - name: Require durable codec evidence env: - CORPUS_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || inputs.corpus_base_ref }} + CORPUS_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event_name == 'push' && github.event.before || inputs.corpus_base_ref }} run: | set -euo pipefail if [[ ! "$CORPUS_BASE_REF" =~ ^[0-9a-f]{40}$ ]] || [[ "$CORPUS_BASE_REF" =~ ^0+$ ]]; then @@ -173,7 +174,7 @@ jobs: - name: Run feature, Nexus, and regression corpus suites shell: bash env: - CORPUS_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'push' && github.event.before || inputs.corpus_base_ref }} + CORPUS_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event_name == 'merge_group' && github.event.merge_group.base_sha || github.event_name == 'push' && github.event.before || inputs.corpus_base_ref }} run: | set -euo pipefail if [[ ! "$CORPUS_BASE_REF" =~ ^[0-9a-f]{40}$ ]] || [[ "$CORPUS_BASE_REF" =~ ^0+$ ]]; then @@ -268,7 +269,7 @@ jobs: - name: Select production image qualification for changed build inputs id: image_inputs run: | - if git diff --quiet '${{ github.event.pull_request.base.sha || github.event.before || inputs.corpus_base_ref }}' HEAD -- Dockerfile Dockerfile.dockerignore .dockerignore composer.json composer.lock ':(glob)docker/**'; then + if git diff --quiet '${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha || github.event.before || inputs.corpus_base_ref }}' HEAD -- Dockerfile Dockerfile.dockerignore .dockerignore composer.json composer.lock ':(glob)docker/**'; then echo 'required=false' >> "$GITHUB_OUTPUT" else echo 'required=true' >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/public-boundary.yml b/.github/workflows/public-boundary.yml index 89bfb4a83..16b048f75 100644 --- a/.github/workflows/public-boundary.yml +++ b/.github/workflows/public-boundary.yml @@ -2,7 +2,11 @@ name: Public Boundary on: pull_request: + merge_group: + # A merge-queue branch is scanned by its merge_group run, over the exact range. push: + branches-ignore: + - gh-readonly-queue/** workflow_dispatch: permissions: @@ -28,6 +32,8 @@ jobs: if [[ "${{ github.event_name }}" == "pull_request" ]]; then range="${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}" + elif [[ "${{ github.event_name }}" == "merge_group" ]]; then + range="${{ github.event.merge_group.base_sha }}..${{ github.event.merge_group.head_sha }}" elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then range="-1 ${{ github.sha }}" elif [[ "${{ github.event.before }}" =~ ^0+$ ]]; then diff --git a/.github/workflows/server-parity.yml b/.github/workflows/server-parity.yml index bdb5bcec3..1416deba5 100644 --- a/.github/workflows/server-parity.yml +++ b/.github/workflows/server-parity.yml @@ -114,6 +114,7 @@ on: - tests/Support/ServerParityAdmissionObservation.mjs - tests/Support/ServerParityVisibilityObservation.mjs - rust/** + merge_group: workflow_dispatch: # A newer push to the same pull request supersedes the run in progress, so a diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 8aad74cac..785953ceb 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -5,6 +5,7 @@ on: branches: [main] pull_request: branches: [main] + merge_group: permissions: contents: read From c1678a81bd0a1605524b3204dae564ce2a7b1755 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:22:15 +0000 Subject: [PATCH 2/4] CI: contract test for the merge queue triggers tests/Unit/MergeGroupTriggersTest.mjs checks that PHPUnit Feature, Shared Server fixtures, SonarQube and Public Boundary run on merge_group, that base-revision checks and the boundary range use the queue group's base and head, that queue branches are not scanned twice, and that superseded-run cancellation never applies to a queue group. It runs in Repository contracts. It fails 6 of 7 checks on the train head without merge_group support. --- .github/workflows/phpunit-feature.yml | 3 ++ tests/Unit/MergeGroupTriggersTest.mjs | 41 +++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/Unit/MergeGroupTriggersTest.mjs diff --git a/.github/workflows/phpunit-feature.yml b/.github/workflows/phpunit-feature.yml index 72bd527cc..676bd0319 100644 --- a/.github/workflows/phpunit-feature.yml +++ b/.github/workflows/phpunit-feature.yml @@ -78,6 +78,9 @@ jobs: - name: Require safe PHP image build context run: node --test tests/Unit/PhpImageContextTest.mjs + - name: Run the required checks for merge queue groups + run: node --test tests/Unit/MergeGroupTriggersTest.mjs + - name: Validate embedded Workflow package authority run: php scripts/ci/test-workflow-package-authority.php diff --git a/tests/Unit/MergeGroupTriggersTest.mjs b/tests/Unit/MergeGroupTriggersTest.mjs new file mode 100644 index 000000000..fef717fc9 --- /dev/null +++ b/tests/Unit/MergeGroupTriggersTest.mjs @@ -0,0 +1,41 @@ +// The checks a GitHub merge queue waits for must run on merge_group, and +// queue branches must not be scanned twice. Source-only: enabling the queue +// is a separate repository setting. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; + +const workflow = name => readFileSync(`.github/workflows/${name}`, 'utf8'); +// The top-level `on:` block, up to the next top-level key. +const triggers = text => { + const start = text.indexOf('\non:\n'); + assert.notEqual(start, -1, 'workflow has an on: block'); + const rest = text.slice(start + 5); + const end = rest.search(/\n[a-z_-]+:/); + return end === -1 ? rest : rest.slice(0, end); +}; + +for (const name of ['phpunit-feature.yml', 'server-parity.yml', 'sonar.yml', 'public-boundary.yml']) { + test(`${name} runs for merge queue groups`, () => { + assert.match(triggers(workflow(name)), /^ {2}merge_group:\s*$/m); + }); +} + +test('PHPUnit Feature diffs a queue group against its base', () => { + const text = workflow('phpunit-feature.yml'); + const bases = text.match(/github\.event_name == 'merge_group' && github\.event\.merge_group\.base_sha/g) ?? []; + assert.equal(bases.length, 3, 'OpenAPI and both corpus base revisions'); + assert.match(text, /github\.event\.pull_request\.base\.sha \|\| github\.event\.merge_group\.base_sha \|\| github\.event\.before/); +}); + +test('Public Boundary scans a queue group over its exact range, once', () => { + const text = workflow('public-boundary.yml'); + assert.match(text, /"merge_group" \]\]; then\n\s+range="\$\{\{ github\.event\.merge_group\.base_sha \}\}\.\.\$\{\{ github\.event\.merge_group\.head_sha \}\}"/); + assert.match(triggers(text), /^ {2}push:\n {4}branches-ignore:\n {6}- gh-readonly-queue\/\*\*$/m); +}); + +test('superseded-run cancellation never cancels a queue group', () => { + for (const name of ['phpunit-feature.yml', 'server-parity.yml', 'sonar.yml']) { + assert.match(workflow(name), /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/, name); + } +}); From c5be73422beb15b476b33774dca67470971ceb3f Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:40:04 +0000 Subject: [PATCH 3/4] Expect the merge-queue base revision in the PHPUnit Feature contracts The OpenAPI evolution and regression corpus contracts pin the exact base revision expression; include the merge_group base so a queued group diffs against the revision it was built on. --- tests/Unit/OpenApiDocumentEvolutionTest.php | 2 +- tests/Unit/PhpunitFeatureWorkflowContractTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/OpenApiDocumentEvolutionTest.php b/tests/Unit/OpenApiDocumentEvolutionTest.php index 135121125..97499930c 100644 --- a/tests/Unit/OpenApiDocumentEvolutionTest.php +++ b/tests/Unit/OpenApiDocumentEvolutionTest.php @@ -55,7 +55,7 @@ public function test_candidate_structure_and_feature_ci_compare_to_the_base_revi $this->assertCount(1, $preflightStep); $this->assertSame( - '${{ github.event_name == \'pull_request\' && github.event.pull_request.base.sha || github.event_name == \'push\' && github.event.before || inputs.corpus_base_ref }}', + '${{ github.event_name == \'pull_request\' && github.event.pull_request.base.sha || github.event_name == \'merge_group\' && github.event.merge_group.base_sha || github.event_name == \'push\' && github.event.before || inputs.corpus_base_ref }}', $preflightStep[0]['env']['OPENAPI_BASE_REF'] ?? null, ); $this->assertSame( diff --git a/tests/Unit/PhpunitFeatureWorkflowContractTest.php b/tests/Unit/PhpunitFeatureWorkflowContractTest.php index 14c48570a..b404d7c95 100644 --- a/tests/Unit/PhpunitFeatureWorkflowContractTest.php +++ b/tests/Unit/PhpunitFeatureWorkflowContractTest.php @@ -106,7 +106,7 @@ public function test_regression_corpus_validation_uses_git_capable_ci_and_an_exa $this->assertSame(0, $checkout['with']['fetch-depth'] ?? null); $this->assertFalse($checkout['with']['persist-credentials'] ?? null); $this->assertSame( - '${{ github.event_name == \'pull_request\' && github.event.pull_request.base.sha || github.event_name == \'push\' && github.event.before || inputs.corpus_base_ref }}', + '${{ github.event_name == \'pull_request\' && github.event.pull_request.base.sha || github.event_name == \'merge_group\' && github.event.merge_group.base_sha || github.event_name == \'push\' && github.event.before || inputs.corpus_base_ref }}', $execution['env']['CORPUS_BASE_REF'] ?? null, ); $this->assertArrayNotHasKey('if', $execution); From 2781a920af76941b68515cdc0092ddccfe70bb37 Mon Sep 17 00:00:00 2001 From: Richard McDaniel <1130888+rmcdaniel@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:42:30 +0000 Subject: [PATCH 4/4] Keep SonarQube and unfiltered push scans out of the merge-queue change The Sonar job receives SONAR_TOKEN and its fork guard is always true for a merge group, so it no longer runs on merge_group; the contract test now requires that. Public Boundary scans every push again, including queue branches, and the test requires the unfiltered push trigger alongside the exact merge-group range. --- .github/workflows/public-boundary.yml | 3 --- .github/workflows/sonar.yml | 1 - tests/Unit/MergeGroupTriggersTest.mjs | 22 +++++++++++++++------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/public-boundary.yml b/.github/workflows/public-boundary.yml index 16b048f75..763b55446 100644 --- a/.github/workflows/public-boundary.yml +++ b/.github/workflows/public-boundary.yml @@ -3,10 +3,7 @@ name: Public Boundary on: pull_request: merge_group: - # A merge-queue branch is scanned by its merge_group run, over the exact range. push: - branches-ignore: - - gh-readonly-queue/** workflow_dispatch: permissions: diff --git a/.github/workflows/sonar.yml b/.github/workflows/sonar.yml index 785953ceb..8aad74cac 100644 --- a/.github/workflows/sonar.yml +++ b/.github/workflows/sonar.yml @@ -5,7 +5,6 @@ on: branches: [main] pull_request: branches: [main] - merge_group: permissions: contents: read diff --git a/tests/Unit/MergeGroupTriggersTest.mjs b/tests/Unit/MergeGroupTriggersTest.mjs index fef717fc9..e8efba169 100644 --- a/tests/Unit/MergeGroupTriggersTest.mjs +++ b/tests/Unit/MergeGroupTriggersTest.mjs @@ -1,6 +1,7 @@ -// The checks a GitHub merge queue waits for must run on merge_group, and -// queue branches must not be scanned twice. Source-only: enabling the queue -// is a separate repository setting. +// The Actions checks a GitHub merge queue waits for must run on merge_group. +// Source-only: enabling the queue is a separate repository setting, and its +// Sonar gate and terminal Shared Server fixtures result are separate +// prerequisites (Server #725). import test from 'node:test'; import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; @@ -15,12 +16,18 @@ const triggers = text => { return end === -1 ? rest : rest.slice(0, end); }; -for (const name of ['phpunit-feature.yml', 'server-parity.yml', 'sonar.yml', 'public-boundary.yml']) { +for (const name of ['phpunit-feature.yml', 'server-parity.yml', 'public-boundary.yml']) { test(`${name} runs for merge queue groups`, () => { assert.match(triggers(workflow(name)), /^ {2}merge_group:\s*$/m); }); } +// The Sonar job receives SONAR_TOKEN and its fork guard is always true for a +// merge group, so candidate source must not reach it through the queue. +test('SonarQube does not run for merge queue groups', () => { + assert.doesNotMatch(triggers(workflow('sonar.yml')), /merge_group/); +}); + test('PHPUnit Feature diffs a queue group against its base', () => { const text = workflow('phpunit-feature.yml'); const bases = text.match(/github\.event_name == 'merge_group' && github\.event\.merge_group\.base_sha/g) ?? []; @@ -28,14 +35,15 @@ test('PHPUnit Feature diffs a queue group against its base', () => { assert.match(text, /github\.event\.pull_request\.base\.sha \|\| github\.event\.merge_group\.base_sha \|\| github\.event\.before/); }); -test('Public Boundary scans a queue group over its exact range, once', () => { +test('Public Boundary scans a queue group over its exact range and still scans every push', () => { const text = workflow('public-boundary.yml'); assert.match(text, /"merge_group" \]\]; then\n\s+range="\$\{\{ github\.event\.merge_group\.base_sha \}\}\.\.\$\{\{ github\.event\.merge_group\.head_sha \}\}"/); - assert.match(triggers(text), /^ {2}push:\n {4}branches-ignore:\n {6}- gh-readonly-queue\/\*\*$/m); + assert.match(triggers(text), /^ {2}push:\s*$/m); + assert.doesNotMatch(triggers(text), /branches-ignore|branches:|paths/); }); test('superseded-run cancellation never cancels a queue group', () => { - for (const name of ['phpunit-feature.yml', 'server-parity.yml', 'sonar.yml']) { + for (const name of ['phpunit-feature.yml', 'server-parity.yml']) { assert.match(workflow(name), /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/, name); } });