@ucdjs/release-scripts 0.1.0-beta.51 → 0.1.0-beta.52

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
@@ -25,6 +25,7 @@ interface ReleaseScriptsOptionsInput {
25
25
  enabled?: boolean;
26
26
  template?: string;
27
27
  emojis?: boolean;
28
+ combinePrereleaseIntoFirstStable?: boolean;
28
29
  };
29
30
  npm?: {
30
31
  otp?: string;
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import farver from "farver";
8
8
  import mri from "mri";
9
9
  import { exec } from "tinyexec";
10
10
  import { dedent } from "@luxass/utils";
11
- import semver, { compare, gt } from "semver";
11
+ import semver, { gt } from "semver";
12
12
  import prompts from "prompts";
13
13
 
14
14
  //#region src/operations/changelog-format.ts
@@ -524,7 +524,8 @@ function normalizeReleaseScriptsOptions(options) {
524
524
  changelog: {
525
525
  enabled: changelog.enabled ?? true,
526
526
  template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE,
527
- emojis: changelog.emojis ?? true
527
+ emojis: changelog.emojis ?? true,
528
+ combinePrereleaseIntoFirstStable: changelog.combinePrereleaseIntoFirstStable ?? false
528
529
  },
529
530
  types: types ? {
530
531
  ...DEFAULT_TYPES,
@@ -740,6 +741,42 @@ async function commitChanges(message, workspaceRoot) {
740
741
  return err(gitError);
741
742
  }
742
743
  }
744
+ async function commitPaths(paths, message, workspaceRoot) {
745
+ try {
746
+ if (paths.length === 0) return ok(false);
747
+ await run("git", [
748
+ "add",
749
+ "--",
750
+ ...paths
751
+ ], { nodeOptions: {
752
+ cwd: workspaceRoot,
753
+ stdio: "pipe"
754
+ } });
755
+ if ((await run("git", [
756
+ "diff",
757
+ "--cached",
758
+ "--name-only"
759
+ ], { nodeOptions: {
760
+ cwd: workspaceRoot,
761
+ stdio: "pipe"
762
+ } })).stdout.trim() === "") return ok(false);
763
+ logger.info(`Committing changes: ${farver.dim(message)}`);
764
+ await runIfNotDry("git", [
765
+ "commit",
766
+ "-m",
767
+ message
768
+ ], { nodeOptions: {
769
+ cwd: workspaceRoot,
770
+ stdio: "pipe"
771
+ } });
772
+ return ok(true);
773
+ } catch (error) {
774
+ const gitError = toGitError("commitPaths", error);
775
+ logger.error(`Git commit failed: ${gitError.message}`);
776
+ if (gitError.stderr) logger.error(`Git stderr: ${gitError.stderr}`);
777
+ return err(gitError);
778
+ }
779
+ }
743
780
  async function pushBranch(branch, workspaceRoot, options) {
744
781
  try {
745
782
  const args = [
@@ -804,6 +841,28 @@ async function getMostRecentPackageTag(workspaceRoot, packageName) {
804
841
  return err(toGitError("getMostRecentPackageTag", error));
805
842
  }
806
843
  }
844
+ async function getMostRecentPackageStableTag(workspaceRoot, packageName) {
845
+ try {
846
+ const { stdout } = await run("git", [
847
+ "tag",
848
+ "--list",
849
+ `${packageName}@*`
850
+ ], { nodeOptions: {
851
+ cwd: workspaceRoot,
852
+ stdio: "pipe"
853
+ } });
854
+ const tags = stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse();
855
+ for (const tag of tags) {
856
+ const atIndex = tag.lastIndexOf("@");
857
+ if (atIndex === -1) continue;
858
+ const version = tag.slice(atIndex + 1);
859
+ if (semver.valid(version) && semver.prerelease(version) == null) return ok(tag);
860
+ }
861
+ return ok(void 0);
862
+ } catch (error) {
863
+ return err(toGitError("getMostRecentPackageStableTag", error));
864
+ }
865
+ }
807
866
  /**
808
867
  * Builds a mapping of commit SHAs to the list of files changed in each commit
809
868
  * within a given inclusive range.
@@ -1120,6 +1179,7 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1120
1179
  const prePatchAlpha = getNextPrereleaseVersion(currentVersion, "prepatch", "alpha");
1121
1180
  const preMinorAlpha = getNextPrereleaseVersion(currentVersion, "preminor", "alpha");
1122
1181
  const preMajorAlpha = getNextPrereleaseVersion(currentVersion, "premajor", "alpha");
1182
+ const isCurrentPrerelease = prereleaseIdentifier != null;
1123
1183
  const choices = [
1124
1184
  {
1125
1185
  value: "skip",
@@ -1133,6 +1193,10 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1133
1193
  value: "as-is",
1134
1194
  title: `as-is ${farver.dim("(keep current version)")}`
1135
1195
  },
1196
+ ...isCurrentPrerelease ? [{
1197
+ value: "next-prerelease",
1198
+ title: `next prerelease ${farver.bold(nextDefaultPrerelease)}`
1199
+ }] : [],
1136
1200
  {
1137
1201
  value: "patch",
1138
1202
  title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
@@ -1157,6 +1221,7 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1157
1221
  const initialValue = defaultChoice === "auto" ? suggestedVersion === currentVersion ? "skip" : "suggested" : defaultChoice;
1158
1222
  const initial = Math.max(0, choices.findIndex((choice) => choice.value === initialValue));
1159
1223
  const prereleaseVersionByChoice = {
1224
+ "next-prerelease": nextDefaultPrerelease,
1160
1225
  "next": nextDefaultPrerelease,
1161
1226
  "next-beta": nextBeta,
1162
1227
  "next-alpha": nextAlpha,
@@ -1410,6 +1475,16 @@ async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
1410
1475
  for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
1411
1476
  return changedPackages;
1412
1477
  }
1478
+ async function getPackageCommitsSinceTag(workspaceRoot, pkg, fromTag) {
1479
+ const allCommits = await getCommits({
1480
+ from: fromTag,
1481
+ to: "HEAD",
1482
+ cwd: workspaceRoot,
1483
+ folder: pkg.path
1484
+ });
1485
+ logger.verbose(`Found ${farver.cyan(allCommits.length)} commits for package ${farver.bold(pkg.name)} since ${farver.cyan(fromTag ?? "start")}`);
1486
+ return allCommits;
1487
+ }
1413
1488
  /**
1414
1489
  * Check if a file path touches any package folder.
1415
1490
  * @param file - The file path to check
@@ -2052,25 +2127,13 @@ async function prepareWorkflow(options) {
2052
2127
  logger.error("Failed to write version overrides file:", e);
2053
2128
  }
2054
2129
  } else if (Object.keys(newOverrides).length > 0) logger.step("Version overrides unchanged. Skipping write.");
2055
- if (Object.keys(newOverrides).length === 0 && Object.keys(existingOverrides).length > 0) {
2056
- let shouldRemoveOverrides = false;
2057
- for (const update of allUpdates) {
2058
- const overriddenVersion = existingOverrides[update.package.name];
2059
- if (overriddenVersion) {
2060
- if (compare(update.newVersion, overriddenVersion.version) > 0) {
2061
- shouldRemoveOverrides = true;
2062
- break;
2063
- }
2064
- }
2065
- }
2066
- if (shouldRemoveOverrides) {
2067
- logger.info("Removing obsolete version overrides file...");
2068
- try {
2069
- await rm(overridesPath);
2070
- logger.success("Successfully removed obsolete version overrides file.");
2071
- } catch (e) {
2072
- logger.error("Failed to remove obsolete version overrides file:", e);
2073
- }
2130
+ if (Object.keys(newOverrides).length === 0 && hasOverrideChanges) {
2131
+ logger.info("Removing obsolete version overrides file...");
2132
+ try {
2133
+ await rm(overridesPath);
2134
+ logger.success("Successfully removed obsolete version overrides file.");
2135
+ } catch (e) {
2136
+ if (formatUnknownError(e).code !== "ENOENT") logger.error("Failed to remove obsolete version overrides file:", e);
2074
2137
  }
2075
2138
  }
2076
2139
  if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) logger.warn("No packages have changes requiring a release");
@@ -2086,26 +2149,44 @@ async function prepareWorkflow(options) {
2086
2149
  const groupedPackageCommits = await getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
2087
2150
  const globalCommitsPerPackage = await getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
2088
2151
  const changelogPromises = allUpdates.map((update) => {
2089
- const pkgCommits = groupedPackageCommits.get(update.package.name) || [];
2090
- const globalCommits = globalCommitsPerPackage.get(update.package.name) || [];
2091
- const allCommits = [...pkgCommits, ...globalCommits];
2092
- if (allCommits.length === 0) {
2093
- logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
2094
- return Promise.resolve();
2095
- }
2096
- logger.verbose(`Updating changelog for ${farver.cyan(update.package.name)}`);
2097
- return updateChangelog({
2098
- normalizedOptions: {
2099
- ...options,
2100
- workspaceRoot: options.workspaceRoot
2101
- },
2102
- githubClient: options.githubClient,
2103
- workspacePackage: update.package,
2104
- version: update.newVersion,
2105
- previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : void 0,
2106
- commits: allCommits,
2107
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
2108
- });
2152
+ return (async () => {
2153
+ let pkgCommits = groupedPackageCommits.get(update.package.name) || [];
2154
+ let globalCommits = globalCommitsPerPackage.get(update.package.name) || [];
2155
+ let previousVersionForChangelog = update.currentVersion !== "0.0.0" ? update.currentVersion : void 0;
2156
+ if (options.changelog.combinePrereleaseIntoFirstStable && semver.prerelease(update.currentVersion) != null && semver.prerelease(update.newVersion) == null) {
2157
+ const stableTagResult = await getMostRecentPackageStableTag(options.workspaceRoot, update.package.name);
2158
+ if (!stableTagResult.ok) logger.warn(`Failed to resolve stable tag for ${update.package.name}: ${stableTagResult.error.message}`);
2159
+ else {
2160
+ const stableTag = stableTagResult.value;
2161
+ if (stableTag) {
2162
+ logger.verbose(`Combining prerelease changelog entries into stable release for ${update.package.name} using base tag ${stableTag}`);
2163
+ const stableBaseCommits = await getPackageCommitsSinceTag(options.workspaceRoot, update.package, stableTag);
2164
+ pkgCommits = stableBaseCommits;
2165
+ globalCommits = (await getGlobalCommitsPerPackage(options.workspaceRoot, new Map([[update.package.name, stableBaseCommits]]), workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode)).get(update.package.name) || [];
2166
+ const atIndex = stableTag.lastIndexOf("@");
2167
+ if (atIndex !== -1) previousVersionForChangelog = stableTag.slice(atIndex + 1);
2168
+ }
2169
+ }
2170
+ }
2171
+ const allCommits = [...pkgCommits, ...globalCommits];
2172
+ if (allCommits.length === 0) {
2173
+ logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
2174
+ return;
2175
+ }
2176
+ logger.verbose(`Updating changelog for ${farver.cyan(update.package.name)}`);
2177
+ await updateChangelog({
2178
+ normalizedOptions: {
2179
+ ...options,
2180
+ workspaceRoot: options.workspaceRoot
2181
+ },
2182
+ githubClient: options.githubClient,
2183
+ workspacePackage: update.package,
2184
+ version: update.newVersion,
2185
+ previousVersion: previousVersionForChangelog,
2186
+ commits: allCommits,
2187
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
2188
+ });
2189
+ })();
2109
2190
  }).filter((p) => p != null);
2110
2191
  const updates = await Promise.all(changelogPromises);
2111
2192
  logger.success(`Updated ${updates.length} changelog(s)`);
@@ -2262,6 +2343,43 @@ async function publishPackage(packageName, version, workspaceRoot, options) {
2262
2343
 
2263
2344
  //#endregion
2264
2345
  //#region src/workflows/publish.ts
2346
+ async function cleanupPublishedOverrides(options, workspacePackages, publishedPackageNames) {
2347
+ if (publishedPackageNames.length === 0) return false;
2348
+ if (options.dryRun) {
2349
+ logger.verbose("Dry-run: skipping override cleanup");
2350
+ return false;
2351
+ }
2352
+ const overridesPath = join(options.workspaceRoot, ucdjsReleaseOverridesPath);
2353
+ let overrides;
2354
+ try {
2355
+ overrides = JSON.parse(await readFile(overridesPath, "utf-8"));
2356
+ } catch {
2357
+ return false;
2358
+ }
2359
+ const versionsByPackage = new Map(workspacePackages.map((pkg) => [pkg.name, pkg.version]));
2360
+ const publishedSet = new Set(publishedPackageNames);
2361
+ const removed = [];
2362
+ for (const [pkgName, override] of Object.entries(overrides)) {
2363
+ if (!publishedSet.has(pkgName)) continue;
2364
+ const currentVersion = versionsByPackage.get(pkgName);
2365
+ const current = currentVersion ? semver.valid(currentVersion) : null;
2366
+ const target = semver.valid(override.version);
2367
+ if (current && target && semver.gte(current, target)) {
2368
+ delete overrides[pkgName];
2369
+ removed.push(pkgName);
2370
+ }
2371
+ }
2372
+ if (removed.length === 0) return false;
2373
+ logger.step(`Cleaning up satisfied overrides (${removed.length})...`);
2374
+ if (Object.keys(overrides).length === 0) {
2375
+ await rm(overridesPath, { force: true });
2376
+ logger.success("Removed release override file (all entries satisfied)");
2377
+ return true;
2378
+ }
2379
+ await writeFile(overridesPath, JSON.stringify(overrides, null, 2), "utf-8");
2380
+ logger.success(`Removed satisfied overrides: ${removed.join(", ")}`);
2381
+ return true;
2382
+ }
2265
2383
  async function publishWorkflow(options) {
2266
2384
  logger.section("📦 Publishing Packages");
2267
2385
  const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
@@ -2346,6 +2464,18 @@ async function publishWorkflow(options) {
2346
2464
  for (const pkg of status.failed) logger.item(` ${farver.red("•")} ${pkg}`);
2347
2465
  }
2348
2466
  if (status.failed.length > 0) exitWithError(`Publishing completed with ${status.failed.length} failure(s)`);
2467
+ if (await cleanupPublishedOverrides(options, workspacePackages, status.published) && !options.dryRun) {
2468
+ logger.step("Committing override cleanup...");
2469
+ const commitResult = await commitPaths([ucdjsReleaseOverridesPath], "chore: cleanup release overrides", options.workspaceRoot);
2470
+ if (!commitResult.ok) exitWithError("Failed to commit override cleanup.", void 0, commitResult.error);
2471
+ if (commitResult.value) {
2472
+ const currentBranch = await getCurrentBranch(options.workspaceRoot);
2473
+ if (!currentBranch.ok) exitWithError("Failed to detect current branch for override cleanup push.", void 0, currentBranch.error);
2474
+ const pushResult = await pushBranch(currentBranch.value, options.workspaceRoot);
2475
+ if (!pushResult.ok) exitWithError("Failed to push override cleanup commit.", void 0, pushResult.error);
2476
+ logger.success(`Pushed override cleanup commit to ${farver.cyan(currentBranch.value)}`);
2477
+ }
2478
+ }
2349
2479
  logger.success("All packages published successfully!");
2350
2480
  }
2351
2481
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.51",
3
+ "version": "0.1.0-beta.52",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",