Repository navigation
Description
Every PR runs the entire suite. Conventions (8 lanes), Release Precheck (the full build matrix), Release Config Check and Changelog Check all trigger unconditionally, so an AGENTS.md-only PR (#833) spins ~80 checks.
This epic makes CI run only what the changed paths imply. The table below is the spec; the sub-issues implement it one check at a time.
The cases
Conventions is one workflow with 8 lane jobs (python-sdk, typescript-sdk, rust, rust-napi-binding, rust-python-binding, internals-checks, internals-distcheck, plugins-embeddings). GitHub paths: filters are workflow-scoped, not job-scoped, so per-lane triggering means splitting that file. Lanes are written Conventions:<lane> below.
| # | Changed paths | Expected CI |
|---|---|---|
| 1 | AGENTS.md, ARCHITECTURE.md, README.md, ROADMAP.md, PARITY.md, SUMMARY.md, CHANGELOG.md, MIGRATIONS.md, docs/**, agents/**, notes/**, scripts/** |
nothing |
| 2 | .mergify.yml |
nothing |
| 3 | packages/rust/** (the core) |
Conventions:rust, :rust-napi-binding, :rust-python-binding, :python-sdk, :typescript-sdk · Rust Test · Python Test · TS Test · Release Precheck · Changelog Check |
| 4 | packages/python/dirsql/** (Python SDK) |
Conventions:python-sdk · Python Test · Python Lint · Python Typecheck · Release Precheck · Changelog Check |
| 5 | packages/python/src/** (PyO3 binding crate) |
Conventions:rust-python-binding · Rust Test · Python Test · Release Precheck · Changelog Check |
| 6 | packages/ts/src/** (TS SDK) |
Conventions:typescript-sdk · TS Test · TS Lint · TS Typecheck · Release Precheck · Changelog Check |
| 7 | packages/ts/napi/** (napi binding crate) |
Conventions:rust-napi-binding · Rust Test · TS Test · Release Precheck · Changelog Check |
| 8 | internals/checks/** |
Conventions:internals-checks · Release Precheck · Changelog Check (it implements jobs in both — see rules) |
| 9 | internals/distcheck/** |
Conventions:internals-distcheck · Python Test · TS Test (their distcheck jobs invoke dirsql-distcheck) |
| 10 | plugins/** |
Conventions:plugins-embeddings · Plugins Test |
| 11 | Cargo.toml, Cargo.lock |
same as case 3, minus Changelog Check (root Cargo files sit outside packages/**, so no fragment is required) |
| 12 | putitoutthere.toml |
Release Config Check · Release Precheck |
| 13 | testing-conventions.toml |
every lane the root config governs: :python-sdk, :typescript-sdk, :rust, :rust-napi-binding, :rust-python-binding, :internals-distcheck — plus Rust Test, whose bespoke coverage job reads the same file (see rules) |
| 14 | packages/<pkg>/changelog.d/**, packages/<pkg>/migrations.d/** |
Changelog Check |
| 15 | packages/<pkg>/e2e-attestations/** |
that package's Conventions lane (the e2e-verify gate) |
| 16 | .github/workflows/<name>.yml |
the workflow that file defines |
| 17 | packages/python/pyproject.toml |
same as case 4 |
| 18 | packages/python/Cargo.toml |
same as case 5 |
| 19 | packages/ts/package.json, packages/ts/tsconfig*.json, packages/ts/biome.json, packages/ts/vitest.config.ts, packages/ts/pnpm-*.yaml |
same as case 6 |
| 20 | any combination of the above | the union of the matching rows |
Rules the table encodes
- Rust fans out. The core compiles into both bindings, so
packages/rust/**runs the Rust, Python and TypeScript lanes. Same forCargo.toml/Cargo.lock.[e2e].extra_scope = ["packages/rust/src"]is why the two SDK lanes are in there: a core change stales both bindings' attestations. - Bindings fan in, not out.
packages/ts/napi/**runs Rust + TS, never Python;packages/python/src/**runs Rust + Python, never TS. - Repo tooling does not ship — so
.mergify.ymlandinternals/distcheck/**never triggerRelease Precheck.internals/checks/**is the exception, and row 8 is not a mistake: that package implements four jobs elsewhere.Release Precheckrunsdirsql-checks artifact-completeness(release-precheck.yml:59),wheel-extension-load(:88) andnpm-binary-extension-load(:119);Changelog Checksyncs the same package and runschangelog-gate(changelog-check.yml:29). A change there can break its consumers, so the consumers run. - The root
testing-conventions.tomlgoverns six lanes, not five. Five name it asconfig:;:internals-distcheckpasses noconfig:at all, and the reusable workflow'sconfiginput defaults totesting-conventions.toml(a package-root file wins when present —internals/checks/and the plugin carry their own,internals/distcheck/does not). So the sixth lane rides on the root file too, and row 13 must re-run it: an unknown key there fails every gate, including that lane's. Rust Testalso reads the root config (added to row 13 while slicing this epic — see CI paths: Rust Test — add testing-conventions.toml #849). Python and TS unit coverage moved into the reusable testing-conventions workflow, but Rust's stayed bespoke:rust-test.yml:131runsnpx testing-conventions unit coverage --language rust, taking its floors from[rust.coverage]and its features from[rust].features. Today a PR editing those floors does not run the job that enforces them.Changelog Checkruns on any non-testpackages/**change (that is when a fragment is required) and on fragment changes.- Package-root manifests ride with their package (rows 17–19).
pyproject.toml,package.json, the crate manifest, tool configs and lockfiles sit outside thedirsql//src/globs but drive builds, installs and lints. The spec keeps that coverage; a literal implementation of rows 4–7 alone would silently drop it. - Docs are free. Nothing under
docs/,agents/,notes/or a root*.mdtriggers anything.Deploy Docsis already push-to-main-only. - A workflow file triggers its own workflow. Already the convention in the four test workflows — but
python-lint.yml,python-typecheck.yml,ts-lint.ymlandts-typecheck.ymldo not yet, so the implementation adds those four self-paths.
How this is validated: the PR is its own test
There are no canary PRs and no red tests. The behaviour under test is which workflows GitHub decides to trigger, and every implementation PR is itself an observation of exactly that.
A PR that adds a paths: filter to workflow X touches .github/workflows/X.yml. Because pull_request triggers are read from the PR head branch, the new filter applies to the very PR that introduces it — so X runs, and every workflow already filtered by a merged slice does not. Each sub-issue states its own expected check list as three groups:
- MUST run — what the touched paths imply under the spec
- MUST NOT run — workflows already filtered whose globs do not match
- May still run — the shrinking residue of workflows this epic has not filtered yet
Read the list of checks, not their conclusions. Merge the PR when the list matches, and close the sub-issue.
The phase-4 stack makes the progression visible: lane 1/8's PR shows 8 checks, lane 2/8's shows 7, and lane 8/8's shows exactly one. That last PR is the epic's closing evidence.
Where a new glob is not exercised by the PR's own file list (e.g. adding internals/distcheck/** to Python Test), the sub-issue names an optional probe: push a throwaway commit touching a file the glob covers, read the list, drop the commit before merging.
The constraint, and the one safety rule
paths: filters alone deadlock the Mergify gate. .mergify.yml's success_conditions anchor on check-success ~= ^python-sdk / and friends; a filtered workflow that does not run reports no check at all, so an anchor naming it pends forever. The anchors and the trigger filters have to change together — that is #832, recoverable only by admin bypass.
#850 gates each anchor behind if: files ~= <the paths it covers>, one merge_protections entry per anchored workflow, while every workflow is still unconditional. Every lane still runs, every applicable anchor is still satisfied, the gate stays green — a zero-risk probe of Mergify's files semantics, which remain unconfirmed from the dev sandbox (docs.mergify.com is egress-blocked). It must be merged before any anchored workflow gets a filter, since Mergify reads its config from main only.
An anchor's
ifmust never apply more broadly than its workflow'spaths:trigger.Narrower → the workflow sometimes runs unanchored. A hole:
#check-failure = 0still catches ordinary failures, only the startup-failure protection is lost. Safe.
Broader → the anchor sometimes demands a check that never appears. A deadlock. Unsafe.When the two cannot be expressed identically, err narrow.
Release PrecheckandChangelog Checkdeliberately do.
The globs therefore live in two places. Per AGENTS.md ("CI logic lives in scripts, not workflow YAML"), #862 adds a dirsql-checks ci-paths subcommand that asserts the invariant above by evaluating both sides against every tracked file — the durable anti-drift fix.
Sub-issues
Phase 1 — corrections to already-filtered workflows. None is anchored in .mergify.yml, so all seven are unblocked and independent; land them first to cut noise from every later PR.
- CI paths: Python Lint — add self-trigger path #843 — Python Lint: add self-trigger path
- CI paths: Python Typecheck — add self-trigger path #844 — Python Typecheck: add self-trigger path
- CI paths: TS Lint — add self-trigger path #845 — TS Lint: add self-trigger path
- CI paths: TS Typecheck — add self-trigger path and tsconfig.build.json #846 — TS Typecheck: add self-trigger path and
tsconfig*.json - CI paths: Python Test — add internals/distcheck, exclude fragment/attestation dirs #847 — Python Test: add
internals/distcheck/**, exclude fragment/attestation dirs - CI paths: TS Test — add internals/distcheck, exclude fragment/attestation dirs #848 — TS Test: add
internals/distcheck/**, exclude fragment/attestation dirs - CI paths: Rust Test — add testing-conventions.toml #849 — Rust Test: add
testing-conventions.toml
Phase 2 — the prerequisite. Must merge before phases 3 and 4.
- CI paths: Mergify — gate each anchor behind the files it covers #850 — Mergify: gate each anchor behind the files it covers
Phase 3 — the unconditional standalone workflows. Independent of each other; land before phase 4 so the lane PRs read cleanly.
- CI paths: Release Precheck — stop running on every PR #851 — Release Precheck: stop running on every PR
- CI paths: Release Config Check — stop running on every PR #852 — Release Config Check: stop running on every PR
- CI paths: Changelog Check — stop running on every PR #853 — Changelog Check: stop running on every PR
Phase 4 — split conventions.yml, one lane per PR. All eight edit conventions.yml, so they are a stack: each PR's base is the previous lane's branch. GitHub retargets a child to main automatically when its parent merges, and diff-scoped gates measure each layer against its stack parent.
- CI paths: Conventions lane 1/8 — extract python-sdk #854 — lane 1/8:
python-sdk - CI paths: Conventions lane 2/8 — extract typescript-sdk #855 — lane 2/8:
typescript-sdk - CI paths: Conventions lane 3/8 — extract rust #856 — lane 3/8:
rust - CI paths: Conventions lane 4/8 — extract rust-napi-binding #857 — lane 4/8:
rust-napi-binding - CI paths: Conventions lane 5/8 — extract rust-python-binding #858 — lane 5/8:
rust-python-binding - CI paths: Conventions lane 6/8 — extract internals-checks #859 — lane 6/8:
internals-checks - CI paths: Conventions lane 7/8 — extract internals-distcheck #860 — lane 7/8:
internals-distcheck - CI paths: Conventions lane 8/8 — extract plugins-embeddings, delete conventions.yml #861 — lane 8/8:
plugins-embeddings, deleteconventions.yml
Phase 5 — lock it in.
- CI paths: dirsql-checks ci-paths — assert workflow filters and Mergify anchors agree #862 —
dirsql-checks ci-paths: assert workflow filters and Mergify anchors agree
Job ids are load-bearing throughout phase 4. Check-run names are <caller job id> / <inner job name> — the workflow filename and its name: do not appear. Every anchor matches a job id, so a lane that changes its job id while moving files breaks the gate.
Already conformant — no sub-issue
Plugins Test—plugins/**+ self-path, exactly row 10.Deploy Docs/Release— push-to-mainonly, never PR-triggered.
(Rust Test's other entries already match rows 3, 5, 7 and 11 exactly; #849 only adds the root-config glob.)
Open unknowns
- Mergify's
filessemantics insidemerge_protections.ifare unverified —docs.mergify.comis egress-blocked from the dev sandbox. CI paths: Mergify — gate each anchor behind the files it covers #850 is the probe, and it is structured so a wrong guess cannot deadlock the gate. CI:Resolved 2026-08-12: ci: drop #check-pending from the Mergify gate, it deadlocks #833 merged; the gate deadlock is gone.#check-pending = 0deadlocks the Mergify gate (it counts its own check) #832 / ci: drop #check-pending from the Mergify gate, it deadlocks #833 —mainstill carries the deadlocking#check-pending = 0.
Activity
[-]CI: run only the relevant checks per PR without deadlocking the Mergify gate[/-][+]CI: run only the checks relevant to the changed paths[/+][-]CI: run only the checks relevant to the changed paths[/-][+]Epic — CI: run only the checks relevant to the changed paths[/+]20 remaining items
ci: remove Mergify, restore pr-monitor as the merge gate
thekevinbot commentedon Aug 20, 2026
Implemented and merged, in a different shape than the sub-issue slices specced: per-domain consolidation rather than per-concern extraction. The stack: #888 (plugin CI), #890 (internals lanes), #892 (
dirsql-python-ci), #893 (dirsql-typescript-ci), #894 (dirsql-rust-ci, deletingconventions.yml), #895 (real paths for release-ci / config-check / changelog-check), capstone #942. Every workflow onmainnow carriespaths:filters; docs-only changes trigger nothing.Sub-issue disposition (the #934 sweep): #843–#849, #851–#861 closed as completed/superseded with individual notes; #850 and #862 closed as obsolete after Mergify's removal (#950). Residual gap re-filed as #961 (distcheck consumers don't trigger on
internals/distcheck/**). Still open and owed: #932 (docs pass — now also covering the Mergify removal) and #961.ci: run the distcheck flows in their own lane, not the SDK lanes
docs: retire the pre-#834 workflow names
docs: state that a package README needs a changelog fragment
thekevinbot commentedon Aug 20, 2026
Spec amendment: READMEs are not free (#932 item 4)
The table's row 1 lists
README.mdamong the paths that trigger nothing. That is wrong for package READMEs, and the maintainer decision during this stack's final review was to keep the gate as written and correct the spec instead. Amending row 1 to:AGENTS.md,ARCHITECTURE.md, rootREADME.md,ROADMAP.md,PARITY.md,SUMMARY.md,CHANGELOG.md,MIGRATIONS.md,docs/**,agents/**,notes/**,scripts/**packages/<pkg>/README.md,plugins/<pkg>/README.mdrelease-ci·changelog-checkRule this encodes: package READMEs ship. They are embedded in all three published artifacts — the PyPI long-description, the npm tarball, the crate — so an edit reaches users the way code does.
release-ci.ymltherefore excludes the fragment dirs, attestation receipts andtesting-conventions.tomlfrom its triggers while deliberately keeping READMEs, and_is_exemptininternals/checks/src/checks/changelog_gate/decide.pyomits READMEs from the changelog gate's exempt set. Both are correct as written; no code change is required or wanted.The "docs are free" rule stated below the table stands for everything else — it just does not reach inside a package root.
Recorded in the repo at
AGENTS.md("Changelog and Migrations") andagents/reference/changelog-migrations.md, since a closed epic is not where contributors look for a live rule.Generated by Claude Code
docs: retire the pre-#834 workflow names
docs: state that a package README needs a changelog fragment
docs: retire the pre-#834 workflow names
docs: state that a package README needs a changelog fragment
docs: retire the pre-#834 workflow names
docs: state that a package README needs a changelog fragment
docs: retire the pre-#834 names in the justfile too
Merge pull request #985 from thekevinscott/claude/tackle-932-4sxof0