@ucdjs/release-scripts 0.1.0-beta.60 → 0.1.0-beta.62

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/README.md CHANGED
@@ -1,3 +1,102 @@
1
1
  # @ucdjs/release-scripts
2
2
 
3
- This repository contains release and publish scripts for the UCDJS organization.
3
+ [![npm version][npm-version-src]][npm-version-href]
4
+ [![npm downloads][npm-downloads-src]][npm-downloads-href]
5
+
6
+ Monorepo release automation for pnpm workspaces. Handles version calculation, dependency graph resolution, changelog generation, and GitHub integration.
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @ucdjs/release-scripts
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```typescript
17
+ import { createReleaseScripts } from "@ucdjs/release-scripts";
18
+
19
+ const release = await createReleaseScripts({
20
+ repo: "owner/repo",
21
+ githubToken: process.env.GITHUB_TOKEN,
22
+ });
23
+
24
+ // Prepare a release (calculate versions, update package.json files, create PR)
25
+ const result = await release.prepare();
26
+
27
+ // Verify a release branch matches expected state
28
+ await release.verify();
29
+
30
+ // Publish packages to npm
31
+ await release.publish();
32
+ ```
33
+
34
+ ### Configuration
35
+
36
+ ```typescript
37
+ const release = await createReleaseScripts({
38
+ repo: "owner/repo",
39
+ githubToken: "...",
40
+ workspaceRoot: process.cwd(),
41
+ dryRun: false,
42
+ safeguards: true,
43
+ globalCommitMode: "dependencies",
44
+ packages: {
45
+ include: ["@scope/pkg-a", "@scope/pkg-b"],
46
+ exclude: ["@scope/internal"],
47
+ excludePrivate: true,
48
+ },
49
+ branch: {
50
+ release: "release/next",
51
+ default: "main",
52
+ },
53
+ npm: {
54
+ provenance: true,
55
+ access: "public",
56
+ },
57
+ changelog: {
58
+ enabled: true,
59
+ emojis: true,
60
+ },
61
+ });
62
+ ```
63
+
64
+ ### Package Discovery
65
+
66
+ ```typescript
67
+ // List all workspace packages
68
+ const packages = await release.packages.list();
69
+
70
+ // Get a specific package
71
+ const pkg = await release.packages.get("@scope/pkg-a");
72
+ ```
73
+
74
+ ### Workflows
75
+
76
+ #### `prepare()`
77
+
78
+ Calculates version bumps from conventional commits, updates `package.json` files, generates changelogs, and creates/updates a release pull request.
79
+
80
+ #### `verify()`
81
+
82
+ Validates that a release branch matches expected release artifacts. Compares expected vs actual versions and dependency ranges, then sets a GitHub commit status.
83
+
84
+ #### `publish()`
85
+
86
+ Publishes packages to npm in topological order with provenance support, creates git tags, and pushes them to the remote.
87
+
88
+ ## CLI Flags
89
+
90
+ When used in a script, the following flags are supported:
91
+
92
+ - `--dry` / `-d` - Dry-run mode, no changes are made
93
+ - `--verbose` / `-v` - Enable verbose logging
94
+
95
+ ## 📄 License
96
+
97
+ Published under [MIT License](./LICENSE).
98
+
99
+ [npm-version-src]: https://img.shields.io/npm/v/@ucdjs/release-scripts?style=flat&colorA=18181B&colorB=4169E1
100
+ [npm-version-href]: https://npmjs.com/package/@ucdjs/release-scripts
101
+ [npm-downloads-src]: https://img.shields.io/npm/dm/@ucdjs/release-scripts?style=flat&colorA=18181B&colorB=4169E1
102
+ [npm-downloads-href]: https://npmjs.com/package/@ucdjs/release-scripts
@@ -1,6 +1,5 @@
1
1
  import * as path from "node:path";
2
2
  import * as fs from "node:fs";
3
-
4
3
  //#region node_modules/.pnpm/eta@4.5.1/node_modules/eta/dist/index.mjs
5
4
  var EtaError = class extends Error {
6
5
  constructor(message) {
@@ -476,6 +475,5 @@ var Eta = class extends Eta$1 {
476
475
  readFile = readFile;
477
476
  resolvePath = resolvePath;
478
477
  };
479
-
480
478
  //#endregion
481
- export { Eta as t };
479
+ export { Eta as t };
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { t as Eta } from "./eta-DAZlmVBQ.mjs";
1
+ import { t as Eta } from "./eta-g9ausaEx.mjs";
2
2
  import process from "node:process";
3
3
  import readline from "node:readline";
4
4
  import { parseArgs } from "node:util";
@@ -10,7 +10,6 @@ import { getCommits, groupByType } from "commit-parser";
10
10
  import { dedent } from "@luxass/utils";
11
11
  import semver, { gt } from "semver";
12
12
  import prompts from "prompts";
13
-
14
13
  //#region src/shared/utils.ts
15
14
  const ucdjsReleaseOverridesPath = ".github/ucdjs-release.overrides.json";
16
15
  function parseCLIFlags() {
@@ -114,7 +113,6 @@ async function dryRun(bin, args, opts) {
114
113
  async function runIfNotDry(bin, args, opts) {
115
114
  return getIsDryRun() ? dryRun(bin, args, opts) : run(bin, args, opts);
116
115
  }
117
-
118
116
  //#endregion
119
117
  //#region src/shared/errors.ts
120
118
  function isRecord(value) {
@@ -219,16 +217,16 @@ function printReleaseError(error) {
219
217
  function exitWithError(message, hint, cause) {
220
218
  throw new ReleaseError(message, hint, cause);
221
219
  }
222
-
223
220
  //#endregion
224
221
  //#region src/operations/changelog-format.ts
222
+ const HASH_PREFIX_RE = /^#/;
225
223
  function formatCommitLine({ commit, owner, repo, authors }) {
226
224
  const commitUrl = `https://github.com/${owner}/${repo}/commit/${commit.hash}`;
227
225
  let line = `${commit.description}`;
228
226
  const references = commit.references ?? [];
229
227
  for (const ref of references) {
230
228
  if (!ref.value) continue;
231
- const number = Number.parseInt(ref.value.replace(/^#/, ""), 10);
229
+ const number = Number.parseInt(ref.value.replace(HASH_PREFIX_RE, ""), 10);
232
230
  if (Number.isNaN(number)) continue;
233
231
  if (ref.type === "issue") {
234
232
  line += ` ([Issue ${ref.value}](https://github.com/${owner}/${repo}/issues/${number}))`;
@@ -263,7 +261,6 @@ function buildTemplateGroups(options) {
263
261
  };
264
262
  });
265
263
  }
266
-
267
264
  //#endregion
268
265
  //#region src/core/github.ts
269
266
  function toGitHubError(operation, error) {
@@ -459,9 +456,10 @@ var GitHubClient = class {
459
456
  function createGitHubClient(options) {
460
457
  return new GitHubClient(options);
461
458
  }
459
+ const NON_WHITESPACE_RE = /\S/;
462
460
  function dedentString(str) {
463
461
  const lines = str.split("\n");
464
- const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(/\S/)), Infinity);
462
+ const minIndent = lines.filter((line) => line.trim().length > 0).reduce((min, line) => Math.min(min, line.search(NON_WHITESPACE_RE)), Infinity);
465
463
  return lines.map((line) => minIndent === Infinity ? line : line.slice(minIndent)).join("\n").trim();
466
464
  }
467
465
  function generatePullRequestBody(updates, body) {
@@ -475,7 +473,6 @@ function generatePullRequestBody(updates, body) {
475
473
  hasDirectChanges: u.hasDirectChanges
476
474
  })) });
477
475
  }
478
-
479
476
  //#endregion
480
477
  //#region src/options.ts
481
478
  const DEFAULT_PR_BODY_TEMPLATE = dedent`
@@ -586,7 +583,6 @@ function normalizeReleaseScriptsOptions(options) {
586
583
  }
587
584
  };
588
585
  }
589
-
590
586
  //#endregion
591
587
  //#region src/types.ts
592
588
  function ok(value) {
@@ -601,9 +597,10 @@ function err(error) {
601
597
  error
602
598
  };
603
599
  }
604
-
605
600
  //#endregion
606
601
  //#region src/core/git.ts
602
+ const CHECKOUT_BRANCH_RE = /Switched to (?:a new )?branch '(.+)'/;
603
+ const COMMIT_HASH_RE = /^[0-9a-f]{40}$/i;
607
604
  function toGitError(operation, error) {
608
605
  const formatted = formatUnknownError(error);
609
606
  return {
@@ -686,6 +683,30 @@ async function isWorkingDirectoryClean(workspaceRoot) {
686
683
  * @param {string} workspaceRoot - The root directory of the workspace
687
684
  * @returns {Promise<boolean>} Promise resolving to true if branch exists, false otherwise
688
685
  */
686
+ /**
687
+ * Check if a remote branch exists on origin
688
+ * @param {string} branch - The branch name to check
689
+ * @param {string} workspaceRoot - The root directory of the workspace
690
+ * @returns {Promise<Result<boolean, GitError>>} Promise resolving to true if remote branch exists
691
+ */
692
+ async function doesRemoteBranchExist(branch, workspaceRoot) {
693
+ try {
694
+ await run("git", [
695
+ "ls-remote",
696
+ "--exit-code",
697
+ "--heads",
698
+ "origin",
699
+ branch
700
+ ], { nodeOptions: {
701
+ cwd: workspaceRoot,
702
+ stdio: "pipe"
703
+ } });
704
+ return ok(true);
705
+ } catch (error) {
706
+ logger.verbose(`Remote branch "origin/${branch}" does not exist: ${formatUnknownError(error).message}`);
707
+ return ok(false);
708
+ }
709
+ }
689
710
  async function doesBranchExist(branch, workspaceRoot) {
690
711
  try {
691
712
  await run("git", [
@@ -751,7 +772,7 @@ async function checkoutBranch(branch, workspaceRoot) {
751
772
  cwd: workspaceRoot,
752
773
  stdio: "pipe"
753
774
  } })).stderr.trim();
754
- const match = output.match(/Switched to (?:a new )?branch '(.+)'/);
775
+ const match = output.match(CHECKOUT_BRANCH_RE);
755
776
  if (match && match[1] === branch) {
756
777
  logger.info(`Successfully switched to branch: ${farver.green(branch)}`);
757
778
  return ok(true);
@@ -985,10 +1006,9 @@ async function getGroupedFilesByCommitSha(workspaceRoot, from, to) {
985
1006
  } });
986
1007
  const lines = stdout.trim().split("\n").filter((line) => line.trim() !== "");
987
1008
  let currentSha = null;
988
- const HASH_REGEX = /^[0-9a-f]{40}$/i;
989
1009
  for (const line of lines) {
990
1010
  const trimmedLine = line.trim();
991
- if (HASH_REGEX.test(trimmedLine)) {
1011
+ if (COMMIT_HASH_RE.test(trimmedLine)) {
992
1012
  currentSha = trimmedLine;
993
1013
  commitsMap.set(currentSha, []);
994
1014
  continue;
@@ -1055,9 +1075,9 @@ async function createAndPushPackageTag(packageName, version, workspaceRoot) {
1055
1075
  if (!createResult.ok) return createResult;
1056
1076
  return pushTag(`${packageName}@${version}`, workspaceRoot);
1057
1077
  }
1058
-
1059
1078
  //#endregion
1060
1079
  //#region src/core/changelog.ts
1080
+ const CHANGELOG_VERSION_RE = /##\s+(?:<small>)?\[?([^\](\s<]+)/;
1061
1081
  const excludeAuthors = [
1062
1082
  /\[bot\]/i,
1063
1083
  /dependabot/i,
@@ -1125,7 +1145,7 @@ async function updateChangelog(options) {
1125
1145
  const insertAt = parsed.headerLineEnd + 1;
1126
1146
  const before = lines.slice(0, insertAt);
1127
1147
  const after = lines.slice(insertAt);
1128
- if (before.length > 0 && before[before.length - 1] !== "") before.push("");
1148
+ if (before.length > 0 && before.at(-1) !== "") before.push("");
1129
1149
  updatedContent = [
1130
1150
  ...before,
1131
1151
  newEntry,
@@ -1154,7 +1174,7 @@ async function resolveCommitAuthors(commits, githubClient) {
1154
1174
  });
1155
1175
  commitAuthors.set(commit.hash, authorsForCommit);
1156
1176
  }
1157
- const authors = Array.from(authorMap.values());
1177
+ const authors = [...authorMap.values()];
1158
1178
  await Promise.all(authors.map((info) => githubClient.resolveAuthorInfo(info)));
1159
1179
  return commitAuthors;
1160
1180
  }
@@ -1174,7 +1194,7 @@ function parseChangelog(content) {
1174
1194
  for (let i = headerLineEnd + 1; i < lines.length; i++) {
1175
1195
  const line = lines[i].trim();
1176
1196
  if (line.startsWith("## ")) {
1177
- const versionMatch = line.match(/##\s+(?:<small>)?\[?([^\](\s<]+)/);
1197
+ const versionMatch = line.match(CHANGELOG_VERSION_RE);
1178
1198
  if (versionMatch) {
1179
1199
  const version = versionMatch[1];
1180
1200
  const lineStart = i;
@@ -1199,7 +1219,6 @@ function parseChangelog(content) {
1199
1219
  headerLineEnd
1200
1220
  };
1201
1221
  }
1202
-
1203
1222
  //#endregion
1204
1223
  //#region src/operations/semver.ts
1205
1224
  function isValidSemver(version) {
@@ -1207,6 +1226,23 @@ function isValidSemver(version) {
1207
1226
  }
1208
1227
  function getNextVersion(currentVersion, bump) {
1209
1228
  if (bump === "none") return currentVersion;
1229
+ if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
1230
+ if (semver.prerelease(currentVersion)) {
1231
+ const identifier = getPrereleaseIdentifier(currentVersion);
1232
+ const next = identifier ? semver.inc(currentVersion, "prerelease", identifier) : semver.inc(currentVersion, "prerelease");
1233
+ if (!next) throw new Error(`Failed to bump prerelease version ${currentVersion}`);
1234
+ return next;
1235
+ }
1236
+ const next = semver.inc(currentVersion, bump);
1237
+ if (!next) throw new Error(`Failed to bump version ${currentVersion} with bump ${bump}`);
1238
+ return next;
1239
+ }
1240
+ /**
1241
+ * Like getNextVersion but always produces a stable version, even when
1242
+ * currentVersion is a prerelease. Use this when the caller explicitly wants
1243
+ * to promote to a stable release (e.g. patch/minor/major prompt choices).
1244
+ */
1245
+ function getNextStableVersion(currentVersion, bump) {
1210
1246
  if (!isValidSemver(currentVersion)) throw new Error(`Cannot bump version for invalid semver: ${currentVersion}`);
1211
1247
  const next = semver.inc(currentVersion, bump);
1212
1248
  if (!next) throw new Error(`Failed to bump version ${currentVersion} with bump ${bump}`);
@@ -1235,7 +1271,6 @@ function getNextPrereleaseVersion(currentVersion, mode, identifier) {
1235
1271
  if (!next) throw new Error(`Failed to compute prerelease version for ${currentVersion}`);
1236
1272
  return next;
1237
1273
  }
1238
-
1239
1274
  //#endregion
1240
1275
  //#region src/core/prompts.ts
1241
1276
  async function selectPackagePrompt(packages) {
@@ -1288,15 +1323,15 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1288
1323
  }] : [],
1289
1324
  {
1290
1325
  value: "patch",
1291
- title: `patch ${farver.bold(getNextVersion(pkg.version, "patch"))}`
1326
+ title: `patch ${farver.bold(getNextStableVersion(pkg.version, "patch"))}`
1292
1327
  },
1293
1328
  {
1294
1329
  value: "minor",
1295
- title: `minor ${farver.bold(getNextVersion(pkg.version, "minor"))}`
1330
+ title: `minor ${farver.bold(getNextStableVersion(pkg.version, "minor"))}`
1296
1331
  },
1297
1332
  {
1298
1333
  value: "major",
1299
- title: `major ${farver.bold(getNextVersion(pkg.version, "major"))}`
1334
+ title: `major ${farver.bold(getNextStableVersion(pkg.version, "major"))}`
1300
1335
  },
1301
1336
  {
1302
1337
  value: "prerelease",
@@ -1398,7 +1433,8 @@ async function selectVersionPrompt(workspaceRoot, pkg, currentVersion, suggested
1398
1433
  }
1399
1434
  const prereleaseVersion = prereleaseVersionByChoice[answers.version];
1400
1435
  if (prereleaseVersion) return prereleaseVersion;
1401
- return getNextVersion(pkg.version, answers.version);
1436
+ const stableBump = answers.version;
1437
+ return getNextStableVersion(pkg.version, stableBump);
1402
1438
  }
1403
1439
  async function confirmOverridePrompt(pkg, overrideVersion) {
1404
1440
  const response = await prompts({
@@ -1417,7 +1453,6 @@ async function confirmOverridePrompt(pkg, overrideVersion) {
1417
1453
  if (!response.choice) return null;
1418
1454
  return response.choice;
1419
1455
  }
1420
-
1421
1456
  //#endregion
1422
1457
  //#region src/core/workspace.ts
1423
1458
  function toWorkspaceError(operation, error) {
@@ -1501,7 +1536,7 @@ async function findWorkspacePackages(workspaceRoot, options) {
1501
1536
  };
1502
1537
  });
1503
1538
  const packages = await Promise.all(promises);
1504
- if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green(Array.from(excludedPackages).join(", "))}`);
1539
+ if (excludedPackages.size > 0) logger.info(`Excluded packages: ${farver.green([...excludedPackages].join(", "))}`);
1505
1540
  return packages.filter((pkg) => pkg !== null);
1506
1541
  } catch (err) {
1507
1542
  logger.error("Error discovering workspace packages:", err);
@@ -1517,7 +1552,6 @@ function shouldIncludePackage(pkg, options) {
1517
1552
  if (options.exclude?.includes(pkg.name)) return false;
1518
1553
  return true;
1519
1554
  }
1520
-
1521
1555
  //#endregion
1522
1556
  //#region src/operations/branch.ts
1523
1557
  async function prepareReleaseBranch(options) {
@@ -1538,9 +1572,13 @@ async function prepareReleaseBranch(options) {
1538
1572
  const checkedOut = await checkoutBranch(releaseBranch, workspaceRoot);
1539
1573
  if (!checkedOut.ok) return checkedOut;
1540
1574
  if (branchExists.value) {
1541
- const pulled = await pullLatestChanges(releaseBranch, workspaceRoot);
1542
- if (!pulled.ok) return pulled;
1543
- if (!pulled.value) logger.warn("Failed to pull latest changes, continuing anyway.");
1575
+ const remoteExists = await doesRemoteBranchExist(releaseBranch, workspaceRoot);
1576
+ if (!remoteExists.ok) return remoteExists;
1577
+ if (remoteExists.value) {
1578
+ const pulled = await pullLatestChanges(releaseBranch, workspaceRoot);
1579
+ if (!pulled.ok) return pulled;
1580
+ if (!pulled.value) logger.warn("Failed to pull latest changes, continuing anyway.");
1581
+ } else logger.info(`Remote branch "origin/${releaseBranch}" does not exist yet, skipping pull.`);
1544
1582
  }
1545
1583
  const rebased = await rebaseBranch(defaultBranch, workspaceRoot);
1546
1584
  if (!rebased.ok) return rebased;
@@ -1557,7 +1595,6 @@ async function syncReleaseChanges(options) {
1557
1595
  if (!pushed.ok) return pushed;
1558
1596
  return ok(true);
1559
1597
  }
1560
-
1561
1598
  //#endregion
1562
1599
  //#region src/versioning/commits.ts
1563
1600
  /**
@@ -1643,7 +1680,7 @@ function findCommitRange(packageCommits) {
1643
1680
  for (const commits of packageCommits.values()) {
1644
1681
  if (commits.length === 0) continue;
1645
1682
  const firstCommit = commits[0].shortHash;
1646
- const lastCommit = commits[commits.length - 1].shortHash;
1683
+ const lastCommit = commits.at(-1).shortHash;
1647
1684
  if (!newestCommit) newestCommit = firstCommit;
1648
1685
  oldestCommit = lastCommit;
1649
1686
  }
@@ -1712,7 +1749,6 @@ async function getGlobalCommitsPerPackage(workspaceRoot, packageCommits, allPack
1712
1749
  }
1713
1750
  return result;
1714
1751
  }
1715
-
1716
1752
  //#endregion
1717
1753
  //#region src/operations/version.ts
1718
1754
  function determineHighestBump(commits) {
@@ -1744,7 +1780,6 @@ function determineBumpType(commit) {
1744
1780
  if (commit.type === "fix" || commit.type === "perf") return "patch";
1745
1781
  return "none";
1746
1782
  }
1747
-
1748
1783
  //#endregion
1749
1784
  //#region src/versioning/package.ts
1750
1785
  /**
@@ -1874,7 +1909,6 @@ function createDependentUpdates(graph, workspacePackages, directUpdates, exclude
1874
1909
  }
1875
1910
  return allUpdates;
1876
1911
  }
1877
-
1878
1912
  //#endregion
1879
1913
  //#region src/versioning/version.ts
1880
1914
  const messageColorMap = {
@@ -2148,7 +2182,6 @@ function getDependencyUpdates(pkg, allUpdates) {
2148
2182
  if (updates.size === 0) logger.verbose(` - No dependency updates needed`);
2149
2183
  return updates;
2150
2184
  }
2151
-
2152
2185
  //#endregion
2153
2186
  //#region src/operations/calculate.ts
2154
2187
  async function calculateUpdates(options) {
@@ -2181,7 +2214,6 @@ function ensureHasPackages(packages) {
2181
2214
  });
2182
2215
  return ok(packages);
2183
2216
  }
2184
-
2185
2217
  //#endregion
2186
2218
  //#region src/operations/pr.ts
2187
2219
  async function syncPullRequest(options) {
@@ -2218,7 +2250,6 @@ async function syncPullRequest(options) {
2218
2250
  created: !doesExist
2219
2251
  });
2220
2252
  }
2221
-
2222
2253
  //#endregion
2223
2254
  //#region src/workflows/prepare.ts
2224
2255
  async function prepareWorkflow(options) {
@@ -2258,6 +2289,23 @@ async function prepareWorkflow(options) {
2258
2289
  logger.info("No existing version overrides file found. Continuing...");
2259
2290
  logger.verbose(`Reading overrides file failed: ${formatUnknownError(error).message}`);
2260
2291
  }
2292
+ if (Object.keys(existingOverrides).length > 0) {
2293
+ const packageNames = new Set(workspacePackages.map((p) => p.name));
2294
+ const staleEntries = [];
2295
+ for (const [pkgName, override] of Object.entries(existingOverrides)) {
2296
+ if (!packageNames.has(pkgName)) {
2297
+ staleEntries.push(pkgName);
2298
+ delete existingOverrides[pkgName];
2299
+ continue;
2300
+ }
2301
+ const pkg = workspacePackages.find((p) => p.name === pkgName);
2302
+ if (pkg && semver.valid(override.version) && semver.gte(pkg.version, override.version)) {
2303
+ staleEntries.push(pkgName);
2304
+ delete existingOverrides[pkgName];
2305
+ }
2306
+ }
2307
+ if (staleEntries.length > 0) logger.info(`Removed ${staleEntries.length} stale override(s): ${staleEntries.join(", ")}`);
2308
+ }
2261
2309
  const updatesResult = await calculateUpdates({
2262
2310
  workspacePackages,
2263
2311
  workspaceRoot: options.workspaceRoot,
@@ -2391,7 +2439,6 @@ async function prepareWorkflow(options) {
2391
2439
  created: prResult.value.created
2392
2440
  };
2393
2441
  }
2394
-
2395
2442
  //#endregion
2396
2443
  //#region src/core/npm.ts
2397
2444
  function toNPMError(operation, error, code) {
@@ -2504,7 +2551,7 @@ async function publishPackage(packageName, version, workspaceRoot, options) {
2504
2551
  } catch (error) {
2505
2552
  const code = classifyPublishErrorCode(error);
2506
2553
  if (code === "EPUBLISHCONFLICT" && attempt < maxAttempts) {
2507
- const delay = backoffMs[attempt - 1] ?? backoffMs[backoffMs.length - 1];
2554
+ const delay = backoffMs[attempt - 1] ?? backoffMs.at(-1);
2508
2555
  logger.warn(`Publish conflict for ${packageName}@${version} (attempt ${attempt}/${maxAttempts}). Retrying in ${Math.ceil(delay / 1e3)}s...`);
2509
2556
  await wait(delay);
2510
2557
  continue;
@@ -2513,7 +2560,6 @@ async function publishPackage(packageName, version, workspaceRoot, options) {
2513
2560
  }
2514
2561
  return err(toNPMError("publishPackage", /* @__PURE__ */ new Error(`Failed to publish ${packageName}@${version} after ${maxAttempts} attempts`), "EPUBLISHCONFLICT"));
2515
2562
  }
2516
-
2517
2563
  //#endregion
2518
2564
  //#region src/workflows/publish.ts
2519
2565
  async function getReleaseBodyFromChangelog(workspaceRoot, packageName, packagePath, version) {
@@ -2609,24 +2655,32 @@ async function publishWorkflow(options) {
2609
2655
  status.failed.push(packageName);
2610
2656
  exitWithError(`Publishing failed for ${packageName}.`, "Check your network connection and NPM registry access", existsResult.error);
2611
2657
  }
2612
- if (existsResult.value) {
2613
- logger.info(`Version ${farver.cyan(version)} already exists on NPM, skipping`);
2658
+ const npmExists = existsResult.value;
2659
+ let changelogEntryExists = false;
2660
+ const changelogPath = join(pkg.path, "CHANGELOG.md");
2661
+ try {
2662
+ changelogEntryExists = parseChangelog(await readFile(changelogPath, "utf-8")).versions.some((v) => v.version === version);
2663
+ } catch {}
2664
+ if (npmExists && changelogEntryExists) {
2665
+ logger.info(`Version ${farver.cyan(version)} already exists on NPM and in changelog, skipping`);
2614
2666
  status.skipped.push(packageName);
2615
2667
  continue;
2616
2668
  }
2617
- logger.step(`Publishing ${farver.cyan(`${packageName}@${version}`)} to NPM...`);
2618
- const publishResult = await publishPackage(packageName, version, options.workspaceRoot, options);
2619
- if (!publishResult.ok) {
2620
- logger.error(`Failed to publish: ${publishResult.error.message}`);
2621
- status.failed.push(packageName);
2622
- let hint;
2623
- if (publishResult.error.code === "E403") hint = "Authentication failed. Ensure your NPM token or OIDC configuration is correct";
2624
- else if (publishResult.error.code === "EPUBLISHCONFLICT") hint = "Version conflict. The version may have been published recently";
2625
- else if (publishResult.error.code === "EOTP") hint = "2FA/OTP required. Provide the otp option or use OIDC authentication";
2626
- exitWithError(`Publishing failed for ${packageName}`, hint, publishResult.error);
2669
+ if (!npmExists) {
2670
+ logger.step(`Publishing ${farver.cyan(`${packageName}@${version}`)} to NPM...`);
2671
+ const publishResult = await publishPackage(packageName, version, options.workspaceRoot, options);
2672
+ if (!publishResult.ok) {
2673
+ logger.error(`Failed to publish: ${publishResult.error.message}`);
2674
+ status.failed.push(packageName);
2675
+ let hint;
2676
+ if (publishResult.error.code === "E403") hint = "Authentication failed. Ensure your NPM token or OIDC configuration is correct";
2677
+ else if (publishResult.error.code === "EPUBLISHCONFLICT") hint = "Version conflict. The version may have been published recently";
2678
+ else if (publishResult.error.code === "EOTP") hint = "2FA/OTP required. Provide the otp option or use OIDC authentication";
2679
+ exitWithError(`Publishing failed for ${packageName}`, hint, publishResult.error);
2680
+ }
2681
+ logger.success(`Published ${farver.cyan(`${packageName}@${version}`)}`);
2682
+ status.published.push(packageName);
2627
2683
  }
2628
- logger.success(`Published ${farver.cyan(`${packageName}@${version}`)}`);
2629
- status.published.push(packageName);
2630
2684
  logger.step(`Creating git tag ${farver.cyan(`${packageName}@${version}`)}...`);
2631
2685
  const tagResult = await createAndPushPackageTag(packageName, version, options.workspaceRoot);
2632
2686
  const tagName = `${packageName}@${version}`;
@@ -2678,7 +2732,6 @@ async function publishWorkflow(options) {
2678
2732
  }
2679
2733
  logger.success("All packages published successfully!");
2680
2734
  }
2681
-
2682
2735
  //#endregion
2683
2736
  //#region src/workflows/verify.ts
2684
2737
  async function verifyWorkflow(options) {
@@ -2773,7 +2826,6 @@ async function verifyWorkflow(options) {
2773
2826
  logger.success("Verification successful. Commit status set to 'success'.");
2774
2827
  }
2775
2828
  }
2776
-
2777
2829
  //#endregion
2778
2830
  //#region src/index.ts
2779
2831
  function withErrorBoundary(fn) {
@@ -2831,6 +2883,5 @@ async function createReleaseScripts(options) {
2831
2883
  }
2832
2884
  };
2833
2885
  }
2834
-
2835
2886
  //#endregion
2836
- export { createReleaseScripts };
2887
+ export { createReleaseScripts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.60",
3
+ "version": "0.1.0-beta.62",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,23 +26,26 @@
26
26
  ],
27
27
  "dependencies": {
28
28
  "@luxass/utils": "2.7.3",
29
- "commit-parser": "1.3.0",
29
+ "commit-parser": "1.3.1",
30
30
  "farver": "1.0.0-beta.1",
31
31
  "prompts": "2.4.2",
32
32
  "semver": "7.7.4",
33
- "tinyexec": "1.0.2"
33
+ "tinyexec": "1.0.4"
34
34
  },
35
35
  "devDependencies": {
36
- "@luxass/eslint-config": "7.2.0",
36
+ "@luxass/eslint-config": "7.3.0",
37
37
  "@types/node": "22.18.12",
38
38
  "@types/prompts": "2.4.9",
39
39
  "@types/semver": "7.7.1",
40
- "eslint": "10.0.0",
40
+ "eslint": "10.0.3",
41
41
  "eta": "4.5.1",
42
- "tsdown": "0.20.3",
42
+ "tsdown": "0.21.2",
43
43
  "typescript": "5.9.3",
44
- "vitest": "4.0.18",
45
- "vitest-testdirs": "4.4.2"
44
+ "vitest": "4.1.0",
45
+ "vitest-testdirs": "4.4.3"
46
+ },
47
+ "inlinedDependencies": {
48
+ "eta": "4.5.1"
46
49
  },
47
50
  "scripts": {
48
51
  "build": "tsdown",