@ucdjs/release-scripts 0.1.0-beta.10 → 0.1.0-beta.12

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
@@ -1,3 +1,5 @@
1
+ import "commit-parser";
2
+
1
3
  //#region src/workspace.d.ts
2
4
  interface WorkspacePackage {
3
5
  name: string;
@@ -10,6 +12,7 @@ interface WorkspacePackage {
10
12
  //#endregion
11
13
  //#region src/types.d.ts
12
14
  type BumpKind = "none" | "patch" | "minor" | "major";
15
+ type GlobalCommitMode = false | "dependencies" | "all";
13
16
  interface SharedOptions {
14
17
  /**
15
18
  * Repository identifier (e.g., "owner/repo")
@@ -99,12 +102,38 @@ interface VersionUpdate {
99
102
  interface PublishOptions extends SharedOptions {}
100
103
  declare function publish(_options: PublishOptions): void;
101
104
  //#endregion
102
- //#region src/release.d.ts
103
- interface ReleaseOptions extends SharedOptions {
105
+ //#region src/changelog.d.ts
106
+ interface ChangelogOptions {
107
+ /**
108
+ * Whether to generate changelogs
109
+ * @default false
110
+ */
111
+ enabled?: boolean;
104
112
  /**
105
- * Branch name for the release PR (defaults to "release/next")
113
+ * Transform function to customize the changelog content
106
114
  */
107
- releaseBranch?: string;
115
+ transform?: (changelog: string, pkg: WorkspacePackage) => string | Promise<string>;
116
+ /**
117
+ * Repository information for generating links
118
+ */
119
+ repository?: {
120
+ owner: string;
121
+ repo: string;
122
+ };
123
+ }
124
+ //#endregion
125
+ //#region src/release.d.ts
126
+ interface ReleaseOptions extends SharedOptions {
127
+ branch?: {
128
+ /**
129
+ * Branch name for the release PR (defaults to "release/next")
130
+ */
131
+ release?: string;
132
+ /**
133
+ * Default branch name (e.g., "main")
134
+ */
135
+ default?: string;
136
+ };
108
137
  /**
109
138
  * Whether to perform a dry run (no changes pushed or PR created)
110
139
  * @default false
@@ -133,6 +162,11 @@ interface ReleaseOptions extends SharedOptions {
133
162
  */
134
163
  body?: string;
135
164
  };
165
+ /**
166
+ * Changelog configuration
167
+ */
168
+ changelog?: ChangelogOptions;
169
+ globalCommitMode?: GlobalCommitMode;
136
170
  }
137
171
  interface ReleaseResult {
138
172
  /**
package/dist/index.mjs CHANGED
@@ -1,11 +1,12 @@
1
1
  import { t as Eta } from "./eta-Boh7yPZi.mjs";
2
- import { getCommits } from "commit-parser";
3
- import process from "node:process";
4
2
  import farver from "farver";
3
+ import { existsSync } from "node:fs";
4
+ import { readFile, writeFile } from "node:fs/promises";
5
+ import { join } from "node:path";
6
+ import process from "node:process";
5
7
  import { exec } from "tinyexec";
8
+ import { getCommits } from "commit-parser";
6
9
  import { dedent } from "@luxass/utils";
7
- import { join } from "node:path";
8
- import { readFile, writeFile } from "node:fs/promises";
9
10
  import prompts from "prompts";
10
11
 
11
12
  //#region src/publish.ts
@@ -77,6 +78,114 @@ function normalizeSharedOptions(options) {
77
78
  };
78
79
  }
79
80
 
81
+ //#endregion
82
+ //#region src/changelog.ts
83
+ /**
84
+ * Get section label for commit type
85
+ */
86
+ function getSectionLabel(type) {
87
+ return {
88
+ feat: "Features",
89
+ fix: "Bug Fixes",
90
+ docs: "Documentation",
91
+ style: "Styles",
92
+ refactor: "Code Refactoring",
93
+ perf: "Performance Improvements",
94
+ test: "Tests",
95
+ build: "Build System",
96
+ ci: "Continuous Integration",
97
+ chore: "Miscellaneous Chores",
98
+ revert: "Reverts"
99
+ }[type] || "Other Changes";
100
+ }
101
+ /**
102
+ * Generate changelog content from commits
103
+ */
104
+ function generateChangelog(pkg, newVersion, commits, previousVersion, repository) {
105
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
106
+ let versionHeader = `## `;
107
+ if (repository && previousVersion) {
108
+ const compareUrl = `https://github.com/${repository.owner}/${repository.repo}/compare/${pkg.name}@${previousVersion}...${pkg.name}@${newVersion}`;
109
+ versionHeader += `[${newVersion}](${compareUrl})`;
110
+ } else versionHeader += newVersion;
111
+ versionHeader += ` (${date})\n\n`;
112
+ let changelog = versionHeader;
113
+ const grouped = /* @__PURE__ */ new Map();
114
+ for (const commit of commits) {
115
+ if (!commit.isConventional || !commit.type) continue;
116
+ const type = commit.type;
117
+ if (!grouped.has(type)) grouped.set(type, []);
118
+ grouped.get(type).push(commit);
119
+ }
120
+ for (const type of [
121
+ "feat",
122
+ "fix",
123
+ "perf",
124
+ "refactor",
125
+ "docs",
126
+ "test",
127
+ "build",
128
+ "ci",
129
+ "chore",
130
+ "revert",
131
+ "style"
132
+ ]) {
133
+ const commits$1 = grouped.get(type);
134
+ if (!commits$1 || commits$1.length === 0) continue;
135
+ const label = getSectionLabel(type);
136
+ changelog += `### ${label}\n\n`;
137
+ for (const commit of commits$1) {
138
+ const scope = commit.scope ? `**${commit.scope}:** ` : "";
139
+ const breaking = commit.isBreaking ? " **BREAKING CHANGE**" : "";
140
+ let entry = `* ${scope}${commit.description}${breaking}`;
141
+ if (repository) {
142
+ const commitUrl = `https://github.com/${repository.owner}/${repository.repo}/commit/${commit.shortHash}`;
143
+ entry += ` ([${commit.shortHash}](${commitUrl}))`;
144
+ } else entry += ` (${commit.shortHash})`;
145
+ changelog += `${entry}\n`;
146
+ }
147
+ changelog += "\n";
148
+ }
149
+ return changelog.trim();
150
+ }
151
+ /**
152
+ * Write changelog to package's CHANGELOG.md file
153
+ */
154
+ async function writeChangelog(pkg, newContent, version) {
155
+ const changelogPath = join(pkg.path, "CHANGELOG.md");
156
+ let existingContent = "";
157
+ if (existsSync(changelogPath)) existingContent = await readFile(changelogPath, "utf-8");
158
+ let updatedContent;
159
+ if (existingContent) {
160
+ const withoutTitle = existingContent.replace(/^# Changelog\n\n/, "");
161
+ if (new RegExp(`^## ${version.replace(/\./g, "\\.")}(\\s|$)`, "m").test(withoutTitle)) {
162
+ const versionSectionRegex = new RegExp(`^## ${version.replace(/\./g, "\\.")}[\\s\\S]*?(?=^## |$)`, "m");
163
+ updatedContent = `# Changelog\n\n${withoutTitle.replace(versionSectionRegex, `${newContent}\n\n`)}`;
164
+ } else updatedContent = `# Changelog\n\n${newContent}\n\n${withoutTitle}`;
165
+ } else updatedContent = `# Changelog\n\n${newContent}\n`;
166
+ await writeFile(changelogPath, updatedContent, "utf-8");
167
+ logger.log(`Updated changelog: ${changelogPath}`);
168
+ }
169
+ /**
170
+ * Generate and write changelogs for all updated packages
171
+ */
172
+ async function updateChangelogs(updates, packageCommits, options) {
173
+ if (!options?.enabled) {
174
+ logger.log("Changelog generation is disabled");
175
+ return;
176
+ }
177
+ logger.log("Generating changelogs...");
178
+ for (const update of updates) {
179
+ if (!update.hasDirectChanges) continue;
180
+ const commits = packageCommits.get(update.package.name) || [];
181
+ if (commits.length === 0) continue;
182
+ let changelog = generateChangelog(update.package, update.newVersion, commits, update.currentVersion, options.repository);
183
+ if (options.transform) changelog = await options.transform(changelog, update.package);
184
+ logger.info(`Generating changelog for package ${update.package.name}`);
185
+ await writeChangelog(update.package, changelog, update.newVersion);
186
+ }
187
+ }
188
+
80
189
  //#endregion
81
190
  //#region src/commits.ts
82
191
  async function getLastPackageTag(packageName, workspaceRoot) {
@@ -140,6 +249,58 @@ async function getWorkspacePackageCommits(workspaceRoot, packages) {
140
249
  for (const { pkgName, commits } of results) changedPackages.set(pkgName, commits);
141
250
  return changedPackages;
142
251
  }
252
+ /**
253
+ * Get all commits for the workspace (not filtered by package)
254
+ */
255
+ async function getAllWorkspaceCommits(workspaceRoot, lastTag) {
256
+ return getCommits({
257
+ from: lastTag,
258
+ to: "HEAD",
259
+ cwd: workspaceRoot
260
+ });
261
+ }
262
+ /**
263
+ * Get files changed in a specific commit
264
+ */
265
+ async function getFilesChangedInCommit(commitHash, workspaceRoot) {
266
+ try {
267
+ const { stdout } = await run("git", [
268
+ "diff-tree",
269
+ "--no-commit-id",
270
+ "--name-only",
271
+ "-r",
272
+ commitHash
273
+ ], { nodeOptions: {
274
+ cwd: workspaceRoot,
275
+ stdio: "pipe"
276
+ } });
277
+ return stdout.split("\n").map((file) => file.trim()).filter(Boolean);
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+ /**
283
+ * Filter and combine package commits with global commits
284
+ */
285
+ function combineWithGlobalCommits(workspaceRoot, packageCommits, allCommits, mode) {
286
+ if (!mode) return packageCommits;
287
+ const packageCommitShas = new Set(packageCommits.map((c) => c.shortHash));
288
+ const globalCommits = allCommits.filter((c) => !packageCommitShas.has(c.shortHash));
289
+ if (mode === "all") return [...packageCommits, ...globalCommits];
290
+ if (mode === "dependencies") {
291
+ const dependencyCommits = globalCommits.filter(async (c) => {
292
+ const affectedFiles = await getFilesChangedInCommit(c.shortHash, workspaceRoot);
293
+ if (affectedFiles == null) return false;
294
+ return affectedFiles.some((file) => [
295
+ "package.json",
296
+ "pnpm-lock.yaml",
297
+ "pnpm-workspace.yaml"
298
+ ].includes(file));
299
+ });
300
+ return [...packageCommits, ...dependencyCommits];
301
+ }
302
+ return packageCommits;
303
+ }
143
304
  function determineBumpType(commit) {
144
305
  if (commit.isBreaking) return "major";
145
306
  if (!commit.isConventional || !commit.type) return "none";
@@ -380,6 +541,15 @@ async function pushBranch(branch, workspaceRoot, options) {
380
541
  exitWithError(`Failed to push branch: ${branch}`, `Make sure you have permission to push to the remote repository`);
381
542
  }
382
543
  }
544
+ async function getDefaultBranch() {
545
+ try {
546
+ const match = (await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { nodeOptions: { stdio: "pipe" } })).stdout.trim().match(/^refs\/remotes\/origin\/(.+)$/);
547
+ if (match && match[1]) return match[1];
548
+ return "main";
549
+ } catch {
550
+ return "main";
551
+ }
552
+ }
383
553
 
384
554
  //#endregion
385
555
  //#region src/github.ts
@@ -590,19 +760,15 @@ function createVersionUpdate(pkg, bump, hasDirectChanges) {
590
760
  /**
591
761
  * Infer version updates from package commits with optional interactive overrides
592
762
  *
593
- * @param workspacePackages - All workspace packages
594
- * @param packageCommits - Map of package names to their commits
595
- * @param workspaceRoot - Root directory for prompts
596
- * @param showPrompt - Whether to show prompts for version overrides
597
763
  * @returns Version updates for packages with changes
598
764
  */
599
- async function inferVersionUpdates(workspacePackages, packageCommits, workspaceRoot, showPrompt) {
765
+ async function inferVersionUpdates({ workspacePackages, packageCommits, allCommits, workspaceRoot, showPrompt, globalCommitMode }) {
600
766
  const versionUpdates = [];
601
- for (const [pkgName, commits] of packageCommits) {
602
- if (commits.length === 0) continue;
767
+ for (const [pkgName, pkgCommits] of packageCommits) {
768
+ if (pkgCommits.length === 0) continue;
603
769
  const pkg = workspacePackages.find((p) => p.name === pkgName);
604
770
  if (!pkg) continue;
605
- const bump = determineHighestBump(commits);
771
+ const bump = determineHighestBump(combineWithGlobalCommits(workspaceRoot, pkgCommits, allCommits, globalCommitMode));
606
772
  if (bump === "none") {
607
773
  logger.info(`No version bump needed for package ${pkg.name}`);
608
774
  continue;
@@ -828,8 +994,12 @@ function shouldIncludePackage(pkg, options) {
828
994
  async function release(options) {
829
995
  const normalizedOptions = normalizeSharedOptions(options);
830
996
  normalizedOptions.dryRun ??= false;
831
- normalizedOptions.releaseBranch ??= "release/next";
997
+ normalizedOptions.branch ??= {};
998
+ normalizedOptions.branch.release ??= "release/next";
999
+ normalizedOptions.branch.default = await getDefaultBranch();
832
1000
  normalizedOptions.safeguards ??= true;
1001
+ normalizedOptions.changelog ??= { enabled: true };
1002
+ normalizedOptions.globalCommitMode ??= "dependencies";
833
1003
  globalOptions.dryRun = normalizedOptions.dryRun;
834
1004
  const workspaceRoot = normalizedOptions.workspaceRoot;
835
1005
  if (normalizedOptions.safeguards && !await isWorkingDirectoryClean(workspaceRoot)) exitWithError("Working directory is not clean. Please commit or stash your changes before proceeding.");
@@ -838,37 +1008,53 @@ async function release(options) {
838
1008
  logger.log("No packages found to release.");
839
1009
  return null;
840
1010
  }
841
- const versionUpdates = await inferVersionUpdates(workspacePackages, await getWorkspacePackageCommits(workspaceRoot, workspacePackages), workspaceRoot, options.prompts?.versions !== false);
1011
+ const packageCommits = await getWorkspacePackageCommits(workspaceRoot, workspacePackages);
1012
+ const versionUpdates = await inferVersionUpdates({
1013
+ workspacePackages,
1014
+ packageCommits,
1015
+ workspaceRoot,
1016
+ showPrompt: options.prompts?.versions !== false,
1017
+ allCommits: await getAllWorkspaceCommits(workspaceRoot),
1018
+ globalCommitMode: options.globalCommitMode
1019
+ });
842
1020
  if (versionUpdates.length === 0) logger.warn("No packages have changes requiring a release");
843
1021
  const allUpdates = createDependentUpdates(buildPackageDependencyGraph(workspacePackages), workspacePackages, versionUpdates);
844
1022
  const currentBranch = await getCurrentBranch(workspaceRoot);
1023
+ if (currentBranch !== normalizedOptions.branch.default) exitWithError(`Current branch is '${currentBranch}'. Please switch to the default branch '${normalizedOptions.branch.default}' before proceeding.`, `git checkout ${normalizedOptions.branch.default}`);
845
1024
  const existingPullRequest = await getExistingPullRequest({
846
1025
  owner: normalizedOptions.owner,
847
1026
  repo: normalizedOptions.repo,
848
- branch: normalizedOptions.releaseBranch,
1027
+ branch: normalizedOptions.branch.release,
849
1028
  githubToken: normalizedOptions.githubToken
850
1029
  });
851
1030
  const prExists = !!existingPullRequest;
852
1031
  if (prExists) logger.log("Existing pull request found:", existingPullRequest.html_url);
853
1032
  else logger.log("No existing pull request found, will create new one");
854
- const branchExists = await doesBranchExist(normalizedOptions.releaseBranch, workspaceRoot);
1033
+ const branchExists = await doesBranchExist(normalizedOptions.branch.release, workspaceRoot);
855
1034
  if (!branchExists) {
856
- logger.log("Creating release branch:", normalizedOptions.releaseBranch);
857
- await createBranch(normalizedOptions.releaseBranch, currentBranch, workspaceRoot);
1035
+ logger.log("Creating release branch:", normalizedOptions.branch.release);
1036
+ await createBranch(normalizedOptions.branch.release, normalizedOptions.branch.default, workspaceRoot);
858
1037
  }
859
- if (!await checkoutBranch(normalizedOptions.releaseBranch, workspaceRoot)) throw new Error(`Failed to checkout branch: ${normalizedOptions.releaseBranch}`);
1038
+ if (!await checkoutBranch(normalizedOptions.branch.release, workspaceRoot)) throw new Error(`Failed to checkout branch: ${normalizedOptions.branch.release}`);
860
1039
  if (branchExists) {
861
1040
  logger.log("Pulling latest changes from remote");
862
- if (!await pullLatestChanges(normalizedOptions.releaseBranch, workspaceRoot)) logger.log("Warning: Failed to pull latest changes, continuing anyway");
1041
+ if (!await pullLatestChanges(normalizedOptions.branch.release, workspaceRoot)) logger.log("Warning: Failed to pull latest changes, continuing anyway");
863
1042
  }
864
- logger.log("Rebasing release branch onto", currentBranch);
865
- await rebaseBranch(currentBranch, workspaceRoot);
1043
+ logger.log("Rebasing release branch onto", normalizedOptions.branch.default);
1044
+ await rebaseBranch(normalizedOptions.branch.default, workspaceRoot);
866
1045
  await updateAllPackageJsonFiles(allUpdates);
1046
+ await updateChangelogs(versionUpdates, packageCommits, {
1047
+ ...options.changelog,
1048
+ repository: options.changelog?.repository || {
1049
+ owner: normalizedOptions.owner,
1050
+ repo: normalizedOptions.repo
1051
+ }
1052
+ });
867
1053
  const hasCommitted = await commitChanges("chore: update release versions", workspaceRoot);
868
- const isBranchAhead = await isBranchAheadOfRemote(normalizedOptions.releaseBranch, workspaceRoot);
1054
+ const isBranchAhead = await isBranchAheadOfRemote(normalizedOptions.branch.release, workspaceRoot);
869
1055
  if (!hasCommitted && !isBranchAhead) {
870
1056
  logger.log("No changes to commit and branch is in sync with remote");
871
- await checkoutBranch(currentBranch, workspaceRoot);
1057
+ await checkoutBranch(normalizedOptions.branch.default, workspaceRoot);
872
1058
  if (prExists) {
873
1059
  logger.log("No updates needed, PR is already up to date");
874
1060
  return {
@@ -882,7 +1068,7 @@ async function release(options) {
882
1068
  }
883
1069
  }
884
1070
  logger.log("Pushing changes to remote");
885
- await pushBranch(normalizedOptions.releaseBranch, workspaceRoot, { forceWithLease: true });
1071
+ await pushBranch(normalizedOptions.branch.release, workspaceRoot, { forceWithLease: true });
886
1072
  const prTitle = existingPullRequest?.title || options.pullRequest?.title || "chore: update package versions";
887
1073
  const prBody = generatePullRequestBody(allUpdates, options.pullRequest?.body);
888
1074
  const pullRequest = await upsertPullRequest({
@@ -891,12 +1077,16 @@ async function release(options) {
891
1077
  pullNumber: existingPullRequest?.number,
892
1078
  title: prTitle,
893
1079
  body: prBody,
894
- head: normalizedOptions.releaseBranch,
895
- base: currentBranch,
1080
+ head: normalizedOptions.branch.release,
1081
+ base: normalizedOptions.branch.default,
896
1082
  githubToken: normalizedOptions.githubToken
897
1083
  });
898
1084
  logger.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
899
- await checkoutBranch(currentBranch, workspaceRoot);
1085
+ await checkoutBranch(normalizedOptions.branch.default, workspaceRoot);
1086
+ if (pullRequest?.html_url) {
1087
+ logger.info();
1088
+ logger.info(`${farver.green("✓")} Pull request ${prExists ? "updated" : "created"}: ${farver.cyan(pullRequest.html_url)}`);
1089
+ }
900
1090
  return {
901
1091
  updates: allUpdates,
902
1092
  prUrl: pullRequest?.html_url,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ucdjs/release-scripts",
3
- "version": "0.1.0-beta.10",
3
+ "version": "0.1.0-beta.12",
4
4
  "description": "@ucdjs release scripts",
5
5
  "type": "module",
6
6
  "license": "MIT",