@peiyanlu/release 0.0.5 → 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/LICENSE +1 -1
- package/dist/cli.mjs +330 -246
- package/dist/index.d.mts +48 -7
- package/dist/index.mjs +338 -1
- package/package.json +11 -11
package/LICENSE
CHANGED
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 { 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
|
|
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 { coerce,
|
|
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
|
-
import { Octokit } from "@octokit/rest";
|
|
14
|
-
import { contentType } from "mime-types";
|
|
15
|
-
import open from "open";
|
|
16
|
-
import { glob } from "tinyglobby";
|
|
17
13
|
import { inspect } from "node:util";
|
|
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,123 +156,6 @@ const MSG = {
|
|
|
151
156
|
}
|
|
152
157
|
};
|
|
153
158
|
|
|
154
|
-
//#endregion
|
|
155
|
-
//#region src/github/release.ts
|
|
156
|
-
const truncateBody = (body) => {
|
|
157
|
-
if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
|
|
158
|
-
return body;
|
|
159
|
-
};
|
|
160
|
-
const uploadAssets = async (ctx, config) => {
|
|
161
|
-
const { github: { assets } } = config;
|
|
162
|
-
const { github: { isReleased, owner, repo, uploadUrl, releaseId, token }, pkg: { name } } = ctx;
|
|
163
|
-
if (!assets?.length || !isReleased) return;
|
|
164
|
-
const octokit = new Octokit({ auth: token });
|
|
165
|
-
const files = await glob(assets);
|
|
166
|
-
await Promise.all(files.map((file) => {
|
|
167
|
-
return octokit.repos.uploadReleaseAsset({
|
|
168
|
-
owner,
|
|
169
|
-
repo,
|
|
170
|
-
name,
|
|
171
|
-
url: uploadUrl,
|
|
172
|
-
release_id: releaseId,
|
|
173
|
-
data: createReadStream(file),
|
|
174
|
-
headers: {
|
|
175
|
-
"content-type": contentType(file) || "application/octet-stream",
|
|
176
|
-
"content-length": statSync(file).size
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}));
|
|
180
|
-
};
|
|
181
|
-
const createWebRelease = async (ctx, config) => {
|
|
182
|
-
const { pkg: { toPreRelease }, github: { owner, repo, changelog, releaseName }, git: { currentTag } } = ctx;
|
|
183
|
-
const { github: { prerelease: prerelease$1 } } = config;
|
|
184
|
-
const github = getGithubUrl(owner, repo);
|
|
185
|
-
const url = new URL(`${github}/releases/new`);
|
|
186
|
-
url.searchParams.set("tag", currentTag);
|
|
187
|
-
url.searchParams.set("title", releaseName);
|
|
188
|
-
url.searchParams.set("body", truncateBody(changelog));
|
|
189
|
-
url.searchParams.set("prelease", String(toPreRelease || prerelease$1));
|
|
190
|
-
const isWindows = process.platform === "win32";
|
|
191
|
-
await open(url.toString(), { wait: isWindows });
|
|
192
|
-
};
|
|
193
|
-
const createCiRelease = async (ctx, config) => {
|
|
194
|
-
const { pkg: { toPreRelease }, github: { owner, repo, token, changelog, releaseName }, git: { currentTag } } = ctx;
|
|
195
|
-
const { github: { prerelease: prerelease$1, draft, autoGenerate } } = config;
|
|
196
|
-
const octokit = new Octokit({ auth: token });
|
|
197
|
-
try {
|
|
198
|
-
const { data } = await octokit.repos.createRelease({
|
|
199
|
-
owner,
|
|
200
|
-
repo,
|
|
201
|
-
tag_name: currentTag,
|
|
202
|
-
target_commitish: void 0,
|
|
203
|
-
name: releaseName,
|
|
204
|
-
body: autoGenerate ? "" : truncateBody(changelog),
|
|
205
|
-
draft: draft ?? false,
|
|
206
|
-
prerelease: (toPreRelease || prerelease$1) ?? false,
|
|
207
|
-
discussion_category_name: void 0,
|
|
208
|
-
generate_release_notes: autoGenerate,
|
|
209
|
-
make_latest: "true"
|
|
210
|
-
});
|
|
211
|
-
const { html_url, upload_url, id, discussion_url } = data;
|
|
212
|
-
Object.assign(ctx.github, {
|
|
213
|
-
isReleased: true,
|
|
214
|
-
releaseId: id,
|
|
215
|
-
releaseUrl: html_url,
|
|
216
|
-
uploadUrl: upload_url,
|
|
217
|
-
discussionUrl: discussion_url
|
|
218
|
-
});
|
|
219
|
-
} catch (e) {
|
|
220
|
-
if (e?.status === 422) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(currentTag));
|
|
221
|
-
throw e;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
const parseGithubUrl = (ctx) => {
|
|
225
|
-
const { git: { remoteUrl } } = ctx;
|
|
226
|
-
const [owner, repo] = parseGitHubRepo(remoteUrl);
|
|
227
|
-
Object.assign(ctx.github, {
|
|
228
|
-
owner,
|
|
229
|
-
repo
|
|
230
|
-
});
|
|
231
|
-
};
|
|
232
|
-
const createRelease = async (ctx, config) => {
|
|
233
|
-
const { github: { owner, repo }, isCI } = ctx;
|
|
234
|
-
const { github: { tokenRef, release, skipChecks } } = config;
|
|
235
|
-
if (!release) return;
|
|
236
|
-
const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
|
|
237
|
-
if (!token) {
|
|
238
|
-
Object.assign(ctx.github, { isWeb: true });
|
|
239
|
-
if (isCI && process.env.GITHUB_ACTIONS) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
|
|
240
|
-
}
|
|
241
|
-
Object.assign(ctx.github, { token });
|
|
242
|
-
const octokit = new Octokit({ auth: token });
|
|
243
|
-
if (token && !skipChecks) if (process.env.GITHUB_ACTIONS) Object.assign(ctx.github, { username: process.env.GITHUB_ACTOR });
|
|
244
|
-
else {
|
|
245
|
-
try {
|
|
246
|
-
const { data: { login: username } } = await octokit.users.getAuthenticated();
|
|
247
|
-
Object.assign(ctx.github, { username });
|
|
248
|
-
} catch (e) {
|
|
249
|
-
throw new Error(MSG.ERROR.GITHUB_AUTH);
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
const { github: { username } } = ctx;
|
|
253
|
-
await octokit.repos.checkCollaborator({
|
|
254
|
-
owner,
|
|
255
|
-
repo,
|
|
256
|
-
username
|
|
257
|
-
});
|
|
258
|
-
} catch (e) {
|
|
259
|
-
throw new Error(MSG.ERROR.GITHUB_USER);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
const { github: { isWeb }, dryRun } = ctx;
|
|
263
|
-
if (dryRun) return true;
|
|
264
|
-
if (isWeb) await createWebRelease(ctx, config);
|
|
265
|
-
else if (isCI) {
|
|
266
|
-
await createCiRelease(ctx, config);
|
|
267
|
-
await uploadAssets(ctx, config);
|
|
268
|
-
}
|
|
269
|
-
};
|
|
270
|
-
|
|
271
159
|
//#endregion
|
|
272
160
|
//#region src/git/commit.ts
|
|
273
161
|
const gitCheck = async (ctx, config) => {
|
|
@@ -289,7 +177,11 @@ const gitCheck = async (ctx, config) => {
|
|
|
289
177
|
remoteName,
|
|
290
178
|
remoteUrl
|
|
291
179
|
});
|
|
292
|
-
|
|
180
|
+
const [owner, repo] = parseGitHubRepo(remoteUrl);
|
|
181
|
+
Object.assign(ctx.github, {
|
|
182
|
+
owner,
|
|
183
|
+
repo
|
|
184
|
+
});
|
|
293
185
|
};
|
|
294
186
|
const commitAndTag = async (ctx, config) => {
|
|
295
187
|
const { git: { remoteName, currentTag, commitMessage, tagMessage }, dryRun } = ctx;
|
|
@@ -428,7 +320,10 @@ const parseVersion = (raw) => {
|
|
|
428
320
|
};
|
|
429
321
|
};
|
|
430
322
|
const getCIVersion = (current) => {
|
|
431
|
-
return inc(current, Release.PATCH)
|
|
323
|
+
return inc(current, Release.PATCH);
|
|
324
|
+
};
|
|
325
|
+
const isNeq = (current, next) => {
|
|
326
|
+
return neq(current, next);
|
|
432
327
|
};
|
|
433
328
|
|
|
434
329
|
//#endregion
|
|
@@ -437,6 +332,7 @@ let Release = /* @__PURE__ */ function(Release$1) {
|
|
|
437
332
|
Release$1["MAJOR"] = "major";
|
|
438
333
|
Release$1["MINOR"] = "minor";
|
|
439
334
|
Release$1["PATCH"] = "patch";
|
|
335
|
+
Release$1["RELEASE"] = "release";
|
|
440
336
|
return Release$1;
|
|
441
337
|
}({});
|
|
442
338
|
let PreRelease = /* @__PURE__ */ function(PreRelease$1) {
|
|
@@ -456,76 +352,45 @@ let PreId = /* @__PURE__ */ function(PreId$1) {
|
|
|
456
352
|
PreId$1["DEV"] = "dev";
|
|
457
353
|
return PreId$1;
|
|
458
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
|
+
});
|
|
459
370
|
const runVersionPrompts = async (ctx, config) => {
|
|
460
371
|
const { pkg: { current, fromPreRelease } } = ctx;
|
|
461
372
|
const selectVersion = async () => {
|
|
462
|
-
const
|
|
463
|
-
const
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
{
|
|
476
|
-
label: "Pre-Patch",
|
|
477
|
-
value: prepatch,
|
|
478
|
-
hint: prepatch
|
|
479
|
-
},
|
|
480
|
-
{
|
|
481
|
-
label: "Pre-Minor",
|
|
482
|
-
value: preminor,
|
|
483
|
-
hint: preminor
|
|
484
|
-
},
|
|
485
|
-
{
|
|
486
|
-
label: "Pre-Major",
|
|
487
|
-
value: premajor,
|
|
488
|
-
hint: premajor
|
|
489
|
-
}
|
|
490
|
-
];
|
|
491
|
-
const release = [
|
|
492
|
-
{
|
|
493
|
-
label: "Patch",
|
|
494
|
-
value: patch,
|
|
495
|
-
hint: patch
|
|
496
|
-
},
|
|
497
|
-
{
|
|
498
|
-
label: "Minor",
|
|
499
|
-
value: minor,
|
|
500
|
-
hint: minor
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
label: "Major",
|
|
504
|
-
value: major,
|
|
505
|
-
hint: major
|
|
506
|
-
}
|
|
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")
|
|
507
386
|
];
|
|
508
|
-
const
|
|
509
|
-
label: "As-Is",
|
|
510
|
-
value: "ignore",
|
|
511
|
-
hint: current
|
|
512
|
-
}, {
|
|
513
|
-
label: "Custom",
|
|
514
|
-
value: "custom",
|
|
515
|
-
hint: "custom specified"
|
|
516
|
-
}];
|
|
517
|
-
const version = await select({
|
|
387
|
+
const type = await select({
|
|
518
388
|
message: question(MSG.PROMPT.SELECT_VERSION, "version"),
|
|
519
|
-
options
|
|
520
|
-
|
|
521
|
-
...release,
|
|
522
|
-
...fromPreRelease ? [] : pre,
|
|
523
|
-
...custom
|
|
524
|
-
],
|
|
525
|
-
initialValue: fromPreRelease ? prerelease$1 : patch
|
|
389
|
+
options,
|
|
390
|
+
initialValue: fromPreRelease ? "prerelease" : "patch"
|
|
526
391
|
});
|
|
527
|
-
abortSinglePrompt(
|
|
528
|
-
return
|
|
392
|
+
abortSinglePrompt(type);
|
|
393
|
+
return type;
|
|
529
394
|
};
|
|
530
395
|
const inputVersion = async () => {
|
|
531
396
|
const version = await text({
|
|
@@ -538,10 +403,12 @@ const runVersionPrompts = async (ctx, config) => {
|
|
|
538
403
|
abortSinglePrompt(version);
|
|
539
404
|
return version;
|
|
540
405
|
};
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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);
|
|
545
412
|
};
|
|
546
413
|
|
|
547
414
|
//#endregion
|
|
@@ -644,8 +511,7 @@ const createDefaultConfig = (isCI) => {
|
|
|
644
511
|
},
|
|
645
512
|
npm: {
|
|
646
513
|
publish: isCI,
|
|
647
|
-
|
|
648
|
-
pushArgs: [],
|
|
514
|
+
publishArgs: [],
|
|
649
515
|
skipChecks: false
|
|
650
516
|
},
|
|
651
517
|
github: {
|
|
@@ -758,11 +624,8 @@ const defaultTypes = [
|
|
|
758
624
|
|
|
759
625
|
//#endregion
|
|
760
626
|
//#region src/git/changelog.ts
|
|
761
|
-
const
|
|
627
|
+
const createGenerator = async ({ getPkgDir, tagPrefix }) => {
|
|
762
628
|
const pkgDir = getPkgDir();
|
|
763
|
-
const infile = join(pkgDir, "CHANGELOG.md");
|
|
764
|
-
if (!existsSync(infile)) await writeFile(infile, "");
|
|
765
|
-
const originalChangelog = readFileSync(infile, "utf-8");
|
|
766
629
|
const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
|
|
767
630
|
...t$1,
|
|
768
631
|
hidden: false
|
|
@@ -815,17 +678,30 @@ const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
|
|
|
815
678
|
{{> commit root=@root}}
|
|
816
679
|
{{/each}}
|
|
817
680
|
{{/each}}`.trim() + eol(2);
|
|
818
|
-
|
|
819
|
-
|
|
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
|
+
});
|
|
820
688
|
let changelog = "";
|
|
821
|
-
for await (const chunk of generator.write())
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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);
|
|
825
702
|
writeStream.write(originalChangelog);
|
|
826
703
|
writeStream.end();
|
|
827
704
|
await finished(writeStream);
|
|
828
|
-
return changelog;
|
|
829
705
|
};
|
|
830
706
|
|
|
831
707
|
//#endregion
|
|
@@ -887,6 +763,172 @@ const runGithubPrompts = async (ctx, config) => {
|
|
|
887
763
|
return res;
|
|
888
764
|
};
|
|
889
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
|
+
|
|
890
932
|
//#endregion
|
|
891
933
|
//#region src/npm/prompts.ts
|
|
892
934
|
const runNpmPublishPrompts = async (ctx, config) => {
|
|
@@ -922,10 +964,10 @@ const runNpmOptPrompts = async (ctx, config) => {
|
|
|
922
964
|
//#region src/npm/publish.ts
|
|
923
965
|
const resolvePublishTag = async (pkgName, version) => {
|
|
924
966
|
const { toPreRelease, preId } = parseVersion(version);
|
|
925
|
-
if (
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
967
|
+
if (toPreRelease) return preId || "next";
|
|
968
|
+
const active = await getPublishedVersion(pkgName);
|
|
969
|
+
if (!active) return "latest";
|
|
970
|
+
return lt(version, active) ? "previous" : "latest";
|
|
929
971
|
};
|
|
930
972
|
const isOtpError = (err) => err instanceof Error && /one-time password|otp/i.test(err.message);
|
|
931
973
|
const npmCheck = async (ctx, config) => {
|
|
@@ -946,7 +988,7 @@ const npmCheck = async (ctx, config) => {
|
|
|
946
988
|
};
|
|
947
989
|
const publishNpm = async (ctx, config) => {
|
|
948
990
|
const { dryRun, selectedPkg, npm: { otp, tag } } = ctx;
|
|
949
|
-
const { npm: { publish,
|
|
991
|
+
const { npm: { publish, publishArgs }, getPkgDir } = config;
|
|
950
992
|
if (!publish) return;
|
|
951
993
|
const otpArgs = otp ? ["--otp", otp] : [];
|
|
952
994
|
const dryRunArg = dryRun ? ["--dry-run"] : [];
|
|
@@ -954,7 +996,7 @@ const publishNpm = async (ctx, config) => {
|
|
|
954
996
|
tag,
|
|
955
997
|
args: [
|
|
956
998
|
...otpArgs,
|
|
957
|
-
...
|
|
999
|
+
...publishArgs,
|
|
958
1000
|
...dryRunArg
|
|
959
1001
|
],
|
|
960
1002
|
cwd: getPkgDir(selectedPkg)
|
|
@@ -964,7 +1006,27 @@ const publishNpm = async (ctx, config) => {
|
|
|
964
1006
|
//#endregion
|
|
965
1007
|
//#region src/action.ts
|
|
966
1008
|
var Action = class {
|
|
967
|
-
async
|
|
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) {
|
|
968
1030
|
const { version: cVersion, name: cName } = readJsonFile(join(__dirname, "../package.json"));
|
|
969
1031
|
const { otp, package: defPkg, ...others } = options;
|
|
970
1032
|
const { showChangelog, showRelease, ci, dryRun } = Object.fromEntries(Object.entries(others).map(([k, v]) => [k, Boolean(v)]));
|
|
@@ -1028,33 +1090,42 @@ var Action = class {
|
|
|
1028
1090
|
const formated = formatMessage(message, pkg$1);
|
|
1029
1091
|
formated && msg("PKG", formated);
|
|
1030
1092
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
await this.npmTask(ctx, config);
|
|
1036
|
-
await this.githubTask(ctx, config);
|
|
1037
|
-
if (dryRun) gitRollback(ctx);
|
|
1038
|
-
taskEnd(MSG.OUTRO(dryRun));
|
|
1093
|
+
return {
|
|
1094
|
+
ctx,
|
|
1095
|
+
config
|
|
1096
|
+
};
|
|
1039
1097
|
}
|
|
1040
1098
|
async checkTask(ctx, config) {
|
|
1041
|
-
const {
|
|
1042
|
-
await tasks([
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
1055
1117
|
},
|
|
1056
|
-
|
|
1057
|
-
|
|
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);
|
|
1058
1129
|
}
|
|
1059
1130
|
async bumpTask(ctx, config) {
|
|
1060
1131
|
const { pkg: { current }, dryRun, isCI, showRelease, showChangelog, selectedPkg } = ctx;
|
|
@@ -1064,7 +1135,9 @@ var Action = class {
|
|
|
1064
1135
|
return !next$1 && isIncrement$1;
|
|
1065
1136
|
};
|
|
1066
1137
|
if (need(ctx)) {
|
|
1067
|
-
const
|
|
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);
|
|
1068
1141
|
Object.assign(ctx.pkg, { ...parsed });
|
|
1069
1142
|
}
|
|
1070
1143
|
const { pkg: { next }, isIncrement } = ctx;
|
|
@@ -1081,9 +1154,15 @@ var Action = class {
|
|
|
1081
1154
|
}]).catch((err) => abortOnError(err, ctx));
|
|
1082
1155
|
await runLifeCycleHook(hooks, "after:bump", dryRun);
|
|
1083
1156
|
const { from, to } = await resolveChangelogRange(isIncrement, isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*");
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1086
|
-
|
|
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);
|
|
1087
1166
|
if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
|
|
1088
1167
|
} else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
|
|
1089
1168
|
}
|
|
@@ -1093,12 +1172,11 @@ var Action = class {
|
|
|
1093
1172
|
await tasks([{
|
|
1094
1173
|
title: MSG.TASK.CHANGELOG.START,
|
|
1095
1174
|
task: async () => {
|
|
1096
|
-
|
|
1175
|
+
await generateChangelog({
|
|
1097
1176
|
getPkgDir: () => getPkgDir(selectedPkg),
|
|
1098
1177
|
tagPrefix: tagPrefix?.(selectedPkg)
|
|
1099
1178
|
});
|
|
1100
|
-
await
|
|
1101
|
-
Object.assign(ctx.github, { changelog });
|
|
1179
|
+
await gitAddAll();
|
|
1102
1180
|
return success(MSG.TASK.CHANGELOG.END, dryRun);
|
|
1103
1181
|
},
|
|
1104
1182
|
enabled: isIncrement
|
|
@@ -1165,8 +1243,14 @@ var Action = class {
|
|
|
1165
1243
|
//#endregion
|
|
1166
1244
|
//#region src/cli.ts
|
|
1167
1245
|
const pkg = readJsonFile(join(__dirname, "..", "package.json"));
|
|
1168
|
-
|
|
1169
|
-
|
|
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);
|
|
1170
1254
|
});
|
|
1171
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) => {
|
|
1172
1256
|
const { force = false, monorepo = false } = options;
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { DeepPartial, DeepRequired } from "@peiyanlu/ts-utils";
|
|
2
1
|
import { log } from "@clack/prompts";
|
|
2
|
+
import { DeepPartial, DeepRequired } from "@peiyanlu/ts-utils";
|
|
3
|
+
import { ConventionalChangelog } from "conventional-changelog";
|
|
3
4
|
|
|
4
5
|
//#region src/types.d.ts
|
|
5
6
|
|
|
@@ -88,14 +89,10 @@ interface ReleaseConfig {
|
|
|
88
89
|
* 是否启用 npm 发布流程
|
|
89
90
|
*/
|
|
90
91
|
publish: boolean | undefined;
|
|
91
|
-
/**
|
|
92
|
-
* npm 发布目标路径,对应 `npm publish <package-spec>`
|
|
93
|
-
*/
|
|
94
|
-
publishPath: string;
|
|
95
92
|
/**
|
|
96
93
|
* 额外的 npm publish 参数(透传给 npm CLI)
|
|
97
94
|
*/
|
|
98
|
-
|
|
95
|
+
publishArgs: string[];
|
|
99
96
|
/**
|
|
100
97
|
* 是否跳过发布前检查(如 ping、whoami 等校验)
|
|
101
98
|
*/
|
|
@@ -155,4 +152,48 @@ declare function defineConfig(config: UserConfigFn): UserConfigFn;
|
|
|
155
152
|
declare function defineConfig(config: UserConfigExport): UserConfigExport;
|
|
156
153
|
declare const mergeConfig: <T extends object>(defaults: DeepRequired<T>, overrides?: DeepPartial<T>) => DeepRequired<T>;
|
|
157
154
|
//#endregion
|
|
158
|
-
|
|
155
|
+
//#region src/git/changelog.d.ts
|
|
156
|
+
interface Options {
|
|
157
|
+
/** @example () => `packages/${pkg}` */
|
|
158
|
+
getPkgDir: () => string;
|
|
159
|
+
/** @example `${pkg}@` */
|
|
160
|
+
tagPrefix?: string;
|
|
161
|
+
}
|
|
162
|
+
declare const createGenerator: ({
|
|
163
|
+
getPkgDir,
|
|
164
|
+
tagPrefix
|
|
165
|
+
}: Options) => Promise<ConventionalChangelog>;
|
|
166
|
+
declare const getChangelog: ({
|
|
167
|
+
getPkgDir,
|
|
168
|
+
tagPrefix
|
|
169
|
+
}: Options) => Promise<string>;
|
|
170
|
+
declare const generateChangelog: ({
|
|
171
|
+
getPkgDir,
|
|
172
|
+
tagPrefix
|
|
173
|
+
}: Options) => Promise<void>;
|
|
174
|
+
//#endregion
|
|
175
|
+
//#region src/index.d.ts
|
|
176
|
+
interface PublishTagOptions {
|
|
177
|
+
/**
|
|
178
|
+
* 支持通过命令行参数提供
|
|
179
|
+
* @example v1.0.0; pkg@1.0.1;
|
|
180
|
+
* @example my-cli v1.0.0;
|
|
181
|
+
*/
|
|
182
|
+
gitTag?: string;
|
|
183
|
+
/** @defaults `@` */
|
|
184
|
+
tagSeparator?: string;
|
|
185
|
+
/**
|
|
186
|
+
* monorepo 仓库子包不使用 `pkg@version` 格式创建 tag 时需要指定
|
|
187
|
+
* @example pkg-demo
|
|
188
|
+
*/
|
|
189
|
+
defaultPackage?: string;
|
|
190
|
+
/**
|
|
191
|
+
* @example (pkg) => `packages/${pkg}`; (pkg) => '.';
|
|
192
|
+
*/
|
|
193
|
+
getPkgDir?: (pkg: string) => string;
|
|
194
|
+
/** npm Trusted Publishing(OIDC) */
|
|
195
|
+
provenance?: boolean;
|
|
196
|
+
}
|
|
197
|
+
declare const publishTagToNpm: (options: PublishTagOptions) => Promise<void>;
|
|
198
|
+
//#endregion
|
|
199
|
+
export { type Logger, type UserConfig, createGenerator, defineConfig, generateChangelog, getChangelog, mergeConfig, publishTagToNpm };
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,128 @@
|
|
|
1
|
+
import { eol, getPackageInfo, getPublishedVersion, publishPackage } from "@peiyanlu/cli-utils";
|
|
2
|
+
import { coerce, lt, parse, valid } from "semver";
|
|
3
|
+
import { dim, green, underline, yellow } from "ansis";
|
|
4
|
+
import { log } from "@clack/prompts";
|
|
1
5
|
import { isPlainObject } from "@peiyanlu/ts-utils";
|
|
6
|
+
import { createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { ConventionalChangelog } from "conventional-changelog";
|
|
9
|
+
import createPreset from "conventional-changelog-conventionalcommits";
|
|
10
|
+
import { writeFile } from "node:fs/promises";
|
|
11
|
+
import { finished } from "node:stream/promises";
|
|
2
12
|
|
|
13
|
+
//#region src/messages.ts
|
|
14
|
+
const n = (b) => b ? "(dry run)" : "";
|
|
15
|
+
const t = () => green(`${Math.floor(process.uptime())}s`);
|
|
16
|
+
const MSG = {
|
|
17
|
+
INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
|
|
18
|
+
OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
|
|
19
|
+
OUTRO_PREPARE: (dryRun) => `🎉 PrepareRelease finished successfully in ${t()} ${n(dryRun)}`,
|
|
20
|
+
LOG: {
|
|
21
|
+
SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
|
|
22
|
+
SHOW_CHANGELOG: "🎉 Changelog collected",
|
|
23
|
+
CHANGELOG_EMPTY: "No commits found since last release",
|
|
24
|
+
CHANGES_EMPTY: "Working tree clean. No files changed"
|
|
25
|
+
},
|
|
26
|
+
CHECK: {
|
|
27
|
+
GIT: {
|
|
28
|
+
CHECKING: "Checking Git repository",
|
|
29
|
+
CHECKED: (name, url) => `Git remote resolved: ${url ? `${dim(name)} → ${underline(dim(url))}` : "not found"}`
|
|
30
|
+
},
|
|
31
|
+
NPM: {
|
|
32
|
+
CHECKING: "Checking npm registry",
|
|
33
|
+
CHECKED: (registry, msg) => `NPM registry: ${underline(dim(registry))} ${underline(dim(msg))}`
|
|
34
|
+
},
|
|
35
|
+
GITHUB: {
|
|
36
|
+
CHECKING: "Checking github repository",
|
|
37
|
+
CHECKED: (repository, msg) => `GitHub repository: ${underline(dim(repository))} ${underline(dim(msg))}`
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
TASK: {
|
|
41
|
+
VERSION: {
|
|
42
|
+
START: "Bumping version",
|
|
43
|
+
END: (to) => `Version bumped: ${underline(dim(to))}`
|
|
44
|
+
},
|
|
45
|
+
CHANGELOG: {
|
|
46
|
+
START: "Generating changelog",
|
|
47
|
+
END: `Changelog generated`
|
|
48
|
+
},
|
|
49
|
+
NPM: {
|
|
50
|
+
START: "Publishing npm package",
|
|
51
|
+
END: (url) => `NPM package published: ${underline(dim(url))}`
|
|
52
|
+
},
|
|
53
|
+
GIT: {
|
|
54
|
+
START: "Creating git commit, tag & push",
|
|
55
|
+
END: "Git commit, tag & push completed"
|
|
56
|
+
},
|
|
57
|
+
GITHUB: {
|
|
58
|
+
START: "Creating GitHub release",
|
|
59
|
+
END: (url) => `GitHub release created: ${underline(dim(url))}`
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
PROMPT: {
|
|
63
|
+
SELECT_PACKAGE: "Select package release:",
|
|
64
|
+
SELECT_VERSION: "Select version bump:",
|
|
65
|
+
INPUT_VERSION: "Enter custom version:",
|
|
66
|
+
NPM_PUBLISH: (pkg) => `Publish to npm ${underline(dim`(${pkg})`)}?`,
|
|
67
|
+
NPM_OTP: "Enter npm one-time password (OTP):",
|
|
68
|
+
GITHUB_RELEASE: (title) => `Create a new GitHub release ${underline(dim`(${title})`)}?`,
|
|
69
|
+
GIT_COMMIT: (msg) => `Create git commit ${underline(dim`(${msg})`)}?`,
|
|
70
|
+
GIT_TAG: (msg) => `Create git tag ${underline(dim`(${msg})`)}?`,
|
|
71
|
+
GIT_PUSH: "Push commits and tags to remote?"
|
|
72
|
+
},
|
|
73
|
+
ERROR: {
|
|
74
|
+
GIT_WORKDIR: `[git] Working dir must be clean`,
|
|
75
|
+
GIT_REGISTRY: (name) => `[git] Repository not found for ${dim(name)}`,
|
|
76
|
+
GIT_REMOTE: (name) => `[git] Remote "${dim(name)}" not found`,
|
|
77
|
+
GITHUB_TAG_EXIT: (tag) => `[github] Release or tag "${tag}" already exists`,
|
|
78
|
+
GITHUB_TOKEN: (tokenRef) => `[github] Missing GitHub token. Please set ${yellow(tokenRef ?? "GITHUB_TOKEN")}`,
|
|
79
|
+
GITHUB_AUTH: "[github] Invalid GitHub token or insufficient permissions",
|
|
80
|
+
GITHUB_USER: "[github] User does not have permission to create releases",
|
|
81
|
+
NPM_REGISTRY: (registry) => `[npm] Unable to reach npm registry ${underline(dim(registry))}`,
|
|
82
|
+
NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again.`,
|
|
83
|
+
NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}.`
|
|
84
|
+
},
|
|
85
|
+
ABORT: {
|
|
86
|
+
CANCEL: "Operation cancelled",
|
|
87
|
+
MONOREPO_CI_NO_PACKAGE: "CI mode requires a target package in monorepo. Please specify it via \"--package <pkg>\"",
|
|
88
|
+
MONOREPO_NO_PACKAGES: "Monorepo detected, but no packages found. Please configure \"config.packages\""
|
|
89
|
+
},
|
|
90
|
+
INFO: {
|
|
91
|
+
TOOL: (name, version) => `${name} ${dim(`v${version}`)}`,
|
|
92
|
+
CONFIG: (path) => `config file: ${underline(dim(path || "not found"))}`
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
//#region src/version/bump.ts
|
|
98
|
+
const isValid = (version) => {
|
|
99
|
+
return Boolean(valid(version));
|
|
100
|
+
};
|
|
101
|
+
const parseVersion = (raw) => {
|
|
102
|
+
const next = isValid(raw) ? raw : coerce(raw)?.toString();
|
|
103
|
+
if (!next) return {};
|
|
104
|
+
const { prerelease } = parse(next);
|
|
105
|
+
const toPreRelease = Boolean(prerelease.length);
|
|
106
|
+
const [preId, preBase] = prerelease.map(String);
|
|
107
|
+
return {
|
|
108
|
+
next,
|
|
109
|
+
toPreRelease,
|
|
110
|
+
preId,
|
|
111
|
+
preBase
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/npm/publish.ts
|
|
117
|
+
const resolvePublishTag = async (pkgName, version) => {
|
|
118
|
+
const { toPreRelease, preId } = parseVersion(version);
|
|
119
|
+
if (toPreRelease) return preId || "next";
|
|
120
|
+
const active = await getPublishedVersion(pkgName);
|
|
121
|
+
if (!active) return "latest";
|
|
122
|
+
return lt(version, active) ? "previous" : "latest";
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
//#endregion
|
|
3
126
|
//#region src/config.ts
|
|
4
127
|
function defineConfig(config) {
|
|
5
128
|
return config;
|
|
@@ -22,4 +145,218 @@ const mergeConfig = (defaults, overrides) => {
|
|
|
22
145
|
};
|
|
23
146
|
|
|
24
147
|
//#endregion
|
|
25
|
-
|
|
148
|
+
//#region src/git/changetype.ts
|
|
149
|
+
const defaultTypes = [
|
|
150
|
+
{
|
|
151
|
+
type: "feat",
|
|
152
|
+
section: "✨ 新功能",
|
|
153
|
+
description: "新增功能"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
type: "feature",
|
|
157
|
+
section: "✨ 新功能",
|
|
158
|
+
description: "新增功能"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "fix",
|
|
162
|
+
section: "🐛 Bug 修复",
|
|
163
|
+
description: "修复 bug"
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
type: "perf",
|
|
167
|
+
section: "⚡ 性能优化",
|
|
168
|
+
description: "提升性能"
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: "revert",
|
|
172
|
+
section: "⏪ 回退",
|
|
173
|
+
description: "回退到之前版本"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
type: "docs",
|
|
177
|
+
section: "📝 文档",
|
|
178
|
+
description: "文档更新"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
type: "style",
|
|
182
|
+
section: "💄 样式调整",
|
|
183
|
+
description: "样式或格式修改"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
type: "chore",
|
|
187
|
+
section: "🎫 其他更新",
|
|
188
|
+
description: "非功能性更改"
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
type: "refactor",
|
|
192
|
+
section: "♻ 代码重构",
|
|
193
|
+
description: "重构代码"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
type: "test",
|
|
197
|
+
section: "✅ 测试",
|
|
198
|
+
description: "测试更新"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
type: "build",
|
|
202
|
+
section: "👷 构建系统",
|
|
203
|
+
description: "构建工具和系统修改"
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "ci",
|
|
207
|
+
section: "🔧 CI 配置",
|
|
208
|
+
description: "持续集成配置"
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
type: "config",
|
|
212
|
+
section: "🔨 配置",
|
|
213
|
+
description: "配置文件更新"
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
type: "deps",
|
|
217
|
+
section: "🔗 依赖更新",
|
|
218
|
+
description: "依赖版本变更"
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: "security",
|
|
222
|
+
section: "🔒 安全修复",
|
|
223
|
+
description: "安全性更新"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
type: "i18n",
|
|
227
|
+
section: "🌐 国际化",
|
|
228
|
+
description: "国际化相关更新"
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
type: "ux",
|
|
232
|
+
section: "🖥️ 用户体验",
|
|
233
|
+
description: "用户体验改进"
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
type: "hotfix",
|
|
237
|
+
section: "🔥 热修复",
|
|
238
|
+
description: "紧急修复"
|
|
239
|
+
}
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/git/changelog.ts
|
|
244
|
+
const createGenerator = async ({ getPkgDir, tagPrefix }) => {
|
|
245
|
+
const pkgDir = getPkgDir();
|
|
246
|
+
const preset = await createPreset({ types: defaultTypes.map((t$1) => ({
|
|
247
|
+
...t$1,
|
|
248
|
+
hidden: false
|
|
249
|
+
})) });
|
|
250
|
+
preset.writer ??= {};
|
|
251
|
+
preset.writer.headerPartial = `
|
|
252
|
+
## {{#if isPatch~}} <small> {{~/if~}}
|
|
253
|
+
{{#if @root.linkCompare~}}
|
|
254
|
+
[{{version}}](
|
|
255
|
+
{{~#if @root.repository~}}
|
|
256
|
+
{{~#if @root.host}}
|
|
257
|
+
{{~@root.host}}/
|
|
258
|
+
{{~/if}}
|
|
259
|
+
{{~#if @root.owner}}
|
|
260
|
+
{{~@root.owner}}/
|
|
261
|
+
{{~/if}}
|
|
262
|
+
{{~@root.repository}}
|
|
263
|
+
{{~else}}
|
|
264
|
+
{{~@root.repoUrl}}
|
|
265
|
+
{{~/if~}}
|
|
266
|
+
/compare/{{previousTag}}...{{currentTag}})
|
|
267
|
+
{{~else}}
|
|
268
|
+
{{~version}}
|
|
269
|
+
{{~/if}}
|
|
270
|
+
{{~#if title}} "{{title}}"
|
|
271
|
+
{{~/if}}
|
|
272
|
+
{{~#if date}} ({{date}})
|
|
273
|
+
{{~/if}}
|
|
274
|
+
{{~#if isPatch~}} </small> {{~/if}}
|
|
275
|
+
`.trim() + eol();
|
|
276
|
+
preset.writer.mainTemplate = `
|
|
277
|
+
{{> header}}
|
|
278
|
+
{{#if noteGroups}}
|
|
279
|
+
{{#each noteGroups}}
|
|
280
|
+
|
|
281
|
+
### ⚠ {{title}}
|
|
282
|
+
|
|
283
|
+
{{#each notes}}
|
|
284
|
+
* {{#if commit.scope}}**{{commit.scope}}:** {{/if}}{{commit.subject}} {{#if commit.hash}}([{{commit.shortHash}}](https://github.com/{{@root.owner}}/{{@root.repository}}/commit/{{commit.hash}})){{/if}}
|
|
285
|
+
{{/each}}
|
|
286
|
+
{{/each}}
|
|
287
|
+
{{/if}}
|
|
288
|
+
{{#each commitGroups}}
|
|
289
|
+
|
|
290
|
+
{{#if title}}
|
|
291
|
+
### {{title}}
|
|
292
|
+
|
|
293
|
+
{{/if}}
|
|
294
|
+
{{#each commits}}
|
|
295
|
+
{{> commit root=@root}}
|
|
296
|
+
{{/each}}
|
|
297
|
+
{{/each}}`.trim() + eol(2);
|
|
298
|
+
return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(preset).options({ releaseCount: 1 }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
|
|
299
|
+
};
|
|
300
|
+
const getChangelog = async ({ getPkgDir, tagPrefix }) => {
|
|
301
|
+
const generator = await createGenerator({
|
|
302
|
+
getPkgDir,
|
|
303
|
+
tagPrefix
|
|
304
|
+
});
|
|
305
|
+
let changelog = "";
|
|
306
|
+
for await (const chunk of generator.write()) changelog += chunk;
|
|
307
|
+
return changelog;
|
|
308
|
+
};
|
|
309
|
+
const generateChangelog = async ({ getPkgDir, tagPrefix }) => {
|
|
310
|
+
const infile = join(getPkgDir(), "CHANGELOG.md");
|
|
311
|
+
if (!existsSync(infile)) await writeFile(infile, "");
|
|
312
|
+
const originalChangelog = readFileSync(infile, "utf-8");
|
|
313
|
+
const generator = await createGenerator({
|
|
314
|
+
getPkgDir,
|
|
315
|
+
tagPrefix
|
|
316
|
+
});
|
|
317
|
+
const writeStream = createWriteStream(infile);
|
|
318
|
+
for await (const chunk of generator.write()) writeStream.write(chunk);
|
|
319
|
+
writeStream.write(originalChangelog);
|
|
320
|
+
writeStream.end();
|
|
321
|
+
await finished(writeStream);
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/index.ts
|
|
326
|
+
const publishTagToNpm = async (options) => {
|
|
327
|
+
const { gitTag, defaultPackage, tagSeparator = "@", getPkgDir = () => ".", provenance } = options;
|
|
328
|
+
const tag = process.argv.slice(2)[0] || gitTag;
|
|
329
|
+
if (!tag) throw new Error(`❌ No git tag specified.`);
|
|
330
|
+
console.log(`\n📦 Start publishing from git tag: ${tag}`);
|
|
331
|
+
let pkgName = defaultPackage;
|
|
332
|
+
let version;
|
|
333
|
+
if (tag.includes(tagSeparator)) {
|
|
334
|
+
[pkgName, version] = tag.split(tagSeparator);
|
|
335
|
+
console.log(`🔍 Parsed tag → package: "${pkgName}", version: "${version}"`);
|
|
336
|
+
} else {
|
|
337
|
+
version = tag;
|
|
338
|
+
console.log(`🔍 Parsed tag → version only: "${version}"`);
|
|
339
|
+
}
|
|
340
|
+
if (version.startsWith("v")) {
|
|
341
|
+
version = version.slice(1);
|
|
342
|
+
console.log(`✂️ Normalized version (strip v): ${version}`);
|
|
343
|
+
}
|
|
344
|
+
if (!pkgName) throw new Error(`❌ Package name should be specified in tag "${tag}" when defaultPackage is not set`);
|
|
345
|
+
console.log(`📁 Resolving package info for "${pkgName}"...`);
|
|
346
|
+
const { pkg, pkgDir } = getPackageInfo(pkgName, getPkgDir);
|
|
347
|
+
console.log(`📄 package.json → name: ${pkg.name}, version: ${pkg.version}`);
|
|
348
|
+
console.log(`📂 Package directory: ${pkgDir}`);
|
|
349
|
+
if (pkg.version !== version) throw new Error(`❌ Package version from tag "${version}" mismatches with current version "${pkg.version}"`);
|
|
350
|
+
const publishTag = await resolvePublishTag(pkg.name, version);
|
|
351
|
+
console.log(`🏷 Resolved npm dist-tag: "${publishTag}" (version ${version})`);
|
|
352
|
+
console.log(`🚀 Publishing "${pkg.name}@${version}" to npm...`);
|
|
353
|
+
await publishPackage({
|
|
354
|
+
tag: publishTag,
|
|
355
|
+
args: provenance ? ["--provenance"] : [],
|
|
356
|
+
cwd: pkgDir
|
|
357
|
+
});
|
|
358
|
+
console.log(`✅ Successfully published ${pkg.name}@${version} with dist-tag "${publishTag}"\n`);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
//#endregion
|
|
362
|
+
export { createGenerator, defineConfig, generateChangelog, getChangelog, mergeConfig, publishTagToNpm };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@peiyanlu/release",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"description": "Release project tool.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,14 +17,6 @@
|
|
|
17
17
|
},
|
|
18
18
|
"./package.json": "./package.json"
|
|
19
19
|
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"dev": "tsdown -w",
|
|
22
|
-
"build": "tsdown",
|
|
23
|
-
"prepublish": "tsdown",
|
|
24
|
-
"release": "tsdown && node index.js",
|
|
25
|
-
"test": "vitest run",
|
|
26
|
-
"test:cov": "vitest run --coverage"
|
|
27
|
-
},
|
|
28
20
|
"dependencies": {
|
|
29
21
|
"@clack/prompts": "^0.11.0",
|
|
30
22
|
"@octokit/rest": "^22.0.1",
|
|
@@ -74,5 +66,13 @@
|
|
|
74
66
|
"bugs": {
|
|
75
67
|
"url": "https://github.com/peiyanlu/release/issues"
|
|
76
68
|
},
|
|
77
|
-
"homepage": "https://github.com/peiyanlu/release#readme"
|
|
78
|
-
|
|
69
|
+
"homepage": "https://github.com/peiyanlu/release#readme",
|
|
70
|
+
"scripts": {
|
|
71
|
+
"dev": "tsdown -w",
|
|
72
|
+
"build": "tsdown",
|
|
73
|
+
"prepublish": "tsdown",
|
|
74
|
+
"release": "node index.js",
|
|
75
|
+
"test": "vitest run",
|
|
76
|
+
"test:cov": "vitest run --coverage"
|
|
77
|
+
}
|
|
78
|
+
}
|