@proteinjs/build 1.9.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,7 +82,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
82
82
  return (mod && mod.__esModule) ? mod : { "default": mod };
83
83
  };
84
84
  Object.defineProperty(exports, "__esModule", { value: true });
85
- exports.evictGitLocks = exports.versionWorkspace = void 0;
85
+ exports.evictGitLocks = exports.classifyCommitMessage = exports.classifyUnpushedCommits = exports.versionWorkspace = void 0;
86
86
  var path = __importStar(require("path"));
87
87
  var child_process_1 = require("child_process");
88
88
  var util_node_1 = require("@proteinjs/util-node");
@@ -95,12 +95,18 @@ var logger = new logger_1.Logger({ name: cw.color('workspace:', logColors_1.prim
95
95
  var fixedVersionWorkspacesToVersion = {};
96
96
  function versionWorkspace() {
97
97
  return __awaiter(this, void 0, void 0, function () {
98
- var dryRun, workspacePath, workspaceRootDirty, _a, packageMap, packageGraph, sortedPackageNames, workspaceToPackageMap, skippedPackages, filteredPackageNames, _i, filteredPackageNames_1, packageName, localPackage, skipBumpingPackageVersion, dependenciesChanged, pushWithoutSync;
98
+ var dryRun, planOnly, workspacePath, workspaceRootDirty, _a, packageMap, packageGraph, sortedPackageNames, workspaceToPackageMap, skippedPackages, filteredPackageNames, commitBumps, scanSummary, commitLeaves, _i, filteredPackageNames_1, packageName, localPackage, skipBumpingPackageVersion, ownBump, dependenciesChanged, cascadeBump, effectiveBump, currentVersion, sourceNote, planPrefix, pushWithoutSync;
99
99
  return __generator(this, function (_b) {
100
100
  switch (_b.label) {
101
101
  case 0:
102
102
  dryRun = isDryRun();
103
- if (dryRun) {
103
+ planOnly = isPlanOnly();
104
+ if (planOnly) {
105
+ logger.info({
106
+ message: 'Plan-only mode enabled. Scan + commit-leaves + would-be bumps will be computed and logged; nothing will be written to disk, built, tested, published, committed, or pushed.',
107
+ });
108
+ }
109
+ else if (dryRun) {
104
110
  logger.info({ message: 'Dry run mode enabled. Publish and push operations will be skipped.' });
105
111
  }
106
112
  workspacePath = process.cwd();
@@ -113,8 +119,8 @@ function versionWorkspace() {
113
119
  if (workspaceRootDirty) {
114
120
  logger.info({ message: "> Workspace root is dirty, will skip pull/push for root repo" });
115
121
  }
116
- if (!dryRun) return [3 /*break*/, 3];
117
- logger.info({ message: "> Dry run: skipping pullWorkspace for (".concat(workspacePath, ")") });
122
+ if (!(dryRun || planOnly)) return [3 /*break*/, 3];
123
+ logger.info({ message: "> Skipping pullWorkspace for (".concat(workspacePath, ")") });
118
124
  return [3 /*break*/, 5];
119
125
  case 3: return [4 /*yield*/, pullWorkspace(workspacePath, workspaceRootDirty)];
120
126
  case 4:
@@ -132,51 +138,118 @@ function versionWorkspace() {
132
138
  !skippedPackages.includes(packageName));
133
139
  });
134
140
  logger.info({ message: "> Versioning workspace (".concat(workspacePath, ")") });
135
- _i = 0, filteredPackageNames_1 = filteredPackageNames;
136
- _b.label = 7;
141
+ return [4 /*yield*/, scanCommitBumps(filteredPackageNames, packageMap)];
137
142
  case 7:
138
- if (!(_i < filteredPackageNames_1.length)) return [3 /*break*/, 14];
143
+ commitBumps = _b.sent();
144
+ if (commitBumps.size === 0) {
145
+ logger.info({ message: "> No packages have unpushed commits" });
146
+ }
147
+ else {
148
+ scanSummary = Array.from(commitBumps.entries())
149
+ .map(function (_a) {
150
+ var name = _a[0], bump = _a[1];
151
+ return "".concat(cw.color(name), ":").concat(bump);
152
+ })
153
+ .join(', ');
154
+ logger.info({ message: "> Packages with unpushed commits: ".concat(scanSummary) });
155
+ }
156
+ commitLeaves = computeCommitLeaves(commitBumps, packageGraph, filteredPackageNames);
157
+ if (commitLeaves.length > 0) {
158
+ logger.info({
159
+ message: "> Commit-leaves (root changes, will publish first): ".concat(commitLeaves.map(function (n) { return cw.color(n); }).join(', ')),
160
+ });
161
+ }
162
+ _i = 0, filteredPackageNames_1 = filteredPackageNames;
163
+ _b.label = 8;
164
+ case 8:
165
+ if (!(_i < filteredPackageNames_1.length)) return [3 /*break*/, 17];
139
166
  packageName = filteredPackageNames_1[_i];
140
167
  localPackage = packageMap[packageName];
141
168
  skipBumpingPackageVersion = isInFixedVersionWorkspace(localPackage);
142
- return [4 /*yield*/, bumpDependencies(localPackage, packageMap, packageGraph, skipBumpingPackageVersion)];
143
- case 8:
169
+ ownBump = skipBumpingPackageVersion ? undefined : commitBumps.get(packageName);
170
+ return [4 /*yield*/, applyDependencyVersionRewrites(localPackage, packageMap, packageGraph)];
171
+ case 9:
144
172
  dependenciesChanged = _b.sent();
145
- if (!dependenciesChanged) {
146
- return [3 /*break*/, 13];
173
+ cascadeBump = dependenciesChanged ? 'patch' : undefined;
174
+ effectiveBump = maxBump(ownBump, cascadeBump);
175
+ if (!effectiveBump && !dependenciesChanged) {
176
+ return [3 /*break*/, 16];
147
177
  }
148
- return [4 /*yield*/, buildAndTest(localPackage)];
149
- case 9:
150
- _b.sent();
151
- if (isInFixedVersionWorkspace(localPackage) && localPackage.workspace) {
152
- fixedVersionWorkspacesToVersion[localPackage.workspace.path] = true;
178
+ if (effectiveBump && !skipBumpingPackageVersion) {
179
+ currentVersion = localPackage.packageJson.version;
180
+ // In-memory version mutation runs in plan-only too so downstream
181
+ // packages see the right dep versions when we simulate cascades.
182
+ localPackage.packageJson.version = semver_1.default.inc(currentVersion, effectiveBump);
183
+ sourceNote = ownBump
184
+ ? cascadeBump
185
+ ? "own+cascade, own=".concat(ownBump)
186
+ : "own=".concat(ownBump)
187
+ : 'dep cascade';
188
+ planPrefix = planOnly ? 'would bump' : 'bumping';
153
189
  logger.info({
154
- message: "(".concat(cw.color(packageName), ") skipping version push for package in a fixed-version workspace"),
190
+ message: "(".concat(cw.color(packageName), ") ").concat(planPrefix, " version (").concat(effectiveBump, "; ").concat(sourceNote, ") from ").concat(currentVersion, " -> ").concat(localPackage.packageJson.version),
155
191
  });
156
- return [3 /*break*/, 13];
157
192
  }
158
- if (!shouldPublishPackage(localPackage)) return [3 /*break*/, 11];
159
- return [4 /*yield*/, publish(localPackage)];
193
+ // Fixed-version workspace tracking: record even in plan-only so we can
194
+ // report the would-be synced version below. This is metadata only — the
195
+ // actual sync (disk write, commit, push) is gated by the planOnly /
196
+ // dryRun guards after the loop.
197
+ if (isInFixedVersionWorkspace(localPackage) && localPackage.workspace) {
198
+ fixedVersionWorkspacesToVersion[localPackage.workspace.path] = true;
199
+ }
200
+ if (planOnly) {
201
+ // Plan-only: skip the disk write, the build/test, publish, push/tag,
202
+ // and the eventual syncFixedVersionWorkspaces pass. The in-memory
203
+ // mutation above is enough to keep the simulated cascade correct for
204
+ // the remaining packages in this loop.
205
+ return [3 /*break*/, 16];
206
+ }
207
+ if (!(effectiveBump || dependenciesChanged)) return [3 /*break*/, 11];
208
+ return [4 /*yield*/, util_node_1.Fs.writeFiles([
209
+ { path: localPackage.filePath, content: JSON.stringify(localPackage.packageJson, null, 2) },
210
+ ])];
160
211
  case 10:
161
212
  _b.sent();
162
213
  _b.label = 11;
163
- case 11: return [4 /*yield*/, pushAndTag(localPackage)];
214
+ case 11: return [4 /*yield*/, buildAndTest(localPackage)];
164
215
  case 12:
165
216
  _b.sent();
166
- _b.label = 13;
217
+ if (isInFixedVersionWorkspace(localPackage) && localPackage.workspace) {
218
+ logger.info({
219
+ message: "(".concat(cw.color(packageName), ") skipping version push for package in a fixed-version workspace"),
220
+ });
221
+ return [3 /*break*/, 16];
222
+ }
223
+ if (!shouldPublishPackage(localPackage)) return [3 /*break*/, 14];
224
+ return [4 /*yield*/, publish(localPackage)];
167
225
  case 13:
226
+ _b.sent();
227
+ _b.label = 14;
228
+ case 14: return [4 /*yield*/, pushAndTag(localPackage)];
229
+ case 15:
230
+ _b.sent();
231
+ _b.label = 16;
232
+ case 16:
168
233
  _i++;
169
- return [3 /*break*/, 7];
170
- case 14:
234
+ return [3 /*break*/, 8];
235
+ case 17:
236
+ if (!planOnly) return [3 /*break*/, 19];
237
+ return [4 /*yield*/, logPlannedFixedVersionSyncs(Object.keys(fixedVersionWorkspacesToVersion), packageMap, workspaceToPackageMap)];
238
+ case 18:
239
+ _b.sent();
240
+ logger.info({ message: "> Plan-only: skipping fixed-version sync, metarepo push, and symlink refresh" });
241
+ logger.info({ message: "> Finished versioning workspace (".concat(workspacePath, ") [plan-only]") });
242
+ return [2 /*return*/];
243
+ case 19:
171
244
  pushWithoutSync = true;
172
245
  return [4 /*yield*/, syncFixedVersionWorkspaces(Object.keys(fixedVersionWorkspacesToVersion), packageMap, workspaceToPackageMap, pushWithoutSync)];
173
- case 15:
246
+ case 20:
174
247
  _b.sent();
175
248
  return [4 /*yield*/, pushMetarepos(workspacePath, workspaceRootDirty)];
176
- case 16:
249
+ case 21:
177
250
  _b.sent();
178
251
  return [4 /*yield*/, symlinkWorkspace(workspacePath, filteredPackageNames, packageMap)];
179
- case 17:
252
+ case 22:
180
253
  _b.sent();
181
254
  logger.info({ message: "> Finished versioning workspace (".concat(workspacePath, ")") });
182
255
  return [2 /*return*/];
@@ -197,6 +270,33 @@ function isDryRun() {
197
270
  }
198
271
  return false;
199
272
  }
273
+ /**
274
+ * Plan-only mode: the strictest possible preview. Runs Phase 0 (commit-bump
275
+ * scan) and Phase 1 (commit-leaf detection) so you can see which packages
276
+ * would be published and at what level, then simulates the topo-ordered
277
+ * cascade in memory (for correct would-be bump reporting) but:
278
+ *
279
+ * - does NOT write any package.json or lerna.json to disk
280
+ * - does NOT run `npm install` / lint / build / test
281
+ * - does NOT publish, commit, push, tag, or refresh symlinks
282
+ *
283
+ * Intended for a fast preview of "what will this release do?" Implies
284
+ * dry-run for the few checks that need a binary answer (e.g. `publish`'s
285
+ * own dry-run guard), but the stricter plan-only guards in the main loop
286
+ * mean those code paths never execute in the first place.
287
+ */
288
+ function isPlanOnly() {
289
+ var _a;
290
+ var args = process.argv.slice(2);
291
+ if (args.includes('--plan-only') || args.includes('--planonly') || args.includes('--plan')) {
292
+ return true;
293
+ }
294
+ var envFlag = (_a = process.env.VERSION_WORKSPACE_PLAN_ONLY) !== null && _a !== void 0 ? _a : process.env.PLAN_ONLY;
295
+ if (envFlag) {
296
+ return envFlag === 'true' || envFlag === '1';
297
+ }
298
+ return false;
299
+ }
200
300
  function isInFixedVersionWorkspace(localPackage) {
201
301
  return (localPackage.workspace &&
202
302
  localPackage.workspace.lernaJson &&
@@ -277,60 +377,117 @@ function pullWorkspace(workspacePath, skipRootRepo) {
277
377
  });
278
378
  });
279
379
  }
280
- function bumpDependencies(localPackage, packageMap, packageGraph, skipBumpingPackageVersion) {
281
- if (skipBumpingPackageVersion === void 0) { skipBumpingPackageVersion = false; }
380
+ /**
381
+ * Rewrite `localPackage`'s dependency-version fields in package.json to match
382
+ * the current in-memory versions of its workspace-local deps. Returns true if
383
+ * any dep version was rewritten (i.e. a dep's already-bumped version now
384
+ * differs from what this package.json records). Pure rewrite — does NOT bump
385
+ * `localPackage`'s own version; the caller decides that by combining this
386
+ * result with the commit-scan map (see `versionWorkspace`).
387
+ *
388
+ * Writing the package.json to disk is left to the caller too, so we can apply
389
+ * the own-version bump in the same write.
390
+ */
391
+ function applyDependencyVersionRewrites(localPackage, packageMap, packageGraph) {
392
+ return __awaiter(this, void 0, void 0, function () {
393
+ var localDependencies, dependenciesChanged, _i, localDependencies_1, localDependency, localDependencyPackage, localDependencyVersion, currentDependencyVersion, newDependencyVersion;
394
+ return __generator(this, function (_a) {
395
+ localDependencies = packageGraph.successors(localPackage.name);
396
+ if (!localDependencies || localDependencies.length == 0) {
397
+ return [2 /*return*/, false];
398
+ }
399
+ dependenciesChanged = false;
400
+ for (_i = 0, localDependencies_1 = localDependencies; _i < localDependencies_1.length; _i++) {
401
+ localDependency = localDependencies_1[_i];
402
+ localDependencyPackage = packageMap[localDependency];
403
+ localDependencyVersion = localDependencyPackage.packageJson.version;
404
+ currentDependencyVersion = getDependencyVersion(localDependency, localPackage);
405
+ if (!currentDependencyVersion) {
406
+ throw new Error("Package (".concat(cw.color(localPackage.name), ") has dependency on ").concat(localDependency, ", but cannot find version in ").concat(cw.color(localPackage.name), "'s package.json"));
407
+ }
408
+ if (currentDependencyVersion.isLocalPath) {
409
+ continue;
410
+ }
411
+ if ((currentDependencyVersion === null || currentDependencyVersion === void 0 ? void 0 : currentDependencyVersion.version) == localDependencyVersion) {
412
+ continue;
413
+ }
414
+ newDependencyVersion = {
415
+ prefix: currentDependencyVersion.prefix,
416
+ version: localDependencyVersion,
417
+ };
418
+ setDependencyVersion(localDependency, currentDependencyVersion, newDependencyVersion, localPackage);
419
+ dependenciesChanged = true;
420
+ }
421
+ return [2 /*return*/, dependenciesChanged];
422
+ });
423
+ });
424
+ }
425
+ /**
426
+ * Up-front scan: for each candidate package, classify the unpushed commits
427
+ * in its own git repo into a semver bump level. Returns a map (only contains
428
+ * entries for packages with classifiable unpushed commits). Packages with no
429
+ * unpushed commits or missing upstream branches are absent from the map.
430
+ *
431
+ * This is the input to `computeCommitLeaves` and to the per-package bump-level
432
+ * combine in the main loop.
433
+ */
434
+ function scanCommitBumps(packageNames, packageMap) {
282
435
  return __awaiter(this, void 0, void 0, function () {
283
- var localDependencies, dependenciesChanged, _i, localDependencies_1, localDependency, localDependencyPackage, localDependencyVersion, currentDependencyVersion, newDependencyVersion, currentVersion, packageDir, bump;
436
+ var result, _i, packageNames_1, packageName, localPackage, packageDir, bump;
284
437
  return __generator(this, function (_a) {
285
438
  switch (_a.label) {
286
439
  case 0:
287
- localDependencies = packageGraph.successors(localPackage.name);
288
- if (!localDependencies || localDependencies.length == 0) {
289
- return [2 /*return*/, false];
290
- }
291
- dependenciesChanged = false;
292
- for (_i = 0, localDependencies_1 = localDependencies; _i < localDependencies_1.length; _i++) {
293
- localDependency = localDependencies_1[_i];
294
- localDependencyPackage = packageMap[localDependency];
295
- localDependencyVersion = localDependencyPackage.packageJson.version;
296
- currentDependencyVersion = getDependencyVersion(localDependency, localPackage);
297
- if (!currentDependencyVersion) {
298
- throw new Error("Package (".concat(cw.color(localPackage.name), ") has dependency on ").concat(localDependency, ", but cannot find version in ").concat(cw.color(localPackage.name), "'s package.json"));
299
- }
300
- if (currentDependencyVersion.isLocalPath) {
301
- continue;
302
- }
303
- if ((currentDependencyVersion === null || currentDependencyVersion === void 0 ? void 0 : currentDependencyVersion.version) == localDependencyVersion) {
304
- continue;
305
- }
306
- newDependencyVersion = {
307
- prefix: currentDependencyVersion.prefix,
308
- version: localDependencyVersion,
309
- };
310
- setDependencyVersion(localDependency, currentDependencyVersion, newDependencyVersion, localPackage);
311
- dependenciesChanged = true;
312
- }
313
- if (!dependenciesChanged) return [3 /*break*/, 4];
314
- if (!!skipBumpingPackageVersion) return [3 /*break*/, 2];
315
- currentVersion = localPackage.packageJson.version;
316
- packageDir = path.dirname(localPackage.filePath);
317
- return [4 /*yield*/, hasFeatureCommits(packageDir)];
440
+ result = new Map();
441
+ _i = 0, packageNames_1 = packageNames;
442
+ _a.label = 1;
318
443
  case 1:
319
- bump = (_a.sent()) ? 'minor' : 'patch';
320
- localPackage.packageJson.version = semver_1.default.inc(currentVersion, bump);
321
- logger.info({
322
- message: "(".concat(cw.color(localPackage.name), ") bumping version (").concat(bump, ") from ").concat(currentVersion, " -> ").concat(localPackage.packageJson.version),
323
- });
324
- _a.label = 2;
325
- case 2: return [4 /*yield*/, util_node_1.Fs.writeFiles([{ path: localPackage.filePath, content: JSON.stringify(localPackage.packageJson, null, 2) }])];
444
+ if (!(_i < packageNames_1.length)) return [3 /*break*/, 4];
445
+ packageName = packageNames_1[_i];
446
+ localPackage = packageMap[packageName];
447
+ packageDir = path.dirname(localPackage.filePath);
448
+ return [4 /*yield*/, classifyUnpushedCommits(packageDir)];
449
+ case 2:
450
+ bump = _a.sent();
451
+ if (bump) {
452
+ result.set(packageName, bump);
453
+ }
454
+ _a.label = 3;
326
455
  case 3:
327
- _a.sent();
328
- _a.label = 4;
329
- case 4: return [2 /*return*/, dependenciesChanged];
456
+ _i++;
457
+ return [3 /*break*/, 1];
458
+ case 4: return [2 /*return*/, result];
330
459
  }
331
460
  });
332
461
  });
333
462
  }
463
+ /**
464
+ * A package is a "commit-leaf" if it has unpushed commits (i.e. appears in
465
+ * `scanMap`) AND none of its direct workspace-local deps have unpushed
466
+ * commits. These are the true sources of change — they trigger the entire
467
+ * cascade, so they must be versioned + published first.
468
+ *
469
+ * Direct deps only (not transitive): the topo-ordered main loop handles
470
+ * transitivity naturally by cascading dep-version rewrites forward.
471
+ *
472
+ * Returns leaves in topo order (deps-first) so independent leaves publish
473
+ * in a deterministic, dependency-respecting sequence.
474
+ */
475
+ function computeCommitLeaves(scanMap, packageGraph, sortedPackageNames) {
476
+ var _a;
477
+ var leaves = [];
478
+ for (var _i = 0, sortedPackageNames_1 = sortedPackageNames; _i < sortedPackageNames_1.length; _i++) {
479
+ var packageName = sortedPackageNames_1[_i];
480
+ if (!scanMap.has(packageName)) {
481
+ continue;
482
+ }
483
+ var directDeps = (_a = packageGraph.successors(packageName)) !== null && _a !== void 0 ? _a : [];
484
+ var anyDepHasCommits = directDeps.some(function (dep) { return scanMap.has(dep); });
485
+ if (!anyDepHasCommits) {
486
+ leaves.push(packageName);
487
+ }
488
+ }
489
+ return leaves;
490
+ }
334
491
  function getDependencyVersion(dependencyPackageName, localPackage) {
335
492
  var currentRawDependencyVersion = localPackage.packageJson.dependencies
336
493
  ? localPackage.packageJson.dependencies[dependencyPackageName]
@@ -412,27 +569,74 @@ function syncFixedVersionWorkspaces(fixedVersionWorkspacePaths, packageMap, work
412
569
  });
413
570
  });
414
571
  }
572
+ /**
573
+ * Plan-only companion to `syncFixedVersionWorkspaces`: for each fixed-version
574
+ * workspace that would be touched, classify unpushed commits at the workspace
575
+ * root and log the version it would sync to. No disk writes, no git ops.
576
+ */
577
+ function logPlannedFixedVersionSyncs(fixedVersionWorkspacePaths, packageMap, workspaceToPackageMap) {
578
+ var _a, _b, _c;
579
+ return __awaiter(this, void 0, void 0, function () {
580
+ var _i, fixedVersionWorkspacePaths_2, workspacePath, workspacePackageNames, anyPackageInWs, lernaJson, repoName, bump, wouldBeVersion;
581
+ return __generator(this, function (_d) {
582
+ switch (_d.label) {
583
+ case 0:
584
+ if (fixedVersionWorkspacePaths.length === 0) {
585
+ return [2 /*return*/];
586
+ }
587
+ logger.info({ message: "> Fixed-version workspaces that would sync:" });
588
+ _i = 0, fixedVersionWorkspacePaths_2 = fixedVersionWorkspacePaths;
589
+ _d.label = 1;
590
+ case 1:
591
+ if (!(_i < fixedVersionWorkspacePaths_2.length)) return [3 /*break*/, 4];
592
+ workspacePath = fixedVersionWorkspacePaths_2[_i];
593
+ workspacePackageNames = (_a = workspaceToPackageMap[workspacePath]) !== null && _a !== void 0 ? _a : [];
594
+ anyPackageInWs = workspacePackageNames.map(function (n) { return packageMap[n]; }).find(function (p) { var _a; return (_a = p === null || p === void 0 ? void 0 : p.workspace) === null || _a === void 0 ? void 0 : _a.lernaJson; });
595
+ lernaJson = (_b = anyPackageInWs === null || anyPackageInWs === void 0 ? void 0 : anyPackageInWs.workspace) === null || _b === void 0 ? void 0 : _b.lernaJson;
596
+ repoName = path.basename(workspacePath.endsWith(path.sep) ? workspacePath.slice(0, -1) : workspacePath);
597
+ if (!lernaJson) {
598
+ logger.info({
599
+ message: " (".concat(cw.color(repoName), ") would sync fixed-version workspace (lerna.json not available; bump level unknown)"),
600
+ });
601
+ return [3 /*break*/, 3];
602
+ }
603
+ return [4 /*yield*/, classifyUnpushedCommits(workspacePath)];
604
+ case 2:
605
+ bump = (_c = (_d.sent())) !== null && _c !== void 0 ? _c : 'patch';
606
+ wouldBeVersion = semver_1.default.inc(lernaJson.version, bump);
607
+ logger.info({
608
+ message: " (".concat(cw.color(repoName), ") would sync fixed-version workspace ").concat(lernaJson.version, " -> ").concat(wouldBeVersion, " (").concat(bump, ")"),
609
+ });
610
+ _d.label = 3;
611
+ case 3:
612
+ _i++;
613
+ return [3 /*break*/, 1];
614
+ case 4: return [2 /*return*/];
615
+ }
616
+ });
617
+ });
618
+ }
415
619
  function syncFixedVersions(workspacePath, localPackages) {
416
- var _a;
620
+ var _a, _b;
417
621
  return __awaiter(this, void 0, void 0, function () {
418
622
  var lernaJson, bump, highestVersion, syncedFixedVersions, _i, localPackages_1, localPackage, currentVersion, lernaJsonPath;
419
- return __generator(this, function (_b) {
420
- switch (_b.label) {
623
+ return __generator(this, function (_c) {
624
+ switch (_c.label) {
421
625
  case 0:
422
626
  lernaJson = (_a = localPackages[0].workspace) === null || _a === void 0 ? void 0 : _a.lernaJson;
423
627
  if (!lernaJson) {
424
628
  throw new Error("Cannot find lerna.json for workspace: ".concat(workspacePath));
425
629
  }
426
- return [4 /*yield*/, hasFeatureCommits(workspacePath)];
630
+ return [4 /*yield*/, classifyUnpushedCommits(workspacePath)];
427
631
  case 1:
428
- bump = (_b.sent()) ? 'minor' : 'patch';
632
+ bump = (_b = (_c.sent())) !== null && _b !== void 0 ? _b : 'patch';
429
633
  highestVersion = semver_1.default.inc(lernaJson.version, bump);
430
634
  if (!highestVersion) {
431
635
  throw new Error("Lerna version not specified for workspace: ".concat(workspacePath));
432
636
  }
433
637
  syncedFixedVersions = false;
434
638
  _i = 0, localPackages_1 = localPackages;
435
- _b.label = 2;
639
+ _c.label = 2;
436
640
  case 2:
437
641
  if (!(_i < localPackages_1.length)) return [3 /*break*/, 5];
438
642
  localPackage = localPackages_1[_i];
@@ -446,9 +650,9 @@ function syncFixedVersions(workspacePath, localPackages) {
446
650
  });
447
651
  return [4 /*yield*/, util_node_1.Fs.writeFiles([{ path: localPackage.filePath, content: JSON.stringify(localPackage.packageJson, null, 2) }])];
448
652
  case 3:
449
- _b.sent();
653
+ _c.sent();
450
654
  syncedFixedVersions = true;
451
- _b.label = 4;
655
+ _c.label = 4;
452
656
  case 4:
453
657
  _i++;
454
658
  return [3 /*break*/, 2];
@@ -458,8 +662,8 @@ function syncFixedVersions(workspacePath, localPackages) {
458
662
  lernaJson.version = highestVersion;
459
663
  return [4 /*yield*/, util_node_1.Fs.writeFiles([{ path: lernaJsonPath, content: JSON.stringify(lernaJson, null, 2) }])];
460
664
  case 6:
461
- _b.sent();
462
- _b.label = 7;
665
+ _c.sent();
666
+ _c.label = 7;
463
667
  case 7: return [2 /*return*/, syncedFixedVersions ? highestVersion : false];
464
668
  }
465
669
  });
@@ -640,6 +844,12 @@ function pushAndTagFixedVersionRepo(dir, version, skipTagging, skipCi) {
640
844
  return [2 /*return*/, undefined];
641
845
  }
642
846
  repoName = path.basename(dir.endsWith(path.sep) ? dir.slice(0, -1) : dir);
847
+ return [4 /*yield*/, hasPendingChanges(dir)];
848
+ case 1:
849
+ if (!!(_a.sent())) return [3 /*break*/, 2];
850
+ logger.info({ message: "(".concat(cw.color(repoName), ") fixed-version repo has no pending changes, skipping commit/push") });
851
+ return [3 /*break*/, 6];
852
+ case 2:
643
853
  if (version) {
644
854
  logger.info({ message: "(".concat(cw.color(repoName), ") pushing latest version (").concat(version, ")") });
645
855
  }
@@ -647,13 +857,13 @@ function pushAndTagFixedVersionRepo(dir, version, skipTagging, skipCi) {
647
857
  logger.info({ message: "(".concat(cw.color(repoName), ") pushing dependency bumps") });
648
858
  }
649
859
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['add', '.'], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
650
- case 1:
860
+ case 3:
651
861
  _a.sent();
652
862
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['commit', '-m', "chore(version): bumping dependency versions".concat(skipCi ? ' [skip ci]' : '')], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
653
- case 2:
863
+ case 4:
654
864
  _a.sent();
655
865
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['push'], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
656
- case 3:
866
+ case 5:
657
867
  _a.sent();
658
868
  if (version) {
659
869
  logger.info({ message: "(".concat(cw.color(repoName), ") pushed latest version (").concat(version, ")") });
@@ -661,25 +871,26 @@ function pushAndTagFixedVersionRepo(dir, version, skipTagging, skipCi) {
661
871
  else {
662
872
  logger.info({ message: "(".concat(cw.color(repoName), ") pushed dependency bumps") });
663
873
  }
664
- return [4 /*yield*/, getLatestCommitSha(dir)];
665
- case 4:
874
+ _a.label = 6;
875
+ case 6: return [4 /*yield*/, getLatestCommitSha(dir)];
876
+ case 7:
666
877
  latestCommitSha = _a.sent();
667
878
  return [4 /*yield*/, getRepoInfo(dir)];
668
- case 5:
879
+ case 8:
669
880
  repoInfo = _a.sent();
670
881
  commit = __assign({ sha: latestCommitSha }, repoInfo);
671
- if (!!skipTagging) return [3 /*break*/, 8];
882
+ if (!!skipTagging) return [3 /*break*/, 11];
672
883
  tagName = "v".concat(version);
673
884
  logger.info({ message: "(".concat(cw.color(repoName), ") pushing tag (").concat(tagName, ")") });
674
885
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['tag', '-a', tagName, '-m', "Release ".concat(tagName)], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
675
- case 6:
886
+ case 9:
676
887
  _a.sent();
677
888
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['push', 'origin', tagName], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
678
- case 7:
889
+ case 10:
679
890
  _a.sent();
680
891
  logger.info({ message: "(".concat(cw.color(repoName), ") pushed tag (").concat(tagName, ")") });
681
- _a.label = 8;
682
- case 8: return [2 /*return*/, commit];
892
+ _a.label = 11;
893
+ case 11: return [2 /*return*/, commit];
683
894
  }
684
895
  });
685
896
  });
@@ -732,18 +943,28 @@ function pushMetarepo(dir) {
732
943
  return [2 /*return*/];
733
944
  }
734
945
  repoName = path.basename(dir.endsWith(path.sep) ? dir.slice(0, -1) : dir);
946
+ return [4 /*yield*/, hasPendingChanges(dir)];
947
+ case 1:
948
+ // Nothing to commit means `git commit` would exit 1 and blow up the whole
949
+ // run. This happens routinely — e.g. the proteinjs metarepo has no pending
950
+ // submodule pointer bumps on a run that only touched unrelated packages.
951
+ // Silently skip those repos instead of aborting.
952
+ if (!(_a.sent())) {
953
+ logger.info({ message: "(".concat(cw.color(repoName), ") metarepo has no pending changes, skipping commit/push") });
954
+ return [2 /*return*/];
955
+ }
735
956
  logger.info({ message: "(".concat(cw.color(repoName), ") pushing metarepo (").concat(dir, ")") });
736
957
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['add', '.'], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
737
- case 1:
958
+ case 2:
738
959
  _a.sent();
739
960
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['commit', '-m', "chore(version): bumping submodule versions [skip ci]"], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
740
- case 2:
961
+ case 3:
741
962
  _a.sent();
742
963
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['pull'], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
743
- case 3:
964
+ case 4:
744
965
  _a.sent();
745
966
  return [4 /*yield*/, (0, util_node_1.cmd)('git', ['push'], { cwd: dir }, { logPrefix: "[".concat(cw.color(repoName), "] ") })];
746
- case 4:
967
+ case 5:
747
968
  _a.sent();
748
969
  logger.info({ message: "(".concat(cw.color(repoName), ") pushed metarepo (").concat(dir, ")") });
749
970
  return [2 /*return*/];
@@ -753,16 +974,16 @@ function pushMetarepo(dir) {
753
974
  }
754
975
  function symlinkWorkspace(workspacePath, packageNames, packageMap) {
755
976
  return __awaiter(this, void 0, void 0, function () {
756
- var _i, packageNames_1, packageName, localPackage;
977
+ var _i, packageNames_2, packageName, localPackage;
757
978
  return __generator(this, function (_a) {
758
979
  switch (_a.label) {
759
980
  case 0:
760
981
  logger.info({ message: "> Symlinking local dependencies in workspace (".concat(workspacePath, ")") });
761
- _i = 0, packageNames_1 = packageNames;
982
+ _i = 0, packageNames_2 = packageNames;
762
983
  _a.label = 1;
763
984
  case 1:
764
- if (!(_i < packageNames_1.length)) return [3 /*break*/, 4];
765
- packageName = packageNames_1[_i];
985
+ if (!(_i < packageNames_2.length)) return [3 /*break*/, 4];
986
+ packageName = packageNames_2[_i];
766
987
  localPackage = packageMap[packageName];
767
988
  return [4 /*yield*/, util_node_1.PackageUtil.symlinkDependencies(localPackage, packageMap)];
768
989
  case 2:
@@ -968,22 +1189,108 @@ function isRepoDirty(dir) {
968
1189
  });
969
1190
  });
970
1191
  }
971
- function hasFeatureCommits(dir) {
1192
+ /**
1193
+ * True iff there is anything staged or unstaged that a `git commit` would
1194
+ * capture. Used to guard commit/push steps in the metarepo and fixed-version
1195
+ * flows — without this, a repo with no pending changes (e.g. a parent
1196
+ * metarepo whose submodule pointers were already bumped in a prior run, or
1197
+ * the proteinjs metarepo on a run that only bumped unrelated packages) will
1198
+ * fail `git commit` with "nothing to commit, working tree clean" and the
1199
+ * entire version-workspace run throws.
1200
+ */
1201
+ function hasPendingChanges(dir) {
972
1202
  return __awaiter(this, void 0, void 0, function () {
973
1203
  return __generator(this, function (_a) {
974
1204
  return [2 /*return*/, new Promise(function (resolve) {
975
- (0, child_process_1.exec)('git log @{u}..HEAD --oneline', { cwd: dir }, function (error, stdout) {
976
- if (error || !stdout.trim()) {
1205
+ (0, child_process_1.exec)('git status --porcelain', { cwd: dir }, function (error, stdout) {
1206
+ if (error) {
977
1207
  resolve(false);
978
1208
  return;
979
1209
  }
980
- var hasFeature = stdout.split('\n').some(function (line) { return /^[a-f0-9]+\s+feat[\s(:]/i.test(line); });
981
- resolve(hasFeature);
1210
+ resolve(stdout.trim().length > 0);
1211
+ });
1212
+ })];
1213
+ });
1214
+ });
1215
+ }
1216
+ var BUMP_ORDER = { patch: 1, minor: 2, major: 3 };
1217
+ function maxBump(a, b) {
1218
+ if (!a) {
1219
+ return b;
1220
+ }
1221
+ if (!b) {
1222
+ return a;
1223
+ }
1224
+ return BUMP_ORDER[a] >= BUMP_ORDER[b] ? a : b;
1225
+ }
1226
+ /**
1227
+ * Classify the net semver bump implied by the set of commits in HEAD that
1228
+ * aren't yet on the tracked upstream branch (`git log @{u}..HEAD`).
1229
+ *
1230
+ * - `major`: any commit declares `BREAKING CHANGE` in subject/body, or
1231
+ * uses the conventional-commits `!` marker (`feat!:`,
1232
+ * `fix(scope)!:`, etc.).
1233
+ * - `minor`: any commit is a `feat` (type, optionally scoped, no `!`).
1234
+ * - `patch`: any other commit is present (fix/chore/refactor/docs/…).
1235
+ * - `undefined`: no unpushed commits, OR no tracked upstream (the `@{u}`
1236
+ * shorthand errors silently — treated as "nothing to ship").
1237
+ *
1238
+ * Uses `%B` (full body) separated by a null byte so footer-style
1239
+ * `BREAKING CHANGE:` lines are visible. Shells out to `git` directly so we
1240
+ * don't depend on any repo state beyond a valid upstream ref.
1241
+ */
1242
+ function classifyUnpushedCommits(dir) {
1243
+ return __awaiter(this, void 0, void 0, function () {
1244
+ return __generator(this, function (_a) {
1245
+ return [2 /*return*/, new Promise(function (resolve) {
1246
+ // `\x00` as record separator; each record is the full commit message body.
1247
+ (0, child_process_1.exec)('git log @{u}..HEAD --format=%B%x00', { cwd: dir }, function (error, stdout) {
1248
+ if (error || !stdout.trim()) {
1249
+ resolve(undefined);
1250
+ return;
1251
+ }
1252
+ var commits = stdout.split('\x00').map(function (s) { return s.trim(); }).filter(function (s) { return s.length > 0; });
1253
+ var result;
1254
+ for (var _i = 0, commits_1 = commits; _i < commits_1.length; _i++) {
1255
+ var commit = commits_1[_i];
1256
+ result = maxBump(result, classifyCommitMessage(commit));
1257
+ if (result === 'major') {
1258
+ break;
1259
+ }
1260
+ }
1261
+ resolve(result);
982
1262
  });
983
1263
  })];
984
1264
  });
985
1265
  });
986
1266
  }
1267
+ exports.classifyUnpushedCommits = classifyUnpushedCommits;
1268
+ /**
1269
+ * Classify a single commit message by conventional-commits rules. Exported
1270
+ * so unit tests can exercise it without spawning git.
1271
+ */
1272
+ function classifyCommitMessage(message) {
1273
+ if (!message.trim()) {
1274
+ return undefined;
1275
+ }
1276
+ var _a = message.split('\n'), subject = _a[0], rest = _a.slice(1);
1277
+ var body = rest.join('\n');
1278
+ // Breaking-change markers. `type!:` / `type(scope)!:` on the subject,
1279
+ // or `BREAKING CHANGE:` / `BREAKING-CHANGE:` anywhere (subject or body/footer).
1280
+ if (/^\w+(\([^)]*\))?!:/.test(subject)) {
1281
+ return 'major';
1282
+ }
1283
+ if (/BREAKING[ -]CHANGE\s*:/i.test("".concat(subject, "\n").concat(body))) {
1284
+ return 'major';
1285
+ }
1286
+ // `feat` type (optionally scoped) on the subject → minor.
1287
+ if (/^feat(\([^)]*\))?:/i.test(subject)) {
1288
+ return 'minor';
1289
+ }
1290
+ // Any other commit counts as patch.
1291
+ return 'patch';
1292
+ }
1293
+ exports.classifyCommitMessage = classifyCommitMessage;
987
1294
  function evictGitLocks(workspacePath) {
988
1295
  return __awaiter(this, void 0, void 0, function () {
989
1296
  var gitDir, lockFiles;