Skip to content

fix(release): publish @loopover/contract, and catch this class of break before it ships (#9749) - #9763

Merged
JSONbored merged 9 commits into
mainfrom
fix/publish-contract-9749
Jul 29, 2026
Merged

fix(release): publish @loopover/contract, and catch this class of break before it ships (#9749)#9763
JSONbored merged 9 commits into
mainfrom
fix/publish-contract-9749

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

The break

@loopover/contract is a runtime dependency of both published CLIs — mcp and miner import it from code that ships, built with plain tsc so the specifiers survive into the emitted JS — but it had no publish workflow and was never on npm.

Nothing has broken yet: the currently-published versions predate the dependency and install fine. The next release of either would publish "@loopover/contract": "^0.1.0" and fail with E404 for every external user — and a published version can't be taken back. It also breaks the documented contributor workflow, since SKILL.md instructs npm install -g @loopover/mcp@latest as the pre-submit oracle.

Option A, and why not B or C

The package is already shaped for publishing — publishConfig.access: public, a files allowlist, no private: true — and is the only workspace package with that shape and no publish workflow. The step was simply never added when #9521 split it out.

Bundling it into both consumers (B) changes two build pipelines and their dist shape. Vendoring the types (C) re-creates exactly the duplication #9521 consolidated into this package.

publish-contract.yml mirrors publish-engine.yml's privilege separation: the unprivileged validate job builds, packs and smoke-tests; the privileged publish job only tags and publishes the tarball that job already tested, so a compromised build dependency can't reach the OIDC token. The smoke test imports both the root export and a subpath (/public-api), since consumers use @loopover/contract/api-schemas and friends.

The more useful half

scripts/check-publishable-deps.tsa published package must not depend on an unpublishable workspace one.

I proved it catches the real bug rather than assuming: removing the new workflow makes it fail, naming both consumers and the reason.

@loopover/mcp -> "@loopover/contract": "^0.1.0"
  @loopover/contract has no .github/workflows/publish-*.yml, so it is not published to npm
@loopover/miner -> "@loopover/contract": "^0.1.0"
  ...

It's deliberately about the class: any future workspace package that becomes a runtime dependency of a published one is caught the same way, with no list to remember to update. "Published" is derived from the publish-*.yml workflows themselves rather than a hand-kept list, so it stays honest when a fifth package appears — and a non-publish workflow mentioning a package can't fake it published. devDependencies are ignored: they never ship, so flagging them would be noise that pushes contributors toward the wrong fix. A private dependency gets a distinct message, because adding a publish workflow fixes the unpublished case and cannot fix that one.

Wired into test:ci beside the other drift checks. 9 unit tests on the pure core.

Two things this deliberately does not do

Both stated where they matter rather than left to be discovered:

  • It cannot enforce ordering between two publishes. A contract release must go out before an mcp release depending on a new version of it. No static check sees that; it stays a runbook step, called out in the workflow header.
  • npm's OIDC cannot create a brand-new package. The first contract publish needs a one-time authenticated bootstrap plus a Trusted Publisher entry in npmjs.com's settings — in the workflow header, so it isn't discovered when the first dispatch fails.

Closes #9749

…ak before it ships (#9749)

@loopover/contract is a RUNTIME dependency of both published CLIs -- mcp and
miner import it from code that ships, built with plain tsc so the specifiers
survive into the emitted JS -- but it had no publish workflow and was never
on npm. The currently-published versions predate the dependency and install
fine, so nothing has broken yet; the NEXT release of either would publish
`"@loopover/contract": "^0.1.0"` and fail with E404 for every external user,
and a published version cannot be taken back.

Option A (publish it) over the alternatives: the package is already shaped
for it (publishConfig.access public, a files allowlist, no private:true) and
is the only workspace package with that shape and no publish workflow --
the step was simply never added when #9521 split it out. Bundling it into
both consumers would change two build pipelines and their dist shape;
vendoring the types would re-create exactly the duplication #9521 removed.

publish-contract.yml mirrors publish-engine.yml's privilege separation
(unprivileged validate builds/packs/smoke-tests, privileged publish only
tags and publishes the tarball that job already tested) with a contract
smoke test that imports both the root and a subpath export, since consumers
use `@loopover/contract/api-schemas` and friends.

The more useful half is scripts/check-publishable-deps.ts: a published
package must not depend on an unpublishable workspace one. Proven against
the real bug by removing the new workflow and watching it name both
consumers and the reason. It is deliberately about the CLASS -- any future
workspace package that becomes a runtime dependency of a published one is
caught the same way, with no list to remember to update -- and it derives
"published" from the publish-*.yml workflows themselves rather than a
hand-kept list, so it stays honest when a fifth package appears.
devDependencies are ignored: they never ship, so flagging them would be
noise that pushes contributors toward the wrong fix.

Two things it deliberately does NOT do, both stated where they matter: it
cannot enforce ORDERING between two publishes (contract must go out before
an mcp release depending on a new version), which stays a runbook step in
the workflow's header; and npm's OIDC cannot create a brand-new package, so
the first contract publish needs a one-time authenticated bootstrap plus a
Trusted Publisher entry -- called out in the workflow header rather than
left to be discovered when the first dispatch fails.
@loopover-orb

loopover-orb Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Warning

⏸️ LoopOver review result - manual review recommended

Review updated: 2026-07-29 09:31:26 UTC

13 files · 1 AI reviewer · 2 blockers · CI green · clean

⏸️ Suggested Action - Manual Review

  • Required AI review was skipped by a submitter-reputation downgrade: This repository requires blocking AI review, and the submitter's recent-submission signal downgraded this PR to deterministic-only checks. Those checks do not read code semantics, so the gate is held for human review instead of passing automatically.

Review summary
This PR adds a workflow_dispatch-only publish-contract.yml (near-identical mirror of the existing publish-miner.yml/publish-engine.yml privilege-separation pattern) to close the actual bug where @​loopover/contract ships as a runtime dependency of @​loopover/mcp and @​loopover/miner but was never published to npm, and pairs it with scripts/check-publishable-deps.ts, a general 'published package can't depend on an unpublishable workspace package' check wired into test:ci. The pure-function core (findPublishableDepViolations/publishedPackageNames) is well-isolated and the accompanying tests actually reproduce the real #9749 failure mode (mcp/miner depending on the unpublished contract package) rather than a fabricated scenario, and pass once the dependency itself is marked published — this is a genuine regression test for the real bug, not a can't-occur guard. One real design soft-spot: publishedPackageNames derives the 'published' set by regex-matching any `@​loopover/xxx` mention anywhere inside a publish-*.yml file (not just the package it actually publishes), so a future publish-X.yml that merely references another workspace package in a build step (e.g. `npm run build --workspace @​loopover/engine`) would silently mark that package as 'published' too, which could mask a real violation the check is meant to catch — not a defect in this diff's current behavior, but worth hardening.

Nits — 7 non-blocking
  • scripts/check-publishable-deps.ts:82-91 `publishedPackageNames` treats ANY `@​loopover/xxx` mention in a publish-*.yml (including build-step references to other workspace packages) as evidence that package is published, which could produce false negatives for a future workflow that references a dependency without actually publishing it — consider scoping the regex to the `npm pack --workspace` / `npm publish` lines specifically.
  • The external brief's 'magic number 9' and 'debug leftovers: console' flags on check-publishable-deps.ts are noise — line ~82 is a version-regex character class, not a literal 9, and console.error/console.log in a CLI script's own error/summary output is the intended UX, not leftover debugging.
  • publish-contract.yml faithfully mirrors publish-miner.yml/publish-engine.yml boilerplate (release-commit resolution via `git log -S`, tag verification, privilege-separated validate/publish/github-release jobs) — worth confirming via code review tooling that the mirrored steps stay in sync if the shared pattern is refactored later, since there's now a fourth near-duplicate copy of this workflow logic.
  • Consider extracting the near-identical validate/publish/github-release job bodies shared across publish-engine.yml, publish-mcp.yml, publish-miner.yml, and this new publish-contract.yml into a reusable workflow or composite action to avoid a fourth copy of the release: reconcile-stale-releases tags at main head instead of the release PR's merge commit #8525 release-commit-resolution logic drifting independently.
  • In scripts/check-publishable-deps.ts, scope publishedPackageNames' matching to the `npm pack --workspace`/`npm publish` command lines rather than the whole file to avoid false 'published' classifications from incidental package mentions.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.
  • Required AI review was skipped by a submitter-reputation downgrade — Review this PR manually, or run AI review with a trusted override, before merging.

Concerns raised — review before merging

  • No linked issue detected: The PR cites an issue number, but it could not be verified as a currently open issue. — If this PR is intended to solve an issue, link it explicitly in the PR body.
  • Possible leaked secret in the diff (private_key_block): The PR diff matches secret pattern(s): private_key_block. Found at: test/unit/check-contract-package.test.ts:34, test/unit/check-contract-package.test.ts:78, test/unit/check-contract-package.test.ts:79. A committed credential must be rotated and removed from the change before merge. — Remove the secret from the diff, rotate the exposed credential, then re-run the gate.
📋 Copy for AI agents — paste into your coding agent
Fix the following blocker(s) from this PR review:

1. No linked issue detected: The PR cites an issue number, but it could not be verified as a currently open issue. — If this PR is intended to solve an issue, link it explicitly in the PR body.

2. Possible leaked secret in the diff (private_key_block): The PR diff matches secret pattern(s): private_key_block. Found at: test/unit/check-contract-package.test.ts:34, test/unit/check-contract-package.test.ts:78, test/unit/check-contract-package.test.ts:79. A committed credential must be rotated and removed from the change before merge. — Remove the secret from the diff, rotate the exposed credential, then re-run the gate.

Decision drivers

  • ❌ Code review — 2 blockers (1 reviewer)
  • ❌ Gate result — Blocking (Repo-configured hard blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9749
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 370 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 370 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR adds publish-contract.yml mirroring publish-engine.yml (Option A as recommended by the issue) to publish @​loopover/contract to npm, and also adds the guard script check-publishable-deps.ts (wired into test:ci) that fails when a published package depends on an unpublished workspace package, directly matching both the core ask and the 'guard worth adding' section.

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is registered but has no active allocation in the current snapshot.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 370 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
  • Then work through the remaining 2 steps in the Signals table above.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask <question> answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat <question> answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: hold · clause: missing_linked_issue
  • config: 0de1f0fcfcfb188e974728c627756139221638a1946af5fb0c0869b8644090a7 · pack: oss-anti-slop · ci: passed
  • record: 08da83ff77320a107a4b25f72c22d835918e8b4204f430db1c1f99cb9d7e373a (schema v5, head 89361c1)

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 29, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui fbf1728 Commit Preview URL

Branch Preview URL
Jul 29 2026, 09:17 AM

@loopover-orb loopover-orb Bot added the manual-review Gittensor contributor context label Jul 29, 2026
…ack check, watched paths (#9654)

#9654 requires all of it in one PR, and my first pass had only the workflow
and the dependency guard. Completing the rest.

- release-please registration in both config and manifest, contract FIRST in
  the packages map so a release PR lists it above its consumers -- it must
  publish before them. Plus the CHANGELOG.md the registration requires.
- RELEASE_AUTOMATION_WATCHED_PATHS gains the contract's package.json,
  CHANGELOG.md and publish workflow, so checkWatchedPathsExist fails if any
  is moved or deleted -- losing one silently returns the package to the
  unpublished state that breaks its consumers' next release.
- check-contract-package.ts with the contract's OWN allowlist, never
  reusing MCP's: the two ship completely different trees (named CLI
  entrypoints plus a scripts/ helper vs a whole dist/ of schema modules), so
  one shared list would over-permit one and under-permit the other -- which
  is how a package starts shipping a file nobody reviewed. Asserted.
- The dependency guard now keys on BOTH signals per the issue's spec: a
  dependency missing from release-please-config.json is never version-bumped
  or released, which fails differently from having no publish workflow but
  ships the same broken tarball.

The pack check earned its keep on first run: dist/telemetry.js carried a
literal `-----BEGIN RSA PRIVATE KEY-----` inside a doc comment explaining
the redaction regex. Not a leaked credential, but a marker that trips every
secret scanner reading the tarball -- ours and a consumer's own. Fixed at
the SOURCE by describing the header instead of quoting it, rather than
excluding the file or weakening the scanner. The redaction pattern itself is
byte-identical and its 28 tests still pass.
…9749

# Conflicts:
#	.github/workflows/publish-contract.yml
#	package.json
#	release-please-config.json
@JSONbored

Copy link
Copy Markdown
Owner Author

Rebased onto main and addressed the review.

Review finding — publishedPackageNames over-matching. Fixed. It now reads the package a workflow actually packs, from its own npm pack --workspace <name> line, instead of any @loopover/* mention anywhere in the file. The looser form meant a publish workflow's own build step (npx turbo run build --filter=@loopover/contract, npm run build --workspace @loopover/engine) would mark a sibling "published" and mask exactly the violation this check exists to catch. A workflow with no recognizable pack line now contributes nothing rather than falling back to a looser match — an unreadable workflow should make the check stricter, never quietly more permissive.

Three tests cover it: the regression case (a workflow that builds/tests three siblings and packs one yields only the packed one), the no-pack-line case, and an invariant asserting every real publish-*.yml in the repo yields exactly one package — so a future workflow that packs two, or none, fails here rather than silently changing what counts as published.

Descoped against main. #9753 landed the publish workflow and the release-please wiring independently, and its version is equivalent — same two-job privilege split, same pack line, and it does build before packing. I took main's wholesale for publish-contract.yml, release-please-config.json, and .release-please-manifest.json. What remains here is what main doesn't have: the two checkers.

One tightening to the workflow main landed. Its secret scan excluded telemetry.js/.map/.d.ts, leaving the one module that handles secrets as the one module nobody scanned. The exclusion existed because a doc comment quoted a full PEM header; this branch already rephrases that comment to describe it instead, which makes the exclusion unnecessary. Verified against the packed tarball: with all three files scanned, the pattern finds nothing — the surviving BEGIN [A-Z ]*PRIVATE KEY is the regex source, which doesn't match BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY. The release scan now covers all 97 packed files, and the redundant "prove the exclusion stayed narrow" follow-up grep is gone with it.

That removal rests on the doc comment staying descriptive, so it's pinned by a regression test plus an invariant on FORBIDDEN_CONTENT — a future widening of the scanner fails in test:ci with an explanation rather than failing an irreversible release.

Local: tsc, actionlint, publishable-deps:check, test:contract-pack, release-linked-versions:check, validate:no-hand-written-js all green; 33 unit tests pass.

…D_SECRET_KINDS

The provider-key shapes in scripts/forbidden-content.ts are hand-copied from
SECRET_PATTERNS, and nothing forced the copy to keep up: a 17th HARD_SECRET_KIND
added and forgotten would leave every published tarball unscanned for that shape
while the whole suite stayed green.

Assert a probe exists for every member of the set, and that the copy is never
NARROWER than canonical for any of them. The copy stays a copy on purpose --
it is deliberately wider on three shapes, and importing would narrow a scanner
that guards immutable artifacts. The header said the copy existed because this
was a .mjs run via node; it is .ts run via tsx, so it now states the real reason.
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.57%. Comparing base (7df6148) to head (fbf1728).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9763      +/-   ##
==========================================
+ Coverage   90.33%   95.57%   +5.24%     
==========================================
  Files         917      814     -103     
  Lines      113904    62930   -50974     
  Branches    26966    21930    -5036     
==========================================
- Hits       102894    60145   -42749     
+ Misses       9681     1474    -8207     
+ Partials     1329     1311      -18     
Flag Coverage Δ
backend 95.57% <ø> (ø)
control-plane ?
engine ?
rees ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/loopover-contract/src/telemetry.ts 100.00% <ø> (ø)

... and 280 files with indirect coverage changes

…an npm

main added publish-mcp-registry.yml, which publishes to the MCP registry and packs
no tarball. publishedPackageNames already handled it correctly -- a workflow with
no pack line contributes nothing -- but the invariant asserted every publish-*.yml
yields exactly one package.

Encode the real rule instead: at most one per workflow, the npm-publishing set is
exactly the five packages actually on npm, and the zero-yield workflow is named so
a NEW one fails rather than being waved through.
@JSONbored
JSONbored merged commit 6dd5791 into main Jul 29, 2026
9 checks passed
@JSONbored
JSONbored deleted the fix/publish-contract-9749 branch July 29, 2026 09:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

manual-review Gittensor contributor context

Projects

None yet

Development

Successfully merging this pull request may close these issues.

@loopover/contract is unpublished but is a runtime dependency of @loopover/mcp and @loopover/miner — the next release of either ships uninstallable

1 participant