@ucdjs/release-scripts 0.1.0-beta.14 → 0.1.0-beta.15

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.
Files changed (2) hide show
  1. package/dist/index.mjs +107 -121
  2. package/package.json +2 -2
package/dist/index.mjs CHANGED
@@ -284,21 +284,15 @@ async function isBranchAheadOfRemote(branch, workspaceRoot) {
284
284
  return true;
285
285
  }
286
286
  }
287
- async function hasChangesToCommit(workspaceRoot) {
288
- return (await run("git", ["status", "--porcelain"], { nodeOptions: {
289
- cwd: workspaceRoot,
290
- stdio: "pipe"
291
- } })).stdout.trim() !== "";
292
- }
293
287
  async function commitChanges(message, workspaceRoot) {
294
288
  try {
295
289
  await run("git", ["add", "."], { nodeOptions: {
296
290
  cwd: workspaceRoot,
297
291
  stdio: "pipe"
298
292
  } });
299
- if (!await hasChangesToCommit(workspaceRoot)) return false;
293
+ if (await isWorkingDirectoryClean(workspaceRoot)) return false;
300
294
  logger.info(`Committing changes: ${farver.dim(message)}`);
301
- await run("git", [
295
+ await runIfNotDry("git", [
302
296
  "commit",
303
297
  "-m",
304
298
  message
@@ -439,7 +433,7 @@ function generatePullRequestBody(updates, body) {
439
433
 
440
434
  //#endregion
441
435
  //#region src/versioning/commits.ts
442
- async function getLastPackageTag(packageName, workspaceRoot) {
436
+ async function getMostRecentPackageTag(workspaceRoot, packageName) {
443
437
  try {
444
438
  const { stdout } = await run("git", [
445
439
  "tag",
@@ -449,7 +443,9 @@ async function getLastPackageTag(packageName, workspaceRoot) {
449
443
  cwd: workspaceRoot,
450
444
  stdio: "pipe"
451
445
  } });
452
- return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse()[0];
446
+ const tags = stdout.split("\n").map((tag) => tag.trim()).filter(Boolean);
447
+ if (tags.length === 0) return;
448
+ return tags.reverse()[0];
453
449
  } catch (err) {
454
450
  logger.warn(`Failed to get tags for package ${packageName}: ${err.message}`);
455
451
  return;
@@ -467,40 +463,27 @@ function determineHighestBump(commits) {
467
463
  return highestBump;
468
464
  }
469
465
  /**
470
- * Retrieves commits that affect a specific workspace package since its last tag.
466
+ * Get commits grouped by workspace package.
467
+ * For each package, retrieves all commits since its last release tag that affect that package.
471
468
  *
472
- * @param {string} workspaceRoot - The root directory of the workspace.
473
- * @param {WorkspacePackage} pkg - The workspace package to analyze.
474
- * @returns {Promise<GitCommit[]>} A promise that resolves to an array of GitCommit objects affecting the package.
469
+ * @param {string} workspaceRoot - The root directory of the workspace
470
+ * @param {WorkspacePackage[]} packages - Array of workspace packages to analyze
471
+ * @returns {Promise<Map<string, GitCommit[]>>} A map of package names to their commits since their last release
475
472
  */
476
- async function getCommitsForWorkspacePackage(workspaceRoot, pkg) {
477
- const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
478
- const allCommits = getCommits({
479
- from: lastTag,
480
- to: "HEAD",
481
- cwd: workspaceRoot
482
- });
483
- logger.verbose("Found commits for package", `${farver.cyan(allCommits.length)} for ${farver.bold(pkg.name)} since ${lastTag || "beginning"}`);
484
- const commitsAffectingPackage = getCommits({
485
- from: lastTag,
486
- to: "HEAD",
487
- cwd: workspaceRoot,
488
- folder: pkg.path
489
- });
490
- const affectingCommitShas = /* @__PURE__ */ new Set();
491
- for (const commit of commitsAffectingPackage) affectingCommitShas.add(commit.shortHash);
492
- const packageCommits = allCommits.filter((commit) => {
493
- return affectingCommitShas.has(commit.shortHash);
494
- });
495
- logger.verbose("Commits affect package", `${farver.cyan(packageCommits.length)} affect ${farver.bold(pkg.name)}`);
496
- return packageCommits;
497
- }
498
- async function getWorkspacePackageCommits(workspaceRoot, packages) {
473
+ async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
499
474
  const changedPackages = /* @__PURE__ */ new Map();
500
475
  const promises = packages.map(async (pkg) => {
476
+ const lastTag = await getMostRecentPackageTag(workspaceRoot, pkg.name);
477
+ const allCommits = await getCommits({
478
+ from: lastTag,
479
+ to: "HEAD",
480
+ cwd: workspaceRoot,
481
+ folder: pkg.path
482
+ });
483
+ logger.verbose(`Found ${farver.cyan(allCommits.length)} commits for package ${farver.bold(pkg.name)} since tag ${farver.cyan(lastTag ?? "N/A")}`);
501
484
  return {
502
485
  pkgName: pkg.name,
503
- commits: await getCommitsForWorkspacePackage(workspaceRoot, pkg)
486
+ commits: allCommits
504
487
  };
505
488
  });
506
489
  const results = await Promise.all(promises);
@@ -508,7 +491,7 @@ async function getWorkspacePackageCommits(workspaceRoot, packages) {
508
491
  return changedPackages;
509
492
  }
510
493
  async function getCommitFileList(workspaceRoot, from, to) {
511
- const map = /* @__PURE__ */ new Map();
494
+ const commits = /* @__PURE__ */ new Map();
512
495
  try {
513
496
  const { stdout } = await run("git", [
514
497
  "log",
@@ -519,22 +502,17 @@ async function getCommitFileList(workspaceRoot, from, to) {
519
502
  cwd: workspaceRoot,
520
503
  stdio: "pipe"
521
504
  } });
522
- const lines = stdout.trim().split("\n");
505
+ const lines = stdout.trim().split("\n").filter((line) => line.trim() !== "");
523
506
  let currentSha = null;
507
+ const HASH_REGEX = /^[0-9a-f]{40}$/i;
524
508
  for (const line of lines) {
525
509
  const trimmedLine = line.trim();
526
- if (trimmedLine === "") {
527
- currentSha = null;
528
- continue;
529
- }
530
- if (currentSha === null) {
510
+ if (HASH_REGEX.test(trimmedLine)) {
531
511
  currentSha = trimmedLine;
532
- map.set(currentSha, []);
533
- continue;
534
- }
535
- map.get(currentSha).push(trimmedLine);
512
+ commits.set(currentSha, []);
513
+ } else if (currentSha !== null) commits.get(currentSha)?.push(trimmedLine);
536
514
  }
537
- return map;
515
+ return commits;
538
516
  } catch {
539
517
  return null;
540
518
  }
@@ -556,15 +534,43 @@ function fileMatchesPackageFolder(file, packagePaths, workspaceRoot) {
556
534
  }
557
535
  /**
558
536
  * Check if a commit is a "global" commit (doesn't touch any package folder).
537
+ * @param workspaceRoot - The workspace root
559
538
  * @param files - Array of files changed in the commit
560
539
  * @param packagePaths - Set of normalized package paths
561
- * @param workspaceRoot - The workspace root
562
540
  * @returns true if this is a global commit
563
541
  */
564
- function isGlobalCommit(files, packagePaths, workspaceRoot) {
542
+ function isGlobalCommit(workspaceRoot, files, packagePaths) {
565
543
  if (!files || files.length === 0) return false;
566
544
  return !files.some((file) => fileMatchesPackageFolder(file, packagePaths, workspaceRoot));
567
545
  }
546
+ const DEPENDENCY_FILES = [
547
+ "package.json",
548
+ "pnpm-lock.yaml",
549
+ "pnpm-workspace.yaml",
550
+ "yarn.lock",
551
+ "package-lock.json"
552
+ ];
553
+ /**
554
+ * Find the oldest and newest commits across all packages.
555
+ * @param packageCommits - Map of package commits
556
+ * @returns Object with oldest and newest commit SHAs, or null if no commits
557
+ */
558
+ function findCommitRange(packageCommits) {
559
+ let oldestCommit = null;
560
+ let newestCommit = null;
561
+ for (const commits of packageCommits.values()) {
562
+ if (commits.length === 0) continue;
563
+ const firstCommit = commits[0].shortHash;
564
+ const lastCommit = commits[commits.length - 1].shortHash;
565
+ if (!newestCommit) newestCommit = firstCommit;
566
+ oldestCommit = lastCommit;
567
+ }
568
+ if (!oldestCommit || !newestCommit) return null;
569
+ return {
570
+ oldest: oldestCommit,
571
+ newest: newestCommit
572
+ };
573
+ }
568
574
  /**
569
575
  * Get global commits for each package based on their individual commit timelines.
570
576
  * This solves the problem where packages with different release histories need different global commits.
@@ -587,18 +593,13 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
587
593
  return result;
588
594
  }
589
595
  logger.verbose(`Computing global commits per-package (mode: ${farver.cyan(mode)})`);
590
- let oldestCommit = null;
591
- let newestCommit = null;
592
- for (const commits of packageCommits.values()) if (commits.length > 0) {
593
- if (!newestCommit) newestCommit = commits[0].shortHash;
594
- oldestCommit = commits[commits.length - 1].shortHash;
595
- }
596
- if (!oldestCommit || !newestCommit) {
596
+ const commitRange = findCommitRange(packageCommits);
597
+ if (!commitRange) {
597
598
  logger.verbose("No commits found across packages");
598
599
  return result;
599
600
  }
600
- logger.verbose("Fetching files for commits range", `${farver.cyan(oldestCommit)}..${farver.cyan(newestCommit)}`);
601
- const commitFilesMap = await getCommitFileList(workspaceRoot, oldestCommit, newestCommit);
601
+ logger.verbose("Fetching files for commits range", `${farver.cyan(commitRange.oldest)}..${farver.cyan(commitRange.newest)}`);
602
+ const commitFilesMap = await getCommitFileList(workspaceRoot, commitRange.oldest, commitRange.newest);
602
603
  if (!commitFilesMap) {
603
604
  logger.warn("Failed to get commit file list, returning empty global commits");
604
605
  return result;
@@ -606,34 +607,29 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
606
607
  logger.verbose("Got file lists for commits", `${farver.cyan(commitFilesMap.size)} commits in ONE git call`);
607
608
  const packagePaths = new Set(allPackages.map((p) => p.path));
608
609
  for (const [pkgName, commits] of packageCommits) {
609
- const globalCommitsForPackage = [];
610
+ const globalCommitsAffectingPackage = [];
610
611
  logger.verbose("Filtering global commits for package", `${farver.bold(pkgName)} from ${farver.cyan(commits.length)} commits`);
611
- for (const commit of commits) if (isGlobalCommit(commitFilesMap.get(commit.shortHash), packagePaths, workspaceRoot)) globalCommitsForPackage.push(commit);
612
- logger.verbose("Package global commits found", `${farver.bold(pkgName)}: ${farver.cyan(globalCommitsForPackage.length)} global commits`);
613
- if (mode === "all") result.set(pkgName, globalCommitsForPackage);
614
- else if (mode === "dependencies") {
615
- const dependencyCommits = [];
616
- const dependencyFiles = [
617
- "package.json",
618
- "pnpm-lock.yaml",
619
- "pnpm-workspace.yaml",
620
- "yarn.lock",
621
- "package-lock.json"
622
- ];
623
- for (const commit of globalCommitsForPackage) {
624
- const files = commitFilesMap.get(commit.shortHash);
625
- if (!files) continue;
626
- if (files.some((file) => {
627
- const normalizedFile = file.startsWith("./") ? file.slice(2) : file;
628
- return dependencyFiles.includes(normalizedFile);
629
- })) {
630
- logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
631
- dependencyCommits.push(commit);
632
- }
612
+ for (const commit of commits) {
613
+ const files = commitFilesMap.get(commit.shortHash);
614
+ if (!files) continue;
615
+ if (isGlobalCommit(workspaceRoot, files, packagePaths)) globalCommitsAffectingPackage.push(commit);
616
+ }
617
+ logger.verbose("Package global commits found", `${farver.bold(pkgName)}: ${farver.cyan(globalCommitsAffectingPackage.length)} global commits`);
618
+ if (mode === "all") {
619
+ result.set(pkgName, globalCommitsAffectingPackage);
620
+ continue;
621
+ }
622
+ const dependencyCommits = [];
623
+ for (const commit of globalCommitsAffectingPackage) {
624
+ const files = commitFilesMap.get(commit.shortHash);
625
+ if (!files) continue;
626
+ if (files.some((file) => DEPENDENCY_FILES.includes(file.startsWith("./") ? file.slice(2) : file))) {
627
+ logger.verbose("Global commit affects dependencies", `${farver.bold(pkgName)}: commit ${farver.cyan(commit.shortHash)} affects dependencies`);
628
+ dependencyCommits.push(commit);
633
629
  }
634
- logger.verbose("Global commits affect dependencies", `${farver.bold(pkgName)}: ${farver.cyan(dependencyCommits.length)} global commits affect dependencies`);
635
- result.set(pkgName, dependencyCommits);
636
630
  }
631
+ logger.verbose("Global commits affect dependencies", `${farver.bold(pkgName)}: ${farver.cyan(dependencyCommits.length)} global commits affect dependencies`);
632
+ result.set(pkgName, dependencyCommits);
637
633
  }
638
634
  return result;
639
635
  }
@@ -866,7 +862,7 @@ async function calculateVersionUpdates({ workspacePackages, packageCommits, work
866
862
  if (selectedVersion === null) continue;
867
863
  newVersion = selectedVersion;
868
864
  }
869
- logger.item(`Version update: ${pkg.version} → ${newVersion}`);
865
+ logger.verbose(`Version update: ${pkg.version} → ${newVersion}`);
870
866
  versionUpdates.push({
871
867
  package: pkg,
872
868
  currentVersion: pkg.version,
@@ -923,35 +919,24 @@ async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
923
919
  const content = await readFile(packageJsonPath, "utf-8");
924
920
  const packageJson = JSON.parse(content);
925
921
  packageJson.version = newVersion;
926
- for (const [depName, depVersion] of dependencyUpdates) {
927
- if (packageJson.dependencies?.[depName]) {
928
- const oldVersion = packageJson.dependencies[depName];
929
- if (oldVersion === "workspace:*") {
930
- logger.verbose(` - Skipping workspace:* dependency: ${depName}`);
931
- continue;
932
- }
933
- packageJson.dependencies[depName] = `^${depVersion}`;
934
- logger.verbose(` - Updated dependency ${depName}: ${oldVersion} → ^${depVersion}`);
935
- }
936
- if (packageJson.devDependencies?.[depName]) {
937
- const oldVersion = packageJson.devDependencies[depName];
938
- if (oldVersion === "workspace:*") {
939
- logger.verbose(` - Skipping workspace:* devDependency: ${depName}`);
940
- continue;
941
- }
942
- packageJson.devDependencies[depName] = `^${depVersion}`;
943
- logger.verbose(` - Updated devDependency ${depName}: ${oldVersion} → ^${depVersion}`);
922
+ function updateDependency(deps, depName, depVersion, isPeerDependency = false) {
923
+ if (!deps) return;
924
+ const oldVersion = deps[depName];
925
+ if (!oldVersion) return;
926
+ if (oldVersion === "workspace:*") {
927
+ logger.verbose(` - Skipping workspace:* dependency: ${depName}`);
928
+ return;
944
929
  }
945
- if (packageJson.peerDependencies?.[depName]) {
946
- const oldVersion = packageJson.peerDependencies[depName];
947
- if (oldVersion === "workspace:*") {
948
- logger.verbose(` - Skipping workspace:* peerDependency: ${depName}`);
949
- continue;
950
- }
930
+ if (isPeerDependency) {
951
931
  const majorVersion = depVersion.split(".")[0];
952
- packageJson.peerDependencies[depName] = `>=${depVersion} <${Number(majorVersion) + 1}.0.0`;
953
- logger.verbose(` - Updated peerDependency ${depName}: ${oldVersion} → ^${depVersion}`);
954
- }
932
+ deps[depName] = `>=${depVersion} <${Number(majorVersion) + 1}.0.0`;
933
+ } else deps[depName] = `^${depVersion}`;
934
+ logger.verbose(` - Updated dependency ${depName}: ${oldVersion} → ${deps[depName]}`);
935
+ }
936
+ for (const [depName, depVersion] of dependencyUpdates) {
937
+ updateDependency(packageJson.dependencies, depName, depVersion);
938
+ updateDependency(packageJson.devDependencies, depName, depVersion);
939
+ updateDependency(packageJson.peerDependencies, depName, depVersion, true);
955
940
  }
956
941
  await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
957
942
  logger.verbose(` - Successfully wrote updated package.json`);
@@ -1140,11 +1125,11 @@ async function release(options) {
1140
1125
  logger.item(` ${farver.gray("→")} ${farver.gray(pkg.path)}`);
1141
1126
  }
1142
1127
  logger.emptyLine();
1143
- const packageCommits = await getWorkspacePackageCommits(workspaceRoot, workspacePackages);
1144
- const globalCommitsPerPackage = await getGlobalCommitsPerPackage(workspaceRoot, packageCommits, workspacePackages, normalizedOptions.globalCommitMode);
1128
+ const groupedPackageCommits = await getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
1129
+ const globalCommitsPerPackage = await getGlobalCommitsPerPackage(workspaceRoot, groupedPackageCommits, workspacePackages, normalizedOptions.globalCommitMode);
1145
1130
  const { allUpdates, applyUpdates } = await calculateAndPrepareVersionUpdates({
1146
1131
  workspacePackages,
1147
- packageCommits,
1132
+ packageCommits: groupedPackageCommits,
1148
1133
  workspaceRoot,
1149
1134
  showPrompt: options.prompts?.versions !== false,
1150
1135
  globalCommitsPerPackage
@@ -1167,10 +1152,12 @@ async function release(options) {
1167
1152
  await applyUpdates();
1168
1153
  if (!await prOps.syncChanges(true)) if (prOps.doesReleasePRExist && prOps.existingPullRequest) {
1169
1154
  logger.item("No updates needed, PR is already up to date");
1155
+ const { pullRequest: pullRequest$1, created: created$1 } = await prOps.syncPullRequest(allUpdates);
1156
+ await prOps.cleanup();
1170
1157
  return {
1171
1158
  updates: allUpdates,
1172
- prUrl: prOps.existingPullRequest.html_url,
1173
- created: false
1159
+ prUrl: pullRequest$1?.html_url,
1160
+ created: created$1
1174
1161
  };
1175
1162
  } else {
1176
1163
  logger.error("No changes to commit, and no existing PR. Nothing to do.");
@@ -1244,7 +1231,6 @@ async function orchestrateReleasePullRequest({ workspaceRoot, owner, repo, githu
1244
1231
  const isBranchAhead = await isBranchAheadOfRemote(releaseBranch, workspaceRoot);
1245
1232
  if (!hasCommitted && !isBranchAhead) {
1246
1233
  logger.item("No changes to commit and branch is in sync with remote");
1247
- await checkoutBranch(defaultBranch, workspaceRoot);
1248
1234
  return false;
1249
1235
  }
1250
1236
  logger.step("Pushing changes to remote");
@@ -1253,7 +1239,7 @@ async function orchestrateReleasePullRequest({ workspaceRoot, owner, repo, githu
1253
1239
  },
1254
1240
  async syncPullRequest(updates) {
1255
1241
  const prTitle = existingPullRequest?.title || pullRequestTitle || "chore: update package versions";
1256
- const prBody = pullRequestBody || generatePullRequestBody(updates);
1242
+ const prBody = generatePullRequestBody(updates, pullRequestBody);
1257
1243
  const pullRequest = await upsertPullRequest({
1258
1244
  owner,
1259
1245
  repo,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.14",
3
+ "version": "0.1.0-beta.15",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,7 +27,7 @@
27
27
  ],
28
28
  "dependencies": {
29
29
  "@luxass/utils": "2.7.2",
30
- "commit-parser": "0.4.5",
30
+ "commit-parser": "1.0.0",
31
31
  "farver": "1.0.0-beta.1",
32
32
  "mri": "1.2.0",
33
33
  "prompts": "2.4.2",