@peiyanlu/release 0.0.4 → 0.0.6

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/cli.mjs CHANGED
@@ -1,24 +1,24 @@
1
1
  import path, { join, resolve } from "node:path";
2
2
  import { fileURLToPath, pathToFileURL } from "node:url";
3
- import { eol, execAsync, isGitRepo, parseGitHubRepo, readJsonFile, runGit, runNpm } from "@peiyanlu/cli-utils";
3
+ import { bumpPackageVersion, coloredChangeset, eol, execAsync, getAllRemotes, getAuthenticatedUser, getCurrentBranch, getGithubReleaseUrl, getGithubUrl, getLatestTag, getLog, getPackageInfo, getPackageUrl, getPublishedVersion, getRemote, getStatus, gitAddAll, gitAddTracked, gitCommit, gitTagAnnotated, hasWriteAccess, isGitRepo, isWorkingDirClean, parseGitHubRepo, pingRegistry, publishPackage, pushBranch, pushTag, readJsonFile, resolveChangelogRange } from "@peiyanlu/cli-utils";
4
4
  import { dim, green, red, rgb, underline, yellow } from "ansis";
5
5
  import { program } from "commander";
6
6
  import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
7
7
  import { cancel, confirm, group, intro, isCancel, log, outro, select, tasks, text } from "@clack/prompts";
8
8
  import { publint } from "publint";
9
9
  import { formatMessage } from "publint/utils";
10
- import { clean, coerce, eq, gt, inc, lt, parse, prerelease, valid } from "semver";
10
+ import { coerce, inc, lt, neq, parse, prerelease, valid } from "semver";
11
11
  import { castArray, isPlainObject, isUndefined } from "@peiyanlu/ts-utils";
12
12
  import { spawnSync } from "node:child_process";
13
13
  import { inspect } from "node:util";
14
- import { Octokit } from "@octokit/rest";
15
- import { contentType } from "mime-types";
16
- import open from "open";
17
- import { glob } from "tinyglobby";
18
14
  import { ConventionalChangelog } from "conventional-changelog";
19
15
  import createPreset from "conventional-changelog-conventionalcommits";
20
16
  import { writeFile } from "node:fs/promises";
21
17
  import { finished } from "node:stream/promises";
18
+ import { Octokit } from "@octokit/rest";
19
+ import { contentType } from "mime-types";
20
+ import open from "open";
21
+ import { glob } from "tinyglobby";
22
22
 
23
23
  //#region node_modules/.pnpm/tsdown@0.18.4_publint@0.3.17_typescript@5.9.3/node_modules/tsdown/esm-shims.js
24
24
  const getFilename = () => fileURLToPath(import.meta.url);
@@ -79,6 +79,7 @@ const t = () => green(`${Math.floor(process.uptime())}s`);
79
79
  const MSG = {
80
80
  INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
81
81
  OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
82
+ OUTRO_PREPARE: (dryRun) => `🎉 PrepareRelease finished successfully in ${t()} ${n(dryRun)}`,
82
83
  LOG: {
83
84
  SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
84
85
  SHOW_CHANGELOG: "🎉 Changelog collected",
@@ -93,6 +94,10 @@ const MSG = {
93
94
  NPM: {
94
95
  CHECKING: "Checking npm registry",
95
96
  CHECKED: (registry, msg$1) => `NPM registry: ${underline(dim(registry))} ${underline(dim(msg$1))}`
97
+ },
98
+ GITHUB: {
99
+ CHECKING: "Checking github repository",
100
+ CHECKED: (repository, msg$1) => `GitHub repository: ${underline(dim(repository))} ${underline(dim(msg$1))}`
96
101
  }
97
102
  },
98
103
  TASK: {
@@ -151,259 +156,8 @@ const MSG = {
151
156
  }
152
157
  };
153
158
 
154
- //#endregion
155
- //#region src/github/release.ts
156
- const getGithubUrl = (owner, repo) => {
157
- return `https://github.com/${owner}/${repo}`;
158
- };
159
- const truncateBody = (body) => {
160
- if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
161
- return body;
162
- };
163
- const uploadAssets = async (ctx, config) => {
164
- const { github: { assets } } = config;
165
- const { github: { isReleased, owner, repo, uploadUrl, releaseId, token }, pkg: { name } } = ctx;
166
- if (!assets?.length || !isReleased) return;
167
- const octokit = new Octokit({ auth: token });
168
- const files = await glob(assets);
169
- await Promise.all(files.map((file) => {
170
- return octokit.repos.uploadReleaseAsset({
171
- owner,
172
- repo,
173
- name,
174
- url: uploadUrl,
175
- release_id: releaseId,
176
- data: createReadStream(file),
177
- headers: {
178
- "content-type": contentType(file) || "application/octet-stream",
179
- "content-length": statSync(file).size
180
- }
181
- });
182
- }));
183
- };
184
- const createWebRelease = async (ctx, config) => {
185
- const { pkg: { toPreRelease }, github: { owner, repo, changelog, releaseName }, git: { currentTag } } = ctx;
186
- const { github: { prerelease: prerelease$1 } } = config;
187
- const github = getGithubUrl(owner, repo);
188
- const url = new URL(`${github}/releases/new`);
189
- url.searchParams.set("tag", currentTag);
190
- url.searchParams.set("title", releaseName);
191
- url.searchParams.set("body", truncateBody(changelog));
192
- url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
193
- const isWindows = process.platform === "win32";
194
- await open(url.toString(), { wait: isWindows });
195
- };
196
- const createCiRelease = async (ctx, config) => {
197
- const { pkg: { toPreRelease }, github: { owner, repo, token, changelog, releaseName }, git: { currentTag } } = ctx;
198
- const { github: { prerelease: prerelease$1, draft, autoGenerate } } = config;
199
- const octokit = new Octokit({ auth: token });
200
- try {
201
- const { data } = await octokit.repos.createRelease({
202
- owner,
203
- repo,
204
- tag_name: currentTag,
205
- target_commitish: void 0,
206
- name: releaseName,
207
- body: autoGenerate ? "" : truncateBody(changelog),
208
- draft: draft ?? false,
209
- prerelease: (toPreRelease || prerelease$1) ?? false,
210
- discussion_category_name: void 0,
211
- generate_release_notes: autoGenerate,
212
- make_latest: "true"
213
- });
214
- const { html_url, upload_url, id, discussion_url } = data;
215
- Object.assign(ctx.github, {
216
- isReleased: true,
217
- releaseId: id,
218
- releaseUrl: html_url,
219
- uploadUrl: upload_url,
220
- discussionUrl: discussion_url
221
- });
222
- } catch (e) {
223
- if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(currentTag));
224
- throw e;
225
- }
226
- };
227
- const parseGithubUrl = (ctx) => {
228
- const { git: { remoteUrl } } = ctx;
229
- const [owner, repo] = parseGitHubRepo(remoteUrl);
230
- Object.assign(ctx.github, {
231
- owner,
232
- repo
233
- });
234
- };
235
- const createRelease = async (ctx, config) => {
236
- const { github: { owner, repo }, isCI } = ctx;
237
- const { github: { tokenRef, release, skipChecks } } = config;
238
- if (!release) return;
239
- const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
240
- if (!token) {
241
- Object.assign(ctx.github, { isWeb: true });
242
- if (isCI && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
243
- }
244
- Object.assign(ctx.github, { token });
245
- const octokit = new Octokit({ auth: token });
246
- if (token && !skipChecks) if (process.env.GITHUB_ACTIONS) Object.assign(ctx.github, { username: process.env.GITHUB_ACTOR });
247
- else {
248
- try {
249
- const { data: { login: username } } = await octokit.users.getAuthenticated();
250
- Object.assign(ctx.github, { username });
251
- } catch (e) {
252
- throw new Error(MSG.ERROR.GITHUB_AUTH);
253
- }
254
- try {
255
- const { github: { username } } = ctx;
256
- await octokit.repos.checkCollaborator({
257
- owner,
258
- repo,
259
- username
260
- });
261
- } catch (e) {
262
- throw new Error(MSG.ERROR.GITHUB_USER);
263
- }
264
- }
265
- const { github: { isWeb }, dryRun } = ctx;
266
- if (dryRun) return true;
267
- if (isWeb) await createWebRelease(ctx, config);
268
- else if (isCI) {
269
- await createCiRelease(ctx, config);
270
- await uploadAssets(ctx, config);
271
- }
272
- };
273
- const getGithubReleaseUrl = (ctx) => {
274
- const { github: { owner, repo }, git: { currentTag } } = ctx;
275
- return `${getGithubUrl(owner, repo)}/releases/tag/${normalizeTag(currentTag)}`;
276
- };
277
-
278
159
  //#endregion
279
160
  //#region src/git/commit.ts
280
- const getBranchName = async () => {
281
- let branch = await runGit(["branch", "--show-current"]);
282
- if (!branch) {
283
- branch = await runGit([
284
- "rev-parse",
285
- "--abbrev-ref",
286
- "HEAD"
287
- ]);
288
- if (branch === "HEAD") return void 0;
289
- }
290
- return branch;
291
- };
292
- const getRemoteForBranch = (branch) => {
293
- return runGit([
294
- "config",
295
- "--get",
296
- `branch.${branch}.remote`
297
- ]);
298
- };
299
- const getAllRemotes = async () => {
300
- return (await runGit(["remote"]))?.split("\n").filter(Boolean) ?? [];
301
- };
302
- const getStatus = () => {
303
- return runGit([
304
- "status",
305
- "--short",
306
- "--untracked-files=no"
307
- ], { trim: false });
308
- };
309
- const coloredChangeset = (log$1) => {
310
- const colorStatusChar = (ch) => {
311
- switch (ch) {
312
- case "M": return `\x1b[33m${ch}\x1b[0m`;
313
- case "A": return `\x1b[32m${ch}\x1b[0m`;
314
- case "D": return `\x1b[31m${ch}\x1b[0m`;
315
- default: return ch;
316
- }
317
- };
318
- const colorStatus = (status) => status.split("").map(colorStatusChar).join("");
319
- return log$1.split("\n").filter(Boolean).map((line) => {
320
- const status = line.slice(0, 2);
321
- const file = line.slice(3);
322
- return `${colorStatus(status)} ${dim(file)}`;
323
- }).join("\n");
324
- };
325
- const getRemote = async () => {
326
- const branch = await getBranchName();
327
- const remoteName = branch && await getRemoteForBranch(branch) || "origin";
328
- return {
329
- remoteName,
330
- remoteUrl: await runGit([
331
- "remote",
332
- "get-url",
333
- remoteName
334
- ]).catch((_) => runGit([
335
- "config",
336
- "--get",
337
- `remote.${remoteName}.url`
338
- ]))
339
- };
340
- };
341
- const getLatestTag = async (match = "*", exclude = "*-beta.*") => {
342
- return runGit([
343
- "describe",
344
- "--tags",
345
- "--abbrev=0",
346
- `--match=${match}`,
347
- `--exclude=${exclude}`
348
- ]);
349
- };
350
- const getPreviousTag = async (current) => {
351
- return runGit([
352
- "describe",
353
- "--tags",
354
- "--abbrev=0",
355
- `${await runGit([
356
- "rev-list",
357
- "--tags",
358
- current || "--skip=1",
359
- "--max-count=1"
360
- ])}^`
361
- ]);
362
- };
363
- const resolveChangelogRange = async (isIncrement = true) => {
364
- const latestTag = await getLatestTag();
365
- const previousTag = await getPreviousTag(latestTag);
366
- if (!latestTag) return {
367
- from: "",
368
- to: "HEAD"
369
- };
370
- if (!isIncrement && previousTag) return {
371
- from: previousTag,
372
- to: `${latestTag}^1`
373
- };
374
- return {
375
- from: latestTag,
376
- to: "HEAD"
377
- };
378
- };
379
- const getLog = async (from = "", to = "HEAD", scope) => {
380
- const cmd = ["log", `--pretty=format:* %s (%h)`];
381
- if (from) cmd.push(`${from}...${to}`);
382
- if (scope) cmd.push(...["--", scope]);
383
- return runGit(cmd, { trim: false });
384
- };
385
- const isWorkingDirClean = async () => {
386
- return (await runGit(["status", "-s"]))?.length === 0;
387
- };
388
- const hasUpstreamBranch = async () => {
389
- const branch = await runGit([
390
- "for-each-ref",
391
- "--format=%(upstream:short)",
392
- await runGit(["symbolic-ref", "HEAD"])
393
- ]);
394
- return Boolean(branch);
395
- };
396
- const getUpstreamArgs = async (remoteName) => {
397
- const hasUpstream = await hasUpstreamBranch();
398
- const branch = await getBranchName();
399
- if (!hasUpstream) return [
400
- "--set-upstream",
401
- remoteName,
402
- branch
403
- ];
404
- return [remoteName, branch];
405
- };
406
- const normalizeTag = (tag) => clean(tag) ?? tag;
407
161
  const gitCheck = async (ctx, config) => {
408
162
  const { pkg: { name } } = ctx;
409
163
  const { git: { requireRepository, requireRemote, requireWorkDirClean } } = config;
@@ -423,60 +177,34 @@ const gitCheck = async (ctx, config) => {
423
177
  remoteName,
424
178
  remoteUrl
425
179
  });
426
- parseGithubUrl(ctx);
180
+ const [owner, repo] = parseGitHubRepo(remoteUrl);
181
+ Object.assign(ctx.github, {
182
+ owner,
183
+ repo
184
+ });
427
185
  };
428
186
  const commitAndTag = async (ctx, config) => {
429
187
  const { git: { remoteName, currentTag, commitMessage, tagMessage }, dryRun } = ctx;
430
188
  const { git: { commit, tag, push, addUntrackedFiles, commitArgs, tagArgs, pushArgs } } = config;
431
189
  if (!commit) return;
432
190
  const args = dryRun ? ["--dry-run"] : [];
433
- await runGit([
434
- "add",
435
- ".",
436
- addUntrackedFiles ? "--all" : "--update",
437
- ...args
438
- ]);
439
- await runGit([
440
- "commit",
441
- "--message",
442
- commitMessage,
443
- ...commitArgs,
444
- ...args
445
- ]);
191
+ addUntrackedFiles ? await gitAddAll() : await gitAddTracked(args);
192
+ await gitCommit(commitMessage, [...commitArgs, ...args]);
446
193
  if (!dryRun) Object.assign(ctx.git, { isCommitted: true });
447
194
  if (tag) {
448
- await runGit([
449
- "tag",
450
- "--annotate",
451
- "--message",
452
- tagMessage,
453
- currentTag,
454
- ...tagArgs,
455
- ...args
456
- ]);
195
+ await gitTagAnnotated(currentTag, tagMessage, [...tagArgs, ...args]);
457
196
  if (!dryRun) Object.assign(ctx.git, { isTagged: true });
458
197
  }
459
198
  if (push) {
460
199
  const remotes = await getAllRemotes();
461
200
  for (const remoteName$1 of remotes) {
462
- const upstreamArgs = await getUpstreamArgs(remoteName$1);
463
- await runGit([
464
- "push",
465
- remoteName$1,
466
- `refs/tags/${currentTag}`,
467
- ...args
468
- ]);
469
- await runGit([
470
- "push",
471
- ...upstreamArgs,
472
- ...pushArgs,
473
- ...args
474
- ]);
201
+ await pushTag(remoteName$1, currentTag, args);
202
+ await pushBranch(remoteName$1, await getCurrentBranch(), [...pushArgs, ...args]);
475
203
  }
476
204
  if (!dryRun) Object.assign(ctx.git, { isPushed: true });
477
205
  }
478
206
  };
479
- const gitRollback$1 = (ctx) => {
207
+ const gitRollback = (ctx) => {
480
208
  const { git: { currentTag, isCommitted, isTagged, isPushed } } = ctx;
481
209
  if (isPushed) return;
482
210
  spawnSync("git", ["restore", "."]);
@@ -492,6 +220,30 @@ const gitRollback$1 = (ctx) => {
492
220
  ]);
493
221
  };
494
222
 
223
+ //#endregion
224
+ //#region src/prompts.ts
225
+ const abortTask = (msg$1) => {
226
+ cancel(`${red("✕")} ${msg$1}`);
227
+ process.exit(0);
228
+ };
229
+ const abortSinglePrompt = (value) => {
230
+ if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
231
+ };
232
+ const abortGroupPrompt = (ctx) => {
233
+ ctx && gitRollback(ctx);
234
+ abortTask(MSG.ABORT.CANCEL);
235
+ };
236
+ const abortOnError = (err, ctx) => {
237
+ ctx && gitRollback(ctx);
238
+ const msg$1 = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
239
+ log.message(`\n${msg$1}\n`);
240
+ process.exit(1);
241
+ };
242
+ const taskEnd = (msg$1) => {
243
+ outro(msg$1);
244
+ process.exit(0);
245
+ };
246
+
495
247
  //#endregion
496
248
  //#region src/utils.ts
497
249
  const info = (msg$1) => {
@@ -524,20 +276,6 @@ const runLifeCycleHook = async (hooks, key, dryRun) => {
524
276
  log.success(success(hook.replace(/^echo$/, dim("echo")), dryRun));
525
277
  }));
526
278
  };
527
- const gitRollback = (ctx) => {
528
- const { git: { currentTag, isCommitted, isTagged } } = ctx;
529
- spawnSync("git", ["restore", "."]);
530
- if (isTagged) spawnSync("git", [
531
- "tag",
532
- "--delete",
533
- currentTag
534
- ]);
535
- spawnSync("git", [
536
- "reset",
537
- "--hard",
538
- isCommitted ? "HEAD~1" : "HEAD"
539
- ]);
540
- };
541
279
  const diff = (from, to, separator = ".") => {
542
280
  const a = from.split(separator);
543
281
  return to.split(separator).map((v, i) => v === a[i] ? dim(v) : green(v)).join(separator);
@@ -559,39 +297,6 @@ const formatTemplate = async (ctx, config) => {
559
297
  });
560
298
  Object.assign(ctx.github, { releaseName });
561
299
  };
562
- const getPackageInfo = (pkgName, getPkgDir = (pkg$1) => `packages/${pkg$1}`) => {
563
- const pkgDir = resolve(getPkgDir(pkgName));
564
- const pkgPath = resolve(pkgDir, "package.json");
565
- return {
566
- pkg: readJsonFile(pkgPath),
567
- pkgDir,
568
- pkgPath
569
- };
570
- };
571
-
572
- //#endregion
573
- //#region src/prompts.ts
574
- const abortTask = (msg$1) => {
575
- cancel(`${red("✕")} ${msg$1}`);
576
- process.exit(0);
577
- };
578
- const abortSinglePrompt = (value) => {
579
- if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
580
- };
581
- const abortGroupPrompt = (ctx) => {
582
- ctx && gitRollback(ctx);
583
- abortTask(MSG.ABORT.CANCEL);
584
- };
585
- const abortOnError = (err, ctx) => {
586
- ctx && gitRollback(ctx);
587
- const msg$1 = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
588
- log.message(`\n${msg$1}\n`);
589
- process.exit(1);
590
- };
591
- const taskEnd = (msg$1) => {
592
- outro(msg$1);
593
- process.exit(0);
594
- };
595
300
 
596
301
  //#endregion
597
302
  //#region src/version/bump.ts
@@ -615,7 +320,10 @@ const parseVersion = (raw) => {
615
320
  };
616
321
  };
617
322
  const getCIVersion = (current) => {
618
- return inc(current, Release.PATCH) ?? "";
323
+ return inc(current, Release.PATCH);
324
+ };
325
+ const isNeq = (current, next) => {
326
+ return neq(current, next);
619
327
  };
620
328
 
621
329
  //#endregion
@@ -624,6 +332,7 @@ let Release = /* @__PURE__ */ function(Release$1) {
624
332
  Release$1["MAJOR"] = "major";
625
333
  Release$1["MINOR"] = "minor";
626
334
  Release$1["PATCH"] = "patch";
335
+ Release$1["RELEASE"] = "release";
627
336
  return Release$1;
628
337
  }({});
629
338
  let PreRelease = /* @__PURE__ */ function(PreRelease$1) {
@@ -643,76 +352,45 @@ let PreId = /* @__PURE__ */ function(PreId$1) {
643
352
  PreId$1["DEV"] = "dev";
644
353
  return PreId$1;
645
354
  }({});
355
+ const createVersionMap = (current) => ({
356
+ patch: inc(current, Release.PATCH),
357
+ minor: inc(current, Release.MINOR),
358
+ major: inc(current, Release.MAJOR),
359
+ release: inc(current, Release.RELEASE),
360
+ prerelease: inc(current, PreRelease.RELEASE),
361
+ prepatch: inc(current, PreRelease.PATCH, PreId.BETA),
362
+ preminor: inc(current, PreRelease.MINOR, PreId.BETA),
363
+ premajor: inc(current, PreRelease.MAJOR, PreId.BETA)
364
+ });
365
+ const option = (label, value, hint) => ({
366
+ label,
367
+ value,
368
+ hint: hint ?? value
369
+ });
646
370
  const runVersionPrompts = async (ctx, config) => {
647
371
  const { pkg: { current, fromPreRelease } } = ctx;
648
372
  const selectVersion = async () => {
649
- const patch = inc(current, Release.PATCH);
650
- const minor = inc(current, Release.MINOR);
651
- const major = inc(current, Release.MAJOR);
652
- const prerelease$1 = inc(current, PreRelease.RELEASE);
653
- const prepatch = inc(current, PreRelease.PATCH, PreId.BETA);
654
- const preminor = inc(current, PreRelease.MINOR, PreId.BETA);
655
- const premajor = inc(current, PreRelease.MAJOR, PreId.BETA);
656
- const pr = [{
657
- label: "Pre-Release",
658
- value: prerelease$1,
659
- hint: prerelease$1
660
- }];
661
- const pre = [
662
- {
663
- label: "Pre-Patch",
664
- value: prepatch,
665
- hint: prepatch
666
- },
667
- {
668
- label: "Pre-Minor",
669
- value: preminor,
670
- hint: preminor
671
- },
672
- {
673
- label: "Pre-Major",
674
- value: premajor,
675
- hint: premajor
676
- }
677
- ];
678
- const release = [
679
- {
680
- label: "Patch",
681
- value: patch,
682
- hint: patch
683
- },
684
- {
685
- label: "Minor",
686
- value: minor,
687
- hint: minor
688
- },
689
- {
690
- label: "Major",
691
- value: major,
692
- hint: major
693
- }
373
+ const versions = createVersionMap(current);
374
+ const options = [
375
+ ...fromPreRelease ? [option("Pre-Release", "prerelease", versions.prerelease)] : [],
376
+ option("Patch", "patch", versions.patch),
377
+ option("Minor", "minor", versions.minor),
378
+ option("Major", "major", versions.major),
379
+ ...!fromPreRelease ? [
380
+ option("Pre-Patch", "prepatch", versions.prepatch),
381
+ option("Pre-Minor", "preminor", versions.preminor),
382
+ option("Pre-Major", "premajor", versions.premajor)
383
+ ] : [],
384
+ option("As-Is", "ignore", current),
385
+ option("Custom", "custom", "custom specified")
694
386
  ];
695
- const custom = [{
696
- label: "As-Is",
697
- value: "ignore",
698
- hint: current
699
- }, {
700
- label: "Custom",
701
- value: "custom",
702
- hint: "custom specified"
703
- }];
704
- const version = await select({
387
+ const type = await select({
705
388
  message: question(MSG.PROMPT.SELECT_VERSION, "version"),
706
- options: [
707
- ...fromPreRelease ? pr : [],
708
- ...release,
709
- ...fromPreRelease ? [] : pre,
710
- ...custom
711
- ],
712
- initialValue: fromPreRelease ? prerelease$1 : patch
389
+ options,
390
+ initialValue: fromPreRelease ? "prerelease" : "patch"
713
391
  });
714
- abortSinglePrompt(version);
715
- return version;
392
+ abortSinglePrompt(type);
393
+ return type;
716
394
  };
717
395
  const inputVersion = async () => {
718
396
  const version = await text({
@@ -725,10 +403,12 @@ const runVersionPrompts = async (ctx, config) => {
725
403
  abortSinglePrompt(version);
726
404
  return version;
727
405
  };
728
- const type = await selectVersion();
729
- const next = type === "custom" ? await inputVersion() : type === "ignore" ? current : type;
730
- ctx.isIncrement = !eq(next, current);
731
- return { next };
406
+ const resolveNextVersion = async (type, current$1) => {
407
+ if (type === "ignore") return current$1;
408
+ if (type === "custom") return inputVersion();
409
+ return createVersionMap(current$1)[type];
410
+ };
411
+ return await resolveNextVersion(await selectVersion(), current);
732
412
  };
733
413
 
734
414
  //#endregion
@@ -814,7 +494,7 @@ const createDefaultConfig = (isCI) => {
814
494
  toTag(pkg$1, version) {
815
495
  return `v${version}`;
816
496
  },
817
- changelogTagPrefix: void 0,
497
+ tagPrefix: void 0,
818
498
  git: {
819
499
  commit: isCI,
820
500
  tag: isCI,
@@ -831,8 +511,7 @@ const createDefaultConfig = (isCI) => {
831
511
  },
832
512
  npm: {
833
513
  publish: isCI,
834
- publishPath: ".",
835
- pushArgs: [],
514
+ publishArgs: [],
836
515
  skipChecks: false
837
516
  },
838
517
  github: {
@@ -945,11 +624,8 @@ const defaultTypes = [
945
624
 
946
625
  //#endregion
947
626
  //#region src/git/changelog.ts
948
- const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
627
+ const createGenerator = async ({ getPkgDir, tagPrefix }) => {
949
628
  const pkgDir = getPkgDir();
950
- const infile = join(pkgDir, "CHANGELOG.md");
951
- if (!existsSync(infile)) await writeFile(infile, "");
952
- const originalChangelog = readFileSync(infile, "utf-8");
953
629
  const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
954
630
  ...t$1,
955
631
  hidden: false
@@ -1002,17 +678,30 @@ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
1002
678
  {{> commit root=@root}}
1003
679
  {{/each}}
1004
680
  {{/each}}`.trim() + eol(2);
1005
- const generator = new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
1006
- const writeStream = createWriteStream(infile);
681
+ return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
682
+ };
683
+ const getChangelog = async ({ getPkgDir, tagPrefix }) => {
684
+ const generator = await createGenerator({
685
+ getPkgDir,
686
+ tagPrefix
687
+ });
1007
688
  let changelog = "";
1008
- for await (const chunk of generator.write()) {
1009
- changelog += chunk;
1010
- writeStream.write(chunk);
1011
- }
689
+ for await (const chunk of generator.write()) changelog += chunk;
690
+ return changelog;
691
+ };
692
+ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
693
+ const infile = join(getPkgDir(), "CHANGELOG.md");
694
+ if (!existsSync(infile)) await writeFile(infile, "");
695
+ const originalChangelog = readFileSync(infile, "utf-8");
696
+ const generator = await createGenerator({
697
+ getPkgDir,
698
+ tagPrefix
699
+ });
700
+ const writeStream = createWriteStream(infile);
701
+ for await (const chunk of generator.write()) writeStream.write(chunk);
1012
702
  writeStream.write(originalChangelog);
1013
703
  writeStream.end();
1014
704
  await finished(writeStream);
1015
- return changelog;
1016
705
  };
1017
706
 
1018
707
  //#endregion
@@ -1074,6 +763,172 @@ const runGithubPrompts = async (ctx, config) => {
1074
763
  return res;
1075
764
  };
1076
765
 
766
+ //#endregion
767
+ //#region src/github/release.ts
768
+ const truncateBody = (body) => {
769
+ if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
770
+ return body;
771
+ };
772
+ const getReleaseByTagSafe = async (octokit, options) => {
773
+ try {
774
+ const { data } = await octokit.repos.getReleaseByTag({ ...options });
775
+ return data;
776
+ } catch (e) {
777
+ if (e?.status === 404) return null;
778
+ throw e;
779
+ }
780
+ };
781
+ const createCRelease = async (octokit, options) => {
782
+ const { owner, repo, tag, name, body, draft, prerelease: prerelease$1, generateReleaseNotes, makeLatest, targetCommitish, discussionCategoryName } = options;
783
+ const { data } = await octokit.repos.createRelease({
784
+ owner,
785
+ repo,
786
+ tag_name: tag,
787
+ name,
788
+ body,
789
+ draft,
790
+ prerelease: prerelease$1,
791
+ generate_release_notes: generateReleaseNotes,
792
+ make_latest: makeLatest,
793
+ target_commitish: targetCommitish,
794
+ discussion_category_name: discussionCategoryName
795
+ });
796
+ return data;
797
+ };
798
+ const updateCRelease = async (octokit, options) => {
799
+ const { owner, repo, tag, name, body, draft, prerelease: prerelease$1, generateReleaseNotes, makeLatest, targetCommitish, discussionCategoryName, releaseId } = options;
800
+ const { data } = await octokit.repos.updateRelease({
801
+ owner,
802
+ repo,
803
+ release_id: releaseId,
804
+ tag_name: tag,
805
+ name,
806
+ body,
807
+ draft,
808
+ prerelease: prerelease$1,
809
+ generate_release_notes: generateReleaseNotes,
810
+ make_latest: makeLatest,
811
+ target_commitish: targetCommitish,
812
+ discussion_category_name: discussionCategoryName
813
+ });
814
+ return data;
815
+ };
816
+ const createCReleaseByTag = async (octokit, options) => {
817
+ const existing = await getReleaseByTagSafe(octokit, options);
818
+ if (!existing) return createCRelease(octokit, { ...options });
819
+ const { draft, id: releaseId } = existing;
820
+ if (!draft) throw new Error(`Release for tag "${options.tag}" already exists and is not a draft`);
821
+ return updateCRelease(octokit, {
822
+ releaseId,
823
+ ...options
824
+ });
825
+ };
826
+ const uploadAssets = async (octokit, options) => {
827
+ const { owner, repo, name, uploadUrl, releaseId, assets } = options;
828
+ const files = await glob(assets);
829
+ await Promise.all(files.map((file) => {
830
+ return octokit.repos.uploadReleaseAsset({
831
+ owner,
832
+ repo,
833
+ name,
834
+ url: uploadUrl,
835
+ release_id: releaseId,
836
+ data: createReadStream(file),
837
+ headers: {
838
+ "content-type": contentType(file) || "application/octet-stream",
839
+ "content-length": statSync(file).size
840
+ }
841
+ });
842
+ }));
843
+ };
844
+ const createWebRelease = async (options) => {
845
+ const { owner, repo, tag, title, body, prerelease: prerelease$1 } = options;
846
+ const url = new URL(`${getGithubUrl(owner, repo)}/releases/new`);
847
+ Object.entries({
848
+ tag,
849
+ title,
850
+ body,
851
+ prerelease: prerelease$1
852
+ }).forEach(([key, value]) => {
853
+ url.searchParams.set(key, value);
854
+ });
855
+ const isWindows = process.platform === "win32";
856
+ await open(url.toString(), { wait: isWindows });
857
+ };
858
+ const githubCheck = async (ctx, config) => {
859
+ const { github: { owner, repo }, isCI } = ctx;
860
+ const { github: { tokenRef, skipChecks } } = config;
861
+ const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
862
+ if (!token) {
863
+ Object.assign(ctx.github, { isWeb: true });
864
+ if (isCI && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
865
+ return "";
866
+ }
867
+ Object.assign(ctx.github, { token });
868
+ if (skipChecks) return "";
869
+ let username = "";
870
+ if (process.env.GITHUB_ACTIONS) username = process.env.GITHUB_ACTOR;
871
+ else {
872
+ const octokit = new Octokit({ auth: token });
873
+ try {
874
+ const { data: { login } } = await octokit.users.getAuthenticated();
875
+ username = login;
876
+ } catch (e) {
877
+ throw new Error(MSG.ERROR.GITHUB_AUTH);
878
+ }
879
+ try {
880
+ await octokit.repos.checkCollaborator({
881
+ owner,
882
+ repo,
883
+ username
884
+ });
885
+ } catch (e) {
886
+ throw new Error(MSG.ERROR.GITHUB_USER);
887
+ }
888
+ }
889
+ return username;
890
+ };
891
+ const createRelease = async (ctx, config) => {
892
+ const { pkg: { name, toPreRelease }, git: { currentTag }, isCI, dryRun } = ctx;
893
+ const { github: { isWeb, token, owner, repo, changelog, releaseName } } = ctx;
894
+ const { github: { release, assets, prerelease: prerelease$1, autoGenerate, draft } } = config;
895
+ if (!release || dryRun) return;
896
+ const octokit = new Octokit({ auth: token });
897
+ if (isWeb) await createWebRelease({
898
+ owner,
899
+ repo,
900
+ tag: currentTag,
901
+ title: releaseName,
902
+ body: truncateBody(changelog),
903
+ prerelease: String(toPreRelease || prerelease$1)
904
+ });
905
+ else if (isCI) {
906
+ const res = await createCReleaseByTag(octokit, {
907
+ owner,
908
+ repo,
909
+ tag: currentTag,
910
+ name: releaseName,
911
+ body: autoGenerate ? "" : truncateBody(changelog),
912
+ draft,
913
+ prerelease: toPreRelease || prerelease$1,
914
+ generateReleaseNotes: autoGenerate,
915
+ makeLatest: "true"
916
+ });
917
+ if (!res) return;
918
+ if (assets.length) {
919
+ const { id: releaseId, upload_url: uploadUrl } = res;
920
+ await uploadAssets(octokit, {
921
+ owner,
922
+ repo,
923
+ name,
924
+ uploadUrl,
925
+ releaseId,
926
+ assets
927
+ });
928
+ }
929
+ }
930
+ };
931
+
1077
932
  //#endregion
1078
933
  //#region src/npm/prompts.ts
1079
934
  const runNpmPublishPrompts = async (ctx, config) => {
@@ -1107,120 +962,71 @@ const runNpmOptPrompts = async (ctx, config) => {
1107
962
 
1108
963
  //#endregion
1109
964
  //#region src/npm/publish.ts
1110
- const resolvePublishRegistry = (publishConfig = {}) => {
1111
- if (publishConfig.registry) return publishConfig.registry;
1112
- return Object.keys(publishConfig).filter((k) => k.endsWith("registry")).map((k) => publishConfig[k])[0];
1113
- };
1114
- const pingRegistry = async () => {
1115
- return void 0 !== await runNpm(["ping"]);
1116
- };
1117
- const getAuthenticatedUser = () => {
1118
- return runNpm(["whoami"]);
1119
- };
1120
- const getVersionByTag = (name, tag) => {
1121
- return runNpm([
1122
- "show",
1123
- `${name}@${tag}`,
1124
- "version"
1125
- ]);
1126
- };
1127
- const getPublishedVersion = (name) => {
1128
- return runNpm([
1129
- "info",
1130
- name,
1131
- "version"
1132
- ]);
1133
- };
1134
965
  const resolvePublishTag = async (pkgName, version) => {
1135
966
  const { toPreRelease, preId } = parseVersion(version);
1136
- if (!toPreRelease) {
1137
- const active = await getPublishedVersion(pkgName);
1138
- return active && lt(version, active) ? "previous" : "latest";
1139
- } else return preId || "prerelease";
1140
- };
1141
- const bumpPackageVersion = (version, args = [], cwd = ".") => {
1142
- return runNpm([
1143
- "version",
1144
- version,
1145
- "--no-git-tag-version",
1146
- "--workspaces=false",
1147
- "--allow-same-version",
1148
- ...args
1149
- ], { cwd });
967
+ if (toPreRelease) return preId || "next";
968
+ const active = await getPublishedVersion(pkgName);
969
+ if (!active) return "latest";
970
+ return lt(version, active) ? "previous" : "latest";
1150
971
  };
1151
972
  const isOtpError = (err) => err instanceof Error && /one-time password|otp/i.test(err.message);
1152
- const publishPackage = (pkg$1 = ".", tag = "latest", args = []) => {
1153
- return runNpm([
1154
- "publish",
1155
- "--access",
1156
- "public",
1157
- "--tag",
1158
- tag,
1159
- "--workspaces=false",
1160
- ...args
1161
- ], { cwd: pkg$1 });
1162
- };
1163
- const getPackageUrl = (name, next) => {
1164
- return `https://www.npmjs.com/package/${name}/v/${next}`;
1165
- };
1166
- const isCollaborator = async (ctx) => {
1167
- const { pkg: { name, publishConfig }, npm: { username } } = ctx;
1168
- const registry = resolvePublishRegistry(publishConfig);
1169
- const registryArg = registry ? ["--registry", registry] : [];
1170
- let npmVersion = await runNpm(["--version"]);
1171
- let accessCommand;
1172
- if (gt(npmVersion, "9.0.0")) accessCommand = [
1173
- "access",
1174
- "list",
1175
- "collaborators",
1176
- "--json"
1177
- ];
1178
- else accessCommand = ["access", "ls-collaborators"];
1179
- const res = await runNpm([
1180
- ...accessCommand,
1181
- name,
1182
- ...registryArg
1183
- ]);
1184
- if (res) return JSON.parse(res ?? "{}")[username]?.includes("write");
1185
- return false;
1186
- };
1187
973
  const npmCheck = async (ctx, config) => {
1188
- const { pkg: { name, current, publishConfig: { registry } }, npm: { username } } = ctx;
974
+ const { pkg: { name, current, publishConfig: { registry } } } = ctx;
1189
975
  const { npm: { skipChecks } } = config;
1190
976
  const tag = await resolvePublishTag(name, current);
1191
977
  Object.assign(ctx.npm, { tag });
1192
978
  if (skipChecks) return "";
1193
- if (!await pingRegistry()) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1194
- const getUser = async () => {
1195
- const username$1 = await getAuthenticatedUser();
1196
- if (!username$1) return false;
1197
- Object.assign(ctx.npm, { username: username$1 });
1198
- return true;
1199
- };
1200
- if (!await getUser()) throw new Error(MSG.ERROR.NPM_AUTH);
1201
- const latest = await getVersionByTag(name, tag);
979
+ const [pinged, username] = await Promise.all([pingRegistry(registry), getAuthenticatedUser(registry)]);
980
+ if (!pinged) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
981
+ if (!username) throw new Error(MSG.ERROR.NPM_AUTH);
982
+ Object.assign(ctx.npm, { username });
983
+ const latest = await getPublishedVersion(`${name}@${tag}`);
1202
984
  if (latest) {
1203
- if (!await isCollaborator(ctx)) throw new Error(MSG.ERROR.NPM_USER(username, name));
985
+ if (!await hasWriteAccess(name, username, registry)) throw new Error(MSG.ERROR.NPM_USER(username, name));
1204
986
  }
1205
987
  return latest ? `(${tag} → ${latest})` : `(${tag} → no published version)`;
1206
988
  };
1207
989
  const publishNpm = async (ctx, config) => {
1208
990
  const { dryRun, selectedPkg, npm: { otp, tag } } = ctx;
1209
- const { npm: { publish, pushArgs }, getPkgDir } = config;
991
+ const { npm: { publish, publishArgs }, getPkgDir } = config;
1210
992
  if (!publish) return;
1211
993
  const otpArgs = otp ? ["--otp", otp] : [];
1212
994
  const dryRunArg = dryRun ? ["--dry-run"] : [];
1213
- return publishPackage(getPkgDir(selectedPkg), tag, [
1214
- ...otpArgs,
1215
- ...pushArgs,
1216
- ...dryRunArg
1217
- ]);
995
+ return publishPackage({
996
+ tag,
997
+ args: [
998
+ ...otpArgs,
999
+ ...publishArgs,
1000
+ ...dryRunArg
1001
+ ],
1002
+ cwd: getPkgDir(selectedPkg)
1003
+ });
1218
1004
  };
1219
1005
 
1220
1006
  //#endregion
1221
1007
  //#region src/action.ts
1222
1008
  var Action = class {
1223
- async handle(cmdArgs, options) {
1009
+ async handleRelease(cmdArgs, options) {
1010
+ const { ctx, config } = await this.createContext(cmdArgs, options);
1011
+ await this.checkTask(ctx, config);
1012
+ await this.bumpTask(ctx, config);
1013
+ await this.changelogTask(ctx, config);
1014
+ await this.gitTask(ctx, config);
1015
+ await this.npmTask(ctx, config);
1016
+ await this.githubTask(ctx, config);
1017
+ if (ctx.dryRun) gitRollback(ctx);
1018
+ taskEnd(MSG.OUTRO(ctx.dryRun));
1019
+ }
1020
+ async handlePrepareRelease(cmdArgs, options) {
1021
+ const { ctx, config } = await this.createContext(cmdArgs, options);
1022
+ await this.checkTask(ctx, config);
1023
+ await this.bumpTask(ctx, config);
1024
+ await this.changelogTask(ctx, config);
1025
+ await this.gitTask(ctx, config);
1026
+ if (ctx.dryRun) gitRollback(ctx);
1027
+ taskEnd(MSG.OUTRO_PREPARE(ctx.dryRun));
1028
+ }
1029
+ async createContext(cmdArgs, options) {
1224
1030
  const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
1225
1031
  const { otp, package: defPkg, ...others } = options;
1226
1032
  const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
@@ -1284,43 +1090,54 @@ var Action = class {
1284
1090
  const formated = formatMessage(message, pkg$1);
1285
1091
  formated && msg("PKG", formated);
1286
1092
  }
1287
- await this.checkTask(ctx, config);
1288
- await this.bumpTask(ctx, config);
1289
- await this.changelogTask(ctx, config);
1290
- await this.gitTask(ctx, config);
1291
- await this.npmTask(ctx, config);
1292
- await this.githubTask(ctx, config);
1293
- if (dryRun) gitRollback$1(ctx);
1294
- taskEnd(MSG.OUTRO(dryRun));
1093
+ return {
1094
+ ctx,
1095
+ config
1096
+ };
1295
1097
  }
1296
1098
  async checkTask(ctx, config) {
1297
- const { noNpm, dryRun } = ctx;
1298
- await tasks([{
1299
- title: MSG.CHECK.GIT.CHECKING,
1300
- task: async () => {
1301
- await gitCheck(ctx, config);
1302
- const { git: { remoteName, remoteUrl } } = ctx;
1303
- return success(MSG.CHECK.GIT.CHECKED(remoteName, remoteUrl), dryRun);
1304
- }
1305
- }, {
1306
- title: MSG.CHECK.NPM.CHECKING,
1307
- task: async () => {
1308
- const msg$1 = await npmCheck(ctx, config);
1309
- const { pkg: { publishConfig: { registry } } } = ctx;
1310
- return success(MSG.CHECK.NPM.CHECKED(registry, msg$1), dryRun);
1099
+ const { dryRun } = ctx;
1100
+ await tasks([
1101
+ {
1102
+ title: MSG.CHECK.GIT.CHECKING,
1103
+ task: async () => {
1104
+ await gitCheck(ctx, config);
1105
+ const { git: { remoteName, remoteUrl } } = ctx;
1106
+ return success(MSG.CHECK.GIT.CHECKED(remoteName, remoteUrl), dryRun);
1107
+ }
1108
+ },
1109
+ {
1110
+ title: MSG.CHECK.NPM.CHECKING,
1111
+ task: async () => {
1112
+ const msg$1 = await npmCheck(ctx, config);
1113
+ const { pkg: { publishConfig: { registry } } } = ctx;
1114
+ return success(MSG.CHECK.NPM.CHECKED(registry, msg$1), dryRun);
1115
+ },
1116
+ enabled: !ctx.noNpm
1311
1117
  },
1312
- enabled: !noNpm
1313
- }]).catch(abortOnError);
1118
+ {
1119
+ title: MSG.CHECK.GITHUB.CHECKING,
1120
+ task: async () => {
1121
+ const msg$1 = await githubCheck(ctx, config);
1122
+ const { github: { owner, repo } } = ctx;
1123
+ const repository = getGithubUrl(owner, repo);
1124
+ return success(MSG.CHECK.GITHUB.CHECKED(repository, msg$1), dryRun);
1125
+ },
1126
+ enabled: !ctx.noGitHub
1127
+ }
1128
+ ]).catch(abortOnError);
1314
1129
  }
1315
1130
  async bumpTask(ctx, config) {
1316
1131
  const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg } = ctx;
1317
- const { hooks, getPkgDir } = config;
1132
+ const { isMonorepo, hooks, getPkgDir, tagPrefix } = config;
1318
1133
  const need = (ctx$1) => {
1319
1134
  const { pkg: { next: next$1 }, isIncrement: isIncrement$1 } = ctx$1;
1320
1135
  return !next$1 && isIncrement$1;
1321
1136
  };
1322
1137
  if (need(ctx)) {
1323
- const parsed = parseVersion(isCI && getCIVersion(current) || (await runVersionPrompts(ctx, config)).next);
1138
+ const next$1 = (isCI ? getCIVersion(current) : void 0) || await runVersionPrompts(ctx, config);
1139
+ ctx.isIncrement = isNeq(current, next$1);
1140
+ const parsed = parseVersion(next$1);
1324
1141
  Object.assign(ctx.pkg, { ...parsed });
1325
1142
  }
1326
1143
  const { pkg: { next }, isIncrement } = ctx;
@@ -1336,25 +1153,30 @@ var Action = class {
1336
1153
  }
1337
1154
  }]).catch((err) => abortOnError(err, ctx));
1338
1155
  await runLifeCycleHook(hooks, "after:bump", dryRun);
1339
- const { from, to } = await resolveChangelogRange(isIncrement);
1340
- const logStr = await getLog(from, to, getPkgDir(selectedPkg));
1341
- if (logStr) {
1342
- msg("GIT", `Changelog:${eol(2)}` + logStr);
1156
+ const { from, to } = await resolveChangelogRange(isIncrement, isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*");
1157
+ const commits = await getLog(from, to, getPkgDir(selectedPkg));
1158
+ const changelog = await getChangelog({
1159
+ getPkgDir: () => getPkgDir(selectedPkg),
1160
+ tagPrefix: tagPrefix?.(selectedPkg)
1161
+ });
1162
+ Object.assign(ctx.github, { changelog });
1163
+ if (commits) {
1164
+ msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog.trimEnd());
1165
+ msg("GIT", `Commits(${from}...${to}):${eol(2)}` + commits);
1343
1166
  if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1344
1167
  } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1345
1168
  }
1346
1169
  async changelogTask(ctx, config) {
1347
1170
  const { isIncrement, dryRun, selectedPkg } = ctx;
1348
- const { getPkgDir, changelogTagPrefix } = config;
1171
+ const { getPkgDir, tagPrefix } = config;
1349
1172
  await tasks([{
1350
1173
  title: MSG.TASK.CHANGELOG.START,
1351
1174
  task: async () => {
1352
- const changelog = await generateChangelog({
1175
+ await generateChangelog({
1353
1176
  getPkgDir: () => getPkgDir(selectedPkg),
1354
- tagPrefix: changelogTagPrefix?.(selectedPkg)
1177
+ tagPrefix: tagPrefix?.(selectedPkg)
1355
1178
  });
1356
- await runGit(["add", `${getPkgDir(selectedPkg)}/CHANGELOG.md`]);
1357
- Object.assign(ctx.github, { changelog });
1179
+ await gitAddAll();
1358
1180
  return success(MSG.TASK.CHANGELOG.END, dryRun);
1359
1181
  },
1360
1182
  enabled: isIncrement
@@ -1408,8 +1230,9 @@ var Action = class {
1408
1230
  await tasks([{
1409
1231
  title: MSG.TASK.GITHUB.START,
1410
1232
  task: async () => {
1233
+ const { github: { owner, repo }, git: { currentTag } } = ctx;
1411
1234
  await createRelease(ctx, config);
1412
- const url = getGithubReleaseUrl(ctx);
1235
+ const url = getGithubReleaseUrl(owner, repo, currentTag);
1413
1236
  return success(MSG.TASK.GITHUB.END(url), dryRun);
1414
1237
  }
1415
1238
  }]).catch((err) => abortOnError(err, ctx));
@@ -1420,8 +1243,14 @@ var Action = class {
1420
1243
  //#endregion
1421
1244
  //#region src/cli.ts
1422
1245
  const pkg = readJsonFile(join(__dirname, "..", "package.json"));
1423
- program.name("release").description(pkg.description).version(pkg.version, "-v, --version", "Print the tool version and exit.").usage("[release-type] [options]").argument("[release-type]", "Version bump type: patch | minor | major").option("-n, --dry-run", "Run in dry mode: show what would be released without making any changes.", false).option("-p, --package <pkg>", "Specify package name for mono-repo release (CI only).", "").option("--otp <code>", "One-time password for npm publish (used for 2FA).", "").option("--ci", "Enable CI mode: disable prompts and fail on missing required options.", false).option("--show-changelog", "Print the generated changelog and exit.", false).option("--show-release", "Print the version that would be released and exit.", false).helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
1424
- await new Action().handle(releaseType, options);
1246
+ function withCommonOptions(cmd) {
1247
+ return cmd.version(pkg.version, "-v, --version", "Print the tool version and exit.").usage("[release-type] [options]").argument("[release-type]", "Version bump type: patch | minor | major").option("-n, --dry-run", "Run in dry mode", false).option("-p, --package <pkg>", "Specify package name (CI only).", "").option("--otp <code>", "One-time password for npm publish.", "").option("--ci", "Enable CI mode.", false).option("--show-changelog", "Print changelog and exit.", false).option("--show-release", "Print release version and exit.", false).helpOption("-h, --help", "Display help information.");
1248
+ }
1249
+ withCommonOptions(program.name("release").description(pkg.description)).action(async (releaseType, options) => {
1250
+ await new Action().handleRelease(releaseType, options);
1251
+ });
1252
+ withCommonOptions(program.command("prepare").description("Prepare a release (bump version, commit, tag)")).action(async (releaseType, options) => {
1253
+ await new Action().handlePrepareRelease(releaseType, options);
1425
1254
  });
1426
1255
  program.command("init").description("Create a release configuration file").option("-f, --force", "Overwrite existing config file", false).option("-m, --monorepo", "Mono-repo project", false).action((options) => {
1427
1256
  const { force = false, monorepo = false } = options;
@@ -1439,7 +1268,7 @@ program.command("init").description("Create a release configuration file").optio
1439
1268
  packages: [ 'demo-a', 'demo-b' ],
1440
1269
  getPkgDir: (pkg) => \`packages/\${ pkg }\`,
1441
1270
  toTag: (pkg, version) => \`\${ pkg }@\${ version }\`,
1442
- changelogTagPrefix: (pkg) => \`\${ pkg }@\`,
1271
+ tagPrefix: (pkg) => \`\${ pkg }@\`,
1443
1272
  }`;
1444
1273
  writeFileSync(configPath, `import { defineConfig } from '@peiyanlu/release'
1445
1274