Skip to content

fix: prevent multi-mode from publishing monorepo workspace roots#48

Merged
lacymorrow merged 2 commits into
mainfrom
fix/multi-workspace-root-publish
May 24, 2026
Merged

fix: prevent multi-mode from publishing monorepo workspace roots#48
lacymorrow merged 2 commits into
mainfrom
fix/multi-workspace-root-publish

Conversation

@lacymorrow

Copy link
Copy Markdown
Owner

Summary

Fixes [LAC-2056] — multi-mode was publishing monorepo workspace roots (package.json with workspaces field) directly to npm, creating broken packages with no bin/main/exports. This caused the lacy v1.8.12–v1.8.15 broken releases (LAC-2055).

  • discover.ts: Detect workspace roots via workspaces field and flag them as non-npm-publishable by default
  • multi.ts: After loading config, re-derive hasNpm from config's actual npm targets — so projects with npm.cwd or npm.targets set in their shipx.config.ts pointing to a sub-package will still publish correctly
  • multi.ts: Show (workspace) label instead of (private) for workspace root projects in the multi-select UI

Test plan

  • bun run typecheck passes
  • bun run build succeeds
  • node dist/cli.js --help smoke test works
  • Manual test: run shipx --multi from ~/repo/ and verify monorepo workspace roots (like lacy) show (workspace) label and are not included in npm publish phase
  • Manual test: verify a project with explicit npm: { cwd: "packages/foo" } in shipx.config.ts correctly publishes from the sub-package

…-2056]

Workspace roots (package.json with `workspaces` field) are almost never
directly publishable — they lack bin/main/exports and publishing them
creates broken npm packages (see LAC-2055 where lacy's root was published
instead of packages/lacy/).

- discover: flag workspace roots as non-npm-publishable by default
- multi: after loading config, re-derive hasNpm from config's npm targets
  so explicit npm.cwd/targets in shipx.config.ts can override discover
- UI: show "(workspace)" instead of "(private)" for workspace roots

Co-Authored-By: Paperclip <noreply@paperclip.ing>

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request enhances project discovery by identifying workspace roots and adjusting npm publishability logic accordingly. It also introduces a mechanism to re-derive the hasNpm flag based on custom configuration targets. The review feedback highlights the need to ensure consistency in workspace detection during this re-derivation, correctly update the project name for registry lookups, and clean up redundant path resolutions.

Comment thread src/multi.ts
Comment on lines +363 to +375
const hasCustomTarget = config.npm.targets.some(
(t) => resolve(t.cwd) !== resolve(project.path),
);
if (hasCustomTarget) {
project.hasNpm = config.npm.targets.some((t) => {
try {
const pkg = readJson(resolve(t.cwd, "package.json"));
return pkg.private !== true;
} catch {
return false;
}
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The re-derivation logic for hasNpm should also check for the workspaces field to maintain consistency with the discovery phase and prevent accidental publishing of nested workspace roots. Additionally, when a custom target is used, the project.name should be updated to match the name of the package being published (taking the name from the first publishable target found). This ensures that reconcileRegistryVersion performs the registry lookup against the correct npm package rather than the monorepo root. Finally, the resolve calls at line 364 are redundant as these paths are already absolute.

Suggested change
const hasCustomTarget = config.npm.targets.some(
(t) => resolve(t.cwd) !== resolve(project.path),
);
if (hasCustomTarget) {
project.hasNpm = config.npm.targets.some((t) => {
try {
const pkg = readJson(resolve(t.cwd, "package.json"));
return pkg.private !== true;
} catch {
return false;
}
});
}
const hasCustomTarget = config.npm.targets.some(
(t) => t.cwd !== project.path,
);
if (hasCustomTarget) {
project.hasNpm = config.npm.targets.some((t) => {
try {
const pkg = readJson(resolve(t.cwd, "package.json"));
const isPublishable = pkg.private !== true && !pkg.workspaces;
if (isPublishable && typeof pkg.name === "string") {
project.name = pkg.name;
}
return isPublishable;
} catch {
return false;
}
});
}

…e override, resolve cleanup [LAC-2056]

- Check `workspaces` field in re-derivation to match discover-time logic
- Update `project.name` from target package.json so reconcileRegistryVersion
  looks up the correct npm package instead of the monorepo root
- Remove redundant `resolve()` calls — paths are already absolute from config

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@lacymorrow

Copy link
Copy Markdown
Owner Author

Addressed Gemini's review in c3d350a:

  1. Workspace check consistency: Added !pkg.workspaces to the re-derivation to match discover-time logic — prevents accidental publishing of nested workspace roots
  2. Project name override: Now updates project.name from the target's package.json so reconcileRegistryVersion looks up the correct npm package instead of the monorepo root
  3. Resolve cleanup: Removed redundant resolve() calls — paths are already absolute from config resolution

@lacymorrow lacymorrow merged commit f49f3a2 into main May 24, 2026
3 checks passed
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.

1 participant