@ucdjs/release-scripts 0.1.0-beta.50 → 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
@@ -347,6 +347,59 @@ var GitHubClient = class {
347
347
  });
348
348
  logger.info(`Commit status set to ${farver.cyan(state)} for ${farver.gray(sha.substring(0, 7))}`);
349
349
  }
350
+ async upsertReleaseByTag({ tagName, name, body, prerelease = false }) {
351
+ const encodedTag = encodeURIComponent(tagName);
352
+ let existingRelease = null;
353
+ try {
354
+ existingRelease = await this.request(`/repos/${this.owner}/${this.repo}/releases/tags/${encodedTag}`);
355
+ } catch (error) {
356
+ if (formatUnknownError(error).status !== 404) throw error;
357
+ }
358
+ if (existingRelease) {
359
+ logger.verbose(`Updating release for tag ${farver.cyan(tagName)}`);
360
+ const updated = await this.request(`/repos/${this.owner}/${this.repo}/releases/${existingRelease.id}`, {
361
+ method: "PATCH",
362
+ body: JSON.stringify({
363
+ name,
364
+ body,
365
+ prerelease,
366
+ draft: false
367
+ })
368
+ });
369
+ logger.info(`Updated GitHub release for ${farver.cyan(tagName)}`);
370
+ return {
371
+ release: {
372
+ id: updated.id,
373
+ tagName: updated.tag_name,
374
+ name: updated.name ?? name,
375
+ htmlUrl: updated.html_url
376
+ },
377
+ created: false
378
+ };
379
+ }
380
+ logger.verbose(`Creating release for tag ${farver.cyan(tagName)}`);
381
+ const created = await this.request(`/repos/${this.owner}/${this.repo}/releases`, {
382
+ method: "POST",
383
+ body: JSON.stringify({
384
+ tag_name: tagName,
385
+ name,
386
+ body,
387
+ prerelease,
388
+ draft: false,
389
+ generate_release_notes: body == null
390
+ })
391
+ });
392
+ logger.info(`Created GitHub release for ${farver.cyan(tagName)}`);
393
+ return {
394
+ release: {
395
+ id: created.id,
396
+ tagName: created.tag_name,
397
+ name: created.name ?? name,
398
+ htmlUrl: created.html_url
399
+ },
400
+ created: true
401
+ };
402
+ }
350
403
  async resolveAuthorInfo(info) {
351
404
  if (info.login) return info;
352
405
  try {
@@ -471,7 +524,8 @@ function normalizeReleaseScriptsOptions(options) {
471
524
  changelog: {
472
525
  enabled: changelog.enabled ?? true,
473
526
  template: changelog.template ?? DEFAULT_CHANGELOG_TEMPLATE,
474
- emojis: changelog.emojis ?? true
527
+ emojis: changelog.emojis ?? true,
528
+ combinePrereleaseIntoFirstStable: changelog.combinePrereleaseIntoFirstStable ?? false
475
529
  },
476
530
  types: types ? {
477
531
  ...DEFAULT_TYPES,
@@ -687,6 +741,42 @@ async function commitChanges(message, workspaceRoot) {
687
741
  return err(gitError);
688
742
  }
689
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
+ }
690
780
  async function pushBranch(branch, workspaceRoot, options) {
691
781
  try {
692
782
  const args = [
@@ -751,6 +841,28 @@ async function getMostRecentPackageTag(workspaceRoot, packageName) {
751
841
  return err(toGitError("getMostRecentPackageTag", error));
752
842
  }
753
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
+ }
754
866
  /**
755
867
  * Builds a mapping of commit SHAs to the list of files changed in each commit
756
868
  * within a given inclusive range.
@@ -1067,6 +1179,7 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1067
1179
  const prePatchAlpha = getNextPrereleaseVersion(currentVersion, "prepatch", "alpha");
1068
1180
  const preMinorAlpha = getNextPrereleaseVersion(currentVersion, "preminor", "alpha");
1069
1181
  const preMajorAlpha = getNextPrereleaseVersion(currentVersion, "premajor", "alpha");
1182
+ const isCurrentPrerelease = prereleaseIdentifier != null;
1070
1183
  const choices = [
1071
1184
  {
1072
1185
  value: "skip",
@@ -1080,6 +1193,10 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1080
1193
  value: "as-is",
1081
1194
  title: `as-is ${farver.dim("(keep current version)")}`
1082
1195
  },
1196
+ ...isCurrentPrerelease ? [{
1197
+ value: "next-prerelease",
1198
+ title: `next prerelease ${farver.bold(nextDefaultPrerelease)}`
1199
+ }] : [],
1083
1200
  {
1084
1201
  value: "patch",
1085
1202
  title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
@@ -1104,6 +1221,7 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1104
1221
  const initialValue = defaultChoice === "auto" ? suggestedVersion === currentVersion ? "skip" : "suggested" : defaultChoice;
1105
1222
  const initial = Math.max(0, choices.findIndex((choice) => choice.value === initialValue));
1106
1223
  const prereleaseVersionByChoice = {
1224
+ "next-prerelease": nextDefaultPrerelease,
1107
1225
  "next": nextDefaultPrerelease,
1108
1226
  "next-beta": nextBeta,
1109
1227
  "next-alpha": nextAlpha,
@@ -1357,6 +1475,16 @@ async function getWorkspacePackageGroupedCommits(workspaceRoot, packages) {
1357
1475
  for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
1358
1476
  return changedPackages;
1359
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
+ }
1360
1488
  /**
1361
1489
  * Check if a file path touches any package folder.
1362
1490
  * @param file - The file path to check
@@ -1999,25 +2127,13 @@ async function prepareWorkflow(options) {
1999
2127
  logger.error("Failed to write version overrides file:", e);
2000
2128
  }
2001
2129
  } else if (Object.keys(newOverrides).length > 0) logger.step("Version overrides unchanged. Skipping write.");
2002
- if (Object.keys(newOverrides).length === 0 && Object.keys(existingOverrides).length > 0) {
2003
- let shouldRemoveOverrides = false;
2004
- for (const update of allUpdates) {
2005
- const overriddenVersion = existingOverrides[update.package.name];
2006
- if (overriddenVersion) {
2007
- if (compare(update.newVersion, overriddenVersion.version) > 0) {
2008
- shouldRemoveOverrides = true;
2009
- break;
2010
- }
2011
- }
2012
- }
2013
- if (shouldRemoveOverrides) {
2014
- logger.info("Removing obsolete version overrides file...");
2015
- try {
2016
- await rm(overridesPath);
2017
- logger.success("Successfully removed obsolete version overrides file.");
2018
- } catch (e) {
2019
- logger.error("Failed to remove obsolete version overrides file:", e);
2020
- }
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);
2021
2137
  }
2022
2138
  }
2023
2139
  if (allUpdates.filter((u) => u.hasDirectChanges).length === 0) logger.warn("No packages have changes requiring a release");
@@ -2033,26 +2149,44 @@ async function prepareWorkflow(options) {
2033
2149
  const groupedPackageCommits = await getWorkspacePackageGroupedCommits(options.workspaceRoot, workspacePackages);
2034
2150
  const globalCommitsPerPackage = await getGlobalCommitsPerPackage(options.workspaceRoot, groupedPackageCommits, workspacePackages, options.globalCommitMode === "none" ? false : options.globalCommitMode);
2035
2151
  const changelogPromises = allUpdates.map((update) => {
2036
- const pkgCommits = groupedPackageCommits.get(update.package.name) || [];
2037
- const globalCommits = globalCommitsPerPackage.get(update.package.name) || [];
2038
- const allCommits = [...pkgCommits, ...globalCommits];
2039
- if (allCommits.length === 0) {
2040
- logger.verbose(`No commits for ${update.package.name}, skipping changelog`);
2041
- return Promise.resolve();
2042
- }
2043
- logger.verbose(`Updating changelog for ${farver.cyan(update.package.name)}`);
2044
- return updateChangelog({
2045
- normalizedOptions: {
2046
- ...options,
2047
- workspaceRoot: options.workspaceRoot
2048
- },
2049
- githubClient: options.githubClient,
2050
- workspacePackage: update.package,
2051
- version: update.newVersion,
2052
- previousVersion: update.currentVersion !== "0.0.0" ? update.currentVersion : void 0,
2053
- commits: allCommits,
2054
- date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0]
2055
- });
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
+ })();
2056
2190
  }).filter((p) => p != null);
2057
2191
  const updates = await Promise.all(changelogPromises);
2058
2192
  logger.success(`Updated ${updates.length} changelog(s)`);
@@ -2209,6 +2343,43 @@ async function publishPackage(packageName, version, workspaceRoot, options) {
2209
2343
 
2210
2344
  //#endregion
2211
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
+ }
2212
2383
  async function publishWorkflow(options) {
2213
2384
  logger.section("📦 Publishing Packages");
2214
2385
  const discovered = await discoverWorkspacePackages(options.workspaceRoot, options);
@@ -2260,10 +2431,26 @@ async function publishWorkflow(options) {
2260
2431
  status.published.push(packageName);
2261
2432
  logger.step(`Creating git tag ${farver.cyan(`${packageName}@${version}`)}...`);
2262
2433
  const tagResult = await createAndPushPackageTag(packageName, version, options.workspaceRoot);
2434
+ const tagName = `${packageName}@${version}`;
2263
2435
  if (!tagResult.ok) {
2264
2436
  logger.error(`Failed to create/push tag: ${tagResult.error.message}`);
2265
- logger.warn(`Package was published but tag was not created. You may need to create it manually.`);
2266
- } else logger.success(`Created and pushed tag ${farver.cyan(`${packageName}@${version}`)}`);
2437
+ status.failed.push(packageName);
2438
+ exitWithError(`Publishing failed for ${packageName}: could not create git tag`, "Ensure the workflow token can push tags (contents: write) and git credentials are configured", tagResult.error);
2439
+ }
2440
+ logger.success(`Created and pushed tag ${farver.cyan(tagName)}`);
2441
+ logger.step(`Creating GitHub release for ${farver.cyan(tagName)}...`);
2442
+ try {
2443
+ const releaseResult = await options.githubClient.upsertReleaseByTag({
2444
+ tagName,
2445
+ name: tagName,
2446
+ prerelease: Boolean(semver.prerelease(version))
2447
+ });
2448
+ if (releaseResult.release.htmlUrl) logger.success(`${releaseResult.created ? "Created" : "Updated"} GitHub release: ${releaseResult.release.htmlUrl}`);
2449
+ else logger.success(`${releaseResult.created ? "Created" : "Updated"} GitHub release for ${farver.cyan(tagName)}`);
2450
+ } catch (error) {
2451
+ status.failed.push(packageName);
2452
+ exitWithError(`Publishing failed for ${packageName}: could not create GitHub release`, "Ensure the workflow token can write repository contents and releases", error);
2453
+ }
2267
2454
  }
2268
2455
  logger.section("📊 Publishing Summary");
2269
2456
  logger.item(`${farver.green("✓")} Published: ${status.published.length} package(s)`);
@@ -2277,6 +2464,18 @@ async function publishWorkflow(options) {
2277
2464
  for (const pkg of status.failed) logger.item(` ${farver.red("•")} ${pkg}`);
2278
2465
  }
2279
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
+ }
2280
2479
  logger.success("All packages published successfully!");
2281
2480
  }
2282
2481
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.50",
3
+ "version": "0.1.0-beta.52",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",