Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 10 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
138 changes: 61 additions & 77 deletions .mergify.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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-
Expand All @@ -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.
Loading