Skip to content

fix(release): make private workspace package boundary packer-independent - #26

Merged
qmarcelle merged 1 commit into
mainfrom
fix/meta-297-private-workspace-package-release-boundary
Aug 19, 2026
Merged

fix(release): make private workspace package boundary packer-independent#26
qmarcelle merged 1 commit into
mainfrom
fix/meta-297-private-workspace-package-release-boundary

Conversation

@qmarcelle

@qmarcelle qmarcelle commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Repairs the release-integrity defect exposed by the failed cli-v0.6.0 publish. 0.6.0 never reached npm; its tarball gate correctly stopped a private @workspacejson/mining-core workspace dependency from leaking into the public CLI manifest.

This PR cuts the corrected patch release as @workspacejson/cli@0.6.1 without changing mining, retrieval, provenance, or artifact semantics.

What changes

  • moves the private mining-core declaration out of the public CLI manifest and into the private root workspace
  • makes the CLI build its bundled mining-core input explicitly so clean builds do not rely on pnpm workspace ordering
  • makes npm the deterministic default packer because publish-cli.yml publishes with npm
  • retains pnpm only as an explicit diagnostic packer and for the frozen agents-audit package that this repository does not publish
  • adds an identity-based guard rejecting references from public packed manifests to private workspace packages under any spelling
  • adds red tests covering npm workspace:*, pnpm-style 0.0.0, concrete-version disguise, ambient npm_execpath, and a clean baseline
  • runs both npm release-path verification and pnpm diagnostic verification in CI
  • records that the old publish invariant became false under META-297 rather than silently replacing the rationale
  • documents 0.6.0 as tagged-but-never-published and prepares 0.6.1

Judgement calls under review

Build ordering

Moving @workspacejson/mining-core to the private root removed the dependency edge pnpm used for recursive build ordering. A clean pnpm -r build exposed this immediately. The CLI build script now builds its own bundled input first, so the requirement travels with the package that needs it instead of depending on workflow ordering or stale dist/ output.

Frozen agents-audit packer

The deterministic npm default also exposed agents-audit's existing sibling workspace:* reference. That package is frozen and not publishable from this repository under OWNERSHIP.md, so this PR does not force an npm-publishability property it does not claim. It is explicitly verified with pnpm, while the packer-independent private-workspace identity guard still applies. If META-243 later transfers publish authority here, this override must be removed and the sibling dependency made release-safe.

Verification reported on the branch

  • clean build from deleted dist/
  • typecheck green
  • 218 tests green
  • architecture guard + 21 red tests green
  • package docs + 12 red tests green
  • release:verify-packs green: CLI via npm, frozen agents-audit via pnpm
  • release:verify-packs:pnpm green for both packages
  • package-tarball guard red tests: 5/5
  • both frozen-source parity harnesses remain at the ratified four expected differences

Merge only after GitHub CI confirms the same state from a clean checkout.

Summary by Sourcery

Harden release packaging so private workspace dependencies cannot leak into public manifests and publish verification consistently matches the npm release path.

Bug Fixes:

  • Prevent public package tarballs from referencing private workspace packages, regardless of packer-specific version rewriting.
  • Release the corrected CLI patch version 0.6.1 after the 0.6.0 publish was stopped by the tarball integrity gate.

Enhancements:

  • Make npm the deterministic release verification packer while retaining explicit pnpm diagnostics for the frozen agents-audit package.
  • Build the CLI's private mining-core input explicitly so clean builds do not depend on workspace build ordering.

CI:

  • Run npm release-path verification, pnpm diagnostic verification, and package tarball guard tests in CI.

Documentation:

  • Document the unpublished 0.6.0 release and update repository package metadata for CLI 0.6.1.

Tests:

  • Add coverage for private workspace references using workspace protocols, pnpm-rewritten versions, concrete versions, and ambient npm_execpath values, plus a clean baseline.

…ETA-297)

`@workspacejson/cli@0.6.0` was tagged but never published. Its publish run
failed at the tarball gate, two steps before `npm publish`:

    package.devDependencies.@workspacejson/mining-core leaks "workspace:*"
    into the packed manifest.

The gate was right. Three things behind it were not.

1. The invariant was false. `publish-cli.yml` justified publishing with npm on
   the grounds that @workspacejson/cli declares no `workspace:` dependencies.
   META-297 added the private @workspacejson/mining-core as a devDependency and
   that stopped being true. The last release predates that commit, so 0.6.0 was
   the first publish attempt since. The standard authority migration did not
   cause this; it touched only `dependencies` and `version`.

2. The gate's verdict depended on how it was invoked. The packer was inferred
   from `npm_execpath`, so `pnpm run release:verify-packs` packed with pnpm
   while CI's `pnpm --filter ... exec ...` packed with npm. The same commit
   verified green locally and red in CI, and the green run measured bytes
   nobody publishes. The packer is now npm unconditionally, because that is
   what `npm publish` ships.

3. The invariant was the wrong shape. It tested for the literal `workspace:`
   string, which is syntactic and packer-dependent:

       npm  pack -> "@workspacejson/mining-core": "workspace:*"   caught
       pnpm pack -> "@workspacejson/mining-core": "0.0.0"         waved through

   `0.0.0` is a dangling reference to a package that exists nowhere, wearing a
   version that reads as legitimate. Switching packers would have published it
   with a green gate. The rule is now identity-based and packer-independent: a
   public package's packed manifest must not reference a private workspace
   package at all, under any spelling. Private packages are discovered by name
   from the workspace, so it needs no maintenance and cannot be evaded by a
   rewrite.

Also fixed, found while proving the above:

* Moving the declaration to the private root workspace removed the edge pnpm
  used to order `pnpm -r build`, and a clean checkout then built the CLI and
  mining-core in parallel — the CLI failing to resolve its own bundle input.
  The CLI's build script now builds that input first, so the guarantee travels
  with the package that needs it rather than depending on invocation order.
* The deterministic packer surfaced the same class of defect in `agents-audit`,
  which depends on its sibling by `workspace:*`. That package is frozen and no
  workflow here publishes it, so it is verified with pnpm — the packer matching
  its actual (non-)publisher — and the reasoning, plus what must change if
  META-243 makes this repository its publisher, is recorded at the override.

Red tests cover both spellings under both packers, and that `npm_execpath` no
longer changes the packer. The publish workflow's invariant comment is replaced
with the one the gate now actually asserts.

Version bumped to 0.6.1. No mining, retrieval, provenance or artifact semantics
change; the 0.5.0 standard authority migration 0.6.0 carried ships unchanged.
Copilot AI lite review requested due to automatic review settings August 19, 2026 09:53

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

qmarcelle has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sourcery-ai

sourcery-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Reviewer's Guide

Enforces a packer-independent release boundary so public packages cannot reference private workspace packages, makes npm the deterministic packer for release verification, relocates the private mining-core dependency to the root workspace while ensuring the CLI builds it explicitly, and extends CI plus new red tests to cover npm vs pnpm behavior and the 0.6.1 documentation changes.

Sequence diagram for verify-package-tarball release guard with packer-independent private boundary

sequenceDiagram
  actor CI
  participant cli_package as @workspacejson/cli
  participant verify_script as verify-package-tarball.mjs
  participant npm
  participant pnpm
  participant workspace as workspace_packages

  CI->>cli_package: pnpm --filter @workspacejson/cli exec node verify-package-tarball.mjs
  cli_package->>verify_script: load package.json (sourceManifest)
  verify_script->>verify_script: packer = WORKSPACEJSON_PACKER ?? "npm"
  alt packer is npm
    verify_script->>npm: npm pack
  else packer is pnpm
    verify_script->>pnpm: pnpm pack
  end
  verify_script->>workspace: privateWorkspacePackageNames()
  workspace-->>verify_script: Set(private_workspace_names)
  verify_script->>verify_script: assertNoPrivateWorkspacePackages(manifest)
  verify_script->>verify_script: assertNoWorkspaceProtocol(manifest, "package")
  verify_script->>verify_script: assertStandardDependenciesArePinned(manifest)
  verify_script->>verify_script: assertRuntimeFiles(manifest, files)
  verify_script-->>CI: exit 0 on success / error on private workspace reference
Loading

File-Level Changes

Change Details Files
Make npm the deterministic default packer for release verification and decouple it from npm_execpath.
  • Set verify-package-tarball.mjs to default WORKSPACEJSON_PACKER to npm instead of inferring from npm_execpath.
  • Document the rationale and limited diagnostic uses of overriding the packer via WORKSPACEJSON_PACKER.
  • Ensure unsupported packers still cause an explicit error.
scripts/verify-package-tarball.mjs
Add an identity-based guard preventing public packed manifests from referencing private workspace packages under any spelling.
  • Introduce privateWorkspacePackageNames() to discover private workspace package names from the workspace tree.
  • Add assertNoPrivateWorkspacePackages(manifest) and invoke it before syntactic checks, skipping only when the packed source manifest is itself private.
  • Emit detailed error messages explaining why private workspace dependencies in public packages are forbidden, including packer-specific notes about workspace: rewriting.
  • Reorder integrity checks so the packer-independent private-package guard runs before workspace: protocol and other existing checks.
scripts/verify-package-tarball.mjs
Add red-test harnesses to regression-test the tarball guard across npm and pnpm and verify deterministic packer selection.
  • Create scripts/verify-package-tarball.test.mjs implementing multiple red-test scenarios for private workspace dependencies under different spellings and environments.
  • Reproduce the exact 0.6.0 defect via deliberate manifest mutations to validate both npm and pnpm paths.
  • Include a baseline green-path test that verifies the unmodified repo packs cleanly with npm and emits expected success wording.
  • Wire a new npm script release:verify-packs:test to run these tests in CI.
scripts/verify-package-tarball.test.mjs
package.json
Move mining-core to the private root workspace and ensure the CLI builds its bundled input explicitly.
  • Add @workspacejson/mining-core as a devDependency in the root package.json instead of the CLI package.
  • Update the CLI build script to build @workspacejson/mining-core via pnpm --filter before running tsup, so clean recursive builds do not rely on pnpm workspace ordering.
  • Remove the mining-core devDependency from packages/cli/package.json to keep the published CLI manifest free of private workspace references.
package.json
packages/cli/package.json
Adjust release and CI workflows to use npm as the release-path packer while running pnpm verification as a diagnostic path.
  • Clarify and update comments in publish-cli.yml to reflect the new identity-based private workspace invariant and the historical failure in 0.6.0.
  • In ci.yml, split tarball verification into an npm-based release path step and a pnpm-based diagnostic step, plus a red-tests step for the tarball guard.
  • Add release:verify-packs:pnpm script that re-runs release:verify-packs with WORKSPACEJSON_PACKER=pnpm.
  • Ensure agents-audit verification explicitly uses pnpm via WORKSPACEJSON_PACKER=pnpm while the CLI uses the default npm packer.
.github/workflows/publish-cli.yml
.github/workflows/ci.yml
package.json
Document the 0.6.1 release, the failed 0.6.0 publish, and the updated registry snapshot.
  • Add a 0.6.1 patch entry to packages/cli/CHANGELOG.md explaining the packaging defect, the npm vs pnpm behavior, and the new invariant.
  • Bump the CLI version to 0.6.1 in its package.json.
  • Update README.md registry snapshot and manifest table to reference 0.6.1 and note that 0.6.0 was tagged but never published due to the gate failure.
packages/cli/CHANGELOG.md
packages/cli/package.json
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sonarqubecloud

Copy link
Copy Markdown

@qmarcelle
qmarcelle merged commit ba5b1b5 into main Aug 19, 2026
7 checks passed

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In verify-package-tarball.test.mjs, the cpSync filter relies on string checks like src.includes("/node_modules") and src.includes("/.git/"), which will break on Windows path separators; consider using path.sep, path.basename, or a more robust directory check to keep the tests cross-platform.
  • The privateWorkspacePackageNames helper in verify-package-tarball.mjs assumes all entries under the parent directory of packageDirectory are packages; if non-package folders are expected there, you might want to guard with an additional check (e.g., skip directories without a package.json at the expected depth or restrict to a known workspace root) to avoid accidental misreads.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `verify-package-tarball.test.mjs`, the `cpSync` filter relies on string checks like `src.includes("/node_modules")` and `src.includes("/.git/")`, which will break on Windows path separators; consider using `path.sep`, `path.basename`, or a more robust directory check to keep the tests cross-platform.
- The `privateWorkspacePackageNames` helper in `verify-package-tarball.mjs` assumes all entries under the parent directory of `packageDirectory` are packages; if non-package folders are expected there, you might want to guard with an additional check (e.g., skip directories without a `package.json` at the expected depth or restrict to a known workspace root) to avoid accidental misreads.

## Individual Comments

### Comment 1
<location path="scripts/verify-package-tarball.test.mjs" line_range="96-99" />
<code_context>
+];
+
+function runVerifier(root, env) {
+  return spawnSync(process.execPath, [join(root, "scripts", "verify-package-tarball.mjs")], {
+    cwd: join(root, "packages", "cli"),
+    encoding: "utf8",
+    env: { ...process.env, WORKSPACEJSON_PACKER: undefined, npm_execpath: undefined, ...env },
+  });
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** Unset env vars using deletion instead of assigning `undefined` to avoid leaking a string `'undefined'` into the child process.

In `spawnSync`, env vars must be strings; setting them to `undefined` won’t unset them and can instead pass the literal `'undefined'` into the child process. That means `WORKSPACEJSON_PACKER` or `npm_execpath` may be set to `'undefined'`, affecting packer selection. To truly clear them, build the env object without those keys (e.g. copy `process.env`, delete those keys, then apply `env` overrides) or only include them when you explicitly want to set them.
</issue_to_address>

### Comment 2
<location path="scripts/verify-package-tarball.test.mjs" line_range="106-107" />
<code_context>
+function scratchCopy() {
+  const directory = mkdtempSync(join(tmpdir(), "wjson-pack-guard-"));
+  const root = join(directory, "repo");
+  cpSync(repoRoot, root, {
+    recursive: true,
+    filter: (src) => !src.includes("/node_modules") && !src.includes("/.git/") && !src.endsWith("/.git"),
+  });
</code_context>
<issue_to_address>
**issue (bug_risk):** Filter used in `cpSync` is POSIX-path-specific and may not exclude `node_modules` or `.git` on Windows.

These checks rely on POSIX-style separators (`/`), but on Windows `src` will have `\`, so the excludes won’t trigger and `node_modules` / `.git` may be copied into the scratch directory, slowing tests and potentially changing behavior. Please normalize `src` (e.g., replace `\` with `/` or split on `path.sep`) before applying these checks, or use a path-aware predicate instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +96 to +99
return spawnSync(process.execPath, [join(root, "scripts", "verify-package-tarball.mjs")], {
cwd: join(root, "packages", "cli"),
encoding: "utf8",
env: { ...process.env, WORKSPACEJSON_PACKER: undefined, npm_execpath: undefined, ...env },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Unset env vars using deletion instead of assigning undefined to avoid leaking a string 'undefined' into the child process.

In spawnSync, env vars must be strings; setting them to undefined won’t unset them and can instead pass the literal 'undefined' into the child process. That means WORKSPACEJSON_PACKER or npm_execpath may be set to 'undefined', affecting packer selection. To truly clear them, build the env object without those keys (e.g. copy process.env, delete those keys, then apply env overrides) or only include them when you explicitly want to set them.

Comment on lines +106 to +107
cpSync(repoRoot, root, {
recursive: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Filter used in cpSync is POSIX-path-specific and may not exclude node_modules or .git on Windows.

These checks rely on POSIX-style separators (/), but on Windows src will have \, so the excludes won’t trigger and node_modules / .git may be copied into the scratch directory, slowing tests and potentially changing behavior. Please normalize src (e.g., replace \ with / or split on path.sep) before applying these checks, or use a path-aware predicate instead.

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.

2 participants