@ucdjs/release-scripts 0.0.0
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 +3 -0
- package/dist/index.d.mts +121 -0
- package/dist/index.mjs +817 -0
- package/package.json +46 -0
package/README.md
ADDED
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type BumpKind = "none" | "patch" | "minor" | "major";
|
|
3
|
+
interface PackageJson {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
dependencies?: Record<string, string>;
|
|
7
|
+
devDependencies?: Record<string, string>;
|
|
8
|
+
peerDependencies?: Record<string, string>;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
interface WorkspacePackage {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
path: string;
|
|
15
|
+
packageJson: PackageJson;
|
|
16
|
+
workspaceDependencies: string[];
|
|
17
|
+
workspaceDevDependencies: string[];
|
|
18
|
+
}
|
|
19
|
+
interface FindWorkspacePackagesOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Package names to exclude
|
|
22
|
+
*/
|
|
23
|
+
excluded?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Only include these packages (if specified, all others are excluded)
|
|
26
|
+
*/
|
|
27
|
+
included?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Whether to exclude private packages (default: false)
|
|
30
|
+
*/
|
|
31
|
+
excludePrivate?: boolean;
|
|
32
|
+
}
|
|
33
|
+
interface VersionUpdate {
|
|
34
|
+
/**
|
|
35
|
+
* The package being updated
|
|
36
|
+
*/
|
|
37
|
+
package: WorkspacePackage;
|
|
38
|
+
/**
|
|
39
|
+
* Current version
|
|
40
|
+
*/
|
|
41
|
+
currentVersion: string;
|
|
42
|
+
/**
|
|
43
|
+
* New version to release
|
|
44
|
+
*/
|
|
45
|
+
newVersion: string;
|
|
46
|
+
/**
|
|
47
|
+
* Type of version bump
|
|
48
|
+
*/
|
|
49
|
+
bumpType: BumpKind;
|
|
50
|
+
/**
|
|
51
|
+
* Whether this package has direct changes (vs being updated due to dependency changes)
|
|
52
|
+
*/
|
|
53
|
+
hasDirectChanges: boolean;
|
|
54
|
+
}
|
|
55
|
+
interface ReleaseOptions {
|
|
56
|
+
/**
|
|
57
|
+
* Repository identifier (e.g., "owner/repo")
|
|
58
|
+
*/
|
|
59
|
+
repo: string;
|
|
60
|
+
/**
|
|
61
|
+
* Root directory of the workspace (defaults to process.cwd())
|
|
62
|
+
*/
|
|
63
|
+
workspaceRoot?: string;
|
|
64
|
+
/**
|
|
65
|
+
* Specific packages to prepare for release.
|
|
66
|
+
* - true: discover all packages
|
|
67
|
+
* - FindWorkspacePackagesOptions: discover with filters
|
|
68
|
+
* - string[]: specific package names
|
|
69
|
+
*/
|
|
70
|
+
packages?: true | FindWorkspacePackagesOptions | string[];
|
|
71
|
+
/**
|
|
72
|
+
* Branch name for the release PR (defaults to "release/next")
|
|
73
|
+
*/
|
|
74
|
+
releaseBranch?: string;
|
|
75
|
+
/**
|
|
76
|
+
* Interactive prompt configuration
|
|
77
|
+
*/
|
|
78
|
+
prompts?: {
|
|
79
|
+
/**
|
|
80
|
+
* Enable package selection prompt (defaults to true when not in CI)
|
|
81
|
+
*/
|
|
82
|
+
packages?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Enable version override prompt (defaults to true when not in CI)
|
|
85
|
+
*/
|
|
86
|
+
versions?: boolean;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Whether to perform a dry run (no changes pushed or PR created)
|
|
90
|
+
* @default false
|
|
91
|
+
*/
|
|
92
|
+
dryRun?: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* Whether to enable safety safeguards (e.g., checking for clean working directory)
|
|
95
|
+
* @default true
|
|
96
|
+
*/
|
|
97
|
+
safeguards?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* GitHub token for authentication
|
|
100
|
+
*/
|
|
101
|
+
githubToken: string;
|
|
102
|
+
}
|
|
103
|
+
interface ReleaseResult {
|
|
104
|
+
/**
|
|
105
|
+
* Packages that will be updated
|
|
106
|
+
*/
|
|
107
|
+
updates: VersionUpdate[];
|
|
108
|
+
/**
|
|
109
|
+
* URL of the created or updated PR
|
|
110
|
+
*/
|
|
111
|
+
prUrl?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Whether a new PR was created (vs updating existing)
|
|
114
|
+
*/
|
|
115
|
+
created: boolean;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/release.d.ts
|
|
119
|
+
declare function release(options: ReleaseOptions): Promise<ReleaseResult | null>;
|
|
120
|
+
//#endregion
|
|
121
|
+
export { release };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,817 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { getCommits } from "commit-parser";
|
|
3
|
+
import createDebug from "debug";
|
|
4
|
+
import farver from "farver";
|
|
5
|
+
import { exec } from "tinyexec";
|
|
6
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import prompts from "prompts";
|
|
9
|
+
|
|
10
|
+
//#region src/logger.ts
|
|
11
|
+
function createDebugger(namespace) {
|
|
12
|
+
const debug$2 = createDebug(namespace);
|
|
13
|
+
if (debug$2.enabled) return debug$2;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/utils.ts
|
|
18
|
+
const globalOptions = { dryRun: false };
|
|
19
|
+
async function run(bin, args, opts = {}) {
|
|
20
|
+
return exec(bin, args, {
|
|
21
|
+
throwOnError: true,
|
|
22
|
+
...opts,
|
|
23
|
+
nodeOptions: {
|
|
24
|
+
stdio: "inherit",
|
|
25
|
+
...opts.nodeOptions
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function dryRun(bin, args, opts) {
|
|
30
|
+
return console.log(farver.blue(`[dryrun] ${bin} ${args.join(" ")}`), opts || "");
|
|
31
|
+
}
|
|
32
|
+
const runIfNotDry = globalOptions.dryRun ? dryRun : run;
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/commits.ts
|
|
36
|
+
const debug$1 = createDebugger("ucdjs:release-scripts:commits");
|
|
37
|
+
async function getLastPackageTag(packageName, workspaceRoot) {
|
|
38
|
+
const { stdout } = await run("git", ["tag", "--list"], { nodeOptions: {
|
|
39
|
+
cwd: workspaceRoot,
|
|
40
|
+
stdio: "pipe"
|
|
41
|
+
} });
|
|
42
|
+
return stdout.split("\n").map((tag) => tag.trim()).filter(Boolean).reverse().find((tag) => tag.startsWith(`${packageName}@`));
|
|
43
|
+
}
|
|
44
|
+
function determineHighestBump(commits) {
|
|
45
|
+
if (commits.length === 0) return "none";
|
|
46
|
+
let highestBump = "none";
|
|
47
|
+
for (const commit of commits) {
|
|
48
|
+
const bump = determineBumpType(commit);
|
|
49
|
+
if (bump === "major") return "major";
|
|
50
|
+
if (bump === "minor") highestBump = "minor";
|
|
51
|
+
else if (bump === "patch" && highestBump === "none") highestBump = "patch";
|
|
52
|
+
}
|
|
53
|
+
return highestBump;
|
|
54
|
+
}
|
|
55
|
+
async function getPackageCommits(pkg, workspaceRoot) {
|
|
56
|
+
const lastTag = await getLastPackageTag(pkg.name, workspaceRoot);
|
|
57
|
+
const allCommits = getCommits({
|
|
58
|
+
from: lastTag,
|
|
59
|
+
to: "HEAD"
|
|
60
|
+
});
|
|
61
|
+
debug$1?.(`Found ${allCommits.length} commits for ${pkg.name} since ${lastTag || "beginning"}`);
|
|
62
|
+
const touchedCommitHashes = await getCommitsTouchingPackage(lastTag || "HEAD", "HEAD", pkg.path, workspaceRoot);
|
|
63
|
+
const touchedSet = new Set(touchedCommitHashes);
|
|
64
|
+
const packageCommits = allCommits.filter((commit) => touchedSet.has(commit.shortHash));
|
|
65
|
+
debug$1?.(`${packageCommits.length} commits affect ${pkg.name}`);
|
|
66
|
+
return packageCommits;
|
|
67
|
+
}
|
|
68
|
+
async function analyzePackageCommits(pkg, workspaceRoot) {
|
|
69
|
+
return determineHighestBump(await getPackageCommits(pkg, workspaceRoot));
|
|
70
|
+
}
|
|
71
|
+
function determineBumpType(commit) {
|
|
72
|
+
if (commit.isBreaking) return "major";
|
|
73
|
+
if (!commit.isConventional || !commit.type) return "none";
|
|
74
|
+
switch (commit.type) {
|
|
75
|
+
case "feat": return "minor";
|
|
76
|
+
case "fix":
|
|
77
|
+
case "perf": return "patch";
|
|
78
|
+
case "docs":
|
|
79
|
+
case "style":
|
|
80
|
+
case "refactor":
|
|
81
|
+
case "test":
|
|
82
|
+
case "build":
|
|
83
|
+
case "ci":
|
|
84
|
+
case "chore":
|
|
85
|
+
case "revert": return "none";
|
|
86
|
+
default: return "none";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function getCommitsTouchingPackage(from, to, packagePath, workspaceRoot) {
|
|
90
|
+
try {
|
|
91
|
+
const { stdout } = await run("git", [
|
|
92
|
+
"log",
|
|
93
|
+
"--pretty=format:%h",
|
|
94
|
+
from === "HEAD" ? "HEAD" : `${from}...${to}`,
|
|
95
|
+
"--",
|
|
96
|
+
packagePath
|
|
97
|
+
], { nodeOptions: {
|
|
98
|
+
cwd: workspaceRoot,
|
|
99
|
+
stdio: "pipe"
|
|
100
|
+
} });
|
|
101
|
+
return stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
102
|
+
} catch (error) {
|
|
103
|
+
debug$1?.(`Error getting commits touching package: ${error}`);
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/validation.ts
|
|
110
|
+
/**
|
|
111
|
+
* Validation utilities for release scripts
|
|
112
|
+
*/
|
|
113
|
+
function isValidSemver(version) {
|
|
114
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(version);
|
|
115
|
+
}
|
|
116
|
+
function validateSemver(version) {
|
|
117
|
+
if (!isValidSemver(version)) throw new Error(`Invalid semver version: ${version}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/version.ts
|
|
122
|
+
/**
|
|
123
|
+
* Calculate the new version based on current version and bump type
|
|
124
|
+
* Pure function - no side effects, easily testable
|
|
125
|
+
*/
|
|
126
|
+
function calculateNewVersion(currentVersion, bump) {
|
|
127
|
+
if (bump === "none") return currentVersion;
|
|
128
|
+
validateSemver(currentVersion);
|
|
129
|
+
const match = currentVersion.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
|
|
130
|
+
if (!match) throw new Error(`Invalid semver version: ${currentVersion}`);
|
|
131
|
+
const [, major, minor, patch, suffix] = match;
|
|
132
|
+
let newMajor = Number.parseInt(major, 10);
|
|
133
|
+
let newMinor = Number.parseInt(minor, 10);
|
|
134
|
+
let newPatch = Number.parseInt(patch, 10);
|
|
135
|
+
switch (bump) {
|
|
136
|
+
case "major":
|
|
137
|
+
newMajor += 1;
|
|
138
|
+
newMinor = 0;
|
|
139
|
+
newPatch = 0;
|
|
140
|
+
break;
|
|
141
|
+
case "minor":
|
|
142
|
+
newMinor += 1;
|
|
143
|
+
newPatch = 0;
|
|
144
|
+
break;
|
|
145
|
+
case "patch":
|
|
146
|
+
newPatch += 1;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
return `${newMajor}.${newMinor}.${newPatch}`;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create a version update object
|
|
153
|
+
*/
|
|
154
|
+
function createVersionUpdate(pkg, bump, hasDirectChanges) {
|
|
155
|
+
const newVersion = calculateNewVersion(pkg.version, bump);
|
|
156
|
+
return {
|
|
157
|
+
package: pkg,
|
|
158
|
+
currentVersion: pkg.version,
|
|
159
|
+
newVersion,
|
|
160
|
+
bumpType: bump,
|
|
161
|
+
hasDirectChanges
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Update a package.json file with new version and dependency versions
|
|
166
|
+
*/
|
|
167
|
+
async function updatePackageJson(pkg, newVersion, dependencyUpdates) {
|
|
168
|
+
const packageJsonPath = join(pkg.path, "package.json");
|
|
169
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
170
|
+
const packageJson = JSON.parse(content);
|
|
171
|
+
packageJson.version = newVersion;
|
|
172
|
+
for (const [depName, depVersion] of dependencyUpdates) {
|
|
173
|
+
if (packageJson.dependencies?.[depName]) {
|
|
174
|
+
if (packageJson.dependencies[depName] === "workspace:*") continue;
|
|
175
|
+
packageJson.dependencies[depName] = `^${depVersion}`;
|
|
176
|
+
}
|
|
177
|
+
if (packageJson.devDependencies?.[depName]) {
|
|
178
|
+
if (packageJson.devDependencies[depName] === "workspace:*") continue;
|
|
179
|
+
packageJson.devDependencies[depName] = `^${depVersion}`;
|
|
180
|
+
}
|
|
181
|
+
if (packageJson.peerDependencies?.[depName]) {
|
|
182
|
+
if (packageJson.peerDependencies[depName] === "workspace:*") continue;
|
|
183
|
+
packageJson.peerDependencies[depName] = `^${depVersion}`;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, "utf-8");
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Get all dependency updates needed for a package
|
|
190
|
+
*/
|
|
191
|
+
function getDependencyUpdates(pkg, allUpdates) {
|
|
192
|
+
const updates = /* @__PURE__ */ new Map();
|
|
193
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
194
|
+
for (const dep of allDeps) {
|
|
195
|
+
const update = allUpdates.find((u) => u.package.name === dep);
|
|
196
|
+
if (update) updates.set(dep, update.newVersion);
|
|
197
|
+
}
|
|
198
|
+
return updates;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
//#endregion
|
|
202
|
+
//#region src/dependencies.ts
|
|
203
|
+
/**
|
|
204
|
+
* Pure function: Determine which packages need updates due to dependency changes
|
|
205
|
+
*
|
|
206
|
+
* When a package is updated, all packages that depend on it should also be updated.
|
|
207
|
+
* This function calculates which additional packages need patch bumps.
|
|
208
|
+
*
|
|
209
|
+
* @param updateOrder - Packages in topological order with their dependency levels
|
|
210
|
+
* @param directUpdates - Packages with direct code changes
|
|
211
|
+
* @returns All updates including dependent packages
|
|
212
|
+
*/
|
|
213
|
+
function createDependentUpdates(updateOrder, directUpdates) {
|
|
214
|
+
const allUpdates = [...directUpdates];
|
|
215
|
+
const updatedPackages = new Set(directUpdates.map((u) => u.package.name));
|
|
216
|
+
for (const { package: pkg } of updateOrder) {
|
|
217
|
+
if (updatedPackages.has(pkg.name)) continue;
|
|
218
|
+
if (hasUpdatedDependencies(pkg, updatedPackages)) {
|
|
219
|
+
allUpdates.push(createVersionUpdate(pkg, "patch", false));
|
|
220
|
+
updatedPackages.add(pkg.name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return allUpdates;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Pure function: Check if a package has any updated dependencies
|
|
227
|
+
*/
|
|
228
|
+
function hasUpdatedDependencies(pkg, updatedPackages) {
|
|
229
|
+
return [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies].some((dep) => updatedPackages.has(dep));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/git.ts
|
|
234
|
+
/**
|
|
235
|
+
* Check if the working directory is clean (no uncommitted changes)
|
|
236
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
237
|
+
* @returns {Promise<boolean>} A Promise resolving to true if clean, false otherwise
|
|
238
|
+
*/
|
|
239
|
+
async function isWorkingDirectoryClean(workspaceRoot) {
|
|
240
|
+
try {
|
|
241
|
+
if ((await run("git", ["status", "--porcelain"], { nodeOptions: {
|
|
242
|
+
cwd: workspaceRoot,
|
|
243
|
+
stdio: "pipe"
|
|
244
|
+
} })).stdout.trim() !== "") return false;
|
|
245
|
+
return true;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
console.error("Error checking git status:", err);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Check if a git branch exists locally
|
|
253
|
+
* @param {string} branch - The branch name to check
|
|
254
|
+
* @param {string} workspaceRoot - The root directory of the workspace
|
|
255
|
+
* @returns {Promise<boolean>} Promise resolving to true if branch exists, false otherwise
|
|
256
|
+
*/
|
|
257
|
+
async function doesBranchExist(branch, workspaceRoot) {
|
|
258
|
+
try {
|
|
259
|
+
await run("git", [
|
|
260
|
+
"rev-parse",
|
|
261
|
+
"--verify",
|
|
262
|
+
branch
|
|
263
|
+
], { nodeOptions: {
|
|
264
|
+
cwd: workspaceRoot,
|
|
265
|
+
stdio: "pipe"
|
|
266
|
+
} });
|
|
267
|
+
return true;
|
|
268
|
+
} catch {
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Pull latest changes from remote branch
|
|
274
|
+
* @param branch - The branch name to pull from
|
|
275
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
276
|
+
* @returns Promise resolving to true if pull succeeded, false otherwise
|
|
277
|
+
*/
|
|
278
|
+
async function pullLatestChanges(branch, workspaceRoot) {
|
|
279
|
+
try {
|
|
280
|
+
await run("git", [
|
|
281
|
+
"pull",
|
|
282
|
+
"origin",
|
|
283
|
+
branch
|
|
284
|
+
], { nodeOptions: {
|
|
285
|
+
cwd: workspaceRoot,
|
|
286
|
+
stdio: "pipe"
|
|
287
|
+
} });
|
|
288
|
+
return true;
|
|
289
|
+
} catch {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Create a new git branch
|
|
295
|
+
* @param branch - The new branch name
|
|
296
|
+
* @param base - The base branch to create from
|
|
297
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
298
|
+
*/
|
|
299
|
+
async function createBranch(branch, base, workspaceRoot) {
|
|
300
|
+
await runIfNotDry("git", [
|
|
301
|
+
"checkout",
|
|
302
|
+
"-b",
|
|
303
|
+
branch,
|
|
304
|
+
base
|
|
305
|
+
], { nodeOptions: { cwd: workspaceRoot } });
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Checkout a git branch
|
|
309
|
+
* @param branch - The branch name to checkout
|
|
310
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
311
|
+
* @returns Promise resolving to true if checkout succeeded, false otherwise
|
|
312
|
+
*/
|
|
313
|
+
async function checkoutBranch(branch, workspaceRoot) {
|
|
314
|
+
try {
|
|
315
|
+
await run("git", ["checkout", branch], { nodeOptions: { cwd: workspaceRoot } });
|
|
316
|
+
return true;
|
|
317
|
+
} catch {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Get the current branch name
|
|
323
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
324
|
+
* @returns Promise resolving to the current branch name
|
|
325
|
+
*/
|
|
326
|
+
async function getCurrentBranch(workspaceRoot) {
|
|
327
|
+
return (await run("git", [
|
|
328
|
+
"rev-parse",
|
|
329
|
+
"--abbrev-ref",
|
|
330
|
+
"HEAD"
|
|
331
|
+
], { nodeOptions: {
|
|
332
|
+
cwd: workspaceRoot,
|
|
333
|
+
stdio: "pipe"
|
|
334
|
+
} })).stdout.trim();
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Rebase current branch onto another branch
|
|
338
|
+
* @param ontoBranch - The target branch to rebase onto
|
|
339
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
340
|
+
*/
|
|
341
|
+
async function rebaseBranch(ontoBranch, workspaceRoot) {
|
|
342
|
+
await run("git", ["rebase", ontoBranch], { nodeOptions: { cwd: workspaceRoot } });
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Check if there are any changes to commit (staged or unstaged)
|
|
346
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
347
|
+
* @returns Promise resolving to true if there are changes, false otherwise
|
|
348
|
+
*/
|
|
349
|
+
async function hasChangesToCommit(workspaceRoot) {
|
|
350
|
+
return (await run("git", ["status", "--porcelain"], { nodeOptions: {
|
|
351
|
+
cwd: workspaceRoot,
|
|
352
|
+
stdio: "pipe"
|
|
353
|
+
} })).stdout.trim() !== "";
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Commit changes with a message
|
|
357
|
+
* @param message - The commit message
|
|
358
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
359
|
+
* @returns Promise resolving to true if commit was made, false if there were no changes
|
|
360
|
+
*/
|
|
361
|
+
async function commitChanges(message, workspaceRoot) {
|
|
362
|
+
await run("git", ["add", "."], { nodeOptions: { cwd: workspaceRoot } });
|
|
363
|
+
if (!await hasChangesToCommit(workspaceRoot)) return false;
|
|
364
|
+
await run("git", [
|
|
365
|
+
"commit",
|
|
366
|
+
"-m",
|
|
367
|
+
message
|
|
368
|
+
], { nodeOptions: { cwd: workspaceRoot } });
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Push branch to remote
|
|
373
|
+
* @param branch - The branch name to push
|
|
374
|
+
* @param workspaceRoot - The root directory of the workspace
|
|
375
|
+
* @param options - Push options
|
|
376
|
+
* @param options.force - Force push (overwrite remote)
|
|
377
|
+
* @param options.forceWithLease - Force push with safety check (won't overwrite unexpected changes)
|
|
378
|
+
*/
|
|
379
|
+
async function pushBranch(branch, workspaceRoot, options) {
|
|
380
|
+
const args = [
|
|
381
|
+
"push",
|
|
382
|
+
"origin",
|
|
383
|
+
branch
|
|
384
|
+
];
|
|
385
|
+
if (options?.forceWithLease) args.push("--force-with-lease");
|
|
386
|
+
else if (options?.force) args.push("--force");
|
|
387
|
+
await run("git", args, { nodeOptions: { cwd: workspaceRoot } });
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Generate PR body from version updates
|
|
391
|
+
* @param updates - Array of version updates to include in the PR body
|
|
392
|
+
* @returns Formatted PR body as a string
|
|
393
|
+
*/
|
|
394
|
+
function generatePRBody(updates) {
|
|
395
|
+
const lines = [];
|
|
396
|
+
lines.push("## Packages");
|
|
397
|
+
lines.push("");
|
|
398
|
+
const directChanges = updates.filter((u) => u.hasDirectChanges);
|
|
399
|
+
const dependencyUpdates = updates.filter((u) => !u.hasDirectChanges);
|
|
400
|
+
if (directChanges.length > 0) {
|
|
401
|
+
lines.push("### Direct Changes");
|
|
402
|
+
lines.push("");
|
|
403
|
+
for (const update of directChanges) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (${update.bumpType})`);
|
|
404
|
+
lines.push("");
|
|
405
|
+
}
|
|
406
|
+
if (dependencyUpdates.length > 0) {
|
|
407
|
+
lines.push("### Dependency Updates");
|
|
408
|
+
lines.push("");
|
|
409
|
+
for (const update of dependencyUpdates) lines.push(`- **${update.package.name}**: ${update.currentVersion} → ${update.newVersion} (dependencies changed)`);
|
|
410
|
+
lines.push("");
|
|
411
|
+
}
|
|
412
|
+
lines.push("---");
|
|
413
|
+
lines.push("");
|
|
414
|
+
lines.push("This release PR was automatically generated.");
|
|
415
|
+
return lines.join("\n");
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/github.ts
|
|
420
|
+
async function getExistingPullRequest({ owner, repo, branch, githubToken }) {
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${branch}`, { headers: {
|
|
423
|
+
Accept: "application/vnd.github.v3+json",
|
|
424
|
+
Authorization: `token ${githubToken}`
|
|
425
|
+
} });
|
|
426
|
+
if (!res.ok) throw new Error(`GitHub API request failed with status ${res.status}`);
|
|
427
|
+
const pulls = await res.json();
|
|
428
|
+
if (pulls == null || !Array.isArray(pulls) || pulls.length === 0) return null;
|
|
429
|
+
const firstPullRequest = pulls[0];
|
|
430
|
+
if (typeof firstPullRequest !== "object" || firstPullRequest === null || !("number" in firstPullRequest) || typeof firstPullRequest.number !== "number" || !("title" in firstPullRequest) || typeof firstPullRequest.title !== "string" || !("body" in firstPullRequest) || typeof firstPullRequest.body !== "string" || !("draft" in firstPullRequest) || typeof firstPullRequest.draft !== "boolean" || !("html_url" in firstPullRequest) || typeof firstPullRequest.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
431
|
+
const pullRequest = {
|
|
432
|
+
number: firstPullRequest.number,
|
|
433
|
+
title: firstPullRequest.title,
|
|
434
|
+
body: firstPullRequest.body,
|
|
435
|
+
draft: firstPullRequest.draft,
|
|
436
|
+
html_url: firstPullRequest.html_url
|
|
437
|
+
};
|
|
438
|
+
console.info(`Found existing pull request: ${farver.yellow(`#${pullRequest.number}`)}`);
|
|
439
|
+
return pullRequest;
|
|
440
|
+
} catch (err) {
|
|
441
|
+
console.error("Error fetching pull request:", err);
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
async function upsertPullRequest({ owner, repo, title, body, head, base, pullNumber, githubToken }) {
|
|
446
|
+
try {
|
|
447
|
+
const isUpdate = pullNumber != null;
|
|
448
|
+
const url = isUpdate ? `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}` : `https://api.github.com/repos/${owner}/${repo}/pulls`;
|
|
449
|
+
const method = isUpdate ? "PATCH" : "POST";
|
|
450
|
+
const requestBody = isUpdate ? {
|
|
451
|
+
title,
|
|
452
|
+
body
|
|
453
|
+
} : {
|
|
454
|
+
title,
|
|
455
|
+
body,
|
|
456
|
+
head,
|
|
457
|
+
base
|
|
458
|
+
};
|
|
459
|
+
const res = await fetch(url, {
|
|
460
|
+
method,
|
|
461
|
+
headers: {
|
|
462
|
+
Accept: "application/vnd.github.v3+json",
|
|
463
|
+
Authorization: `token ${githubToken}`
|
|
464
|
+
},
|
|
465
|
+
body: JSON.stringify(requestBody)
|
|
466
|
+
});
|
|
467
|
+
if (!res.ok) throw new Error(`GitHub API request failed with status ${res.status}`);
|
|
468
|
+
const pr = await res.json();
|
|
469
|
+
if (typeof pr !== "object" || pr === null || !("number" in pr) || typeof pr.number !== "number" || !("title" in pr) || typeof pr.title !== "string" || !("body" in pr) || typeof pr.body !== "string" || !("draft" in pr) || typeof pr.draft !== "boolean" || !("html_url" in pr) || typeof pr.html_url !== "string") throw new TypeError("Pull request data validation failed");
|
|
470
|
+
const action = isUpdate ? "Updated" : "Created";
|
|
471
|
+
console.info(`${action} pull request: ${farver.yellow(`#${pr.number}`)}`);
|
|
472
|
+
return {
|
|
473
|
+
number: pr.number,
|
|
474
|
+
title: pr.title,
|
|
475
|
+
body: pr.body,
|
|
476
|
+
draft: pr.draft,
|
|
477
|
+
html_url: pr.html_url
|
|
478
|
+
};
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error(`Error upserting pull request:`, err);
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
//#endregion
|
|
486
|
+
//#region src/prompts.ts
|
|
487
|
+
async function promptPackageSelection(packages) {
|
|
488
|
+
const response = await prompts({
|
|
489
|
+
type: "multiselect",
|
|
490
|
+
name: "selectedPackages",
|
|
491
|
+
message: "Select packages to release",
|
|
492
|
+
choices: packages.map((pkg) => ({
|
|
493
|
+
title: `${pkg.name} (${pkg.version})`,
|
|
494
|
+
value: pkg.name,
|
|
495
|
+
selected: true
|
|
496
|
+
})),
|
|
497
|
+
min: 1,
|
|
498
|
+
hint: "Space to select/deselect. Return to submit."
|
|
499
|
+
});
|
|
500
|
+
if (!response.selectedPackages || response.selectedPackages.length === 0) throw new Error("No packages selected");
|
|
501
|
+
return response.selectedPackages;
|
|
502
|
+
}
|
|
503
|
+
async function promptVersionOverride(packageName, currentVersion, suggestedVersion, suggestedBumpType) {
|
|
504
|
+
const choices = [{
|
|
505
|
+
title: `Use suggested: ${suggestedVersion} (${suggestedBumpType})`,
|
|
506
|
+
value: "suggested"
|
|
507
|
+
}];
|
|
508
|
+
for (const bumpType of [
|
|
509
|
+
"patch",
|
|
510
|
+
"minor",
|
|
511
|
+
"major"
|
|
512
|
+
]) if (bumpType !== suggestedBumpType) {
|
|
513
|
+
const version = calculateNewVersion(currentVersion, bumpType);
|
|
514
|
+
choices.push({
|
|
515
|
+
title: `${bumpType}: ${version}`,
|
|
516
|
+
value: bumpType
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
choices.push({
|
|
520
|
+
title: "Custom version",
|
|
521
|
+
value: "custom"
|
|
522
|
+
});
|
|
523
|
+
const response = await prompts([{
|
|
524
|
+
type: "select",
|
|
525
|
+
name: "choice",
|
|
526
|
+
message: `${packageName} (${currentVersion}):`,
|
|
527
|
+
choices,
|
|
528
|
+
initial: 0
|
|
529
|
+
}, {
|
|
530
|
+
type: (prev) => prev === "custom" ? "text" : null,
|
|
531
|
+
name: "customVersion",
|
|
532
|
+
message: "Enter custom version:",
|
|
533
|
+
initial: suggestedVersion,
|
|
534
|
+
validate: (value) => {
|
|
535
|
+
return /^\d+\.\d+\.\d+(?:[-+].+)?$/.test(value) || "Invalid semver version (e.g., 1.0.0)";
|
|
536
|
+
}
|
|
537
|
+
}]);
|
|
538
|
+
if (response.choice === "suggested") return suggestedVersion;
|
|
539
|
+
else if (response.choice === "custom") return response.customVersion;
|
|
540
|
+
else return calculateNewVersion(currentVersion, response.choice);
|
|
541
|
+
}
|
|
542
|
+
async function promptVersionOverrides(packages) {
|
|
543
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
544
|
+
for (const pkg of packages) {
|
|
545
|
+
const newVersion = await promptVersionOverride(pkg.name, pkg.currentVersion, pkg.suggestedVersion, pkg.bumpType);
|
|
546
|
+
overrides.set(pkg.name, newVersion);
|
|
547
|
+
}
|
|
548
|
+
return overrides;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/workspace.ts
|
|
553
|
+
const debug = createDebugger("ucdjs:release-scripts:workspace");
|
|
554
|
+
function shouldIncludePackage(pkg, options) {
|
|
555
|
+
if (!options) return true;
|
|
556
|
+
if (options.excludePrivate && pkg.private) return false;
|
|
557
|
+
if (options.included && options.included.length > 0) {
|
|
558
|
+
if (!options.included.includes(pkg.name)) return false;
|
|
559
|
+
}
|
|
560
|
+
if (options.excluded?.includes(pkg.name)) return false;
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
async function findWorkspacePackages(workspaceRoot, options) {
|
|
564
|
+
const result = await run("pnpm", [
|
|
565
|
+
"-r",
|
|
566
|
+
"ls",
|
|
567
|
+
"--json"
|
|
568
|
+
], { nodeOptions: {
|
|
569
|
+
cwd: workspaceRoot,
|
|
570
|
+
stdio: "pipe"
|
|
571
|
+
} });
|
|
572
|
+
const rawProjects = JSON.parse(result.stdout);
|
|
573
|
+
const packages = [];
|
|
574
|
+
const allPackageNames = new Set(rawProjects.map((p) => p.name));
|
|
575
|
+
for (const rawProject of rawProjects) {
|
|
576
|
+
const content = await readFile(join(rawProject.path, "package.json"), "utf-8");
|
|
577
|
+
const packageJson = JSON.parse(content);
|
|
578
|
+
if (!shouldIncludePackage(packageJson, options)) {
|
|
579
|
+
debug?.(`Excluding package ${rawProject.name}`);
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const workspaceDeps = extractWorkspaceDependencies(rawProject.dependencies, allPackageNames);
|
|
583
|
+
const workspaceDevDeps = extractWorkspaceDependencies(rawProject.devDependencies, allPackageNames);
|
|
584
|
+
packages.push({
|
|
585
|
+
name: rawProject.name,
|
|
586
|
+
version: rawProject.version,
|
|
587
|
+
path: rawProject.path,
|
|
588
|
+
packageJson,
|
|
589
|
+
workspaceDependencies: workspaceDeps,
|
|
590
|
+
workspaceDevDependencies: workspaceDevDeps
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
return packages;
|
|
594
|
+
}
|
|
595
|
+
function extractWorkspaceDependencies(dependencies, workspacePackages) {
|
|
596
|
+
if (!dependencies) return [];
|
|
597
|
+
return Object.keys(dependencies).filter((dep) => {
|
|
598
|
+
return workspacePackages.has(dep);
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
function buildDependencyGraph(packages) {
|
|
602
|
+
const packagesMap = /* @__PURE__ */ new Map();
|
|
603
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
604
|
+
for (const pkg of packages) {
|
|
605
|
+
packagesMap.set(pkg.name, pkg);
|
|
606
|
+
dependents.set(pkg.name, /* @__PURE__ */ new Set());
|
|
607
|
+
}
|
|
608
|
+
for (const pkg of packages) {
|
|
609
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
610
|
+
for (const dep of allDeps) {
|
|
611
|
+
const depSet = dependents.get(dep);
|
|
612
|
+
if (depSet) depSet.add(pkg.name);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
packages: packagesMap,
|
|
617
|
+
dependents
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
function getPackageUpdateOrder(graph, changedPackages) {
|
|
621
|
+
const result = [];
|
|
622
|
+
const visited = /* @__PURE__ */ new Set();
|
|
623
|
+
const toUpdate = new Set(changedPackages);
|
|
624
|
+
const packagesToProcess = new Set(changedPackages);
|
|
625
|
+
for (const pkg of changedPackages) {
|
|
626
|
+
const deps = graph.dependents.get(pkg);
|
|
627
|
+
if (deps) for (const dep of deps) {
|
|
628
|
+
packagesToProcess.add(dep);
|
|
629
|
+
toUpdate.add(dep);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function visit(pkgName, level) {
|
|
633
|
+
if (visited.has(pkgName)) return;
|
|
634
|
+
visited.add(pkgName);
|
|
635
|
+
const pkg = graph.packages.get(pkgName);
|
|
636
|
+
if (!pkg) return;
|
|
637
|
+
const allDeps = [...pkg.workspaceDependencies, ...pkg.workspaceDevDependencies];
|
|
638
|
+
let maxDepLevel = level;
|
|
639
|
+
for (const dep of allDeps) if (toUpdate.has(dep)) {
|
|
640
|
+
visit(dep, level);
|
|
641
|
+
const depResult = result.find((r) => r.package.name === dep);
|
|
642
|
+
if (depResult && depResult.level >= maxDepLevel) maxDepLevel = depResult.level + 1;
|
|
643
|
+
}
|
|
644
|
+
result.push({
|
|
645
|
+
package: pkg,
|
|
646
|
+
level: maxDepLevel
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
for (const pkg of toUpdate) visit(pkg, 0);
|
|
650
|
+
result.sort((a, b) => a.level - b.level);
|
|
651
|
+
return result;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/release.ts
|
|
656
|
+
const isCI = process.env.CI === "true";
|
|
657
|
+
async function release(options) {
|
|
658
|
+
const { dryRun: dryRun$1 = false, safeguards = true, workspaceRoot = process.cwd(), releaseBranch = "release/next", githubToken } = options;
|
|
659
|
+
globalOptions.dryRun = dryRun$1;
|
|
660
|
+
if (githubToken.trim() === "" || githubToken == null) throw new Error("GitHub token is required");
|
|
661
|
+
const [owner, repo] = options.repo.split("/");
|
|
662
|
+
if (!owner || !repo) throw new Error(`Invalid repo format: ${options.repo}. Expected "owner/repo".`);
|
|
663
|
+
if (safeguards && !isWorkingDirectoryClean(workspaceRoot)) {
|
|
664
|
+
console.error("Working directory is not clean. Please commit or stash your changes before proceeding.");
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
const { workspacePackages, packagesToAnalyze: initialPackages } = await discoverPackages(workspaceRoot, options);
|
|
668
|
+
if (initialPackages.length === 0) return null;
|
|
669
|
+
const isPackagePromptEnabled = options.prompts?.packages !== false;
|
|
670
|
+
const isPackagesPreConfigured = Array.isArray(options.packages) || typeof options.packages === "object" && options.packages.included != null;
|
|
671
|
+
let packagesToAnalyze = initialPackages;
|
|
672
|
+
if (!isCI && isPackagePromptEnabled && !isPackagesPreConfigured) {
|
|
673
|
+
const selectedNames = await promptPackageSelection(initialPackages);
|
|
674
|
+
packagesToAnalyze = initialPackages.filter((pkg) => selectedNames.includes(pkg.name));
|
|
675
|
+
}
|
|
676
|
+
const changedPackages = await analyzeCommits(packagesToAnalyze, workspaceRoot);
|
|
677
|
+
if (changedPackages.size === 0) throw new Error("No packages have changes requiring a release");
|
|
678
|
+
let versionUpdates = calculateVersions(workspacePackages, changedPackages);
|
|
679
|
+
const isVersionPromptEnabled = options.prompts?.versions !== false;
|
|
680
|
+
if (!isCI && isVersionPromptEnabled) {
|
|
681
|
+
const versionOverrides = await promptVersionOverrides(versionUpdates.map((u) => ({
|
|
682
|
+
name: u.package.name,
|
|
683
|
+
currentVersion: u.currentVersion,
|
|
684
|
+
suggestedVersion: u.newVersion,
|
|
685
|
+
bumpType: u.bumpType
|
|
686
|
+
})));
|
|
687
|
+
versionUpdates = versionUpdates.map((update) => {
|
|
688
|
+
const overriddenVersion = versionOverrides.get(update.package.name);
|
|
689
|
+
if (overriddenVersion && overriddenVersion !== update.newVersion) return {
|
|
690
|
+
...update,
|
|
691
|
+
newVersion: overriddenVersion
|
|
692
|
+
};
|
|
693
|
+
return update;
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
const allUpdates = createDependentUpdates(getPackageUpdateOrder(buildDependencyGraph(workspacePackages), new Set(versionUpdates.map((u) => u.package.name))), versionUpdates);
|
|
697
|
+
const currentBranch = await getCurrentBranch(workspaceRoot);
|
|
698
|
+
const existingPullRequest = await getExistingPullRequest({
|
|
699
|
+
owner,
|
|
700
|
+
repo,
|
|
701
|
+
branch: releaseBranch,
|
|
702
|
+
githubToken
|
|
703
|
+
});
|
|
704
|
+
const prExists = !!existingPullRequest;
|
|
705
|
+
if (prExists) console.log("Existing pull request found:", existingPullRequest.html_url);
|
|
706
|
+
else console.log("No existing pull request found, will create new one");
|
|
707
|
+
const branchExists = await doesBranchExist(releaseBranch, workspaceRoot);
|
|
708
|
+
if (!branchExists) {
|
|
709
|
+
console.log("Creating release branch:", releaseBranch);
|
|
710
|
+
await createBranch(releaseBranch, currentBranch, workspaceRoot);
|
|
711
|
+
}
|
|
712
|
+
if (!await checkoutBranch(releaseBranch, workspaceRoot)) throw new Error(`Failed to checkout branch: ${releaseBranch}`);
|
|
713
|
+
if (branchExists) {
|
|
714
|
+
console.log("Pulling latest changes from remote");
|
|
715
|
+
if (!await pullLatestChanges(releaseBranch, workspaceRoot)) console.log("Warning: Failed to pull latest changes, continuing anyway");
|
|
716
|
+
}
|
|
717
|
+
console.log("Rebasing release branch onto", currentBranch);
|
|
718
|
+
await rebaseBranch(currentBranch, workspaceRoot);
|
|
719
|
+
await updatePackageJsonFiles(allUpdates);
|
|
720
|
+
if (!await commitChanges("chore: update release versions", workspaceRoot)) {
|
|
721
|
+
console.log("No changes to commit");
|
|
722
|
+
await checkoutBranch(currentBranch, workspaceRoot);
|
|
723
|
+
if (prExists) {
|
|
724
|
+
console.log("No updates needed, PR is already up to date");
|
|
725
|
+
return {
|
|
726
|
+
updates: allUpdates,
|
|
727
|
+
prUrl: existingPullRequest.html_url,
|
|
728
|
+
created: false
|
|
729
|
+
};
|
|
730
|
+
} else {
|
|
731
|
+
console.error("No changes to commit, and no existing PR. Nothing to do.");
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
console.log("Pushing changes to remote");
|
|
736
|
+
await pushBranch(releaseBranch, workspaceRoot, { forceWithLease: true });
|
|
737
|
+
const prTitle = existingPullRequest?.title || "Release: Update package versions";
|
|
738
|
+
const prBody = generatePRBody(allUpdates);
|
|
739
|
+
const pullRequest = await upsertPullRequest({
|
|
740
|
+
owner,
|
|
741
|
+
repo,
|
|
742
|
+
pullNumber: existingPullRequest?.number,
|
|
743
|
+
title: prTitle,
|
|
744
|
+
body: prBody,
|
|
745
|
+
head: releaseBranch,
|
|
746
|
+
base: currentBranch,
|
|
747
|
+
githubToken
|
|
748
|
+
});
|
|
749
|
+
console.log(prExists ? "Updated pull request:" : "Created pull request:", pullRequest?.html_url);
|
|
750
|
+
await checkoutBranch(currentBranch, workspaceRoot);
|
|
751
|
+
return {
|
|
752
|
+
updates: allUpdates,
|
|
753
|
+
prUrl: pullRequest?.html_url,
|
|
754
|
+
created: !prExists
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async function discoverPackages(workspaceRoot, options) {
|
|
758
|
+
let workspacePackages;
|
|
759
|
+
let packagesToAnalyze;
|
|
760
|
+
if (typeof options.packages === "boolean" && options.packages === true) {
|
|
761
|
+
workspacePackages = await findWorkspacePackages(workspaceRoot, { excludePrivate: false });
|
|
762
|
+
packagesToAnalyze = workspacePackages;
|
|
763
|
+
return {
|
|
764
|
+
workspacePackages,
|
|
765
|
+
packagesToAnalyze
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
if (Array.isArray(options.packages)) {
|
|
769
|
+
const packageNames = options.packages;
|
|
770
|
+
workspacePackages = await findWorkspacePackages(workspaceRoot, {
|
|
771
|
+
excludePrivate: false,
|
|
772
|
+
included: packageNames
|
|
773
|
+
});
|
|
774
|
+
packagesToAnalyze = workspacePackages.filter((pkg) => packageNames.includes(pkg.name));
|
|
775
|
+
if (packagesToAnalyze.length !== packageNames.length) {
|
|
776
|
+
const found = new Set(packagesToAnalyze.map((p) => p.name));
|
|
777
|
+
const missing = packageNames.filter((p) => !found.has(p));
|
|
778
|
+
throw new Error(`Packages not found in workspace: ${missing.join(", ")}`);
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
workspacePackages,
|
|
782
|
+
packagesToAnalyze
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
workspacePackages = await findWorkspacePackages(workspaceRoot, options.packages);
|
|
786
|
+
packagesToAnalyze = workspacePackages;
|
|
787
|
+
return {
|
|
788
|
+
workspacePackages,
|
|
789
|
+
packagesToAnalyze
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
async function analyzeCommits(packages, workspaceRoot) {
|
|
793
|
+
const changedPackages = /* @__PURE__ */ new Map();
|
|
794
|
+
for (const pkg of packages) {
|
|
795
|
+
const bump = await analyzePackageCommits(pkg, workspaceRoot);
|
|
796
|
+
if (bump !== "none") changedPackages.set(pkg.name, bump);
|
|
797
|
+
}
|
|
798
|
+
return changedPackages;
|
|
799
|
+
}
|
|
800
|
+
function calculateVersions(allPackages, changedPackages) {
|
|
801
|
+
const updates = [];
|
|
802
|
+
for (const [pkgName, bump] of changedPackages) {
|
|
803
|
+
const pkg = allPackages.find((p) => p.name === pkgName);
|
|
804
|
+
if (!pkg) continue;
|
|
805
|
+
updates.push(createVersionUpdate(pkg, bump, true));
|
|
806
|
+
}
|
|
807
|
+
return updates;
|
|
808
|
+
}
|
|
809
|
+
async function updatePackageJsonFiles(updates) {
|
|
810
|
+
await Promise.all(updates.map(async (update) => {
|
|
811
|
+
const depUpdates = getDependencyUpdates(update.package, updates);
|
|
812
|
+
await updatePackageJson(update.package, update.newVersion, depUpdates);
|
|
813
|
+
}));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
//#endregion
|
|
817
|
+
export { release };
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ucdjs/release-scripts",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "@ucdjs release scripts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/ucdjs/release-scripts.git"
|
|
10
|
+
},
|
|
11
|
+
"exports": {
|
|
12
|
+
".": "./dist/index.mjs",
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"main": "./dist/index.mjs",
|
|
16
|
+
"module": "./dist/index.mjs",
|
|
17
|
+
"types": "./dist/index.d.mts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"commit-parser": "0.4.5",
|
|
23
|
+
"debug": "4.4.3",
|
|
24
|
+
"farver": "1.0.0-beta.1",
|
|
25
|
+
"prompts": "2.4.2",
|
|
26
|
+
"tinyexec": "1.0.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@luxass/eslint-config": "6.0.1",
|
|
30
|
+
"@types/debug": "4.1.12",
|
|
31
|
+
"@types/node": "22.18.12",
|
|
32
|
+
"@types/prompts": "2.4.9",
|
|
33
|
+
"eslint": "9.39.1",
|
|
34
|
+
"tsdown": "0.16.0",
|
|
35
|
+
"typescript": "5.9.3",
|
|
36
|
+
"vitest": "4.0.4"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown",
|
|
40
|
+
"dev": "tsdown --watch",
|
|
41
|
+
"test": "vitest --run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"lint": "eslint .",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
}
|
|
46
|
+
}
|