Skip to content

Epic — CI: run only the checks relevant to the changed paths #834

Closed
20 of 20 issues completed
Closed

Description

@thekevinbot

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 for Cargo.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.yml and internals/distcheck/** never trigger Release Precheck. internals/checks/** is the exception, and row 8 is not a mistake: that package implements four jobs elsewhere. Release Precheck runs dirsql-checks artifact-completeness (release-precheck.yml:59), wheel-extension-load (:88) and npm-binary-extension-load (:119); Changelog Check syncs the same package and runs changelog-gate (changelog-check.yml:29). A change there can break its consumers, so the consumers run.
  • The root testing-conventions.toml governs six lanes, not five. Five name it as config:; :internals-distcheck passes no config: at all, and the reusable workflow's config input defaults to testing-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 Test also 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:131 runs npx 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 Check runs on any non-test packages/** 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 the dirsql/ / 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 *.md triggers anything. Deploy Docs is 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.yml and ts-typecheck.yml do 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 if must never apply more broadly than its workflow's paths: trigger.

Narrower → the workflow sometimes runs unanchored. A hole: #check-failure = 0 still 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 Precheck and Changelog Check deliberately 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.

Phase 2 — the prerequisite. Must merge before phases 3 and 4.

Phase 3 — the unconditional standalone workflows. Independent of each other; land before phase 4 so the lane PRs read cleanly.

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.

Phase 5 — lock it in.

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-main only, 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

Activity

  1. changed the title [-]CI: run only the relevant checks per PR without deadlocking the Mergify gate[/-] [+]CI: run only the checks relevant to the changed paths[/+] on Aug 12, 2026
  2. changed the title [-]CI: run only the checks relevant to the changed paths[/-] [+]Epic — CI: run only the checks relevant to the changed paths[/+] on Aug 12, 2026
  3. 20 remaining items

  4. thekevinbot commented on Aug 20, 2026

    @thekevinbot
    CollaboratorAuthor

    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, deleting conventions.yml), #895 (real paths for release-ci / config-check / changelog-check), capstone #942. Every workflow on main now carries paths: 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.

  5. thekevinbot commented on Aug 20, 2026

    @thekevinbot
    CollaboratorAuthor

    Spec amendment: READMEs are not free (#932 item 4)

    The table's row 1 lists README.md among 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:

    # Changed paths Expected CI
    1 AGENTS.md, ARCHITECTURE.md, root README.md, ROADMAP.md, PARITY.md, SUMMARY.md, CHANGELOG.md, MIGRATIONS.md, docs/**, agents/**, notes/**, scripts/** nothing
    1a packages/<pkg>/README.md, plugins/<pkg>/README.md release-ci · changelog-check

    Rule 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.yml therefore excludes the fragment dirs, attestation receipts and testing-conventions.toml from its triggers while deliberately keeping READMEs, and _is_exempt in internals/checks/src/checks/changelog_gate/decide.py omits 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") and agents/reference/changelog-migrations.md, since a closed epic is not where contributors look for a live rule.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions