-
Notifications
You must be signed in to change notification settings - Fork 3
✨ Add cascade version bump tooling (pnpm versions / pnpm versions:sync) #185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { resolve } from "node:path"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import process from "node:process"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { readTextFile, writeTextFile } from "@effectionx/fs"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { x } from "@effectionx/tinyexec"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { main } from "effection"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { inc as semverInc } from "semver"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { buildDepGraph, getTransitiveDependents } from "./lib/dep-graph.ts"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const mode = process.argv[2] ?? "check"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!["check", "sync"].includes(mode)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("Usage: node .internal/check-versions.ts [check|sync]"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isSyncMode = mode === "sync"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await main(function* () { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const graph = yield* buildDepGraph(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Determine which packages have a version not yet published to npm. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pendingRelease = new Set<string>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const pkg of graph.packages.values()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const npmCheck = yield* x( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "npm", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ["view", `${pkg.name}@${pkg.version}`, "version"], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { throwOnError: false }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const result = yield* npmCheck; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (result.exitCode !== 0 || result.stdout.trim() === "") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRelease.add(pkg.name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (pendingRelease.size === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "All package versions are already published. Nothing to check.", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Pending releases: ${[...pendingRelease] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((name) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pkg = graph.packages.get(name)!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${name}@${pkg.version}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join(", ")}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find all transitive dependents of the pending releases. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const requiredBumps = getTransitiveDependents(graph, pendingRelease); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Remove packages that are already pending release — they're fine. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const name of pendingRelease) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| requiredBumps.delete(name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (requiredBumps.size === 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("All cascade version bumps are in order."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isSyncMode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Apply patch bumps to all packages that need them. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const bumped: Array<{ name: string; from: string; to: string }> = []; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const name of requiredBumps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pkg = graph.packages.get(name)!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newVersion = semverInc(pkg.version, "patch"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!newVersion) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Failed to increment version for ${name}@${pkg.version}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const packageJsonPath = resolve(pkg.workspacePath, "package.json"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const raw = yield* readTextFile(packageJsonPath); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const json = JSON.parse(raw) as Record<string, unknown>; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| json.version = newVersion; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| yield* writeTextFile( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| packageJsonPath, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `${JSON.stringify(json, null, 2)}\n`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+78
to
+85
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial JSON serialization may alter field ordering and formatting.
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| bumped.push({ name, from: pkg.version, to: newVersion }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("\nBumped cascade versions:"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const { name, from, to } of bumped) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(` ${name} ${from} → ${to}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Check mode: report missing bumps and fail. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("\nMissing cascade version bumps:\n"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Group by the dependency that triggered the cascade. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| for (const name of requiredBumps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const pkg = graph.packages.get(name)!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Find which of its deps triggered this. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const triggers = pkg.publishedWorkspaceDeps.filter((dep) => | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pendingRelease.has(dep), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const triggerStr = triggers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .map((t) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const tp = graph.packages.get(t)!; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return `${t}@${tp.version}`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .join(", "); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(` ${name} (${pkg.version}) — depends on ${triggerStr}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve the trigger chain for transitive failures. For multi-hop cascades, 🛠️ Minimal fix- const triggers = pkg.publishedWorkspaceDeps.filter((dep) =>
- pendingRelease.has(dep),
- );
+ const triggers = pkg.publishedWorkspaceDeps.filter(
+ (dep) => pendingRelease.has(dep) || requiredBumps.has(dep),
+ );📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error("\nRun `pnpm versions:sync` to fix automatically.\n"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| process.exit(1); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| import { resolve } from "node:path"; | ||
| import process from "node:process"; | ||
| import { readTextFile } from "@effectionx/fs"; | ||
| import type { Operation } from "effection"; | ||
|
|
||
| export interface WorkspacePackage { | ||
| /** Package name, e.g. "@effectionx/bdd". */ | ||
| name: string; | ||
| /** Current version from package.json. */ | ||
| version: string; | ||
| /** Workspace directory name, e.g. "bdd". */ | ||
| workspace: string; | ||
| /** Absolute path to workspace directory. */ | ||
| workspacePath: string; | ||
| /** True if the package is private. */ | ||
| private: boolean; | ||
| /** | ||
| * Names of workspace packages listed in `dependencies` or | ||
| * `peerDependencies` with `workspace:*` protocol. | ||
| */ | ||
| publishedWorkspaceDeps: string[]; | ||
| } | ||
|
|
||
| export interface DepGraph { | ||
| /** All non-private workspace packages indexed by name. */ | ||
| packages: Map<string, WorkspacePackage>; | ||
| /** | ||
| * Reverse map: package name → names of packages that depend on it | ||
| * via published deps (`dependencies` or `peerDependencies`). | ||
| */ | ||
| dependents: Map<string, string[]>; | ||
| } | ||
|
|
||
| /** | ||
| * Build the published dependency graph for all workspace packages. | ||
| * | ||
| * Only `dependencies` and `peerDependencies` with `workspace:*` are | ||
| * considered because `devDependencies` are stripped at publish time. | ||
| */ | ||
| export function* buildDepGraph(): Operation<DepGraph> { | ||
| const rootDir = process.cwd(); | ||
|
|
||
| const workspaceYaml = yield* readTextFile( | ||
| resolve(rootDir, "pnpm-workspace.yaml"), | ||
| ); | ||
|
|
||
| const workspaces: string[] = []; | ||
| for (const line of workspaceYaml.split("\n")) { | ||
| const trimmed = line.trim(); | ||
| if (trimmed.startsWith("-")) { | ||
| const value = trimmed.replace(/^-\s*/, "").replace(/^["']|["']$/g, ""); | ||
| if (value) { | ||
| workspaces.push(value); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Read all package.json files and build WorkspacePackage entries. | ||
| const allPackages: WorkspacePackage[] = []; | ||
|
|
||
| for (const workspace of workspaces) { | ||
| const workspacePath = resolve(rootDir, workspace); | ||
| const raw = yield* readTextFile(resolve(workspacePath, "package.json")); | ||
| const json = JSON.parse(raw) as Record<string, unknown>; | ||
|
Comment on lines
+63
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Avoid reading The file is read once at line 63 during the initial workspace scan, then read again at line 90 for non-private packages. Store the parsed JSON in the first pass to avoid redundant I/O. ♻️ Suggested approach // Read all package.json files and build WorkspacePackage entries.
const allPackages: WorkspacePackage[] = [];
+ const packageJsonCache = new Map<string, Record<string, unknown>>();
for (const workspace of workspaces) {
const workspacePath = resolve(rootDir, workspace);
const raw = yield* readTextFile(resolve(workspacePath, "package.json"));
const json = JSON.parse(raw) as Record<string, unknown>;
+ packageJsonCache.set(workspace, json);
allPackages.push({
name: json.name as string,
// ...
});
}
// Later, when resolving deps:
for (const pkg of packages.values()) {
- const raw = yield* readTextFile(resolve(pkg.workspacePath, "package.json"));
- const json = JSON.parse(raw) as Record<string, unknown>;
+ const json = packageJsonCache.get(pkg.workspace)!;
const deps = collectWorkspaceDeps(
json.dependencies as Record<string, string> | undefined,
packageNames,
);Also applies to: 89-91 🤖 Prompt for AI Agents |
||
|
|
||
| allPackages.push({ | ||
| name: json.name as string, | ||
| version: json.version as string, | ||
|
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing validation for required The code assumes 🛡️ Suggested validation+ if (typeof json.name !== "string" || !json.name) {
+ throw new Error(`Missing or invalid "name" in ${workspacePath}/package.json`);
+ }
+ if (typeof json.version !== "string" || !json.version) {
+ throw new Error(`Missing or invalid "version" in ${workspacePath}/package.json`);
+ }
+
allPackages.push({
name: json.name as string,
version: json.version as string,🤖 Prompt for AI Agents |
||
| workspace, | ||
| workspacePath, | ||
| private: Boolean(json.private), | ||
| publishedWorkspaceDeps: [], // filled below | ||
| }); | ||
| } | ||
|
|
||
| // Index non-private packages by name. | ||
| const packages = new Map<string, WorkspacePackage>(); | ||
| for (const pkg of allPackages) { | ||
| if (!pkg.private) { | ||
| packages.set(pkg.name, pkg); | ||
| } | ||
| } | ||
|
|
||
| const packageNames = new Set(packages.keys()); | ||
|
|
||
| // Resolve published workspace deps and build reverse map. | ||
| const dependents = new Map<string, string[]>(); | ||
|
|
||
| for (const pkg of packages.values()) { | ||
| const raw = yield* readTextFile(resolve(pkg.workspacePath, "package.json")); | ||
| const json = JSON.parse(raw) as Record<string, unknown>; | ||
|
|
||
| const deps = collectWorkspaceDeps( | ||
| json.dependencies as Record<string, string> | undefined, | ||
| packageNames, | ||
| ); | ||
| const peerDeps = collectWorkspaceDeps( | ||
| json.peerDependencies as Record<string, string> | undefined, | ||
| packageNames, | ||
| ); | ||
|
|
||
| const combined = [...new Set([...deps, ...peerDeps])].sort(); | ||
| pkg.publishedWorkspaceDeps = combined; | ||
|
|
||
| for (const dep of combined) { | ||
| if (!dependents.has(dep)) { | ||
| dependents.set(dep, []); | ||
| } | ||
| dependents.get(dep)!.push(pkg.name); | ||
| } | ||
| } | ||
|
|
||
| return { packages, dependents }; | ||
| } | ||
|
|
||
| /** | ||
| * Walk the dependency graph and return all transitive dependents of the | ||
| * given root package names. | ||
| */ | ||
| export function getTransitiveDependents( | ||
| graph: DepGraph, | ||
| roots: Iterable<string>, | ||
| ): Set<string> { | ||
| const visited = new Set<string>(); | ||
| const queue = [...roots]; | ||
|
|
||
| while (queue.length > 0) { | ||
| const current = queue.pop()!; | ||
| const deps = graph.dependents.get(current) ?? []; | ||
| for (const dep of deps) { | ||
| if (!visited.has(dep)) { | ||
| visited.add(dep); | ||
| queue.push(dep); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return visited; | ||
| } | ||
|
|
||
| /** Extract workspace package names that use `workspace:*` protocol. */ | ||
| function collectWorkspaceDeps( | ||
| depsObject: Record<string, string> | undefined, | ||
| packageNames: Set<string>, | ||
| ): string[] { | ||
| if (!depsObject || typeof depsObject !== "object") { | ||
| return []; | ||
| } | ||
|
|
||
| const result: string[] = []; | ||
| for (const [name, range] of Object.entries(depsObject)) { | ||
| if ( | ||
| packageNames.has(name) && | ||
| typeof range === "string" && | ||
| range.startsWith("workspace:") | ||
| ) { | ||
| result.push(name); | ||
| } | ||
| } | ||
|
Comment on lines
+145
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
python - <<'PY'
import json
import pathlib
root = pathlib.Path(".")
entries = []
workspace_file = root / "pnpm-workspace.yaml"
for line in workspace_file.read_text().splitlines():
trimmed = line.strip()
if trimmed.startswith("-"):
value = trimmed[1:].strip().strip('"').strip("'")
if value:
entries.append(value)
for workspace in entries:
pkgfile = root / workspace / "package.json"
if not pkgfile.exists():
continue
data = json.loads(pkgfile.read_text())
for section in ("dependencies", "peerDependencies"):
deps = data.get(section) or {}
if not isinstance(deps, dict):
continue
for name, rng in deps.items():
if isinstance(rng, str) and rng.startswith("workspace:") and rng != "workspace:*":
print(f"{pkgfile}: {section}.{name} = {rng}")
PYRepository: thefrontside/effectionx Length of output: 49 Code doesn't match documented policy scope for workspace dependency cascade. The - range.startsWith("workspace:")
+ range === "workspace:*"🤖 Prompt for AI Agents |
||
| return result; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: thefrontside/effectionx
Length of output: 49
🏁 Script executed:
Repository: thefrontside/effectionx
Length of output: 4469
🏁 Script executed:
Repository: thefrontside/effectionx
Length of output: 1719
🏁 Script executed:
rg -n "versions:sync|check-versions" --type tsRepository: thefrontside/effectionx
Length of output: 49
Don't treat every failed
npm viewas "version not published" — distinguish package existence from transient failure.The code conflates three distinct cases: version genuinely missing, package never published, and registry/auth/network failure. The last should hard-fail rather than cascade, but this design also loses the first-publish distinction already tracked in
.internal/publish-matrix.ts:20-50. Inversions:syncmode, a transient npm failure triggers version rewrites on unrelated packages (lines 66-88). Additionally, the cascade failure explanation (lines 101-110) can report empty trigger strings when conditions align.Unlike
publish-matrix.ts, which runs separatenpm view ${name}andnpm view ${name}@${version}checks to distinguish package existence,check-versions.tsruns only the version check. Any non-zero exit code becomes a false positive pending release.🤖 Prompt for AI Agents