Skip to content

fix: stop a no-op merge from republishing every package - #979

Merged
thekevinscott merged 3 commits into
mainfrom
claude/tackle-944-iegeyj
Aug 20, 2026
Merged

thekevinscott merged 3 commits into
mainfrom
claude/tackle-944-iegeyj

Conversation

@thekevinbot

@thekevinbot thekevinbot commented Aug 20, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #944.

What

putitoutthere.toml globs each package's whole subtree (packages/rust/**),
so putitoutthere cascades a release on any change under it — including files
that never reach the built artifact. release-ci.yml already excludes several
of those paths from the PR-time build precheck, so the two files disagree in
the one direction that skips verification but keeps publishing.

path ships? precheck publish (before)
packages/*/changelog.d/**, migrations.d/**, e2e-attestations/** no excluded cascades
packages/*/testing-conventions.toml no not excluded cascades
plugins/dirsql-plugin-embeddings/tests/** yes (sdist) excluded cascades

Row 2 is not hypothetical: #971 merged as 3ec31c6 and fired Release run
371,
publishing dirsql-py and dirsql-npm byte-identical to the release before
them off two new gate-config files. Row 3 is the same asymmetry read the other
way — the plugin's tests/conftest.py and tests/**/__init__.py do land in its
sdist ([tool.hatch.build] exclude drops only **/*_test.py), so excluding
them from the precheck lets a merge release what the matrix never built.

Verifying putitoutthere's glob syntax at @v0

The issue's preferred option was contingent on negation being supported.
Checked against the pinned v0 tag, sha 8f5876751f679ee3450617957c97495205002499:

  • No exclusion field exists. globs is z.array(z.string()).min(1) and
    every package schema is .strict(), so an exclude-shaped key is a config
    error (packages/engine/src/config.ts).

  • Leading-! negation is worse than a no-op. Both consumers — cascade.ts's
    seed pass and check.ts — call glob.ts's matchesAny, a plain OR that
    returns on the first hit, under minimatch 10.2.6 with {dot, matchBase:false, nocomment}. Reproduced with those exact options:

    matchesGlob('!packages/rust/changelog.d/**', 'packages/rust/changelog.d/x.md') = false
    matchesGlob('!packages/rust/changelog.d/**', 'README.md')                      = true
    matchesAny([...globs, '!packages/rust/changelog.d/**'], 'README.md')           = true
    

    So adding the negation neither subtracts the fragment (the earlier
    packages/rust/** already returned true) nor stays inert — it makes every
    path in the repo match, and the package would cascade on every commit.

  • minimatch's extglob does work, because it excludes inside a single
    positive pattern. That is what this PR uses.

The fix

globs = [
  "packages/rust/!(testing-conventions.toml)",
  "packages/rust/!(changelog.d|migrations.d|e2e-attestations)/**",
  "Cargo.toml",
  "Cargo.lock",
]

Two patterns because one extglob cannot span both depths. It stays a
carve-out, not an allowlist: a subdirectory or root file added later is
matched automatically, and only the named names drop out.

release-ci.yml gains the matching !<root>/testing-conventions.toml
exclusions and loses !plugins/dirsql-plugin-embeddings/tests/**, so the
precheck now builds a superset of what publishing ships.

The gate

dirsql-checks release-globs (new subcommand, wired into
putitoutthere-config-check.yml) holds the two files to one invariant:
everything the publish globs match, the precheck also builds, and neither
reaches a non-shipping path. It compares by name, not by re-matching paths,
so it carries no second copy of upstream's glob semantics to drift against —
the same reasoning artifact-completeness uses for not re-deriving artifact
names. It catches a new [[package]] added with naive globs, a carve-out
"simplified" back to <root>/**, and the leading-! footgun above.

Red/green: the gate landed alone in cc3add5 and CI went red on it for exactly
the seven assertions it makes
(job);
bfa0d84 is the fix that flips it.

Manual run

$ uv run --project internals/checks dirsql-checks release-globs   # at cc3add5
::error::dirsql-rust: publish globs for packages/rust are packages/rust/** -- expected …
… 7 problems, exit 1

$ uv run --project internals/checks dirsql-checks release-globs   # at bfa0d84
ok release-globs: putitoutthere.toml and .github/workflows/release-ci.yml agree on what ships.
exit 0

And the globs themselves, replaying putitoutthere @v0's matchesAny verbatim
(minimatch 10.2.6, same options) over the repo's 559 tracked files plus
synthetic future paths:

--- a change to these must cascade NOTHING ---
  ok   packages/rust/changelog.d/2026-08-20-unquoted-path-table-hint.md
  ok   packages/python/e2e-attestations/claude-issue-951-0eease.json
  ok   packages/ts/testing-conventions.toml
  ok   plugins/dirsql-plugin-embeddings/testing-conventions.toml
--- a change to these must still cascade ---
  ok   packages/rust/src/lib.rs                        -> dirsql-rust, dirsql-py, dirsql-npm
  ok   packages/ts/.npmignore                          -> dirsql-npm
  ok   plugins/dirsql-plugin-embeddings/tests/conftest.py -> dirsql-plugin-embeddings
  ok   packages/rust/brand-new-dir/deep/x.rs           -> dirsql-rust, dirsql-py, dirsql-npm
  ok   packages/ts/new-root-file.json                  -> dirsql-npm
--- nothing shipping was dropped across all 559 tracked files ---
  ok   dirsql-rust                dropped  59 (all non-shipping), added 0
  ok   dirsql-py                  dropped  92 (all non-shipping), added 0
  ok   dirsql-npm                 dropped  92 (all non-shipping), added 0
  ok   dirsql-plugin-embeddings   dropped  15 (all non-shipping), added 0

E2E Verification

N/A - CI/release-config only. No packages/** or plugins/** source changed,
so no SDK e2e suite or attestation is affected. internals/checks changed, so
its own receipt is refreshed here.

  • Ran e2e suites locally for every affected SDK — N/A, none affected
  • Python SDK e2e: N/A
  • TypeScript SDK e2e: N/A
  • Rust core e2e (if applicable): N/A
  • packages/python/e2e-attestation.json refreshed if packages/python changed — N/A, unchanged
  • packages/ts/e2e-attestation.json refreshed if packages/ts changed — N/A, unchanged
  • Command(s) run: uv run python -m pytest tests/e2e -q in internals/checks,
    attested via uvx testing-conventions e2e attest
  • Result summary: 17 passed; receipt at
    internals/checks/e2e-attestations/claude-tackle-944-iegeyj.json

Changelog / Migrations

  • Changelog fragment — not required: no non-test source under
    packages/<pkg>/ or plugins/<pkg>/ changed, so changelog-gate does
    not fire. The change alters when packages publish, not what they do.
  • Migration fragment — not required: no public API change.

PARITY.md is untouched for the same reason — no SDK public API is involved.

claude added 3 commits August 20, 2026 18:25
RED half of dirsql#944. `dirsql-checks release-globs` holds
`putitoutthere.toml` and `release-ci.yml` to one invariant: everything the
publish globs match, the PR-time build precheck also builds, and neither
reaches a path that never lands in an artifact.

It fails on the current config, which is the bug: six packages/roots glob
`<root>/**` with no carve-out, so a changelog-fragment-only merge republishes
all three packages, and release run 371 already shipped `dirsql-py` and
`dirsql-npm` byte-identical off two new `testing-conventions.toml` files. The
seventh finding is the same asymmetry in the other file --
`!plugins/dirsql-plugin-embeddings/tests/**` skips the precheck for a path
publishing still cascades on.

putitoutthere has no exclusion field and its leading-`!` negations do the
opposite of what they look like; the check's module docstring records the
verification at the pinned `@v0` sha (8f58767) and names the extglob form that
does work. Both halves compare by name rather than re-matching paths, so the
check carries no second copy of upstream's glob semantics to drift against.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9pxEJux3YgFtKnWKXZAT6
GREEN half of dirsql#944. Each package subtree is now globbed as a pair of
minimatch extglob patterns instead of a bare `<root>/**`, so a merge that only
touches a changelog fragment, a migration fragment, an e2e attestation receipt
or a package's `testing-conventions.toml` cascades nothing.

Two patterns per root because one extglob cannot span both depths -- the first
covers files directly in the package root, the second everything under its
subdirectories. It stays a carve-out rather than an allowlist: a subdirectory or
root file added later is still matched, only the named names drop out. Replaying
putitoutthere @v0's `matchesAny` over the 559 tracked files, the change removes
59/92/92/15 paths per package and every one of them is a fragment, a receipt or
a gate config; nothing new matches, and synthetic future paths
(`packages/rust/brand-new-dir/deep/x.rs`, `packages/ts/new-root-file.json`)
still cascade.

`release-ci.yml` gains the matching `testing-conventions.toml` exclusions so the
precheck stops building for a gate-config edit. It also LOSES
`!plugins/dirsql-plugin-embeddings/tests/**`: hatchling excludes only
`**/*_test.py`, so `tests/conftest.py` and the package inits do land in the
plugin's sdist, and skipping the precheck there was the same publish-without-
building asymmetry the rest of this change removes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D9pxEJux3YgFtKnWKXZAT6
@thekevinscott
thekevinscott merged commit ea247f2 into main Aug 20, 2026
27 checks passed
@thekevinscott
thekevinscott deleted the claude/tackle-944-iegeyj branch August 20, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A changelog-fragment-only merge publishes all three packages without ever running release-ci

3 participants