diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d04adce1..16bb3f5c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,6 @@ on: push: branches: [main] pull_request: - # `labeled` / `unlabeled` listed so adding the `ci:run` label fires a - # workflow run. Defaults [opened, synchronize, reopened] must be re- - # declared because specifying `types:` drops the implicit defaults. - types: [opened, synchronize, reopened, labeled, unlabeled] branches: [main] # Merge queue: GitHub builds a temporary `gh-readonly-queue/main/...` branch # combining PRs + main, and runs CI on that combined state. Without this @@ -199,25 +195,14 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - # PR-time test gating: the author opts in by adding the `ci:run` label. - # Pattern is the GitHub-personal-repo substitute for merge queue / self- - # approve (PR authors can't approve their own PRs). Once labeled, - # subsequent `synchronize` events keep firing test as long as the label - # is present; remove the label to pause test on further pushes. - # - # Non-PR events (push:main / merge_group / workflow_dispatch) always run - # test — they're the post-merge / pre-merge-queue / manual paths and have - # no opt-in semantic. - # - # test-result aggregator below distinguishes PR-skipped (blocking — author - # forgot the label) from non-PR-skipped (allowed — docs-only short-circuit - # on push:main is legitimate). + # Test fires unconditionally on every PR + push + merge_group. Author + # opt-in via `ci:run` label was removed once Mergify queue_conditions + # took over the merge gate — the label dance conflicted with auto- + # routing (queue needs Test green; label toggle to fire Test caused + # cancellation races) and was redundant with `detect-changes` already + # skipping the test job for docs-only changes. if: | !(contains(github.event.pull_request.title, '[skip ci]') || contains(github.event.head_commit.message, '[skip ci]')) - && ( - github.event_name != 'pull_request' - || contains(github.event.pull_request.labels.*.name, 'ci:run') - ) env: # 14 tree-sitter parsers × 30+ test binaries × debug-info defaulted to # `true` produced ~30 GB in target/debug/deps/ and filled the ubuntu @@ -306,11 +291,8 @@ jobs: # # Skip-handling rules: # - `success` → pass (real green) - # - `skipped` on non-PR event → pass (docs-only short-circuit on push:main - # is legitimate; nothing to validate) - # - `skipped` on PR event → FAIL (author opted out by not adding `ci:run` - # label, but branch protection requires test green before merge — this - # forces the label to come on before merge can happen) + # - `skipped` → pass (only possible via [skip ci] in title/message; legit + # opt-out, gate is title-prefix not label) # - anything else (failure / cancelled) → FAIL test-result: name: Test (all platforms) @@ -327,13 +309,9 @@ jobs: if [ "$TEST_RESULT" = "success" ]; then exit 0 fi - if [ "$TEST_RESULT" = "skipped" ] && [ "$EVENT_NAME" != "pull_request" ]; then - echo "✓ Skipped on $EVENT_NAME — docs-only short-circuit OK" - exit 0 - fi if [ "$TEST_RESULT" = "skipped" ]; then - echo "::error::Test was skipped on PR — add 'ci:run' label to fire it before merge" - exit 1 + echo "✓ Skipped — [skip ci] in commit/title" + exit 0 fi echo "::error::At least one test shard failed or was cancelled" exit 1 diff --git a/.mergify.yml b/.mergify.yml index 0a0f4485..8c8a24cf 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -1,58 +1,35 @@ # Mergify config. # -# Per Mergify best practice: -# - queue_rules pin `base=` so cross-target-branch PRs can't batch together -# (Mergify speculative-trial would re-base each onto the wrong tip). -# - One pull_request_rule per (area, base) pair selects the target queue -# explicitly; without an explicit routing rule, Mergify can't know which -# area-specific queue a PR belongs in. -# - Priority is set declaratively per rule via `queue.priority`, so the -# risk-low PRs land at the front of their area queue. +# Routing pattern (modern Mergify, post-2024): +# - Each queue_rule carries its own `queue_conditions` — labels, base, and +# required-check list a PR must satisfy to auto-enter THIS queue. +# - `merge_conditions` is what gates the actual merge once embarked. +# - No `pull_request_rules` needed for routing: Mergify checks each PR +# against every queue's `queue_conditions` and embarks it into the first +# match. Order queue_rules from most-specific to least-specific. # -# The required-check list is duplicated across routing rules — YAML anchors -# splice nested lists which Mergify rejects, so DRY isn't available here. +# Why not pull_request_rules.queue? +# - That action exists for EXPLICIT enqueuing (commands, advanced filters), +# not auto-routing. With only pull_request_rules, the Mergify Merge Queue +# check stays at "Merge queue is ready" without ever embarking — confirmed +# on PR #386 (manual `@Mergifyio queue` worked, auto did not). +# +# The required-check list is duplicated across queue_conditions — YAML +# anchors splice nested lists which Mergify rejects, so DRY isn't available. # Keep the list synced with `gh api repos/.../branches/main/protection`. # -# Adding a new target branch (e.g. `develop`): replicate the 5 queue_rules -# under `develop-*` names + 5 routing pull_request_rules with `base=develop`. +# Where does `ecp/cross-pr-conflict` belong? +# queue_conditions, NOT merge_conditions. Mergify's speculative trial +# creates a fresh commit (PR diff + main tip) — ecp-pr-analyze.yml +# triggers on `pull_request` events only, so the trial commit has no +# cross-pr-conflict status reported against it. Anything required in +# merge_conditions would thus block the trial indefinitely → dequeue +# (PR #386 was the live repro). Keeping the check in queue_conditions +# evaluates it on the PR HEAD where the status actually exists. queue_rules: - name: main-test-only - merge_conditions: - - base=main - - check-success=ecp/cross-pr-conflict - batch_size: 10 - batch_max_wait_time: 30s - - - name: main-parser-changes - merge_conditions: - - base=main - - check-success=ecp/cross-pr-conflict - batch_size: 1 - - - name: main-cli-changes - merge_conditions: - - base=main - - check-success=ecp/cross-pr-conflict - batch_size: 3 - batch_max_wait_time: 2m - - - name: main-docs-only - merge_conditions: - - base=main - - check-success=ecp/cross-pr-conflict - batch_size: 20 - batch_max_wait_time: 10s - - - name: main-default - merge_conditions: - - base=main - - check-success=ecp/cross-pr-conflict - batch_size: 2 - -pull_request_rules: - - name: route area-test to main-test-only - conditions: + queue_conditions: - base=main - label=merge-queue - label=ecp:area-test @@ -62,12 +39,14 @@ pull_request_rules: - check-success=Lint GitHub Actions workflows - check-success=CodeQL - check-success=dependency-review - actions: - queue: - name: main-test-only + - check-success=ecp/cross-pr-conflict + merge_conditions: + - base=main + batch_size: 10 + batch_max_wait_time: 30s - - name: route area-parser to main-parser-changes - conditions: + - name: main-parser-changes + queue_conditions: - base=main - label=merge-queue - label=ecp:area-parser @@ -77,12 +56,13 @@ pull_request_rules: - check-success=Lint GitHub Actions workflows - check-success=CodeQL - check-success=dependency-review - actions: - queue: - name: main-parser-changes + - check-success=ecp/cross-pr-conflict + merge_conditions: + - base=main + batch_size: 1 - - name: route area-cli to main-cli-changes - conditions: + - name: main-cli-changes + queue_conditions: - base=main - label=merge-queue - label=ecp:area-cli @@ -92,12 +72,14 @@ pull_request_rules: - check-success=Lint GitHub Actions workflows - check-success=CodeQL - check-success=dependency-review - actions: - queue: - name: main-cli-changes + - check-success=ecp/cross-pr-conflict + merge_conditions: + - base=main + batch_size: 3 + batch_max_wait_time: 2m - - name: route area-docs to main-docs-only - conditions: + - name: main-docs-only + queue_conditions: - base=main - label=merge-queue - label=ecp:area-docs @@ -107,12 +89,16 @@ pull_request_rules: - check-success=Lint GitHub Actions workflows - check-success=CodeQL - check-success=dependency-review - actions: - queue: - name: main-docs-only + - check-success=ecp/cross-pr-conflict + merge_conditions: + - base=main + batch_size: 20 + batch_max_wait_time: 10s - - name: route default to main-default - conditions: + # Fallback queue — catches PRs with `merge-queue` label but no area label. + # Must come last; queue_conditions match via negation on the area-label set. + - name: main-default + queue_conditions: - base=main - label=merge-queue - -label~=^ecp:area- @@ -122,14 +108,12 @@ pull_request_rules: - check-success=Lint GitHub Actions workflows - check-success=CodeQL - check-success=dependency-review - actions: - queue: - name: main-default + - check-success=ecp/cross-pr-conflict + merge_conditions: + - base=main + batch_size: 2 - # Priority overrides removed in this PR. - # Modern Mergify exposes priority via `queue_rules.priority_rules` (per-queue - # nested rule list), not via `actions.queue.priority` on pull_request_rules. - # Implementing it cleanly requires duplicating priority_rules across all 5 - # queue_rules (no shared anchor since Mergify YAML rejects nested-list - # splice). Tracked in FU-2026-05-23-037 — to be added when priority gets - # observed value (i.e. when same-area queue actually backs up). +# Priority overrides: deferred to FU-2026-05-23-037. +# Modern Mergify exposes priority via `queue_rules.priority_rules` (per-queue +# nested rule list). Adding it cleanly requires duplicating priority_rules +# across all 5 queue_rules. Implement when same-area queue actually backs up.