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 |
|---|---|---|
|
PR + push to main |
Lint, unit tests, optional-solver-backends matrix.
Three jobs: |
|
PR + push to main |
Sphinx build (strict mode — warnings as errors).
Includes the |
|
PR + push to main |
Runs |
|
|
Walks every PR carrying |
|
|
Opens / updates a tracker issue when a failure on
|
|
schedule: nightly |
Runs the perf harness, posts trend updates. See Performance — profiling, snapshots, the perf tracker. |
|
push to |
Diffs perf numbers against the previous tagged release; regressions block docs deploy. |
|
schedule: nightly |
Runs Sphinx |
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:
Author opens a PR. CI runs.
Author adds the
automergelabel when the PR is ready to ship — code review passed, comments addressed, no pending changes. No label = the PR sits.The
Auto-update PRs behind mainworkflow 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.
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.
If a rebase produces a merge conflict (
DIRTYstate), 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
automergePR is rebased on everypush: 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 tomainlocally.
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
automergelabel. 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 nextpush: mainworkflow_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/mainresolution (resulting merge commit gets squashed at merge time).Setting
cancel-in-progress: trueon 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.mdwhen release-readiness fires. Every regression has a written diagnosis (see Performance — profiling, snapshots, the perf tracker).
Where things live#
Concern |
Path |
|---|---|
GitHub workflows |
|
Pre-commit config |
|
PR-workflow rules (the |
|
Failure-tracker workflow |
|
Auto-update workflow source |
|