Skip to content

Commit f5adbcd

Browse files
authored
ci(release): wire @loopover/contract into the release and publish automation (#9753)
* ci(release): wire @loopover/contract into the release and publish automation @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 * fix(release): put contract in the linked-versions group, not on ui-kit'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. * ci(release): derive linked-versions membership from the real dependency 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.
1 parent 7ffb51e commit f5adbcd

8 files changed

Lines changed: 1313 additions & 22 deletions

File tree

.github/workflows/mcp-release-please.yml

Lines changed: 37 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ name: Package Release Please
1717
# targets/workflows, and bundling them would work against this repo's small-focused-PR convention.
1818
#
1919
# linked-versions plugin, group "engine-and-dependents" (release-please-config.json) -- the
20-
# exception to the above: packages/loopover-mcp and packages/loopover-miner both carry a REAL
21-
# runtime `dependencies` entry on packages/loopover-engine, so they are not actually independent of
22-
# it. The node-workspace plugin bumps a dependent's `@loopover/engine` version range the moment
23-
# engine's own version changes, but under plain separate-pull-requests: true that bump lands on the
20+
# exception to the above: packages/loopover-mcp and packages/loopover-miner both carry REAL runtime
21+
# `dependencies` entries on packages/loopover-engine AND (since #9749) packages/loopover-contract,
22+
# so they are not actually independent of either. The node-workspace plugin bumps a dependent's
23+
# `@loopover/engine` / `@loopover/contract` version range the moment that package's own version
24+
# changes, but under plain separate-pull-requests: true that bump lands on the
2425
# dependent's OWN separate branch/PR -- meaning the dependent's package.json ends up requiring an
2526
# engine version that exists NOWHERE (not locally on that branch, since engine's bump lives on a
2627
# different unmerged branch; not on npm either, since it hasn't published yet). `npm ci` then fails
@@ -30,12 +31,15 @@ name: Package Release Please
3031
# NOT override separate-pull-requests -- verified against release-please's manifest.ts and
3132
# docs/manifest-releaser.md: separate-pull-requests is enforced as the final PR-splitting decision
3233
# regardless of what plugins attempt internally). linked-versions is the mechanism that actually
33-
# overrides per-component isolation, but ONLY for the named `components` in its group: engine's new
34-
# version and its dependents' bumped ranges land together in ONE PR/commit, so npm workspaces
35-
# resolves the dependency from the LOCAL checkout (always satisfies the range, regardless of npm
36-
# registry publish timing or ordering) instead of hitting the ETARGET race. packages/loopover-ui-kit
37-
# has no dependency edge to any other package here and is not in the group, so it keeps its fully
38-
# independent release cadence untouched.
34+
# overrides per-component isolation, but ONLY for the named `components` in its group: the
35+
# prerequisite's new version and its dependents' bumped ranges land together in ONE PR/commit, so
36+
# npm workspaces resolves the dependency from the LOCAL checkout (always satisfies the range,
37+
# regardless of npm registry publish timing or ordering) instead of hitting the ETARGET race.
38+
# `contract` is in the group for exactly the same reason engine is, and NOT because it is
39+
# "published alongside" them: membership tracks the dependency EDGE, not release cadence.
40+
# packages/loopover-ui-kit has no dependency edge to any other package here and is not in the group,
41+
# so it keeps its fully independent release cadence untouched -- which is also why "publish it like
42+
# ui-kit" is the wrong instinct for any future package that something here actually depends on.
3943
#
4044
# Runs on every push to main, not just a schedule: release-please is idempotent (it recomputes
4145
# purely from conventional-commit history since each component's last release tag), so the extra
@@ -181,6 +185,12 @@ jobs:
181185
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
182186
run: gh workflow run publish-engine.yml --ref "${{ steps.release.outputs['packages/loopover-engine--tag_name'] }}" -f released_by_release_please=true
183187

188+
- name: Dispatch Contract publish
189+
if: ${{ steps.release.outputs['packages/loopover-contract--release_created'] == 'true' }}
190+
env:
191+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
192+
run: gh workflow run publish-contract.yml --ref "${{ steps.release.outputs['packages/loopover-contract--tag_name'] }}" -f released_by_release_please=true
193+
184194
- name: Dispatch Miner publish
185195
if: ${{ steps.release.outputs['packages/loopover-miner--release_created'] == 'true' }}
186196
env:
@@ -320,24 +330,32 @@ jobs:
320330
return 1
321331
}
322332
323-
engine_ok=true
333+
# packages/loopover-mcp and packages/loopover-miner carry REAL runtime `dependencies` entries on
334+
# BOTH @loopover/engine and @loopover/contract (#9749), so both prerequisites must be live on
335+
# npm before either dependent publishes -- otherwise the dependent resolves a version that
336+
# exists nowhere and fails ETARGET, the same race this whole block exists to avoid.
337+
deps_ok=true
338+
deps_published=false
339+
if needs_publish packages/loopover-contract @loopover/contract; then
340+
if dispatch_and_wait publish-contract.yml; then deps_published=true; else deps_ok=false; fi
341+
fi
324342
if needs_publish packages/loopover-engine @loopover/engine; then
325-
dispatch_and_wait publish-engine.yml || engine_ok=false
326-
if [ "$engine_ok" = "true" ]; then
327-
echo "Waiting for npm registry propagation before publishing engine's dependents..."
328-
sleep 30
329-
fi
343+
if dispatch_and_wait publish-engine.yml; then deps_published=true; else deps_ok=false; fi
344+
fi
345+
if [ "$deps_ok" = "true" ] && [ "$deps_published" = "true" ]; then
346+
echo "Waiting for npm registry propagation before publishing engine/contract dependents..."
347+
sleep 30
330348
fi
331349
332-
if [ "$engine_ok" = "true" ]; then
350+
if [ "$deps_ok" = "true" ]; then
333351
if needs_publish packages/loopover-mcp @loopover/mcp; then
334352
dispatch_and_wait_with_retry publish-mcp.yml || echo "::warning::publish-mcp.yml did not succeed after retries -- left for manual follow-up."
335353
fi
336354
if needs_publish packages/loopover-miner @loopover/miner; then
337355
dispatch_and_wait_with_retry publish-miner.yml || echo "::warning::publish-miner.yml did not succeed after retries -- left for manual follow-up."
338356
fi
339357
else
340-
echo "::warning::Skipping mcp/miner reconciliation -- publish-engine.yml did not succeed, and they depend on it."
358+
echo "::warning::Skipping mcp/miner reconciliation -- publish-engine.yml and/or publish-contract.yml did not succeed, and they depend on both."
341359
fi
342360
343361
if needs_publish packages/loopover-ui-kit @loopover/ui-kit; then
@@ -353,7 +371,7 @@ jobs:
353371
GH_TOKEN: ${{ secrets.RELEASE_PLEASE_TOKEN }}
354372
run: |
355373
set -euo pipefail
356-
for dir_pkg in "packages/loopover-engine:@loopover/engine" "packages/loopover-mcp:@loopover/mcp" "packages/loopover-miner:@loopover/miner" "packages/loopover-ui-kit:@loopover/ui-kit"; do
374+
for dir_pkg in "packages/loopover-contract:@loopover/contract" "packages/loopover-engine:@loopover/engine" "packages/loopover-mcp:@loopover/mcp" "packages/loopover-miner:@loopover/miner" "packages/loopover-ui-kit:@loopover/ui-kit"; do
357375
dir="${dir_pkg%%:*}"
358376
pkg="${dir_pkg##*:}"
359377
local_version="$(node -p "require('./$dir/package.json').version")"

0 commit comments

Comments
 (0)