CI workflows + merge queue#

The repo runs on GitHub Free, which means no native merge queue. A custom label-driven auto-update + auto-merge workflow stands in: PRs that opt in via the automerge label get rebased onto main whenever main moves and merged the moment CI is green.

This page is the contract for that workflow plus a tour of the five permanent CI workflows under .github/workflows/.

Workflows at a glance#

Workflow

Triggers on

What it does

Tests

PR + push to main

Lint, unit tests, optional-solver-backends matrix. Three jobs: Lint, Unit tests (py3.10), Optional solver backends. pytest-xdist for parallelism.

docs

PR + push to main

Sphinx build (strict mode — warnings as errors). Includes the Verification manual tests job (split out in #538) and the docs-CI gates landed in #583 (numpydoc per-symbol, doctest, linkcheck).

Verification manual tests

PR + push to main

Runs tests/cross_solver/test_verification_round_trip.py as its own status check so the harness’s signal is independent from the rest of the suite.

Auto-update PRs behind main

push: main, workflow_run from Tests / docs, workflow_dispatch

Walks every PR carrying automerge, rebases each onto the new main, and merges any that are green-and-current.

Track main / release CI failures

workflow_run from Tests / docs / Auto-update

Opens / updates a tracker issue when a failure on main (or in the merge queue) needs follow-up.

Benchmarks (nightly trend)

schedule: nightly

Runs the perf harness, posts trend updates. See Performance — profiling, snapshots, the perf tracker.

Release readiness

push to main

Diffs perf numbers against the previous tagged release; regressions block docs deploy.

docs-linkcheck

schedule: nightly

Runs Sphinx linkcheck on the published docs; external-link rot opens a tracker issue.

Each workflow lives at .github/workflows/<name>.yml.

The label-driven merge queue#

GitHub Free doesn’t ship a merge queue, so the repo synthesises one with a custom workflow. The automerge label is the opt-in.

Lifecycle:

  1. Author opens a PR. CI runs.

  2. Author adds the automerge label when the PR is ready to ship — code review passed, comments addressed, no pending changes. No label = the PR sits.

  3. The Auto-update PRs behind main workflow watches:

    • push: main — main moved, walk every labelled PR and rebase any that are behind.

    • workflow_run: [Tests, docs] completed — CI just finished on a labelled PR; if green and current with main, merge it.

    • workflow_dispatch — manual kick to drain the queue.

  4. The workflow performs two passes per fire:

    • Rebase pass — parallelise over labelled PRs that are behind main; for each, do a server-side rebase via gh pr update-branch.

    • Merge pass — serial over labelled PRs that are now CLEAN; merge with squash, refresh between merges so successive PRs each see the latest main.

  5. If a rebase produces a merge conflict (DIRTY state), the workflow stops on that PR and posts a comment. The PR author resolves manually (per Fixtures and decks if it’s a fixture conflict; via merge-commit if force-push isn’t allowed on the branch).

The automerge label is opt-in for two reasons:

  • PR authors sometimes want a stale-PR window after CI passes (e.g. waiting on a downstream review). Without the label the workflow ignores the PR.

  • Stale CI on a no-longer-current PR shouldn’t get rerun on every push: main — that saturates runners.

Concurrency#

The workflow uses concurrency: group: auto-update-prs, cancel-in-progress: false. false matters: cancelling an in-flight merge pass can leave the queue in a partial state (some PRs merged, some not, with inconsistent main references). Better to let each fire finish.

The known limitation: the GitHub-issued GITHUB_TOKEN cannot trigger downstream workflow_run events. When the workflow merges a PR (which produces a push: main), Tests/docs run on that push but the auto-update-prs workflow does not re-fire on workflow_run from those runs. The workflow handles this by draining the queue serially within a single fire — never deferring to “the next workflow_run will pick it up.”

The active-PR cap#

A standing rule per ~/.claude/skills/pr-workflow:

> Active-PR cap: at most 2 active labelled PRs per agent at a > time.

The cap exists because:

  • Every automerge PR is rebased on every push: main. N PRs × M main-pushes-per-day = N × M CI runs. With N > 2 the runner saturation is visible in queue times.

  • The merge pass serialises across PRs. More than 2 outstanding means the second one always blocks behind the first.

When the cap is exceeded, hold off opening more PRs until at least one drains. The “no stale green PRs” rule in the pr-workflow skill enforces the other side: every CLEAN labelled PR you own must be merged immediately, never queued indefinitely.

Pre-commit#

pre-commit runs locally on every commit. Hooks:

  • ruff — lint + format. No CI lint failures should be surprises.

  • trim trailing whitespace.

  • fix end of files.

  • check yaml, check toml, check for added large files, check for merge conflicts.

  • don't commit to branch — refuses commits to main locally.

When you commit and a hook reformats a file, the commit didn’t happen. Re-stage and re-commit.

Failure tracking#

The Track main / release CI failures workflow watches Tests / docs / Auto-update outcomes. When a failure surfaces on the main branch (or in the merge queue), it opens or updates a tracker issue tagged ci-failure-merge-queue. The PR author who triggered the failure (or the agent on point) drives the fix.

Common pitfalls#

  • Merging without the automerge label. The workflow ignores unlabelled PRs. If you bypass auto-merge and merge directly, the queue’s other PRs miss a chance to rebase against your push for the few seconds between the push and the next push: main workflow_run firing.

  • Force-pushing to a feature branch with the label. Most permission policies in this repo deny force-push. When a rebase needs an in-place rewrite, prefer a git merge origin/main resolution (resulting merge commit gets squashed at merge time).

  • Setting cancel-in-progress: true on the queue workflow. Don’t. An in-flight merge pass cancelling mid-rebase / mid-merge leaves the queue in a partial state.

  • Pushing N > 2 labelled PRs as one agent. Watch the active count; the cap is per agent, not global.

  • Skipping pre-commit hooks (--no-verify). Don’t — the hooks are the same lint configuration CI uses; bypassing them locally just means CI fails 5 minutes later.

  • Not flagging a regression in PERFORMANCE.md when release-readiness fires. Every regression has a written diagnosis (see Performance — profiling, snapshots, the perf tracker).

Where things live#

Concern

Path

GitHub workflows

.github/workflows/<name>.yml

Pre-commit config

.pre-commit-config.yaml (repo root)

PR-workflow rules (the automerge label, active-PR cap, no-stale-greens)

~/.claude/skills/pr-workflow/SKILL.md

Failure-tracker workflow

.github/workflows/ci-failure-tracker.yml

Auto-update workflow source

.github/workflows/auto-update-prs.yml