Skip to content

CI paths: Mergify — gate each anchor behind the files it covers #850

Closed as not planned

Description

@thekevinbot

Part of #834. Phase 2 — hard prerequisite for every phase 3 and phase 4 slice. Nothing that adds a paths: filter to an anchored workflow may merge before this does.

Why this is first

.mergify.yml's success_conditions anchor on check-success ~= ^python-sdk / and friends. A path-filtered workflow that does not run reports no check at all, so an anchor naming it can never be satisfied and the gate pends forever — that is #832 all over again, recoverable only by admin bypass.

This slice makes each anchor conditional on the PR having touched files that lane covers, while every workflow is still unconditional. Every lane still runs, so every applicable anchor is still satisfied, and the gate stays green. It is therefore a zero-risk probe of two unknowns at once: that Mergify accepts files inside merge_protections.if, and that per-lane protections behave.

.mergify.yml is read from main only — this must be merged, not merely open, before phase 3/4 filters land.

The one safety rule, for this slice and every slice after it

An anchor's if must never apply more broadly than its workflow's paths: trigger.

  • if narrower than paths: → on some PRs the workflow runs unanchored. A hole: #check-failure = 0 still catches ordinary failures, only the startup-failure protection is lost. Safe.
  • if broader than paths: → on some PRs the anchor demands a check that never appears. A deadlock. Unsafe.

When the two cannot be expressed identically, deliberately err narrow. That is why the release-precheck and changelog-check entries below enumerate directories positively instead of mirroring the workflow's packages/** + ! exclusions: an exclusion is hard to express as a regex without lookahead, and guessing wrong in the broad direction deadlocks the repo.

Exact change

Replace .mergify.yml in full with:

# Mergify reads this file from the default branch only. Edits on a feature
# branch are inert until they land on main.

# No summary comment on PRs -- the Required check run carries the same
# condition breakdown and stays current, while the comment is a stale
# point-in-time snapshot cluttering the thread.
merge_protections_settings:
  post_comment: false

# Each anchored workflow gets its own protection, gated by `if:` on the files
# it covers. Without the gate, path-filtering a workflow deadlocks the merge
# gate: a workflow that does not run reports no check, so an unconditional
# anchor naming it pends forever (#832).
#
# INVARIANT: an entry's `if` must never match more PRs than its workflow's
# `paths:` filter does. Narrower is safe (the workflow runs unanchored);
# broader deadlocks (the anchor waits on a check that never appears). Where
# the workflow uses `!` exclusions, the `if` below enumerates directories
# positively instead -- deliberately narrower.
merge_protections:
  - name: Nothing failed
    description: No check on this PR reported a failure.
    if:
      - base = main
    success_conditions:
      # Blind to a workflow that dies before emitting any check-run (bad YAML,
      # or a `testing-conventions@v0` input the moving tag no longer defines):
      # zero check-runs contributed reads as "nothing failed". Also vacuously
      # true before CI has started. The per-workflow anchors below close both
      # holes, and are what keep the gate from going green mid-run.
      #
      # Do NOT add `#check-pending = 0` here. Mergify counts its own
      # in-progress check toward that total, so it can never reach zero and the
      # gate deadlocks (#832).
      - "#check-failure = 0"

  # --- Conventions lanes -----------------------------------------------------
  # Anchors match the caller job id, which this repo owns, rather than inner job
  # names owned by the `@v0` reusable workflows upstream. An upstream rename must
  # not silently unblock the gate. Job ids therefore MUST survive the lane split.

  - name: Conventions python-sdk reported
    if:
      - base = main
      - or:
          - files ~= ^packages/python/dirsql/
          - files ~= ^packages/python/pyproject\.toml$
          - files ~= ^packages/python/e2e-attestations/
          - files ~= ^packages/rust/
          - files ~= ^Cargo\.(toml|lock)$
          - files ~= ^testing-conventions\.toml$
          - files ~= ^\.github/workflows/conventions-python-sdk\.yml$
    success_conditions:
      - "check-success ~= ^python-sdk /"

  - name: Conventions typescript-sdk reported
    if:
      - base = main
      - or:
          - files ~= ^packages/ts/src/
          - files ~= ^packages/ts/package\.json$
          - files ~= ^packages/ts/tsconfig.*\.json$
          - files ~= ^packages/ts/biome\.json$
          - files ~= ^packages/ts/vitest\.config\.ts$
          - files ~= ^packages/ts/pnpm-.*\.yaml$
          - files ~= ^packages/ts/e2e-attestations/
          - files ~= ^packages/rust/
          - files ~= ^Cargo\.(toml|lock)$
          - files ~= ^testing-conventions\.toml$
          - files ~= ^\.github/workflows/conventions-typescript-sdk\.yml$
    success_conditions:
      - "check-success ~= ^typescript-sdk /"

  - name: Conventions rust reported
    if:
      - base = main
      - or:
          - files ~= ^packages/rust/
          - files ~= ^Cargo\.(toml|lock)$
          - files ~= ^testing-conventions\.toml$
          - files ~= ^\.github/workflows/conventions-rust\.yml$
    success_conditions:
      # The trailing space in `^rust ` is load-bearing: it stops this matching
      # `rust-napi-binding / ...` and `rust-python-binding / ...`.
      - "check-success ~= ^rust /"

  - name: Conventions rust-napi-binding reported
    if:
      - base = main
      - or:
          - files ~= ^packages/ts/napi/
          - files ~= ^packages/rust/
          - files ~= ^Cargo\.(toml|lock)$
          - files ~= ^testing-conventions\.toml$
          - files ~= ^\.github/workflows/conventions-rust-napi-binding\.yml$
    success_conditions:
      - "check-success ~= ^rust-napi-binding /"

  - name: Conventions rust-python-binding reported
    if:
      - base = main
      - or:
          - files ~= ^packages/python/src/
          - files ~= ^packages/python/Cargo\.toml$
          - files ~= ^packages/rust/
          - files ~= ^Cargo\.(toml|lock)$
          - files ~= ^testing-conventions\.toml$
          - files ~= ^\.github/workflows/conventions-rust-python-binding\.yml$
    success_conditions:
      - "check-success ~= ^rust-python-binding /"

  - name: Conventions internals-checks reported
    if:
      - base = main
      - or:
          - files ~= ^internals/checks/
          - files ~= ^\.github/workflows/conventions-internals-checks\.yml$
    success_conditions:
      - "check-success ~= ^internals-checks /"

  - name: Conventions internals-distcheck reported
    if:
      - base = main
      - or:
          - files ~= ^internals/distcheck/
          # This lane passes no `config:`, so the reusable workflow's default
          # applies and the ROOT testing-conventions.toml governs it -- an
          # unknown key there fails this lane too.
          - files ~= ^testing-conventions\.toml$
          - files ~= ^\.github/workflows/conventions-internals-distcheck\.yml$
    success_conditions:
      - "check-success ~= ^internals-distcheck /"

  - name: Conventions plugins-embeddings reported
    if:
      - base = main
      - or:
          - files ~= ^plugins/
          - files ~= ^\.github/workflows/conventions-plugins-embeddings\.yml$
    success_conditions:
      - "check-success ~= ^plugins-embeddings /"

  # --- Release Precheck ------------------------------------------------------
  # Deliberately narrower than the workflow's `packages/**` + exclusions: this
  # enumerates the shipping subtrees positively, so a fragment-only or
  # attestation-only PR cannot demand a check that will not run.

  - name: Release Precheck reported
    if:
      - base = main
      - or:
          - files ~= ^packages/[^/]+/(dirsql|src|napi)/
          - files ~= ^packages/[^/]+/(pyproject\.toml|Cargo\.toml|package\.json)$
          - files ~= ^internals/checks/
          - files ~= ^Cargo\.(toml|lock)$
          - files ~= ^putitoutthere\.toml$
          - files ~= ^\.github/workflows/release-precheck\.yml$
    success_conditions:
      - "check-success ~= ^precheck /"
      - "check-success = Artifact completeness"

  # --- Release Config Check / Changelog Check --------------------------------
  # Release Config Check calls a reusable workflow from its `check` job, so its
  # checks are `check / <inner>`. Changelog Check runs its `check` job directly,
  # so its check is the bare name. Both job ids are `check`; the trailing
  # separator is what tells them apart.

  - name: Release Config Check reported
    if:
      - base = main
      - or:
          - files ~= ^putitoutthere\.toml$
          - files ~= ^\.github/workflows/release-config-check\.yml$
    success_conditions:
      - "check-success ~= ^check /"

  - name: Changelog Check reported
    if:
      - base = main
      - or:
          - files ~= ^packages/[^/]+/(dirsql|src|napi|tests|changelog\.d|migrations\.d)/
          - files ~= ^packages/[^/]+/(pyproject\.toml|Cargo\.toml|package\.json)$
          - files ~= ^internals/checks/
          - files ~= ^\.github/workflows/changelog-check\.yml$
    success_conditions:
      - "check-success = check"

pull_request_rules:
  - name: Auto-merge labelled PRs once every check is green
    conditions:
      - base = main
      - label = auto-merge
      - "#approved-reviews-by >= 1"
      - "#check-failure = 0"
      - "#check-pending = 0"
    actions:
      merge:
        method: merge

The seven conventions-*.yml filenames referenced above do not exist yet. Those clauses are inert until phase 4 creates them, and harmlessly so — while conventions.yml is still unconditional every lane runs on every PR, so no lane anchor can go unsatisfied.

Expected checks on this PR

Files touched: .mergify.yml only.

MUST run

  • Conventions (all 8 lanes), Release Precheck, Release Config Check, Changelog Check — all still unconditional at this point
  • Mergify's own configuration-validation check

MUST NOT run

  • Python Test, TS Test, Rust Test, Plugins Test, Python Lint, Python Typecheck, TS Lint, TS Typecheck — spec row 2: .mergify.yml triggers no workflow

Note that none of the 11 new anchors applies to this PR — no if matches a .mergify.yml-only change. The gate passes on #check-failure = 0 alone. That is the expected and correct outcome, and it is also the proof that if gating works.

Acceptance

  • Mergify's configuration check on this PR is green — this is the syntax probe. If Mergify reports the config invalid, the whole file is rejected repo-wide; fix it on this PR, never merge a rejected config
  • Mergify Merge Protections green
  • The Merge Protections check summary lists the new per-workflow entries
  • Merge, close this issue
  • After merge, confirm on the next unrelated PR that the gate still resolves normally before starting phase 3

Rollback

If the gate misbehaves after merge, revert this commit on main immediately — the previous config is unconditional and always satisfiable. Do not attempt to fix forward while main's gate is broken.

Notes

  • Mergify's files semantics inside merge_protections.if could not be confirmed from the dev sandbox (docs.mergify.com is egress-blocked). This slice is the confirmation, which is why it lands while every workflow still runs unconditionally.
  • No changelog fragment, no e2e attestation.

Activity

  1. thekevinbot commented on Aug 20, 2026

    @thekevinbot
    CollaboratorAuthor

    Closing in the #934 sweep. Obsolete: Mergify was removed entirely in #950 (pr-monitor restored as the merge gate), so there are no anchors to gate. The zero-check-merge consequence this slice managed is now pr-monitor's concern and is covered by the docs pass (#932, item 3).

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions