From d8464141c49d444123ef25db72aee99679c4c2b1 Mon Sep 17 00:00:00 2001 From: Byron Williams Date: Thu, 3 Sep 2026 10:03:39 -0700 Subject: [PATCH 1/2] ci: add merge_group trigger to required-check workflows The merge_queue ruleset (grouping_strategy: ALLGREEN) requires CI Gate, Security Gate Validation, Dependency & Standards Validation, and Check REUSE Compliance, but none of the pull_request workflows that emit these contexts also trigger on merge_group. A queued PR's merge-group build never dispatches these workflows, so the required contexts never report, the entry waits out the 60 minute timeout, and it is ejected UNMERGEABLE. Without this fix the merge queue can never merge anything. - ci.yml: adds merge_group so job 'CI Gate' reports under the merge queue. - security-analysis.yml: adds merge_group so job 'Security Gate Validation' reports under the merge queue. - reuse.yml: adds merge_group so job 'Check REUSE Compliance' reports under the merge queue. - pr-validation.yml: adds merge_group so job 'Dependency & Standards Validation' reports under the merge queue; also guards title-check and body-check to pull_request only (github.event.pull_request.title and .body are null under merge_group), and updates the validation-gate and aggregator scripts to pass on a non-pull_request event instead of treating the now-skipped upstream jobs as failures. The concurrency group falls back to github.ref when github.event.pull_request.number is unset so a merge-group build cannot collide with, or cancel, a PR build's concurrency group. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 1 + .github/workflows/pr-validation.yml | 63 +++++++++++++++++++------ .github/workflows/reuse.yml | 1 + .github/workflows/security-analysis.yml | 1 + 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 30d9cb0..d6c5e4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,7 @@ on: pull_request: types: [opened, synchronize, reopened] branches: [main, master, develop] + merge_group: workflow_dispatch: permissions: diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 2c5b801..d114aa0 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -8,17 +8,28 @@ on: branches: - main - master + merge_group: permissions: {} concurrency: - group: pr-validation-${{ github.event.pull_request.number }} + # #ASSUME github.event.pull_request.number is null under merge_group; + # fall back to github.ref (unique per merge-group ref) so a merge-group + # build never collides with, or is cancelled by, a PR build's group. + # #VERIFY confirm no cross-cancellation between a PR run and its + # corresponding merge-group run after the first live merge-queue pass. + group: pr-validation-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: title-check: name: PR Title Format runs-on: ubuntu-latest + # #CRITICAL github.event.pull_request.title is null under merge_group, + # so this check has nothing to validate there; restrict to pull_request. + # #VERIFY validate-dependencies below still reports success when this + # job is skipped under merge_group. + if: github.event_name == 'pull_request' permissions: pull-requests: read steps: @@ -45,6 +56,11 @@ jobs: body-check: name: PR Body Non-Empty runs-on: ubuntu-latest + # #CRITICAL github.event.pull_request.body is null under merge_group, + # so this check has nothing to validate there; restrict to pull_request. + # #VERIFY validate-dependencies below still reports success when this + # job is skipped under merge_group. + if: github.event_name == 'pull_request' permissions: pull-requests: read steps: @@ -81,19 +97,25 @@ jobs: env: TITLE_RESULT: ${{ needs.title-check.result }} BODY_RESULT: ${{ needs.body-check.result }} + EVENT_NAME: ${{ github.event_name }} run: | - echo "## PR Validation Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Check | Result |" >> $GITHUB_STEP_SUMMARY - echo "|---|---|" >> $GITHUB_STEP_SUMMARY - echo "| PR Title Format | $TITLE_RESULT |" >> $GITHUB_STEP_SUMMARY - echo "| PR Body Non-Empty | $BODY_RESULT |" >> $GITHUB_STEP_SUMMARY + echo "## PR Validation Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "| Check | Result |" >> "$GITHUB_STEP_SUMMARY" + echo "|---|---|" >> "$GITHUB_STEP_SUMMARY" + echo "| PR Title Format | $TITLE_RESULT |" >> "$GITHUB_STEP_SUMMARY" + echo "| PR Body Non-Empty | $BODY_RESULT |" >> "$GITHUB_STEP_SUMMARY" + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "Event is $EVENT_NAME, not pull_request; title/body checks skipped." >> "$GITHUB_STEP_SUMMARY" + echo "All PR validation checks passed." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi if [ "$TITLE_RESULT" != "success" ] || [ "$BODY_RESULT" != "success" ]; then echo "::error::One or more PR validation checks failed." exit 1 fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "All PR validation checks passed." >> $GITHUB_STEP_SUMMARY + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "All PR validation checks passed." >> "$GITHUB_STEP_SUMMARY" validate-dependencies: # Aggregator named to satisfy the org ruleset @@ -114,18 +136,29 @@ jobs: env: TITLE_RESULT: ${{ needs.title-check.result }} BODY_RESULT: ${{ needs.body-check.result }} + EVENT_NAME: ${{ github.event_name }} run: | - echo "## Dependency & Standards Validation" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY + echo "## Dependency & Standards Validation" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + # #CRITICAL required status check for the merge queue; a + # merge_group build has no PR title/body to check, so this + # aggregator must pass on the absence of those jobs rather than + # on their (impossible) success. + # #VERIFY watch the first live merge-group run to confirm this + # context reports success instead of stalling the queue. + if [ "$EVENT_NAME" != "pull_request" ]; then + echo "Event is $EVENT_NAME, not pull_request; standards not applicable." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi if [ "$TITLE_RESULT" = "success" ]; then - echo "Conventional Commits standard: Passed" >> $GITHUB_STEP_SUMMARY + echo "Conventional Commits standard: Passed" >> "$GITHUB_STEP_SUMMARY" else - echo "Conventional Commits standard: $TITLE_RESULT" >> $GITHUB_STEP_SUMMARY + echo "Conventional Commits standard: $TITLE_RESULT" >> "$GITHUB_STEP_SUMMARY" fi if [ "$BODY_RESULT" = "success" ]; then - echo "PR Body standard: Passed" >> $GITHUB_STEP_SUMMARY + echo "PR Body standard: Passed" >> "$GITHUB_STEP_SUMMARY" else - echo "PR Body standard: $BODY_RESULT" >> $GITHUB_STEP_SUMMARY + echo "PR Body standard: $BODY_RESULT" >> "$GITHUB_STEP_SUMMARY" fi if [ "$TITLE_RESULT" != "success" ] || [ "$BODY_RESULT" != "success" ]; then echo "::error::Dependency and standards validation failed." diff --git a/.github/workflows/reuse.yml b/.github/workflows/reuse.yml index 849a86b..e9d3bee 100644 --- a/.github/workflows/reuse.yml +++ b/.github/workflows/reuse.yml @@ -12,6 +12,7 @@ on: - "REUSE.toml" - "LICENSES/**" - ".github/workflows/reuse.yml" + merge_group: push: branches: - main diff --git a/.github/workflows/security-analysis.yml b/.github/workflows/security-analysis.yml index 91e5ca3..3c7e017 100644 --- a/.github/workflows/security-analysis.yml +++ b/.github/workflows/security-analysis.yml @@ -7,6 +7,7 @@ on: branches: [main, master] pull_request: branches: [main, master] + merge_group: schedule: # Weekly scan on Mondays at 09:00 UTC - cron: '0 9 * * 1' From cdc65fea9679b5c99ab79c63947e43ee2ee8236d Mon Sep 17 00:00:00 2001 From: Byron Williams Date: Thu, 3 Sep 2026 11:07:51 -0700 Subject: [PATCH 2/2] fix(ci): make merge_group gate skip-tolerant, not event-name-bypassed Per-repo review flagged that checking github.event_name and returning success unconditionally would blanket-pass the Dependency & Standards Validation gate under merge_group, regardless of what any real upstream job found. Replace that with a narrower rule: each upstream job's result must be 'success' or 'skipped', evaluated individually. title-check and body-check are the only jobs this gate aggregates today, and they are 'skipped' under merge_group by the pull_request-only guard added in the previous commit (there is no PR title or body to check there); a real 'failure' on either job still fails the gate under both events. This aggregator does not currently wrap any dependency-scan job (needs has always been [title-check, body-check] on main), so no scan result is being bypassed; the comment documents that the same per-job rule must be applied if a real scan job is ever added here. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/pr-validation.yml | 42 ++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index d114aa0..5abc0eb 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -97,7 +97,6 @@ jobs: env: TITLE_RESULT: ${{ needs.title-check.result }} BODY_RESULT: ${{ needs.body-check.result }} - EVENT_NAME: ${{ github.event_name }} run: | echo "## PR Validation Summary" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" @@ -105,12 +104,16 @@ jobs: echo "|---|---|" >> "$GITHUB_STEP_SUMMARY" echo "| PR Title Format | $TITLE_RESULT |" >> "$GITHUB_STEP_SUMMARY" echo "| PR Body Non-Empty | $BODY_RESULT |" >> "$GITHUB_STEP_SUMMARY" - if [ "$EVENT_NAME" != "pull_request" ]; then - echo "Event is $EVENT_NAME, not pull_request; title/body checks skipped." >> "$GITHUB_STEP_SUMMARY" - echo "All PR validation checks passed." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - if [ "$TITLE_RESULT" != "success" ] || [ "$BODY_RESULT" != "success" ]; then + # #CRITICAL a required status check for the merge queue; a + # merge_group build skips title-check/body-check by design + # (no PR title/body to read there), so "skipped" is accepted + # here on top of "success". A real "failure" still fails this + # gate under either event, so the gate is not weakened, only + # made compatible with the merge_group event. + # #VERIFY watch the first live merge-group run to confirm this + # context reports success instead of stalling the queue. + ok() { [ "$1" = "success" ] || [ "$1" = "skipped" ]; } + if ! ok "$TITLE_RESULT" || ! ok "$BODY_RESULT"; then echo "::error::One or more PR validation checks failed." exit 1 fi @@ -136,31 +139,34 @@ jobs: env: TITLE_RESULT: ${{ needs.title-check.result }} BODY_RESULT: ${{ needs.body-check.result }} - EVENT_NAME: ${{ github.event_name }} run: | echo "## Dependency & Standards Validation" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" # #CRITICAL required status check for the merge queue; a - # merge_group build has no PR title/body to check, so this - # aggregator must pass on the absence of those jobs rather than - # on their (impossible) success. + # merge_group build skips title-check/body-check by design + # (no PR title/body to read there), so "skipped" is accepted + # here on top of "success" for each upstream job individually. + # A real "failure" still fails this gate under either event, so + # the gate is not weakened, only made compatible with the + # merge_group event. This aggregator currently only wraps + # title-check/body-check (no separate dependency-scan job feeds + # it on main); if a real scan job is added to this aggregator + # later, apply the same per-job success-or-skipped rule to it, + # never a blanket event-name bypass. # #VERIFY watch the first live merge-group run to confirm this # context reports success instead of stalling the queue. - if [ "$EVENT_NAME" != "pull_request" ]; then - echo "Event is $EVENT_NAME, not pull_request; standards not applicable." >> "$GITHUB_STEP_SUMMARY" - exit 0 - fi - if [ "$TITLE_RESULT" = "success" ]; then + ok() { [ "$1" = "success" ] || [ "$1" = "skipped" ]; } + if ok "$TITLE_RESULT"; then echo "Conventional Commits standard: Passed" >> "$GITHUB_STEP_SUMMARY" else echo "Conventional Commits standard: $TITLE_RESULT" >> "$GITHUB_STEP_SUMMARY" fi - if [ "$BODY_RESULT" = "success" ]; then + if ok "$BODY_RESULT"; then echo "PR Body standard: Passed" >> "$GITHUB_STEP_SUMMARY" else echo "PR Body standard: $BODY_RESULT" >> "$GITHUB_STEP_SUMMARY" fi - if [ "$TITLE_RESULT" != "success" ] || [ "$BODY_RESULT" != "success" ]; then + if ! ok "$TITLE_RESULT" || ! ok "$BODY_RESULT"; then echo "::error::Dependency and standards validation failed." exit 1 fi