ci(release): wire @loopover/contract into the release and publish automation - #9753
Conversation
…omation @loopover/contract is a runtime `dependencies` entry of both @loopover/mcp and @loopover/miner, but it had no publish path — no publish workflow, and no entry in release-please's config or manifest. It was the only workspace package shaped for publishing (no `private: true`, `publishConfig.access: public`, a `files` allowlist) with no automation behind it. Left alone, the next mcp/miner release ships uninstallable: both are at 3.15.2 locally against 3.14.x on npm, so the pending publish already carries `"@loopover/contract": "^0.1.0"` against a package that did not exist. 0.1.0 has now been bootstrap-published by hand (npm's OIDC trusted publishing cannot create a brand-new package, the same bootstrap @loopover/engine needed), and a Trusted Publisher is configured against publish-contract.yml — so renaming that file breaks the connection and every publish with it. publish-contract.yml mirrors publish-engine.yml/publish-ui-kit.yml: unprivileged validate resolves the version, verifies the commit is on main, resolves the release commit from the version string rather than tagging HEAD (#8525), builds, packs, and smoke-tests; privileged publish tags and pushes the exact tested tarball via OIDC behind environment `release`. No separate typecheck step — this package's `build` IS `tsc -p tsconfig.json`. The secret scan needed one deliberate change. Unlike its siblings, this package SHIPS the redaction logic: dist/telemetry.js defines SECRET_VALUE_PATTERN, whose source text contains a literal PEM `PRIVATE KEY` header (plus a doc comment quoting a full one, describing the bug that alternative fixes). The siblings' verbatim pattern is a guaranteed false positive on every release — verified against the 0.1.0 tarball. Excluding that one module keeps the check meaningful for the other 92 files, and a second, narrower scan still runs across everything including the excluded module, so a real token-shaped leak there still fails. mcp/miner depend on contract exactly as they depend on engine, so the reconciliation job now treats both as prerequisites: contract and engine publish first, and mcp/miner are skipped entirely if either fails, rather than resolving a dependency version that exists nowhere and failing ETARGET. Contract stays independently versioned (like ui-kit) rather than joining the engine-and-dependents linked-versions group: it has only just shipped 0.1.0, and renumbering it into the 3.x train before it has any consumers costs more than it buys. Revisit at its next major. Also adds the LICENSE file the package declares (AGPL-3.0-only) but never shipped — engine, mcp and ui-kit all carry one. Closes #9749
|
Important 🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨🟨 ⏳ LoopOver is waiting…LoopOver has seen this pull request and is waiting on CI checks to finish before reviewing it. This comment will update once the review runs. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed · 🟨 Waiting |
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9753 +/- ##
=======================================
Coverage 90.27% 90.27%
=======================================
Files 904 904
Lines 113178 113178
Branches 26840 26840
=======================================
Hits 102172 102172
Misses 9676 9676
Partials 1330 1330
Flags with carried forward coverage won't be shown. Click here to find out more. |
…t's independent cadence Review catch on #9753: contract was left out of the "engine-and-dependents" linked-versions group on the reasoning that it should version independently "like ui-kit". That reasoning was wrong, and this repo has already paid for the bug it reintroduces. mcp-release-please.yml's own header is explicit about what the group is for: mcp/miner carry a REAL runtime `dependencies` entry, so node-workspace bumps their range the moment the prerequisite's version changes -- and under separate-pull-requests: true that bump lands on the dependent's OWN branch, leaving its package.json requiring a version that exists nowhere (not locally, since the prerequisite's bump is on a different unmerged branch; not on npm, since it hasn't published). `npm ci` then fails ETARGET. That was confirmed live across several release cycles (#7086/#7087/#7107/#7108/#7119/#7120/#7121), and node-workspace's own `merge` option was already tried and does NOT override separate-pull-requests. ui-kit is outside the group because it has NO dependency edge to anything here -- not because independent versioning is the default. contract has exactly the edge engine has, so it belongs in the group for exactly the same reason. Membership tracks the dependency edge, not release cadence; the header now says so, since "publish it like ui-kit" is the trap that produced this. The reconciliation ordering added in this PR stays: the group fixes the release-PR stage (ranges and versions land in one commit), reconciliation fixes the publish stage (registry propagation and dispatch ordering). Engine needs both today and contract needs both for the same reasons. Cost of the fix, stated plainly: contract joins the group's version train rather than staying at 0.1.0, so its next release will not be 0.2.0. That is a worse version story than independence and a better correctness story, and 0.1.0 is already published either way.
|
Valid blocker — fixed in the latest push. You were right, and the repo's own documentation settles it against my original reasoning.
My "independent like ui-kit" justification inverted that last point: ui-kit is independent precisely because it lacks the edge contract now has. And this was not hypothetical — the header cites it as confirmed live across several mcp/miner cycles (#7086/#7087/#7107/#7108/#7119/#7120/#7121). So rather than confirm cross-group reconciliation works, I've added The reconciliation ordering added in this PR stays as-is. The two mechanisms cover different stages: the group fixes the release-PR stage (prerequisite version and dependents' ranges land in one commit), reconciliation fixes the publish stage (dispatch ordering and registry propagation). Engine needs both today; contract needs both for the same reasons. Cost, stated plainly: contract joins the group's version train rather than staying at
|
…cy graph Adding contract to the group fixed today's instance. It did not fix the class: group membership is a function of the dependency graph, nothing enforced that, and the last time an edge was added the config was not updated. #9521 made @loopover/contract a runtime dependency of both @loopover/mcp and @loopover/miner without touching linked-versions, and that sat unnoticed until #9749 -- by which point the package was not even published, so the next mcp/miner release would have shipped uninstallable. check-release-linked-versions.ts derives the requirement instead of trusting memory. It reads the real package.json edges between release-please-tracked packages and fails when two are joined by a shipping dependency but not by a linked-versions group -- the exact ETARGET race mcp-release-please.yml's header documents, and which node-workspace's own `merge` option provably does not fix. It also closes a second silent gap found while wiring this: sync-release-manifest builds its work list from the MANIFEST's own keys (`if (!(workspacePath in manifest)) continue`), so a package added to release-please-config.json but not to the manifest is silently exempt from the staleness check that stops release-please re-proposing an already-published version. Adding a package to one file and not the other had no feedback at all; now it fails. Deliberately scoped to dependencies/peerDependencies/optionalDependencies. devDependencies are excluded because they never reach a consumer, and forcing two packages into permanent version lockstep over a build-time-only edge costs more than the failure it prevents -- which surfaces immediately in CI on the branch that introduced it rather than in a published artifact. That tradeoff is written down at the constant, so the next person hitting it can revisit deliberately. ui-kit is the case that proves the check is not merely "group everything": it has no dependency edge, stays ungrouped, and there is an invariant test asserting it is never reported -- otherwise the check would drag unrelated packages into lockstep, which is the opposite of the goal. Verified both ways: removing contract from the group reproduces the #9521 gap and exits 1 naming both offending edges; removing its manifest entry exits 1 naming the consequence; the real config exits 0.
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
loopover-ui | 86d36f2 | Commit Preview URL Branch Preview URL |
Jul 29 2026, 06:45 AM |
|
Second push: the config fix alone closed today's instance but not the class, so I've added enforcement. The actual problem is that linked-versions membership is a function of the dependency graph, and nothing checked that. #9521 made
It also closes a second silent gap I hit while wiring this: Scope decision, written down at the constant: only
Verified both directions:
15 unit tests, pure-core/thin-IO split mirroring |
Bundle ReportChanges will decrease total bundle size by 843 bytes (-0.01%) ⬇️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: loopover-uiAssets Changed:
|
Summary
@loopover/contractis a runtimedependenciesentry of both@loopover/mcpand@loopover/miner, but it had no publish path — no publish workflow, and no entry in release-please's config or manifest. It was the only workspace package shaped for publishing (noprivate: true,publishConfig.access: public, afilesallowlist) with no automation behind it.Left alone the next mcp/miner release ships uninstallable: both are at 3.15.2 locally against 3.14.x on npm, so the already-cut publish carries
"@loopover/contract": "^0.1.0"against a package that did not exist.0.1.0has now been bootstrap-published by hand — npm's OIDC trusted publishing cannot create a brand-new package, the same bootstrap@loopover/engineneeded — and a Trusted Publisher is configured againstpublish-contract.yml(GitHub Actions ·JSONbored/loopover· environmentrelease). Renaming that file breaks the connection and every publish with it, which is why the workflow header says so.Closes #9749
What's here
publish-contract.ymlmirrorspublish-engine.yml/publish-ui-kit.yml: unprivilegedvalidateresolves the version, verifies the commit is onmain, resolves the release commit from the version string rather than tagging HEAD (#8525), builds, packs and smoke-tests; privilegedpublishtags and pushes the exact tested tarball via OIDC behindenvironment: release. No separate typecheck step — this package'sbuildistsc -p tsconfig.json.The secret scan needed one deliberate change. Unlike its siblings, this package ships the redaction logic:
dist/telemetry.jsdefinesSECRET_VALUE_PATTERN, whose source text contains a literal PEMPRIVATE KEYheader, plus a doc comment quoting a full one describing the bug that alternative fixes. The siblings' verbatim pattern is a guaranteed false positive on every release — verified against the real 0.1.0 tarball. So the scan excludes that one module, and a second, narrower token-shaped scan still runs across everything including the excluded module, so a genuine leak there still fails the build.Prerequisite ordering. mcp/miner depend on contract exactly as they depend on engine, so the reconciliation job now treats both as prerequisites — contract and engine publish first, and mcp/miner are skipped entirely if either fails, rather than resolving a version that exists nowhere and failing ETARGET.
Versioning. Contract stays independently versioned (like
ui-kit) rather than joining theengine-and-dependentslinked-versions group. It has only just shipped0.1.0; renumbering it into the 3.x train before it has consumers costs more than it buys. Worth revisiting at its next major.LICENSE. The package declares
AGPL-3.0-onlybut shipped no LICENSE file;engine,mcpandui-kitall carry one. (mineris also missing one — out of scope here, but worth a follow-up.)Validation
Everything below was run against the real 0.1.0 tarball built from this tree:
npm run actionlintnpm run lint:composite-actionsnpm run release-manifest:sync:checkchecked 5 package version(s), all in sync(was 4)release-please-config.json/.release-please-manifest.json0.1.0dist/+package.json+README.mdindex/public-api/api-schemasTOOL_CATEGORIES= 10git diff --checkThe reconciliation ordering was also behaviorally tested against five scenarios with stubbed dispatch helpers — nothing-to-do, contract+mcp happy path, contract-fails, engine-fails-contract-ok, and mcp-only-with-deps-already-live. The load-bearing case is engine failing while contract succeeded: mcp/miner are still skipped, because both are prerequisites.
Scope
type(scope): short summaryConventional Commit format, for examplefix(api): restore profile access checks.CONTRIBUTING.mdand does not reintroduce GitHub Pages, VitePress,site/, orCNAME.Closes #123) — a linked open issue is required for every contributor PR.Safety
UI Evidencesection below with JPG/JPEG or PNG screenshots arranged as organized, captioned, clickable thumbnails. SVG screenshots are not used as review evidence. Review-only screenshots or recordings are not committed to the repository.No runtime/
srccode changes and no UI output, hence no coverage obligation and no UI Evidence table. The privilegedpublishjob holdsid-token: writebut never runsnpm installor a build — it only downloads and publishes the tarball the unprivileged job already produced and tested, preserving the same privilege separation as every sibling publish workflow. No credential value appears anywhere in the diff.