@ucdjs/release-scripts 0.1.0-beta.17 → 0.1.0-beta.19

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.
package/dist/index.d.mts CHANGED
@@ -180,7 +180,13 @@ interface ReleaseResult {
180
180
  declare function release(options: ReleaseOptions): Promise<ReleaseResult | null>;
181
181
  //#endregion
182
182
  //#region src/verify.d.ts
183
- interface VerifyOptions extends SharedOptions {}
184
- declare function verify(_options: VerifyOptions): void;
183
+ interface VerifyOptions extends SharedOptions {
184
+ branch?: {
185
+ release?: string;
186
+ default?: string;
187
+ };
188
+ safeguards?: boolean;
189
+ }
190
+ declare function verify(options: VerifyOptions): Promise<void>;
185
191
  //#endregion
186
192
  export { type PublishOptions, type ReleaseOptions, type ReleaseResult, type VerifyOptions, publish, release, verify };
package/dist/index.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  import { t as Eta } from "./eta-j5TFRbI4.mjs";
2
- import { readFile, writeFile } from "node:fs/promises";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
3
3
  import { join, relative } from "node:path";
4
4
  import process from "node:process";
5
+ import readline from "node:readline";
5
6
  import farver from "farver";
6
7
  import mri from "mri";
7
8
  import { exec } from "tinyexec";
8
9
  import { dedent } from "@luxass/utils";
9
10
  import { getCommits, groupByType } from "commit-parser";
10
11
  import prompts from "prompts";
12
+ import { compare, gt } from "semver";
11
13
 
12
14
  //#region src/publish.ts
13
15
  function publish(_options) {}
@@ -57,6 +59,13 @@ const logger = {
57
59
  },
58
60
  success: (message) => {
59
61
  console.log(` ${farver.green("✓")} ${message}`);
62
+ },
63
+ clearScreen: () => {
64
+ const repeatCount = process.stdout.rows - 2;
65
+ const blank = repeatCount > 0 ? "\n".repeat(repeatCount) : "";
66
+ console.log(blank);
67
+ readline.cursorTo(process.stdout, 0, 0);
68
+ readline.clearScreenDown(process.stdout);
60
69
  }
61
70
  };
62
71
  async function run(bin, args$1, opts = {}) {
@@ -386,6 +395,7 @@ async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
386
395
 
387
396
  //#endregion
388
397
  //#region src/core/changelog.ts
398
+ const globalAuthorCache = /* @__PURE__ */ new Map();
389
399
  const DEFAULT_CHANGELOG_TEMPLATE = dedent`
390
400
  <% if (it.previousVersion) { -%>
391
401
  ## [<%= it.version %>](<%= it.compareUrl %>) (<%= it.date %>)
@@ -492,24 +502,28 @@ async function updateChangelog(options) {
492
502
  await writeFile(changelogPath, updatedContent, "utf-8");
493
503
  }
494
504
  async function resolveCommitAuthors(commits, githubClient) {
495
- const authorsByEmail = /* @__PURE__ */ new Map();
505
+ const authorsToResolve = /* @__PURE__ */ new Set();
496
506
  const commitAuthors = /* @__PURE__ */ new Map();
497
507
  for (const commit of commits) {
498
508
  const authorsForCommit = [];
499
509
  commit.authors.forEach((author, idx) => {
500
510
  if (!author.email || !author.name) return;
501
- if (!authorsByEmail.has(author.email)) authorsByEmail.set(author.email, {
502
- commits: [],
503
- name: author.name,
504
- email: author.email
505
- });
506
- const info = authorsByEmail.get(author.email);
511
+ let info = globalAuthorCache.get(author.email);
512
+ if (!info) {
513
+ info = {
514
+ commits: [],
515
+ name: author.name,
516
+ email: author.email
517
+ };
518
+ globalAuthorCache.set(author.email, info);
519
+ }
507
520
  if (idx === 0) info.commits.push(commit.shortHash);
508
521
  authorsForCommit.push(info);
522
+ if (!info.login) authorsToResolve.add(info);
509
523
  });
510
524
  commitAuthors.set(commit.hash, authorsForCommit);
511
525
  }
512
- await Promise.all(Array.from(authorsByEmail.values()).map((info) => githubClient.resolveAuthorInfo(info)));
526
+ await Promise.all(Array.from(authorsToResolve).map((info) => githubClient.resolveAuthorInfo(info)));
513
527
  return commitAuthors;
514
528
  }
515
529
  function formatCommitLine({ commit, owner, repo, authors }) {
@@ -1016,9 +1030,6 @@ function getNextVersion(currentVersion, bump) {
1016
1030
  }
1017
1031
  return `${newMajor}.${newMinor}.${newPatch}`;
1018
1032
  }
1019
- /**
1020
- * Create a version update object
1021
- */
1022
1033
  function createVersionUpdate(pkg, bump, hasDirectChanges) {
1023
1034
  const newVersion = getNextVersion(pkg.version, bump);
1024
1035
  return {
@@ -1084,12 +1095,16 @@ function formatCommitsForDisplay(commits) {
1084
1095
  if (hasMore) return `${formattedCommits}\n ${farver.dim(`... and ${commits.length - maxCommitsToShow} more commits`)}`;
1085
1096
  return formattedCommits;
1086
1097
  }
1087
- /**
1088
- * Calculate version updates for packages based on their commits
1089
- */
1090
- async function calculateVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage }) {
1098
+ async function calculateVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage, overrides: initialOverrides = {} }) {
1091
1099
  const versionUpdates = [];
1092
1100
  const processedPackages = /* @__PURE__ */ new Set();
1101
+ const newOverrides = { ...initialOverrides };
1102
+ const bumpRanks = {
1103
+ major: 3,
1104
+ minor: 2,
1105
+ patch: 1,
1106
+ none: 0
1107
+ };
1093
1108
  logger.verbose(`Starting version inference for ${packageCommits.size} packages with commits`);
1094
1109
  for (const [pkgName, pkgCommits] of packageCommits) {
1095
1110
  const pkg = workspacePackages.find((p) => p.name === pkgName);
@@ -1099,30 +1114,45 @@ async function calculateVersionUpdates({ workspacePackages, packageCommits, work
1099
1114
  }
1100
1115
  processedPackages.add(pkgName);
1101
1116
  const globalCommits = globalCommitsPerPackage.get(pkgName) || [];
1102
- if (globalCommits.length > 0) logger.verbose(` - Global commits for this package: ${globalCommits.length}`);
1103
1117
  const allCommitsForPackage = [...pkgCommits, ...globalCommits];
1104
- const bump = determineHighestBump(allCommitsForPackage);
1105
- if (bump === "none") continue;
1106
- let newVersion = getNextVersion(pkg.version, bump);
1118
+ const determinedBump = determineHighestBump(allCommitsForPackage);
1119
+ const override = newOverrides[pkgName];
1120
+ const effectiveBump = override?.type || determinedBump;
1121
+ if (effectiveBump === "none") continue;
1122
+ let newVersion = override?.version || getNextVersion(pkg.version, effectiveBump);
1123
+ let finalBumpType = effectiveBump;
1107
1124
  if (!isCI && showPrompt) {
1108
- logger.section("📝 Commits affecting this package");
1125
+ logger.clearScreen();
1126
+ logger.section(`📝 Commits for ${farver.cyan(pkg.name)}`);
1109
1127
  formatCommitsForDisplay(allCommitsForPackage).split("\n").forEach((line) => logger.item(line));
1110
1128
  logger.emptyLine();
1111
1129
  const selectedVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, newVersion);
1112
1130
  if (selectedVersion === null) continue;
1131
+ const userBump = _calculateBumpType(pkg.version, selectedVersion);
1132
+ finalBumpType = userBump;
1133
+ if (bumpRanks[userBump] < bumpRanks[determinedBump]) {
1134
+ newOverrides[pkgName] = {
1135
+ type: userBump,
1136
+ version: selectedVersion
1137
+ };
1138
+ logger.info(`Version override recorded for ${pkgName}: ${determinedBump} → ${userBump}`);
1139
+ } else if (newOverrides[pkgName] && bumpRanks[userBump] >= bumpRanks[determinedBump]) {
1140
+ delete newOverrides[pkgName];
1141
+ logger.info(`Version override removed for ${pkgName}.`);
1142
+ }
1113
1143
  newVersion = selectedVersion;
1114
1144
  }
1115
- logger.verbose(`Version update: ${pkg.version} → ${newVersion}`);
1116
1145
  versionUpdates.push({
1117
1146
  package: pkg,
1118
1147
  currentVersion: pkg.version,
1119
1148
  newVersion,
1120
- bumpType: bump,
1121
- hasDirectChanges: true
1149
+ bumpType: finalBumpType,
1150
+ hasDirectChanges: allCommitsForPackage.length > 0
1122
1151
  });
1123
1152
  }
1124
1153
  if (!isCI && showPrompt) for (const pkg of workspacePackages) {
1125
1154
  if (processedPackages.has(pkg.name)) continue;
1155
+ logger.clearScreen();
1126
1156
  logger.section(`📦 Package: ${pkg.name}`);
1127
1157
  logger.item("No direct commits found");
1128
1158
  const newVersion = await selectVersionPrompt(workspaceRoot, pkg, pkg.version, pkg.version);
@@ -1138,19 +1168,23 @@ async function calculateVersionUpdates({ workspacePackages, packageCommits, work
1138
1168
  });
1139
1169
  }
1140
1170
  }
1141
- return versionUpdates;
1171
+ return {
1172
+ updates: versionUpdates,
1173
+ overrides: newOverrides
1174
+ };
1142
1175
  }
1143
1176
  /**
1144
1177
  * Calculate version updates and prepare dependent updates
1145
1178
  * Returns both the updates and a function to apply them
1146
1179
  */
1147
- async function calculateAndPrepareVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage }) {
1148
- const directUpdates = await calculateVersionUpdates({
1180
+ async function calculateAndPrepareVersionUpdates({ workspacePackages, packageCommits, workspaceRoot, showPrompt, globalCommitsPerPackage, overrides }) {
1181
+ const { updates: directUpdates, overrides: newOverrides } = await calculateVersionUpdates({
1149
1182
  workspacePackages,
1150
1183
  packageCommits,
1151
1184
  workspaceRoot,
1152
1185
  showPrompt,
1153
- globalCommitsPerPackage
1186
+ globalCommitsPerPackage,
1187
+ overrides
1154
1188
  });
1155
1189
  const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, directUpdates);
1156
1190
  const applyUpdates = async () => {
@@ -1161,7 +1195,8 @@ async function calculateAndPrepareVersionUpdates({ workspacePackages, packageCom
1161
1195
  };
1162
1196
  return {
1163
1197
  allUpdates,
1164
- applyUpdates
1198
+ applyUpdates,
1199
+ overrides: newOverrides
1165
1200
  };
1166
1201
  }
1167
1202
  async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
@@ -1458,17 +1493,6 @@ async function release(options) {
1458
1493
  logger.emptyLine();
1459
1494
  const groupedPackageCommits = await getWorkspacePackageGroupedCommits(workspaceRoot, workspacePackages);
1460
1495
  const globalCommitsPerPackage = await getGlobalCommitsPerPackage(workspaceRoot, groupedPackageCommits, workspacePackages, normalizedOptions.globalCommitMode);
1461
- const { allUpdates, applyUpdates } = await calculateAndPrepareVersionUpdates({
1462
- workspacePackages,
1463
- packageCommits: groupedPackageCommits,
1464
- workspaceRoot,
1465
- showPrompt: options.prompts?.versions !== false,
1466
- globalCommitsPerPackage
1467
- });
1468
- if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) logger.warn("No packages have changes requiring a release");
1469
- logger.section("🔄 Version Updates");
1470
- logger.item(`Updating ${allUpdates.length} packages (including dependents)`);
1471
- for (const update of allUpdates) logger.item(`${update.package.name}: ${update.currentVersion} → ${update.newVersion}`);
1472
1496
  const githubClient = createGitHubClient({
1473
1497
  owner: normalizedOptions.owner,
1474
1498
  repo: normalizedOptions.repo,
@@ -1483,6 +1507,58 @@ async function release(options) {
1483
1507
  pullRequestBody: options.pullRequest?.body
1484
1508
  });
1485
1509
  await prOps.prepareBranch();
1510
+ const overridesPath = join(workspaceRoot, ".github", "ucdjs-release.overrides.json");
1511
+ let existingOverrides = {};
1512
+ try {
1513
+ const overridesContent = await readFile(overridesPath, "utf-8");
1514
+ existingOverrides = JSON.parse(overridesContent);
1515
+ logger.info("Found existing version overrides file.");
1516
+ } catch {
1517
+ logger.info("No existing version overrides file found. Continuing...");
1518
+ }
1519
+ const { allUpdates, applyUpdates, overrides: newOverrides } = await calculateAndPrepareVersionUpdates({
1520
+ workspacePackages,
1521
+ packageCommits: groupedPackageCommits,
1522
+ workspaceRoot,
1523
+ showPrompt: options.prompts?.versions !== false,
1524
+ globalCommitsPerPackage,
1525
+ overrides: existingOverrides
1526
+ });
1527
+ if (Object.keys(newOverrides).length > 0) {
1528
+ logger.info("Writing version overrides file...");
1529
+ try {
1530
+ await mkdir(join(workspaceRoot, ".github"), { recursive: true });
1531
+ await writeFile(overridesPath, JSON.stringify(newOverrides, null, 2), "utf-8");
1532
+ logger.success("Successfully wrote version overrides file.");
1533
+ } catch (e) {
1534
+ logger.error("Failed to write version overrides file:", e);
1535
+ }
1536
+ }
1537
+ if (Object.keys(newOverrides).length === 0 && Object.keys(existingOverrides).length > 0) {
1538
+ let shouldRemoveOverrides = false;
1539
+ for (const update of allUpdates) {
1540
+ const overriddenVersion = existingOverrides[update.package.name];
1541
+ if (overriddenVersion) {
1542
+ if (compare(update.newVersion, overriddenVersion.version) > 0) {
1543
+ shouldRemoveOverrides = true;
1544
+ break;
1545
+ }
1546
+ }
1547
+ }
1548
+ if (shouldRemoveOverrides) {
1549
+ logger.info("Removing obsolete version overrides file...");
1550
+ try {
1551
+ await rm(overridesPath);
1552
+ logger.success("Successfully removed obsolete version overrides file.");
1553
+ } catch (e) {
1554
+ logger.error("Failed to remove obsolete version overrides file:", e);
1555
+ }
1556
+ }
1557
+ }
1558
+ if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) logger.warn("No packages have changes requiring a release");
1559
+ logger.section("🔄 Version Updates");
1560
+ logger.item(`Updating ${allUpdates.length} packages (including dependents)`);
1561
+ for (const update of allUpdates) logger.item(`${update.package.name}: ${update.currentVersion} → ${update.newVersion}`);
1486
1562
  await applyUpdates();
1487
1563
  if (normalizedOptions.changelog.enabled) {
1488
1564
  logger.step("Updating changelogs");
@@ -1593,7 +1669,87 @@ async function orchestrateReleasePullRequest({ workspaceRoot, githubClient, rele
1593
1669
 
1594
1670
  //#endregion
1595
1671
  //#region src/verify.ts
1596
- function verify(_options) {}
1672
+ async function verify(options) {
1673
+ const { workspaceRoot,...normalizedOptions } = await normalizeReleaseOptions(options);
1674
+ if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
1675
+ const githubClient = createGitHubClient({
1676
+ owner: normalizedOptions.owner,
1677
+ repo: normalizedOptions.repo,
1678
+ githubToken: normalizedOptions.githubToken
1679
+ });
1680
+ const releaseBranch = normalizedOptions.branch.release;
1681
+ const defaultBranch = normalizedOptions.branch.default;
1682
+ const releasePr = await githubClient.getExistingPullRequest(releaseBranch);
1683
+ if (!releasePr || !releasePr.head) {
1684
+ logger.warn(`No open release pull request found for branch "${releaseBranch}". Nothing to verify.`);
1685
+ return;
1686
+ }
1687
+ logger.info(`Found release PR #${releasePr.number}. Verifying against default branch "${defaultBranch}"...`);
1688
+ const originalBranch = await getCurrentBranch(workspaceRoot);
1689
+ if (originalBranch !== defaultBranch) await checkoutBranch(defaultBranch, workspaceRoot);
1690
+ const overridesPath = join(".github", "ucdjs.release.overrides.json");
1691
+ let existingOverrides = {};
1692
+ try {
1693
+ const overridesContent = await readFileFromGit(workspaceRoot, releasePr.head.sha, overridesPath);
1694
+ if (overridesContent) {
1695
+ existingOverrides = JSON.parse(overridesContent);
1696
+ logger.info("Found existing version overrides file on release branch.");
1697
+ }
1698
+ } catch {
1699
+ logger.info("No version overrides file found on release branch. Continuing...");
1700
+ }
1701
+ const mainPackages = await discoverWorkspacePackages(workspaceRoot, options);
1702
+ const mainCommits = await getWorkspacePackageGroupedCommits(workspaceRoot, mainPackages);
1703
+ const { allUpdates: expectedUpdates } = await calculateAndPrepareVersionUpdates({
1704
+ workspacePackages: mainPackages,
1705
+ packageCommits: mainCommits,
1706
+ workspaceRoot,
1707
+ showPrompt: false,
1708
+ globalCommitsPerPackage: await getGlobalCommitsPerPackage(workspaceRoot, mainCommits, mainPackages, normalizedOptions.globalCommitMode),
1709
+ overrides: existingOverrides
1710
+ });
1711
+ const expectedVersionMap = new Map(expectedUpdates.map((u) => [u.package.name, u.newVersion]));
1712
+ const prVersionMap = /* @__PURE__ */ new Map();
1713
+ for (const pkg of mainPackages) {
1714
+ const pkgJsonPath = join(pkg.path.replace(workspaceRoot, ""), "package.json").substring(1);
1715
+ const pkgJsonContent = await readFileFromGit(workspaceRoot, releasePr.head.sha, pkgJsonPath);
1716
+ if (pkgJsonContent) {
1717
+ const pkgJson = JSON.parse(pkgJsonContent);
1718
+ prVersionMap.set(pkg.name, pkgJson.version);
1719
+ }
1720
+ }
1721
+ if (originalBranch !== defaultBranch) await checkoutBranch(originalBranch, workspaceRoot);
1722
+ let isOutOfSync = false;
1723
+ for (const [pkgName, expectedVersion] of expectedVersionMap.entries()) {
1724
+ const prVersion = prVersionMap.get(pkgName);
1725
+ if (!prVersion) {
1726
+ logger.warn(`Package "${pkgName}" found in default branch but not in release branch. Skipping.`);
1727
+ continue;
1728
+ }
1729
+ if (gt(expectedVersion, prVersion)) {
1730
+ logger.error(`Package "${pkgName}" is out of sync. Expected version >= ${expectedVersion}, but PR has ${prVersion}.`);
1731
+ isOutOfSync = true;
1732
+ } else logger.success(`Package "${pkgName}" is up to date (PR version: ${prVersion}, Expected: ${expectedVersion})`);
1733
+ }
1734
+ const statusContext = "ucdjs/release-verify";
1735
+ if (isOutOfSync) {
1736
+ await githubClient.setCommitStatus({
1737
+ sha: releasePr.head.sha,
1738
+ state: "failure",
1739
+ context: statusContext,
1740
+ description: "Release PR is out of sync with the default branch. Please re-run the release process."
1741
+ });
1742
+ logger.error("Verification failed. Commit status set to 'failure'.");
1743
+ } else {
1744
+ await githubClient.setCommitStatus({
1745
+ sha: releasePr.head.sha,
1746
+ state: "success",
1747
+ context: statusContext,
1748
+ description: "Release PR is up to date."
1749
+ });
1750
+ logger.success("Verification successful. Commit status set to 'success'.");
1751
+ }
1752
+ }
1597
1753
 
1598
1754
  //#endregion
1599
1755
  export { publish, release, verify };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.17",
3
+ "version": "0.1.0-beta.19",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -32,12 +32,14 @@
32
32
  "farver": "1.0.0-beta.1",
33
33
  "mri": "1.2.0",
34
34
  "prompts": "2.4.2",
35
+ "semver": "7.7.3",
35
36
  "tinyexec": "1.0.2"
36
37
  },
37
38
  "devDependencies": {
38
39
  "@luxass/eslint-config": "6.0.1",
39
40
  "@types/node": "22.18.12",
40
41
  "@types/prompts": "2.4.9",
42
+ "@types/semver": "7.7.1",
41
43
  "eslint": "9.39.1",
42
44
  "eta": "4.0.1",
43
45
  "tsdown": "0.16.0",