@peiyanlu/release 1.3.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -356
- package/dist/action.mjs +1036 -0
- package/dist/cli.mjs +878 -1412
- package/dist/index.d.mts +850 -253
- package/dist/index.mjs +2848 -149
- package/package.json +27 -12
- package/template/release.config.hbs +29 -0
- package/template/release.text +82 -0
package/dist/index.mjs
CHANGED
|
@@ -1,157 +1,160 @@
|
|
|
1
|
-
import { isEmpty, isFunction, isPlainObject, isUndefined, isZero } from "@peiyanlu/ts-utils";
|
|
2
1
|
import { createWriteStream, existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { dirname, isAbsolute, join, parse, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { eol, execAsync, getAuthenticatedUser, getGithubUrl, getPublishedVersion, gitAdd, gitBranch, gitBranchCurrent, gitCheckout, gitCheckoutBranch, gitCheckoutBranchForce, gitCommitAmendNoEdit, gitCommitMessage, gitConfigGet, gitConfigSet, gitDiff, gitFetch, gitLog, gitLsRemote, gitPush, gitPushBranch, gitRevParse, gitTag, hasWriteAccess, isGitRepo, isPrerelease, isValidVersion, isWorkingTreeClean, parseGitHubRepo, pingRegistry, runGit } from "@peiyanlu/cli-utils";
|
|
5
|
+
import { copyDir, createMatcher, createTempDir, editFile, normalizePath, readJsonFile, writeJsonFile } from "@peiyanlu/node-utils";
|
|
6
|
+
import { assert, dedent, equals, isBlankString, isBoolean, isEmpty, isEmptyObject, isFunction, isIndex, isNotEmpty, isNull, isString, isUndefined, isZero, newline, omit, onceAsync, partition, sleep, throwError, tryCall, tryCallSync, unset } from "@peiyanlu/ts-utils";
|
|
5
7
|
import { ConventionalChangelog } from "conventional-changelog";
|
|
6
8
|
import createPreset from "conventional-changelog-conventionalcommits";
|
|
7
|
-
import "conventional-recommended-bump";
|
|
8
|
-
import { writeFile } from "node:fs/promises";
|
|
9
|
+
import { Bumper } from "conventional-recommended-bump";
|
|
10
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
9
11
|
import { finished } from "node:stream/promises";
|
|
10
|
-
import { bold, compareUrl, each, heading, link, list, newline, segments, small, words } from "@conventional-changelog/template";
|
|
11
|
-
import "
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
import { bold, compareUrl, each, heading, link, list, newline as newline$1, segments, small, words } from "@conventional-changelog/template";
|
|
13
|
+
import semver, { SemVer, clean, gt, gte, inc, lt, major, prerelease, rcompare, valid } from "semver";
|
|
14
|
+
import { Octokit } from "@octokit/rest";
|
|
15
|
+
import open from "open";
|
|
16
|
+
import { glob, globSync } from "tinyglobby";
|
|
17
|
+
import { parse as parse$1 } from "yaml";
|
|
18
|
+
import { context } from "@actions/github";
|
|
19
|
+
import { getBooleanInput, getInput } from "@actions/core";
|
|
20
|
+
import { cancel, confirm, group, intro, isCancel, log, note, outro, select, text } from "@clack/prompts";
|
|
21
|
+
import { blue, blueBright, cyan, cyanBright, dim, green, greenBright, magenta, magentaBright, red, redBright, yellow } from "ansis";
|
|
22
|
+
//#region src/config/index.ts
|
|
23
|
+
const defineConfig = (config) => config;
|
|
24
|
+
const loadJavaScriptFile = async (file) => {
|
|
25
|
+
const { href } = pathToFileURL(file);
|
|
26
|
+
const mod = await import(href);
|
|
27
|
+
const temp = "default" in mod ? mod.default : mod;
|
|
28
|
+
return typeof temp === "function" ? temp() : temp;
|
|
29
|
+
};
|
|
30
|
+
const findFile = (cwd, filename, extensions) => {
|
|
31
|
+
for (const ext of extensions) {
|
|
32
|
+
const path = resolve(cwd, `${filename}.${ext}`);
|
|
33
|
+
if (existsSync(path)) return path;
|
|
28
34
|
}
|
|
29
|
-
|
|
35
|
+
};
|
|
36
|
+
const loadConfig = async (cwd = process.cwd()) => {
|
|
37
|
+
const file = findFile(cwd, "release.config", [
|
|
38
|
+
"ts",
|
|
39
|
+
"mts",
|
|
40
|
+
"js",
|
|
41
|
+
"mjs"
|
|
42
|
+
]);
|
|
43
|
+
if (!file) return {
|
|
44
|
+
file: "",
|
|
45
|
+
config: {}
|
|
46
|
+
};
|
|
47
|
+
return {
|
|
48
|
+
file,
|
|
49
|
+
config: await loadJavaScriptFile(file)
|
|
50
|
+
};
|
|
30
51
|
};
|
|
31
52
|
//#endregion
|
|
32
|
-
//#region src/
|
|
53
|
+
//#region src/core/changelog/committype.ts
|
|
33
54
|
const defaultTypes = [
|
|
34
55
|
{
|
|
35
56
|
type: "feat",
|
|
36
57
|
section: "✨ 新功能",
|
|
37
|
-
effect: "bump"
|
|
38
|
-
description: "新增功能"
|
|
58
|
+
effect: "bump"
|
|
39
59
|
},
|
|
40
60
|
{
|
|
41
61
|
type: "feature",
|
|
42
62
|
section: "✨ 新功能",
|
|
43
|
-
effect: "bump"
|
|
44
|
-
description: "新增功能"
|
|
63
|
+
effect: "bump"
|
|
45
64
|
},
|
|
46
65
|
{
|
|
47
66
|
type: "fix",
|
|
48
67
|
section: "🐛 Bug 修复",
|
|
49
|
-
effect: "bump"
|
|
50
|
-
description: "修复 bug"
|
|
68
|
+
effect: "bump"
|
|
51
69
|
},
|
|
52
70
|
{
|
|
53
71
|
type: "perf",
|
|
54
72
|
section: "⚡ 性能优化",
|
|
55
|
-
effect: "bump"
|
|
56
|
-
description: "提升性能"
|
|
73
|
+
effect: "bump"
|
|
57
74
|
},
|
|
58
75
|
{
|
|
59
76
|
type: "revert",
|
|
60
77
|
section: "⏪ 回退",
|
|
61
|
-
effect: "bump"
|
|
62
|
-
description: "回退到之前版本"
|
|
78
|
+
effect: "bump"
|
|
63
79
|
},
|
|
64
80
|
{
|
|
65
81
|
type: "docs",
|
|
66
82
|
section: "📝 文档",
|
|
67
|
-
effect: "changelog"
|
|
68
|
-
description: "文档更新"
|
|
83
|
+
effect: "changelog"
|
|
69
84
|
},
|
|
70
85
|
{
|
|
71
86
|
type: "style",
|
|
72
87
|
section: "💄 样式调整",
|
|
73
|
-
effect: "hidden"
|
|
74
|
-
description: "样式或格式修改"
|
|
88
|
+
effect: "hidden"
|
|
75
89
|
},
|
|
76
90
|
{
|
|
77
91
|
type: "chore",
|
|
78
92
|
section: "🧹 其他更新",
|
|
79
|
-
effect: "hidden"
|
|
80
|
-
description: "非功能性更改"
|
|
93
|
+
effect: "hidden"
|
|
81
94
|
},
|
|
82
95
|
{
|
|
83
96
|
type: "refactor",
|
|
84
97
|
section: "♻️ 代码重构",
|
|
85
|
-
effect: "changelog"
|
|
86
|
-
description: "重构代码"
|
|
98
|
+
effect: "changelog"
|
|
87
99
|
},
|
|
88
100
|
{
|
|
89
101
|
type: "test",
|
|
90
102
|
section: "🧪 测试",
|
|
91
|
-
effect: "hidden"
|
|
92
|
-
description: "测试更新"
|
|
103
|
+
effect: "hidden"
|
|
93
104
|
},
|
|
94
105
|
{
|
|
95
106
|
type: "build",
|
|
96
107
|
section: "🏗️ 构建系统",
|
|
97
|
-
effect: "hidden"
|
|
98
|
-
description: "构建工具和系统修改"
|
|
108
|
+
effect: "hidden"
|
|
99
109
|
},
|
|
100
110
|
{
|
|
101
111
|
type: "ci",
|
|
102
112
|
section: "🤖 CI",
|
|
103
|
-
effect: "hidden"
|
|
104
|
-
description: "持续集成配置"
|
|
113
|
+
effect: "hidden"
|
|
105
114
|
},
|
|
106
115
|
{
|
|
107
116
|
type: "security",
|
|
108
117
|
section: "🛡️ 安全修复",
|
|
109
|
-
effect: "changelog"
|
|
110
|
-
description: "安全性更新"
|
|
118
|
+
effect: "changelog"
|
|
111
119
|
},
|
|
112
120
|
{
|
|
113
121
|
type: "hotfix",
|
|
114
122
|
section: "🚑 热修复",
|
|
115
|
-
effect: "changelog"
|
|
116
|
-
description: "紧急修复"
|
|
123
|
+
effect: "changelog"
|
|
117
124
|
},
|
|
118
125
|
{
|
|
119
126
|
type: "i18n",
|
|
120
127
|
section: "🌐 国际化",
|
|
121
|
-
effect: "changelog"
|
|
122
|
-
description: "国际化相关更新"
|
|
128
|
+
effect: "changelog"
|
|
123
129
|
},
|
|
124
130
|
{
|
|
125
131
|
type: "ux",
|
|
126
132
|
section: "🎨 用户体验",
|
|
127
|
-
effect: "changelog"
|
|
128
|
-
description: "用户体验改进"
|
|
133
|
+
effect: "changelog"
|
|
129
134
|
},
|
|
130
135
|
{
|
|
131
136
|
type: "deps",
|
|
132
137
|
section: "📦 依赖更新",
|
|
133
|
-
effect: "changelog"
|
|
134
|
-
description: "依赖版本变更"
|
|
138
|
+
effect: "changelog"
|
|
135
139
|
},
|
|
136
140
|
{
|
|
137
141
|
type: "tool",
|
|
138
142
|
section: "🛠️ 工具链",
|
|
139
|
-
effect: "hidden"
|
|
140
|
-
description: "工具链变更"
|
|
143
|
+
effect: "hidden"
|
|
141
144
|
},
|
|
142
145
|
{
|
|
143
146
|
type: "config",
|
|
144
147
|
section: "⚙️ 配置",
|
|
145
|
-
effect: "hidden"
|
|
146
|
-
description: "配置文件更新"
|
|
148
|
+
effect: "hidden"
|
|
147
149
|
}
|
|
148
150
|
];
|
|
149
151
|
//#endregion
|
|
150
|
-
//#region src/
|
|
152
|
+
//#region src/core/changelog/templates.ts
|
|
151
153
|
const isLikelyHash = (value) => /^[0-9a-f]{7,40}$/i.test(value);
|
|
152
154
|
function headerPartial(context) {
|
|
153
155
|
const { isPatch, linkCompare, version, title, date, previousTag } = context;
|
|
154
|
-
const
|
|
156
|
+
const versionText = linkCompare && !isLikelyHash(previousTag) ? link(version, compareUrl(context)) : version;
|
|
157
|
+
const fullTitle = words(versionText, title && `"${title}"`, date && `(${date})`);
|
|
155
158
|
return heading(2, isPatch ? small(fullTitle) : fullTitle);
|
|
156
159
|
}
|
|
157
160
|
function preamblePartial(contest) {
|
|
@@ -161,95 +164,2791 @@ function preamblePartial(contest) {
|
|
|
161
164
|
function template(context) {
|
|
162
165
|
const { headerPartial, preamblePartial, commitPartial, footerPartial, noteGroups, commitGroups } = context;
|
|
163
166
|
return segments(headerPartial(context), preamblePartial(context), each(noteGroups, (group) => segments(heading(3, words("⚠", group.title)), list(group.notes, (note) => {
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
const line1 = words(note.commit.scope && bold(`${note.commit.scope}:`), commitPartial(context, note.commit));
|
|
168
|
+
const line2 = note.commit.subject.trim() !== note.text.trim() ? list([note.text], (t) => t) : "";
|
|
169
|
+
return each([line1, line2], (t) => t, newline$1());
|
|
170
|
+
})), newline$1(2)), each(commitGroups, (group) => segments(group.title && heading(3, group.title), list(group.commits, (commit) => commitPartial(context, commit))), newline$1(2)), footerPartial(context));
|
|
166
171
|
}
|
|
167
172
|
//#endregion
|
|
168
|
-
//#region src/
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
173
|
+
//#region src/core/changelog/changelog.ts
|
|
174
|
+
const versionHeaderRegex = /^#+ (?:<small>)?\[?([^[\]()\s]*\d+\.\d+\.\d+[^[\]()\s]*)]?(?:<\/small>)?/m;
|
|
175
|
+
const tagsRegex = /\/([^/)]*)\.\.\.([^/)]*)\)/;
|
|
176
|
+
const decodeTag = (tag) => {
|
|
177
|
+
return tryCallSync(() => decodeURIComponent(tag), tag);
|
|
178
|
+
};
|
|
179
|
+
const extractLastRelease = async (input) => {
|
|
180
|
+
let stream = input;
|
|
181
|
+
let inLastChanges = false;
|
|
182
|
+
let notes = "";
|
|
183
|
+
let chunk;
|
|
184
|
+
let lines;
|
|
185
|
+
let line;
|
|
186
|
+
let previousTag = "";
|
|
187
|
+
let nextTag = "";
|
|
188
|
+
let version = "";
|
|
189
|
+
if (typeof stream === "string") stream = [stream];
|
|
190
|
+
top: for await (chunk of stream) {
|
|
191
|
+
lines = chunk.toString("utf8").split(/\n/);
|
|
192
|
+
for (line of lines) {
|
|
193
|
+
const versionMatch = line.match(versionHeaderRegex);
|
|
194
|
+
if (versionMatch) {
|
|
195
|
+
if (inLastChanges) break top;
|
|
196
|
+
[, version] = versionMatch;
|
|
197
|
+
inLastChanges = true;
|
|
198
|
+
const tagsMatch = line.match(tagsRegex);
|
|
199
|
+
if (tagsMatch) {
|
|
200
|
+
previousTag = decodeTag(tagsMatch[1]);
|
|
201
|
+
nextTag = decodeTag(tagsMatch[2]);
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (inLastChanges) notes += `${line}\n`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
version,
|
|
210
|
+
notes: notes.trim(),
|
|
211
|
+
previousTag,
|
|
212
|
+
nextTag
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
var Changelog = class {
|
|
216
|
+
options;
|
|
217
|
+
constructor(options = {}) {
|
|
218
|
+
this.options = options;
|
|
219
|
+
}
|
|
220
|
+
get infile() {
|
|
221
|
+
const { pkgDir = ".", filename = "CHANGELOG.md" } = this.options;
|
|
222
|
+
return normalizePath(join(pkgDir, filename));
|
|
223
|
+
}
|
|
224
|
+
setOptions(options) {
|
|
225
|
+
Object.assign(this.options, options);
|
|
226
|
+
}
|
|
227
|
+
parsePreset() {
|
|
228
|
+
const { transformTypes, scope } = this.options;
|
|
229
|
+
const types = transformTypes?.(defaultTypes) ?? defaultTypes;
|
|
230
|
+
return createPreset({
|
|
231
|
+
types,
|
|
232
|
+
scope
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async inferReleaseType() {
|
|
236
|
+
const { pkgDir, tagPrefix } = this.options ?? {};
|
|
237
|
+
const preset = this.parsePreset();
|
|
238
|
+
const res = await new Bumper().config(preset).commits({ path: pkgDir }).tag({ prefix: tagPrefix }).bump(preset.whatBump);
|
|
239
|
+
if (!("releaseType" in res)) return {};
|
|
240
|
+
const { releaseType, reason } = res;
|
|
241
|
+
return {
|
|
242
|
+
releaseType,
|
|
243
|
+
reason
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
createGenerator() {
|
|
247
|
+
const { pkgDir = ".", tagPrefix = "v", releaseCount = 1 } = this.options;
|
|
248
|
+
const { preamble, version, previousTag } = this.options;
|
|
249
|
+
return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(this.parsePreset()).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix }).readRepository().context({
|
|
250
|
+
preamble,
|
|
251
|
+
version,
|
|
252
|
+
previousTag
|
|
253
|
+
}).writer({
|
|
254
|
+
preamblePartial,
|
|
255
|
+
headerPartial,
|
|
256
|
+
template
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
async read() {
|
|
260
|
+
const generator = this.createGenerator();
|
|
261
|
+
let content = "";
|
|
262
|
+
for await (const chunk of generator.write()) content += chunk;
|
|
263
|
+
return content.trimEnd();
|
|
264
|
+
}
|
|
265
|
+
async write() {
|
|
266
|
+
const { infile, options } = this;
|
|
267
|
+
const { releaseCount = 1 } = options;
|
|
268
|
+
if (!existsSync(infile)) await writeFile(infile, "");
|
|
269
|
+
const exist = readFileSync(infile, "utf-8");
|
|
270
|
+
const writeStream = createWriteStream(infile);
|
|
271
|
+
const generator = this.createGenerator();
|
|
272
|
+
let content = "";
|
|
273
|
+
for await (const chunk of generator.write()) {
|
|
274
|
+
content += chunk;
|
|
275
|
+
writeStream.write(chunk);
|
|
276
|
+
}
|
|
277
|
+
const override = isZero(releaseCount);
|
|
278
|
+
if (!(isBlankString(exist) || override)) {
|
|
279
|
+
writeStream.write(eol());
|
|
280
|
+
writeStream.write(exist);
|
|
281
|
+
}
|
|
282
|
+
writeStream.end();
|
|
283
|
+
await finished(writeStream);
|
|
284
|
+
return content.trimEnd();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
//#endregion
|
|
288
|
+
//#region src/core/hosting/hosting.ts
|
|
289
|
+
/** Git 仓库托管服务的基类 */
|
|
290
|
+
var GitRepositoryHosting = class {};
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region src/core/manifest/manifest.ts
|
|
293
|
+
var ProjectManifest = class {
|
|
294
|
+
filepath;
|
|
295
|
+
static Filename;
|
|
296
|
+
projectDir;
|
|
297
|
+
constructor(filepath) {
|
|
298
|
+
this.filepath = filepath;
|
|
299
|
+
this.projectDir = dirname(filepath);
|
|
300
|
+
}
|
|
301
|
+
async getPrereleaseVersion() {
|
|
302
|
+
const version = await this.getVersion();
|
|
303
|
+
return prerelease(version);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
//#endregion
|
|
307
|
+
//#region src/core/manifest/packageJson.ts
|
|
308
|
+
var PackageJsonManifest = class extends ProjectManifest {
|
|
309
|
+
static Filename = "package.json";
|
|
310
|
+
contents;
|
|
311
|
+
manifest;
|
|
312
|
+
async readManifest() {
|
|
313
|
+
if (!this.contentsMutex) {
|
|
314
|
+
this.contentsMutex = readFile(this.filepath, "utf-8");
|
|
315
|
+
this.contents = await this.contentsMutex;
|
|
316
|
+
this.manifest = JSON.parse(this.contents);
|
|
317
|
+
return this.manifest;
|
|
318
|
+
}
|
|
319
|
+
await this.contentsMutex;
|
|
320
|
+
return this.manifest;
|
|
321
|
+
}
|
|
322
|
+
async getName() {
|
|
323
|
+
const { name } = await this.readManifest();
|
|
324
|
+
return name;
|
|
325
|
+
}
|
|
326
|
+
async getVersion() {
|
|
327
|
+
const { version } = await this.readManifest();
|
|
328
|
+
return version;
|
|
329
|
+
}
|
|
330
|
+
async isPrivate() {
|
|
331
|
+
const { private: isPrivate } = await this.readManifest();
|
|
332
|
+
return Boolean(isPrivate);
|
|
333
|
+
}
|
|
334
|
+
async writeVersion(version, dryRun) {
|
|
335
|
+
await this.readManifest();
|
|
336
|
+
const { name, version: from } = this.manifest;
|
|
337
|
+
this.manifest.version = version;
|
|
338
|
+
this.contents = this.contents.replace(/"version":(\s*)"[^"]+"/, `"version":$1"${version}"`);
|
|
339
|
+
if (!dryRun) await writeFile(this.filepath, this.contents, "utf-8");
|
|
340
|
+
return {
|
|
341
|
+
name,
|
|
342
|
+
from,
|
|
343
|
+
to: version,
|
|
344
|
+
files: [this.filepath]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
contentsMutex;
|
|
348
|
+
};
|
|
349
|
+
//#endregion
|
|
350
|
+
//#region src/core/manifest/composed.ts
|
|
351
|
+
var ComposedProjectManifest = class extends ProjectManifest {
|
|
352
|
+
mainManifest;
|
|
353
|
+
subManifests;
|
|
354
|
+
constructor(mainManifest, subManifests) {
|
|
355
|
+
super(mainManifest.filepath);
|
|
356
|
+
this.mainManifest = mainManifest;
|
|
357
|
+
this.subManifests = subManifests;
|
|
358
|
+
}
|
|
359
|
+
get contents() {
|
|
360
|
+
return this.mainManifest.contents;
|
|
361
|
+
}
|
|
362
|
+
get manifest() {
|
|
363
|
+
return this.mainManifest.manifest;
|
|
364
|
+
}
|
|
365
|
+
readManifest() {
|
|
366
|
+
return this.mainManifest.readManifest();
|
|
367
|
+
}
|
|
368
|
+
getName() {
|
|
369
|
+
return this.mainManifest.getName();
|
|
370
|
+
}
|
|
371
|
+
getVersion() {
|
|
372
|
+
return this.mainManifest.getVersion();
|
|
373
|
+
}
|
|
374
|
+
isPrivate() {
|
|
375
|
+
return this.mainManifest.isPrivate();
|
|
376
|
+
}
|
|
377
|
+
async writeVersion(version, dryRun) {
|
|
378
|
+
const update = await this.mainManifest.writeVersion(version, dryRun);
|
|
379
|
+
for (let sub of this.subManifests) {
|
|
380
|
+
const { files } = await sub.writeVersion(version, dryRun);
|
|
381
|
+
update.files.push(...files);
|
|
382
|
+
}
|
|
383
|
+
return update;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
//#endregion
|
|
387
|
+
//#region src/core/utils.ts
|
|
388
|
+
const nonPreReleaseTypes = [
|
|
389
|
+
"patch",
|
|
390
|
+
"minor",
|
|
391
|
+
"major"
|
|
392
|
+
];
|
|
393
|
+
const getTypePriority = (type) => nonPreReleaseTypes.indexOf(type);
|
|
394
|
+
const getCurrentActiveType = (version) => {
|
|
395
|
+
for (const type of nonPreReleaseTypes) if (semver[type](version)) return type;
|
|
396
|
+
};
|
|
397
|
+
const getSnapshotTimestamp = (date = /* @__PURE__ */ new Date()) => date.toISOString().replace(/\D/g, "").slice(0, 14);
|
|
398
|
+
const setSnapshotIdentifier = (version, snapshot) => {
|
|
399
|
+
if (!snapshot) return version;
|
|
400
|
+
const semverVersion = new SemVer(version);
|
|
401
|
+
semverVersion.prerelease = [snapshot, getSnapshotTimestamp()];
|
|
402
|
+
return semverVersion.format();
|
|
403
|
+
};
|
|
404
|
+
const getPrereleaseIdentifier = (prerelease, snapshot) => {
|
|
405
|
+
if (snapshot) return `${snapshot}.${getSnapshotTimestamp()}`;
|
|
406
|
+
return prerelease;
|
|
407
|
+
};
|
|
408
|
+
const getPrereleaseIdentifierBase = (snapshot) => snapshot ? false : void 0;
|
|
409
|
+
const getReleaseType = (releaseType, version, prerelease$1) => {
|
|
410
|
+
if (!isUndefined(prerelease$1)) {
|
|
411
|
+
if (Array.isArray(prerelease(version))) {
|
|
412
|
+
const currentActiveType = getCurrentActiveType(version);
|
|
413
|
+
if (currentActiveType === releaseType || getTypePriority(currentActiveType) > getTypePriority(releaseType)) return "prerelease";
|
|
414
|
+
}
|
|
415
|
+
return `pre${releaseType}`;
|
|
416
|
+
}
|
|
417
|
+
return releaseType;
|
|
418
|
+
};
|
|
419
|
+
const when = (condition, ...value) => condition ? [...value] : [];
|
|
420
|
+
const unless = (condition, ...value) => condition ? [] : [...value];
|
|
421
|
+
async function firstFromStream(stream) {
|
|
422
|
+
const { value, done } = await stream[Symbol.asyncIterator]().next();
|
|
423
|
+
return done ? void 0 : value;
|
|
424
|
+
}
|
|
425
|
+
async function* getTags(params = {}) {
|
|
426
|
+
const { path, from = "", to = "HEAD", all } = params;
|
|
427
|
+
const stdout = await runGit([
|
|
428
|
+
"log",
|
|
429
|
+
"--decorate=short",
|
|
430
|
+
"--no-color",
|
|
431
|
+
"--date-order",
|
|
432
|
+
"--format=%D",
|
|
433
|
+
...when(all, "--all"),
|
|
434
|
+
[from, to].filter(Boolean).join(".."),
|
|
435
|
+
...path ? ["--", path] : []
|
|
436
|
+
]);
|
|
437
|
+
for (const record of stdout.split(/\r?\n/)) for (const [, tag] of record.matchAll(/tag:\s*([^\s,]+)/gi)) yield tag.trim();
|
|
438
|
+
}
|
|
439
|
+
async function* getSemverTags(params = {}) {
|
|
440
|
+
const { prefix, skipUnstable, clean: clean$1, ...getTagsParams } = params;
|
|
441
|
+
const tagsStream = getTags(getTagsParams);
|
|
442
|
+
const unstableTagRegex = /\d+\.\d+\.\d+-.+/;
|
|
443
|
+
const cleanTag = clean$1 ? (tag, unprefixed) => clean(unprefixed || tag) : (tag) => tag;
|
|
444
|
+
let unprefixed;
|
|
445
|
+
let tag;
|
|
446
|
+
for await (tag of tagsStream) {
|
|
447
|
+
if (skipUnstable && unstableTagRegex.test(tag)) continue;
|
|
448
|
+
if (prefix) {
|
|
449
|
+
if (typeof prefix === "string" ? tag.startsWith(prefix) : prefix.test(tag)) {
|
|
450
|
+
unprefixed = tag.replace(prefix, "");
|
|
451
|
+
if (valid(unprefixed)) {
|
|
452
|
+
tag = cleanTag(tag, unprefixed);
|
|
453
|
+
if (tag) yield tag;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} else if (valid(tag)) {
|
|
457
|
+
tag = cleanTag(tag);
|
|
458
|
+
if (tag) yield tag;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function getLastSemverTag(params = {}) {
|
|
463
|
+
return firstFromStream(getSemverTags(params));
|
|
464
|
+
}
|
|
465
|
+
async function getVersionFromTags(params = {}) {
|
|
466
|
+
const semverTagsStream = getSemverTags({
|
|
467
|
+
clean: true,
|
|
468
|
+
...params
|
|
174
469
|
});
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
470
|
+
const semverTags = [];
|
|
471
|
+
for await (const tag of semverTagsStream) semverTags.push(tag);
|
|
472
|
+
if (!semverTags.length) return;
|
|
473
|
+
return semverTags.sort(rcompare)[0];
|
|
474
|
+
}
|
|
475
|
+
const readTextSync = (path) => {
|
|
476
|
+
if (!existsSync(path)) return "";
|
|
477
|
+
return readFileSync(path).toString("utf8");
|
|
478
|
+
};
|
|
479
|
+
const parseHostedGitUrl = (url) => {
|
|
480
|
+
const [domain = "", prefix = "", owner = "", repo = ""] = [...(url.trim().match(/((?:github|gitlab|bitbucket|gitee|codeberg|sourceforge|git\.sr|\d+\.\d+\.\d+\.\d+:\d+)\.?(?:com|org|net|ht)?)[:\/](~)?([^/]+)\/(.+?)(?:[#/?](?:.+?)?)?(?:\.git)?$/) ?? []).slice(1)];
|
|
481
|
+
return {
|
|
482
|
+
host: domain ? `http${url.includes("https://") ? "s" : ""}://${domain}` : "",
|
|
483
|
+
prefix,
|
|
484
|
+
owner,
|
|
485
|
+
repo
|
|
486
|
+
};
|
|
487
|
+
};
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/core/project/project.ts
|
|
490
|
+
const DEFAULT_TAG_PREFIX = "v";
|
|
491
|
+
const DEFAULT_CHANGELOG_FILE = "CHANGELOG.md";
|
|
492
|
+
const DEFAULT_PUBLISH_TAG = "latest";
|
|
493
|
+
const DEFAULT_PRE_PUBLISH_TAG = "next";
|
|
494
|
+
var Project = class {
|
|
495
|
+
options;
|
|
496
|
+
manifest;
|
|
497
|
+
changedFiles = [];
|
|
498
|
+
versionUpdates = [];
|
|
499
|
+
constructor(options) {
|
|
500
|
+
this.options = options;
|
|
501
|
+
this.options.changelogFile ??= DEFAULT_CHANGELOG_FILE;
|
|
502
|
+
const { compose, manifest } = options;
|
|
503
|
+
this.manifest = compose?.(manifest) ?? manifest;
|
|
504
|
+
}
|
|
505
|
+
get hasBump() {
|
|
506
|
+
return this.versionUpdates.length > 0;
|
|
507
|
+
}
|
|
508
|
+
async getHostedGitInfo() {
|
|
509
|
+
const remote = await gitConfigGet("remote.origin.url");
|
|
510
|
+
return parseHostedGitUrl(remote ?? "");
|
|
511
|
+
}
|
|
512
|
+
isMonorepoMode() {
|
|
513
|
+
return "mode" in this;
|
|
514
|
+
}
|
|
515
|
+
async getCommitMessage() {
|
|
516
|
+
const { versionUpdates, versionUpdates: [{ to }] } = this;
|
|
517
|
+
if (isEmpty(versionUpdates)) throwError("Nothing to commit");
|
|
518
|
+
return `chore(release): ${to}`;
|
|
519
|
+
}
|
|
520
|
+
toTag(tagPrefix, version) {
|
|
521
|
+
return `${tagPrefix}${version}`;
|
|
522
|
+
}
|
|
523
|
+
toBranch(branchPrefix, version) {
|
|
524
|
+
return `${branchPrefix}${major(version)}`;
|
|
525
|
+
}
|
|
526
|
+
async getTagPrefix(override) {
|
|
527
|
+
if (!isUndefined(override)) return override;
|
|
528
|
+
const { options: { tagPrefix = "v" } } = this;
|
|
529
|
+
return isFunction(tagPrefix) ? tagPrefix("") : tagPrefix;
|
|
530
|
+
}
|
|
531
|
+
async getTags(options = {}) {
|
|
532
|
+
const { manifest } = this;
|
|
533
|
+
const { tagPrefix: override, verify = true } = options;
|
|
534
|
+
const tagPrefix = await this.getTagPrefix(override);
|
|
535
|
+
const version = await manifest.getVersion();
|
|
536
|
+
const tag = this.toTag(tagPrefix, version);
|
|
537
|
+
if (verify) {
|
|
538
|
+
const isTagExists = await gitRevParse(["--verify", tag]);
|
|
539
|
+
if (isTagExists) {
|
|
540
|
+
console.log("isTagExists:", isTagExists);
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return [tag];
|
|
545
|
+
}
|
|
546
|
+
async getReleaseData(options = {}) {
|
|
547
|
+
const { manifest, versionUpdates, options: { changelogFile } } = this;
|
|
548
|
+
const { projectDir } = manifest;
|
|
549
|
+
const { tagPrefix: override } = options;
|
|
550
|
+
const tagPrefix = await this.getTagPrefix(override);
|
|
551
|
+
const version = await manifest.getVersion();
|
|
552
|
+
const isPrerelease = Boolean(await manifest.getPrereleaseVersion());
|
|
553
|
+
const lastRelease = await extractLastRelease(isEmpty(versionUpdates) ? readTextSync(join(projectDir, changelogFile)) : versionUpdates[0].notes);
|
|
554
|
+
if (Object.values(lastRelease).every(isEmpty) || lastRelease.version && lastRelease.version !== version) return [];
|
|
555
|
+
if (!lastRelease.nextTag) {
|
|
556
|
+
const tags = await this.getTags({
|
|
557
|
+
tagPrefix,
|
|
558
|
+
verify: false
|
|
559
|
+
});
|
|
560
|
+
if (tags.length) [lastRelease.nextTag] = tags;
|
|
561
|
+
}
|
|
562
|
+
return [{
|
|
563
|
+
...lastRelease,
|
|
564
|
+
title: this.toTag(tagPrefix, version),
|
|
565
|
+
version,
|
|
566
|
+
isPrerelease,
|
|
567
|
+
isLatest: await this.isLatestRelease(options)
|
|
568
|
+
}];
|
|
569
|
+
}
|
|
570
|
+
async isFirstRelease(options = {}) {
|
|
571
|
+
return !await this.getLastReleaseTag(options);
|
|
572
|
+
}
|
|
573
|
+
async getLastReleaseTag(options = {}) {
|
|
574
|
+
const { manifest: { projectDir } } = this;
|
|
575
|
+
const { tagPrefix: override } = options;
|
|
576
|
+
return getLastSemverTag({
|
|
577
|
+
path: projectDir,
|
|
578
|
+
prefix: await this.getTagPrefix(override)
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
async isLatestRelease(options = {}) {
|
|
582
|
+
const { manifest } = this;
|
|
583
|
+
const { tagPrefix: override } = options;
|
|
584
|
+
const version = await manifest.getVersion();
|
|
585
|
+
const currentVersion = valid(version);
|
|
586
|
+
if (!currentVersion) return true;
|
|
587
|
+
const latestVersion = await getVersionFromTags({
|
|
588
|
+
prefix: await this.getTagPrefix(override),
|
|
589
|
+
skipUnstable: true,
|
|
590
|
+
all: true
|
|
591
|
+
});
|
|
592
|
+
return !latestVersion || gte(currentVersion, latestVersion);
|
|
593
|
+
}
|
|
594
|
+
async getDefaultPublishTag(options = {}) {
|
|
595
|
+
const { manifest } = this;
|
|
596
|
+
const name = await manifest.getName();
|
|
597
|
+
const version = await manifest.getVersion();
|
|
598
|
+
const map = /* @__PURE__ */ new Map();
|
|
599
|
+
map.set(name, DEFAULT_PUBLISH_TAG);
|
|
600
|
+
const prerelease = await manifest.getPrereleaseVersion();
|
|
601
|
+
if (prerelease) {
|
|
602
|
+
const [identifier] = prerelease;
|
|
603
|
+
const tag = isString(identifier) ? identifier : DEFAULT_PRE_PUBLISH_TAG;
|
|
604
|
+
map.set(name, tag);
|
|
605
|
+
return map;
|
|
606
|
+
}
|
|
607
|
+
if (!await this.isLatestRelease(options)) {
|
|
608
|
+
const tag = `release.${major(version)}.x`;
|
|
609
|
+
map.set(name, tag);
|
|
610
|
+
return map;
|
|
611
|
+
}
|
|
612
|
+
return map;
|
|
613
|
+
}
|
|
614
|
+
/** 在主要版本发布后创建维护分支引用 */
|
|
615
|
+
async getMaintenanceBranches(options = {}) {
|
|
616
|
+
const { manifest } = this;
|
|
617
|
+
const { tagPrefix: override, branchPrefix } = options;
|
|
618
|
+
const tagPrefix = await this.getTagPrefix(override);
|
|
619
|
+
const version = await manifest.getVersion();
|
|
620
|
+
const currentVersion = valid(version);
|
|
621
|
+
if (!currentVersion) return [];
|
|
622
|
+
const [releaseData] = await this.getReleaseData(options);
|
|
623
|
+
const previousTag = releaseData?.previousTag || await this.getLastReleaseTag({ tagPrefix });
|
|
624
|
+
if (previousTag) {
|
|
625
|
+
const previousVersion = valid(previousTag.startsWith(tagPrefix) ? previousTag.slice(tagPrefix.length) : previousTag);
|
|
626
|
+
if (previousVersion && major(currentVersion) > major(previousVersion)) return [{
|
|
627
|
+
from: previousTag,
|
|
628
|
+
to: this.toBranch(branchPrefix ?? tagPrefix, previousVersion)
|
|
629
|
+
}];
|
|
630
|
+
}
|
|
631
|
+
return [];
|
|
632
|
+
}
|
|
633
|
+
changelog(options = {}) {
|
|
634
|
+
const { manifest: { projectDir }, options: { changelogFile } } = this;
|
|
635
|
+
const { tagPrefix, releaseCount, preamble, transformTypes, scope } = options;
|
|
636
|
+
return new Changelog({
|
|
637
|
+
pkgDir: projectDir,
|
|
638
|
+
tagPrefix,
|
|
639
|
+
filename: changelogFile,
|
|
640
|
+
releaseCount,
|
|
641
|
+
preamble,
|
|
642
|
+
transformTypes,
|
|
643
|
+
scope
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
async getNextVersion(options = {}) {
|
|
647
|
+
const { manifest } = this;
|
|
648
|
+
const { forcePrivate, logger } = options;
|
|
649
|
+
if (!forcePrivate && await manifest.isPrivate()) return;
|
|
650
|
+
const { version: forcedVersion, baseVersion, as, prerelease, snapshot, firstRelease, tagPrefix: override } = options;
|
|
651
|
+
let isFirstRelease = firstRelease;
|
|
652
|
+
const tagPrefix = await this.getTagPrefix(override);
|
|
653
|
+
const name = await manifest.getName();
|
|
654
|
+
if (forcedVersion && valid(forcedVersion)) {
|
|
655
|
+
logger?.verbose(`Using specified version "${forcedVersion}" for "${name}".`);
|
|
656
|
+
return setSnapshotIdentifier(forcedVersion, snapshot);
|
|
657
|
+
}
|
|
658
|
+
if (isUndefined(firstRelease)) isFirstRelease = await this.isFirstRelease({ tagPrefix });
|
|
659
|
+
const version = baseVersion ?? await manifest.getVersion();
|
|
660
|
+
if (isFirstRelease) {
|
|
661
|
+
logger?.verbose(`First release detected for "${name}".`);
|
|
662
|
+
return setSnapshotIdentifier(version, snapshot);
|
|
663
|
+
}
|
|
664
|
+
const forcePrerelease = equals("prerelease", as);
|
|
665
|
+
let releaseType = as;
|
|
666
|
+
if (!as || forcePrerelease) {
|
|
667
|
+
const inferred = await this.inferReleaseType({
|
|
668
|
+
...options,
|
|
669
|
+
tagPrefix
|
|
670
|
+
});
|
|
671
|
+
if (inferred) releaseType = inferred;
|
|
672
|
+
}
|
|
673
|
+
if (!releaseType) {
|
|
674
|
+
if (!snapshot && !forcePrerelease) {
|
|
675
|
+
logger?.verbose(`No release type detected for "${name}".`);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
logger?.verbose(`No release type detected for "${name}", defaulting to "patch" for snapshot or forced prerelease mode.`);
|
|
679
|
+
releaseType = "patch";
|
|
680
|
+
}
|
|
681
|
+
const identifier = getPrereleaseIdentifier(forcePrerelease ? prerelease ?? "beta" : prerelease, snapshot);
|
|
682
|
+
return inc(version, getReleaseType(releaseType, version, identifier), false, identifier, getPrereleaseIdentifierBase(snapshot));
|
|
683
|
+
}
|
|
684
|
+
async inferReleaseType(options = {}) {
|
|
685
|
+
const { logger, tagPrefix: override } = options;
|
|
686
|
+
const { manifest } = this;
|
|
687
|
+
const tagPrefix = await this.getTagPrefix(override);
|
|
688
|
+
const { releaseType, reason } = await this.changelog({
|
|
689
|
+
...options,
|
|
690
|
+
tagPrefix
|
|
691
|
+
}).inferReleaseType();
|
|
692
|
+
if (reason) logger?.verbose(`${reason} for "${await manifest.getName()}"`);
|
|
693
|
+
return releaseType;
|
|
694
|
+
}
|
|
695
|
+
async bump(options = {}) {
|
|
696
|
+
const { logger, firstRelease } = options;
|
|
697
|
+
const { manifest } = this;
|
|
698
|
+
const name = await manifest.getName();
|
|
699
|
+
const version = await manifest.getVersion();
|
|
700
|
+
const nextVersion = await this.getNextVersion(options);
|
|
701
|
+
if (!nextVersion || version === nextVersion && !firstRelease) {
|
|
702
|
+
logger?.verbose(`No version changes for "${name}". Skipping.`);
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
const { tagPrefix: override, skipChangelog, dryRun } = options;
|
|
706
|
+
const versionUpdate = {
|
|
707
|
+
...await manifest.writeVersion(nextVersion, dryRun),
|
|
708
|
+
notes: ""
|
|
709
|
+
};
|
|
710
|
+
this.versionUpdates.push(versionUpdate);
|
|
711
|
+
this.changedFiles.push(...versionUpdate.files);
|
|
712
|
+
if (version === nextVersion) logger?.verbose(`${name}: ${nextVersion}`);
|
|
713
|
+
else logger?.verbose(`${name}: ${version} -> ${nextVersion}`);
|
|
714
|
+
if (!skipChangelog) {
|
|
715
|
+
const tagPrefix = await this.getTagPrefix(override);
|
|
716
|
+
const lastReleaseTag = await this.getLastReleaseTag({ tagPrefix });
|
|
717
|
+
const log = this.changelog({
|
|
718
|
+
...options,
|
|
719
|
+
tagPrefix
|
|
720
|
+
});
|
|
721
|
+
log.setOptions({
|
|
722
|
+
version: nextVersion,
|
|
723
|
+
previousTag: lastReleaseTag
|
|
724
|
+
});
|
|
725
|
+
versionUpdate.notes = dryRun ? await log.read() : await log.write();
|
|
726
|
+
logger?.verbose(`Release notes:\n\n${versionUpdate.notes ?? "..."}`);
|
|
727
|
+
this.changedFiles.push(log.infile);
|
|
728
|
+
}
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
async revert(options = {}, filesSet = /* @__PURE__ */ new Set()) {
|
|
732
|
+
const { manifest, versionUpdates } = this;
|
|
733
|
+
const { dryRun, logger } = options;
|
|
734
|
+
if (isEmpty(versionUpdates)) return false;
|
|
735
|
+
for (const update of versionUpdates) {
|
|
736
|
+
logger?.verbose(`Reverting ${update.name}: ${update.to} -> ${update.from}`);
|
|
737
|
+
update.files.forEach((file) => filesSet.add(file));
|
|
738
|
+
await manifest.writeVersion(update.from, dryRun);
|
|
739
|
+
}
|
|
740
|
+
this.changedFiles = this.changedFiles.filter((file) => !filesSet.has(file));
|
|
741
|
+
this.versionUpdates.length = 0;
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
//#endregion
|
|
746
|
+
//#region src/core/project/monorepo.ts
|
|
747
|
+
var MonorepoProject = class extends Project {
|
|
748
|
+
mode;
|
|
749
|
+
async getCommitMessage() {
|
|
750
|
+
if (this.isFixedMode) return super.getCommitMessage();
|
|
751
|
+
return this.getIndependentCommitMessage();
|
|
752
|
+
}
|
|
753
|
+
async getTagPrefix(scope) {
|
|
754
|
+
if (this.options.tagPrefix) return this.options.tagPrefix(scope ?? "");
|
|
755
|
+
if (this.isFixedMode) return super.getTagPrefix();
|
|
756
|
+
return `${scope}@`;
|
|
757
|
+
}
|
|
758
|
+
async getTags(options = {}) {
|
|
759
|
+
if (this.isFixedMode) return super.getTags(options);
|
|
760
|
+
return this.getIndependentTags(options);
|
|
761
|
+
}
|
|
762
|
+
async getReleaseData(options = {}) {
|
|
763
|
+
if (this.isFixedMode) return super.getReleaseData(options);
|
|
764
|
+
return this.getIndependentReleaseData(options);
|
|
765
|
+
}
|
|
766
|
+
async isFirstRelease(options = {}) {
|
|
767
|
+
if (this.isFixedMode) return super.isFirstRelease(options);
|
|
768
|
+
return this.isIndependentFirstRelease();
|
|
769
|
+
}
|
|
770
|
+
async getDefaultPublishTag(options = {}) {
|
|
771
|
+
if (this.isFixedMode) return super.getDefaultPublishTag(options);
|
|
772
|
+
return this.getIndependentDefaultPublishTag(options);
|
|
773
|
+
}
|
|
774
|
+
async getMaintenanceBranches(options = {}) {
|
|
775
|
+
if (this.isFixedMode) return super.getMaintenanceBranches(options);
|
|
776
|
+
return this.getIndependentMaintenanceBranches(options);
|
|
777
|
+
}
|
|
778
|
+
getNextVersion(options = {}) {
|
|
779
|
+
if (this.isFixedMode && options.version) return super.getNextVersion(options);
|
|
780
|
+
throwError("Monorepo project \"independent\" mode does not support getNextVersion");
|
|
781
|
+
}
|
|
782
|
+
async bump(options = {}) {
|
|
783
|
+
if (this.isFixedMode) return this.fixedBump(options);
|
|
784
|
+
return this.independentBump(options);
|
|
785
|
+
}
|
|
786
|
+
async revert(options = {}) {
|
|
787
|
+
const { manifest, versionUpdates } = this;
|
|
788
|
+
const { dryRun, logger } = options;
|
|
789
|
+
const files = /* @__PURE__ */ new Set();
|
|
790
|
+
const rootUpdate = versionUpdates.find(({ files }) => files.includes(manifest.filepath));
|
|
791
|
+
let hasRevert = false;
|
|
792
|
+
if (rootUpdate) {
|
|
793
|
+
logger?.verbose(`Reverting ${rootUpdate.name}: ${rootUpdate.to} -> ${rootUpdate.from}`);
|
|
794
|
+
rootUpdate.files.forEach((file) => files.add(file));
|
|
795
|
+
await manifest.writeVersion(rootUpdate.from, dryRun);
|
|
796
|
+
hasRevert = true;
|
|
797
|
+
}
|
|
798
|
+
for await (const project of this.getProjects()) hasRevert = await project.revert(options, files) || hasRevert;
|
|
799
|
+
this.changedFiles = this.changedFiles.filter((file) => !files.has(file));
|
|
800
|
+
this.versionUpdates.length = 0;
|
|
801
|
+
return hasRevert;
|
|
802
|
+
}
|
|
803
|
+
projectsMutex;
|
|
804
|
+
constructor(options) {
|
|
805
|
+
const { mode, compose } = options;
|
|
806
|
+
super({
|
|
807
|
+
...options,
|
|
808
|
+
compose: compose ? (manifest, isRoot = true) => compose(manifest, isRoot) : (manifest) => manifest
|
|
809
|
+
});
|
|
810
|
+
this.mode = mode;
|
|
811
|
+
}
|
|
812
|
+
get isFixedMode() {
|
|
813
|
+
return this.mode === "fixed";
|
|
814
|
+
}
|
|
815
|
+
get isIndependentMode() {
|
|
816
|
+
return this.mode === "independent";
|
|
817
|
+
}
|
|
818
|
+
async *getProjects() {
|
|
819
|
+
if (!this.projectsMutex) {
|
|
820
|
+
const { compose, getProjects } = this.options;
|
|
821
|
+
const options = {
|
|
822
|
+
compose: (manifest) => compose(manifest, false),
|
|
823
|
+
manifest: this.manifest
|
|
824
|
+
};
|
|
825
|
+
this.projectsMutex = (async () => {
|
|
826
|
+
const projects = [];
|
|
827
|
+
for await (const project of getProjects(options)) if (!await project.manifest.isPrivate()) projects.push(project);
|
|
828
|
+
return projects;
|
|
829
|
+
})();
|
|
830
|
+
}
|
|
831
|
+
yield* await this.projectsMutex;
|
|
832
|
+
}
|
|
833
|
+
async getScope(name) {
|
|
834
|
+
if (this.options.scope) return this.options.scope(name, await this.manifest.getName());
|
|
835
|
+
return name.replace(/@[^/]+\//, "");
|
|
836
|
+
}
|
|
837
|
+
async getMaxVersion() {
|
|
838
|
+
const { manifest } = this;
|
|
839
|
+
let max = await manifest.getVersion();
|
|
840
|
+
const packages = [];
|
|
841
|
+
for await (const project of this.getProjects()) {
|
|
842
|
+
const name = await project.manifest.getName();
|
|
843
|
+
const version = await project.manifest.getVersion();
|
|
844
|
+
if (gt(version, max)) {
|
|
845
|
+
max = version;
|
|
846
|
+
packages.length = 0;
|
|
847
|
+
}
|
|
848
|
+
if (version === max) packages.push(name);
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
version: max,
|
|
852
|
+
packages
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
getIndependentCommitMessage() {
|
|
856
|
+
const { versionUpdates } = this;
|
|
857
|
+
if (isEmpty(versionUpdates)) throwError("Nothing to commit");
|
|
858
|
+
return `chore(release): monorepo release\n\n${versionUpdates.map(({ name, to }) => `- ${name}@${to}`).join("\n")}`;
|
|
859
|
+
}
|
|
860
|
+
async getIndependentTags(options) {
|
|
861
|
+
const tags = [];
|
|
862
|
+
for await (const project of this.getProjects()) if (project.hasBump) {
|
|
863
|
+
const name = await project.manifest.getName();
|
|
864
|
+
const scope = await this.getScope(name);
|
|
865
|
+
const tagPrefix = await this.getTagPrefix(scope);
|
|
866
|
+
tags.push(...await project.getTags({
|
|
867
|
+
...options,
|
|
868
|
+
tagPrefix
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
return tags;
|
|
872
|
+
}
|
|
873
|
+
async getIndependentReleaseData(options) {
|
|
874
|
+
const data = [];
|
|
875
|
+
for await (const project of this.getProjects()) if (project.hasBump) {
|
|
876
|
+
const name = await project.manifest.getName();
|
|
877
|
+
const scope = await this.getScope(name);
|
|
878
|
+
const tagPrefix = await this.getTagPrefix(scope);
|
|
879
|
+
const version = await project.manifest.getVersion();
|
|
880
|
+
const releaseData = await project.getReleaseData({
|
|
881
|
+
...options,
|
|
882
|
+
tagPrefix
|
|
883
|
+
});
|
|
884
|
+
data.push(...releaseData.map((data) => ({
|
|
885
|
+
...data,
|
|
886
|
+
title: this.toTag(tagPrefix, version)
|
|
887
|
+
})));
|
|
888
|
+
}
|
|
889
|
+
return data;
|
|
890
|
+
}
|
|
891
|
+
async getIndependentDefaultPublishTag(options) {
|
|
892
|
+
const map = /* @__PURE__ */ new Map();
|
|
893
|
+
for await (const project of this.getProjects()) {
|
|
894
|
+
const name = await project.manifest.getName();
|
|
895
|
+
const scope = await this.getScope(name);
|
|
896
|
+
const tagPrefix = await this.getTagPrefix(scope);
|
|
897
|
+
const tag = await project.getDefaultPublishTag({
|
|
898
|
+
...options,
|
|
899
|
+
tagPrefix
|
|
900
|
+
});
|
|
901
|
+
map.set(name, tag.get(name));
|
|
902
|
+
}
|
|
903
|
+
return map;
|
|
904
|
+
}
|
|
905
|
+
async getIndependentMaintenanceBranches(options) {
|
|
906
|
+
const branches = [];
|
|
907
|
+
for await (const project of this.getProjects()) {
|
|
908
|
+
const name = await project.manifest.getName();
|
|
909
|
+
const scope = await this.getScope(name);
|
|
910
|
+
const tagPrefix = await this.getTagPrefix(scope);
|
|
911
|
+
branches.push(...await project.getMaintenanceBranches({
|
|
912
|
+
...options,
|
|
913
|
+
tagPrefix
|
|
914
|
+
}));
|
|
915
|
+
}
|
|
916
|
+
return branches;
|
|
917
|
+
}
|
|
918
|
+
async getBumpOptions(project, options, baseVersion, firstRelease) {
|
|
919
|
+
const { extraScopes = [], byProject, force, ...bumpOptions } = options;
|
|
920
|
+
const name = await project.manifest.getName();
|
|
921
|
+
const scope = await this.getScope(name);
|
|
922
|
+
const tagPrefix = await this.getTagPrefix(scope);
|
|
923
|
+
return {
|
|
924
|
+
firstRelease,
|
|
925
|
+
...bumpOptions,
|
|
926
|
+
...byProject?.[name],
|
|
927
|
+
scope: [scope, ...extraScopes],
|
|
928
|
+
tagPrefix,
|
|
929
|
+
baseVersion
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
async independentBump(options = {}) {
|
|
933
|
+
let hasBump = false;
|
|
934
|
+
for await (const project of this.getProjects()) {
|
|
935
|
+
const projectBumpOptions = await this.getBumpOptions(project, options);
|
|
936
|
+
hasBump = await project.bump(projectBumpOptions) || hasBump;
|
|
937
|
+
this.changedFiles.push(...project.changedFiles);
|
|
938
|
+
this.versionUpdates.push(...project.versionUpdates);
|
|
939
|
+
}
|
|
940
|
+
return hasBump;
|
|
941
|
+
}
|
|
942
|
+
async fixedBump(options = {}) {
|
|
943
|
+
const { force } = options;
|
|
944
|
+
const { manifest } = this;
|
|
945
|
+
const rootVersion = await manifest.getVersion();
|
|
946
|
+
const name = await manifest.getName();
|
|
947
|
+
const tagPrefix = await this.getTagPrefix(name);
|
|
948
|
+
const firstRelease = await this.isFirstRelease({ tagPrefix });
|
|
949
|
+
let hasBump = false;
|
|
950
|
+
let fixedVersion;
|
|
951
|
+
const updatedProjects = [];
|
|
952
|
+
for await (const project of this.getProjects()) {
|
|
953
|
+
const projectBumpOptions = await this.getBumpOptions(project, options, rootVersion, firstRelease);
|
|
954
|
+
const nextVersion = await project.getNextVersion(projectBumpOptions);
|
|
955
|
+
if (nextVersion || force) {
|
|
956
|
+
updatedProjects.push({
|
|
957
|
+
project,
|
|
958
|
+
options: projectBumpOptions
|
|
959
|
+
});
|
|
960
|
+
if (nextVersion && (!fixedVersion || gt(nextVersion, fixedVersion))) fixedVersion = nextVersion;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (fixedVersion) {
|
|
964
|
+
hasBump = await super.bump({
|
|
965
|
+
...options,
|
|
966
|
+
forcePrivate: true,
|
|
967
|
+
version: fixedVersion,
|
|
968
|
+
tagPrefix
|
|
969
|
+
});
|
|
970
|
+
for (const { project, options } of updatedProjects) {
|
|
971
|
+
hasBump = await project.bump({
|
|
972
|
+
...options,
|
|
973
|
+
version: fixedVersion
|
|
974
|
+
}) || hasBump;
|
|
975
|
+
this.changedFiles.push(...project.changedFiles);
|
|
976
|
+
this.versionUpdates.push(...project.versionUpdates);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
return hasBump;
|
|
980
|
+
}
|
|
981
|
+
async isIndependentFirstRelease(options = {}) {
|
|
982
|
+
let allFirst = true;
|
|
983
|
+
for await (const project of this.getProjects()) {
|
|
984
|
+
const name = await project.manifest.getName();
|
|
985
|
+
const scope = await this.getScope(name);
|
|
986
|
+
const tagPrefix = await this.getTagPrefix(scope);
|
|
987
|
+
allFirst &&= await project.isFirstRelease({
|
|
988
|
+
...options,
|
|
989
|
+
tagPrefix
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
return allFirst;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
//#endregion
|
|
996
|
+
//#region src/core/project/packageJson.ts
|
|
997
|
+
var PackageJsonProject = class extends Project {
|
|
998
|
+
constructor(options = {}) {
|
|
999
|
+
const { path = PackageJsonManifest.Filename } = options;
|
|
1000
|
+
super({
|
|
1001
|
+
...options,
|
|
1002
|
+
manifest: new PackageJsonManifest(path)
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
publish(_options) {
|
|
1006
|
+
throwError("Publishing a package.json project is not supported");
|
|
1007
|
+
}
|
|
1008
|
+
publishCheck(_options) {
|
|
1009
|
+
throwError("Publishing a package.json project is not supported");
|
|
1010
|
+
}
|
|
1011
|
+
};
|
|
1012
|
+
//#endregion
|
|
1013
|
+
//#region src/core/project/packageJsonMonorepo.ts
|
|
1014
|
+
/**
|
|
1015
|
+
* 基于 package.json 的单体仓库项目。
|
|
1016
|
+
*/
|
|
1017
|
+
var PackageJsonMonorepoProject = class extends MonorepoProject {
|
|
1018
|
+
constructor(options) {
|
|
1019
|
+
const { root = process.cwd(), getProjects } = options;
|
|
1020
|
+
super({
|
|
1021
|
+
...options,
|
|
1022
|
+
manifest: new PackageJsonManifest(normalizePath(join(root, PackageJsonManifest.Filename))),
|
|
1023
|
+
async *getProjects(options) {
|
|
1024
|
+
for await (const path of getProjects(options)) yield new PackageJsonProject({
|
|
1025
|
+
path: isAbsolute(path) ? path : normalizePath(join(root, path)),
|
|
1026
|
+
...options
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
publish(_options) {
|
|
1032
|
+
throwError("Publishing a package.json monorepo project is not supported");
|
|
1033
|
+
}
|
|
1034
|
+
publishCheck(_options) {
|
|
1035
|
+
throwError("Publishing a package.json monorepo project is not supported");
|
|
1036
|
+
}
|
|
1037
|
+
};
|
|
1038
|
+
//#endregion
|
|
1039
|
+
//#region src/core/logger.ts
|
|
1040
|
+
const defaultPrinter = ({ dryRun, stage, message }) => {
|
|
1041
|
+
console.log(`${dryRun ? `[dry-run]` : ""}[${stage}]:`, message);
|
|
1042
|
+
};
|
|
1043
|
+
/** 一个子消息记录器,绑定到特定阶段 */
|
|
1044
|
+
var ChildLogger = class {
|
|
1045
|
+
parent;
|
|
1046
|
+
stage;
|
|
1047
|
+
/**
|
|
1048
|
+
* 创建子日志记录器。
|
|
1049
|
+
* @param parent - 要继承其选项的父日志记录器。
|
|
1050
|
+
* @param stage - 此日志记录器所属的处理阶段。
|
|
1051
|
+
*/
|
|
1052
|
+
constructor(parent, stage) {
|
|
1053
|
+
this.parent = parent;
|
|
1054
|
+
this.stage = stage;
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* 记录一条普通信息。
|
|
1058
|
+
* @param message - 要记录的信息。
|
|
1059
|
+
*/
|
|
1060
|
+
info(message) {
|
|
1061
|
+
this.parent.info(this.stage, message);
|
|
1062
|
+
}
|
|
1063
|
+
/**
|
|
1064
|
+
* 记录一条详细信息。
|
|
1065
|
+
* @param message - 要记录的信息。
|
|
1066
|
+
*/
|
|
1067
|
+
verbose(message) {
|
|
1068
|
+
this.parent.verbose(this.stage, message);
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1071
|
+
/** 消息记录器 */
|
|
1072
|
+
var Logger = class {
|
|
1073
|
+
options;
|
|
1074
|
+
printer;
|
|
1075
|
+
colors;
|
|
1076
|
+
colorMap = /* @__PURE__ */ new Map();
|
|
1077
|
+
constructor(options = {}) {
|
|
1078
|
+
this.options = options;
|
|
1079
|
+
const { colors = [], printer = defaultPrinter } = options;
|
|
1080
|
+
this.colors = colors;
|
|
1081
|
+
this.printer = printer;
|
|
1082
|
+
}
|
|
1083
|
+
info(stage, message) {
|
|
1084
|
+
const { silent, dryRun } = this.options;
|
|
1085
|
+
if (!silent) {
|
|
1086
|
+
const color = this.getColor(stage);
|
|
1087
|
+
this.printer({
|
|
1088
|
+
dryRun,
|
|
1089
|
+
stage,
|
|
1090
|
+
message,
|
|
1091
|
+
color
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
verbose(stage, message) {
|
|
1096
|
+
const { silent, verbose, dryRun } = this.options;
|
|
1097
|
+
if (!silent && (verbose || dryRun)) {
|
|
1098
|
+
const color = this.getColor(stage);
|
|
1099
|
+
this.printer({
|
|
1100
|
+
dryRun,
|
|
1101
|
+
stage,
|
|
1102
|
+
message,
|
|
1103
|
+
color
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
createChild(stage) {
|
|
1108
|
+
return new ChildLogger(this, stage);
|
|
1109
|
+
}
|
|
1110
|
+
getColor(stage) {
|
|
1111
|
+
const cached = this.colorMap.get(stage);
|
|
1112
|
+
if (cached) return cached;
|
|
1113
|
+
const index = this.colorMap.size % this.colors.length;
|
|
1114
|
+
const color = this.colors[index] ?? ((text) => text);
|
|
1115
|
+
this.colorMap.set(stage, color);
|
|
1116
|
+
return color;
|
|
1117
|
+
}
|
|
1118
|
+
};
|
|
1119
|
+
//#endregion
|
|
1120
|
+
//#region src/core/releaser.ts
|
|
1121
|
+
var Releaser = class {
|
|
1122
|
+
options;
|
|
1123
|
+
project;
|
|
1124
|
+
hosting;
|
|
1125
|
+
logger;
|
|
1126
|
+
stepsOptions = {};
|
|
1127
|
+
skippedSteps = {};
|
|
1128
|
+
state = {};
|
|
1129
|
+
queue = [];
|
|
1130
|
+
constructor(options) {
|
|
1131
|
+
this.options = options;
|
|
1132
|
+
const { project, hosting, logger, ...log } = options;
|
|
1133
|
+
this.project = project;
|
|
1134
|
+
this.hosting = hosting;
|
|
1135
|
+
this.logger = logger ?? new Logger(log);
|
|
1136
|
+
}
|
|
1137
|
+
enqueue(fn) {
|
|
1138
|
+
this.queue.push(fn);
|
|
1139
|
+
return this;
|
|
1140
|
+
}
|
|
1141
|
+
setOptions(options) {
|
|
1142
|
+
const logger = this.logger.createChild("set-options");
|
|
1143
|
+
logger.verbose("Setting options for steps:");
|
|
1144
|
+
logger.verbose(options);
|
|
1145
|
+
for (const [key, value] of Object.entries(options)) Object.assign(this.stepsOptions[key] ??= {}, value);
|
|
1146
|
+
logger.verbose("Final options for steps:");
|
|
1147
|
+
logger.verbose(this.stepsOptions);
|
|
1148
|
+
return this;
|
|
1149
|
+
}
|
|
1150
|
+
setUser(options) {
|
|
1151
|
+
return this.enqueue(async () => {
|
|
1152
|
+
const { options: { dryRun }, stepsOptions: { setUser } } = this;
|
|
1153
|
+
const { username, email } = {
|
|
1154
|
+
...setUser,
|
|
1155
|
+
...options
|
|
1156
|
+
};
|
|
1157
|
+
const logger = this.logger.createChild("set-user");
|
|
1158
|
+
if (this.shouldSkip("setUser")) {
|
|
1159
|
+
logger.info("Skipping set git user.");
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
logger.info("Setting git user...");
|
|
1163
|
+
if (!username && !email) {
|
|
1164
|
+
logger.info("No git user configuration provided.");
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
if (username) {
|
|
1168
|
+
logger.verbose(`Setting git user.name to "${username}".`);
|
|
1169
|
+
if (!dryRun) await gitConfigSet("user.name", username);
|
|
1170
|
+
}
|
|
1171
|
+
if (email) {
|
|
1172
|
+
logger.verbose(`Setting git user.email to "${email}".`);
|
|
1173
|
+
if (!dryRun) await gitConfigSet("user.email", email);
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
setSkippedSteps(options) {
|
|
1178
|
+
const logger = this.logger.createChild("set-skipped-steps");
|
|
1179
|
+
logger.verbose("Setting skipped for steps:");
|
|
1180
|
+
logger.verbose(options);
|
|
1181
|
+
Object.assign(this.skippedSteps, options);
|
|
1182
|
+
logger.verbose("Final skipped for steps:");
|
|
1183
|
+
logger.verbose(this.skippedSteps);
|
|
1184
|
+
return this;
|
|
1185
|
+
}
|
|
1186
|
+
checkout(options) {
|
|
1187
|
+
return this.enqueue(async () => {
|
|
1188
|
+
const { options: { dryRun }, stepsOptions: { checkout } } = this;
|
|
1189
|
+
const { branch: headBranch = "release-branch", fetch, force } = {
|
|
1190
|
+
...checkout,
|
|
1191
|
+
...options
|
|
1192
|
+
};
|
|
1193
|
+
const logger = this.logger.createChild("checkout");
|
|
1194
|
+
if (this.shouldSkip("checkout")) {
|
|
1195
|
+
logger.info("Skipping checkout.");
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
logger.info(`Checking out branch ${headBranch}...`);
|
|
1199
|
+
this.state.headBranch = headBranch;
|
|
1200
|
+
this.state.baseBranch = await this.getBaseBranch();
|
|
1201
|
+
if (fetch) {
|
|
1202
|
+
logger.verbose(`Fetching all commits and tags from the remote repository...`);
|
|
1203
|
+
if (!dryRun) await gitFetch([
|
|
1204
|
+
"--prune",
|
|
1205
|
+
"--unshallow",
|
|
1206
|
+
"--tags",
|
|
1207
|
+
"--",
|
|
1208
|
+
"origin",
|
|
1209
|
+
"HEAD"
|
|
1210
|
+
]);
|
|
1211
|
+
}
|
|
1212
|
+
if (force) {
|
|
1213
|
+
logger.verbose(`Force checking out branch ${headBranch}...`);
|
|
1214
|
+
if (!dryRun) await gitCheckoutBranchForce(headBranch);
|
|
1215
|
+
} else {
|
|
1216
|
+
logger.verbose(`Checking out branch ${headBranch}...`);
|
|
1217
|
+
if (!dryRun) await gitCheckoutBranch(headBranch);
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
bump(options) {
|
|
1222
|
+
return this.enqueue(async () => {
|
|
1223
|
+
const { project, options: { dryRun }, stepsOptions: { bump } } = this;
|
|
1224
|
+
const logger = this.logger.createChild("bump");
|
|
1225
|
+
if (this.shouldSkip("bump")) {
|
|
1226
|
+
logger.info("Skipping bump.");
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
logger.info("Bumping version...");
|
|
1230
|
+
const done = await project.bump({
|
|
1231
|
+
dryRun,
|
|
1232
|
+
logger,
|
|
1233
|
+
...bump,
|
|
1234
|
+
...options
|
|
1235
|
+
});
|
|
1236
|
+
this.state.bump = done;
|
|
1237
|
+
if (!done) logger.info("No version changes detected.");
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
commit(options) {
|
|
1241
|
+
return this.enqueue(async () => {
|
|
1242
|
+
const { project, options: { dryRun }, state, stepsOptions: { commit } } = this;
|
|
1243
|
+
const { sign, allowEmpty } = {
|
|
1244
|
+
...commit,
|
|
1245
|
+
...options
|
|
1246
|
+
};
|
|
1247
|
+
const logger = this.logger.createChild("commit");
|
|
1248
|
+
if (this.shouldSkip("commit")) {
|
|
1249
|
+
logger.info("Skipping commit.");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
logger.info(`Committing changes...`);
|
|
1253
|
+
if (!state.bump) {
|
|
1254
|
+
logger.info("No changes to commit.");
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
const files = project.changedFiles;
|
|
1258
|
+
const message = await project.getCommitMessage();
|
|
1259
|
+
logger.verbose(`Files to commit:`);
|
|
1260
|
+
files.forEach((file) => {
|
|
1261
|
+
logger.verbose(`- ${file}`);
|
|
1262
|
+
});
|
|
1263
|
+
logger.verbose(message.includes("\n") ? `Commit message:\n\n${message}` : `Commit message: ${message}`);
|
|
1264
|
+
if (!dryRun) {
|
|
1265
|
+
await gitAdd(["--", ...files]);
|
|
1266
|
+
await gitCommitMessage(message, [
|
|
1267
|
+
...when(sign, "--gpg-sign"),
|
|
1268
|
+
...when(allowEmpty, "--allow-empty"),
|
|
1269
|
+
"--no-verify"
|
|
1270
|
+
]);
|
|
1271
|
+
}
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
revert() {
|
|
1275
|
+
return this.enqueue(async () => {
|
|
1276
|
+
const { project, logger, options: { dryRun } } = this;
|
|
1277
|
+
logger.info("revert", "Reverting version updates...");
|
|
1278
|
+
if (!await project.revert({
|
|
1279
|
+
dryRun,
|
|
1280
|
+
logger: logger.createChild("revert")
|
|
1281
|
+
})) {
|
|
1282
|
+
logger.info("revert", "No version updates to revert.");
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
this.state.bump = false;
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
maintenanceBranch(options) {
|
|
1289
|
+
return this.enqueue(async () => {
|
|
1290
|
+
const { project, options: { dryRun }, stepsOptions: { maintenanceBranch } } = this;
|
|
1291
|
+
const { enabled, force } = {
|
|
1292
|
+
...maintenanceBranch,
|
|
1293
|
+
...options
|
|
1294
|
+
};
|
|
1295
|
+
const logger = this.logger.createChild("maintenance-branch");
|
|
1296
|
+
if (this.shouldSkip("maintenanceBranch")) {
|
|
1297
|
+
logger.info("Skipping create maintenance branches.");
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
logger.info("Creating maintenance branches...");
|
|
1301
|
+
if (!enabled) {
|
|
1302
|
+
logger.info("Maintenance branch creation is disabled.");
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
logger.verbose("Fetching fresh tags from the remote repository...");
|
|
1306
|
+
if (!dryRun) await gitFetch(["--tags"]);
|
|
1307
|
+
const branches = await project.getMaintenanceBranches();
|
|
1308
|
+
if (!branches.length) {
|
|
1309
|
+
logger.info("No maintenance branches to create.");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
logger.verbose("Branches to create:");
|
|
1313
|
+
for (const { from, to } of branches) {
|
|
1314
|
+
logger.verbose(`- ${to} from ${from}`);
|
|
1315
|
+
if (dryRun) continue;
|
|
1316
|
+
if ((await gitRevParse(["--verify", `refs/heads/${to}`]) || await gitLsRemote([
|
|
1317
|
+
"--heads",
|
|
1318
|
+
"origin",
|
|
1319
|
+
to
|
|
1320
|
+
])) && !force) {
|
|
1321
|
+
logger.info(`Branch ${to} already exists, skipping.`);
|
|
1322
|
+
continue;
|
|
1323
|
+
}
|
|
1324
|
+
const args = when(force, "--force");
|
|
1325
|
+
await gitBranch([
|
|
1326
|
+
...args,
|
|
1327
|
+
"--",
|
|
1328
|
+
to,
|
|
1329
|
+
from
|
|
1330
|
+
]);
|
|
1331
|
+
await gitPushBranch("origin", to, ["--no-verify", ...args]);
|
|
1332
|
+
}
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
tag(options) {
|
|
1336
|
+
return this.enqueue(async () => {
|
|
1337
|
+
const { project, options: { dryRun }, stepsOptions: { tag } } = this;
|
|
1338
|
+
const { fetch, sign } = {
|
|
1339
|
+
...tag,
|
|
1340
|
+
...options
|
|
1341
|
+
};
|
|
1342
|
+
const logger = this.logger.createChild("tag");
|
|
1343
|
+
if (this.shouldSkip("tag")) {
|
|
1344
|
+
logger.info("Skipping tag.");
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
logger.info("Tagging version...");
|
|
1348
|
+
if (fetch) {
|
|
1349
|
+
logger.verbose("Fetching fresh tags from the remote repository...");
|
|
1350
|
+
if (!dryRun) await gitFetch(["--tags"]);
|
|
1351
|
+
}
|
|
1352
|
+
const tags = await project.getTags();
|
|
1353
|
+
this.state.tags = tags.length > 0;
|
|
1354
|
+
if (!this.state.tags) {
|
|
1355
|
+
logger.info("No version changes detected.");
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
logger.verbose(`Tags to create:`);
|
|
1359
|
+
for (const tag of tags) {
|
|
1360
|
+
logger.verbose(`- ${tag}`);
|
|
1361
|
+
if (!dryRun) await gitTag([
|
|
1362
|
+
sign ? "--sign" : "--annotate",
|
|
1363
|
+
"--message",
|
|
1364
|
+
tag,
|
|
1365
|
+
tag
|
|
1366
|
+
]);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
push(options) {
|
|
1371
|
+
return this.enqueue(async () => {
|
|
1372
|
+
const { options: { dryRun }, state, stepsOptions: { push } } = this;
|
|
1373
|
+
const { force } = {
|
|
1374
|
+
...push,
|
|
1375
|
+
...options
|
|
1376
|
+
};
|
|
1377
|
+
const logger = this.logger.createChild("push");
|
|
1378
|
+
if (this.shouldSkip("push")) {
|
|
1379
|
+
logger.info("Skipping push.");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const branch = await gitBranchCurrent();
|
|
1383
|
+
logger.info(`Pushing changes to "${branch}"...`);
|
|
1384
|
+
if (!state.bump && !state.tags) {
|
|
1385
|
+
logger.info("Nothing to push.");
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (!dryRun) {
|
|
1389
|
+
if (state.bump) await gitPushBranch("origin", branch, [...when(force, "--force"), "--no-verify"]);
|
|
1390
|
+
if (state.tags) await gitPush([
|
|
1391
|
+
"origin",
|
|
1392
|
+
"--tags",
|
|
1393
|
+
"--no-verify"
|
|
1394
|
+
]);
|
|
1395
|
+
}
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
publish(options) {
|
|
1399
|
+
return this.enqueue(async () => {
|
|
1400
|
+
const { project, options: { dryRun }, stepsOptions: { publish } } = this;
|
|
1401
|
+
const logger = this.logger.createChild("publish");
|
|
1402
|
+
if (this.shouldSkip("publish")) {
|
|
1403
|
+
logger.info("Skipping publish.");
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
logger.info("Publishing...");
|
|
1407
|
+
const tagsMap = await project.getDefaultPublishTag();
|
|
1408
|
+
const name = await project.manifest.getName();
|
|
1409
|
+
const tag = tagsMap.get(name);
|
|
1410
|
+
await project.publish({
|
|
1411
|
+
dryRun,
|
|
1412
|
+
logger,
|
|
1413
|
+
tag,
|
|
1414
|
+
...publish,
|
|
1415
|
+
...options
|
|
1416
|
+
});
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
release(option) {
|
|
1420
|
+
const { hosting } = this;
|
|
1421
|
+
if (!hosting) throwError("Hosting service is not configured. Please set the hosting option.");
|
|
1422
|
+
return this.enqueue(async () => {
|
|
1423
|
+
const { project, options: { dryRun }, stepsOptions: { release } } = this;
|
|
1424
|
+
const logger = this.logger.createChild("release");
|
|
1425
|
+
if (this.shouldSkip("release")) {
|
|
1426
|
+
logger.info("Skipping release.");
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
logger.info("Creating release...");
|
|
1430
|
+
await hosting.createRelease({
|
|
1431
|
+
project,
|
|
1432
|
+
dryRun,
|
|
1433
|
+
logger,
|
|
1434
|
+
...release,
|
|
1435
|
+
...option
|
|
1436
|
+
});
|
|
1437
|
+
});
|
|
1438
|
+
}
|
|
1439
|
+
pullRequest(option) {
|
|
1440
|
+
const { hosting } = this;
|
|
1441
|
+
if (!hosting) throwError("Hosting service is not configured. Please set the hosting option.");
|
|
1442
|
+
return this.enqueue(async () => {
|
|
1443
|
+
const { project, state, options: { dryRun }, stepsOptions: { pullRequest } } = this;
|
|
1444
|
+
const logger = this.logger.createChild("pull-request");
|
|
1445
|
+
if (this.shouldSkip("pullRequest")) {
|
|
1446
|
+
logger.info("Skipping pull request.");
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
logger.info("Creating pull request...");
|
|
1450
|
+
if (!state.bump) {
|
|
1451
|
+
logger.info("No version changes detected, skipping pull request creation.");
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
if (!state.headBranch) {
|
|
1455
|
+
logger.info("Head branch is not set, skipping pull request creation.");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
await hosting.createPullRequest({
|
|
1459
|
+
project,
|
|
1460
|
+
dryRun,
|
|
1461
|
+
logger,
|
|
1462
|
+
from: state.headBranch,
|
|
1463
|
+
to: await this.getBaseBranch(),
|
|
1464
|
+
...pullRequest,
|
|
1465
|
+
...option
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
async run(condition) {
|
|
1470
|
+
let shouldRun = true;
|
|
1471
|
+
if (condition) {
|
|
1472
|
+
const logger = this.logger.createChild("run");
|
|
1473
|
+
logger.info("Checking condition...");
|
|
1474
|
+
shouldRun = await condition(this);
|
|
1475
|
+
if (shouldRun) logger.info("Condition passed, running release steps...");
|
|
1476
|
+
else logger.info("Condition failed, skipping release steps.");
|
|
1477
|
+
}
|
|
1478
|
+
if (shouldRun) for (const fn of this.queue) await fn();
|
|
1479
|
+
const logger = this.logger.createChild("done");
|
|
1480
|
+
this.queue.length = 0;
|
|
1481
|
+
logger.info(`Done!`);
|
|
1482
|
+
}
|
|
1483
|
+
shouldSkip(step) {
|
|
1484
|
+
return this.skippedSteps[step] === true;
|
|
1485
|
+
}
|
|
1486
|
+
async getBaseBranch() {
|
|
1487
|
+
const { state: { baseBranch } } = this;
|
|
1488
|
+
if (baseBranch) return baseBranch;
|
|
1489
|
+
return gitBranchCurrent();
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
//#endregion
|
|
1493
|
+
//#region src/hosting-github/github.ts
|
|
1494
|
+
var GithubHosting = class extends GitRepositoryHosting {
|
|
1495
|
+
options;
|
|
1496
|
+
octokit;
|
|
1497
|
+
browser;
|
|
1498
|
+
constructor(options = {}) {
|
|
1499
|
+
const { octokit, token, browser } = options;
|
|
1500
|
+
super();
|
|
1501
|
+
this.options = options;
|
|
1502
|
+
this.browser = browser ?? (!octokit && !token);
|
|
1503
|
+
this.octokit = octokit ?? new Octokit({ auth: token });
|
|
1504
|
+
}
|
|
1505
|
+
async createRelease(options) {
|
|
1506
|
+
const { project, dryRun, logger } = options;
|
|
1507
|
+
const { octokit, browser } = this;
|
|
1508
|
+
const releaseData = await project.getReleaseData();
|
|
1509
|
+
if (isEmpty(releaseData)) {
|
|
1510
|
+
logger?.info("No release data found. Skipping release creation.");
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1513
|
+
const [owner, repo] = await this.getRepositoryId(project);
|
|
1514
|
+
for (const data of releaseData) {
|
|
1515
|
+
logger?.verbose(`Creating GitHub release "${data.title}"...`);
|
|
1516
|
+
const browserRelease = async () => {
|
|
1517
|
+
const url = new URL(`${getGithubUrl(owner, repo)}/releases/new`);
|
|
1518
|
+
Object.entries({
|
|
1519
|
+
tag: data.nextTag,
|
|
1520
|
+
title: data.title,
|
|
1521
|
+
body: data.notes,
|
|
1522
|
+
prerelease: data.isPrerelease
|
|
1523
|
+
}).forEach(([key, value]) => {
|
|
1524
|
+
url.searchParams.set(key, String(value));
|
|
1525
|
+
});
|
|
1526
|
+
logger?.verbose(`Opening GitHub release page for tag "${data.nextTag}"...`);
|
|
1527
|
+
await open(url.toString());
|
|
1528
|
+
await sleep(1500);
|
|
1529
|
+
};
|
|
1530
|
+
const resetRelease = async () => {
|
|
1531
|
+
const res = await (async () => {
|
|
1532
|
+
const release = await this.getRelease({
|
|
1533
|
+
owner,
|
|
1534
|
+
repo,
|
|
1535
|
+
tag: data.nextTag
|
|
1536
|
+
});
|
|
1537
|
+
const opts = {
|
|
1538
|
+
owner,
|
|
1539
|
+
repo,
|
|
1540
|
+
tag_name: data.nextTag,
|
|
1541
|
+
name: data.title,
|
|
1542
|
+
body: data.notes,
|
|
1543
|
+
prerelease: data.isPrerelease,
|
|
1544
|
+
make_latest: data.isLatest ? "true" : "false"
|
|
1545
|
+
};
|
|
1546
|
+
if (release) {
|
|
1547
|
+
const { data } = await octokit.rest.repos.updateRelease({
|
|
1548
|
+
...opts,
|
|
1549
|
+
release_id: release.id
|
|
1550
|
+
});
|
|
1551
|
+
return data;
|
|
1552
|
+
} else {
|
|
1553
|
+
const { data } = await octokit.rest.repos.createRelease({
|
|
1554
|
+
...opts,
|
|
1555
|
+
draft: false
|
|
1556
|
+
});
|
|
1557
|
+
return data;
|
|
1558
|
+
}
|
|
1559
|
+
})();
|
|
1560
|
+
if (!res) return;
|
|
1561
|
+
await this.uploadAssets({
|
|
1562
|
+
owner,
|
|
1563
|
+
repo,
|
|
1564
|
+
release_id: res.id,
|
|
1565
|
+
assets: []
|
|
1566
|
+
});
|
|
1567
|
+
};
|
|
1568
|
+
if (!dryRun) browser ? await browserRelease() : await resetRelease();
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
async createPullRequest(options) {
|
|
1572
|
+
const { project, dryRun, draft, logger } = options;
|
|
1573
|
+
let { from, to } = options;
|
|
1574
|
+
const { octokit } = this;
|
|
1575
|
+
const [owner, repo] = await this.getRepositoryId(project);
|
|
1576
|
+
from ||= await gitBranchCurrent();
|
|
1577
|
+
to ||= await this.getDefaultBranch(owner, repo);
|
|
1578
|
+
if (from === to) throwError("Cannot create a pull request from the same branch.");
|
|
1579
|
+
const [pr] = await this.listPullRequest({
|
|
1580
|
+
owner,
|
|
1581
|
+
repo,
|
|
1582
|
+
from
|
|
1583
|
+
});
|
|
1584
|
+
if (pr) logger?.info(`Updating existing pull request #${pr.number}`);
|
|
1585
|
+
logger?.verbose("Pull request data:");
|
|
1586
|
+
if (!pr && draft) logger?.verbose("Draft: true");
|
|
1587
|
+
const { title, body } = await this.getPullRequestData(project, from, to);
|
|
1588
|
+
logger?.verbose(`${from} -> ${to}`);
|
|
1589
|
+
logger?.verbose(`Title: ${title}`);
|
|
1590
|
+
logger?.verbose(`Body:\n\n${body}`);
|
|
1591
|
+
const opts = {
|
|
1592
|
+
owner,
|
|
1593
|
+
repo,
|
|
1594
|
+
title,
|
|
1595
|
+
body
|
|
1596
|
+
};
|
|
1597
|
+
if (!dryRun) {
|
|
1598
|
+
if (pr) await octokit.rest.pulls.update({
|
|
1599
|
+
...opts,
|
|
1600
|
+
pull_number: pr.number
|
|
1601
|
+
});
|
|
1602
|
+
else await octokit.rest.pulls.create({
|
|
1603
|
+
...opts,
|
|
1604
|
+
base: to,
|
|
1605
|
+
head: from,
|
|
1606
|
+
draft
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
async releaseCheck(options = {}) {
|
|
1611
|
+
const { logger } = options;
|
|
1612
|
+
logger?.verbose("Checking GitHub repository configuration...");
|
|
1613
|
+
const remoteUrl = await gitConfigGet("remote.origin.url");
|
|
1614
|
+
const useGithub = isNotEmpty(parseGitHubRepo(remoteUrl));
|
|
1615
|
+
assert(useGithub, "Git remote \"origin\" is not a GitHub repository");
|
|
1616
|
+
}
|
|
1617
|
+
async getRepositoryId(project) {
|
|
1618
|
+
let { options: { owner, repo } } = this;
|
|
1619
|
+
const i = project.getHostedGitInfo();
|
|
1620
|
+
return [owner ?? (await i).owner, repo ?? (await i).repo];
|
|
1621
|
+
}
|
|
1622
|
+
async uploadAssets(options) {
|
|
1623
|
+
const { octokit } = this;
|
|
1624
|
+
const { owner, repo, release_id, assets } = options;
|
|
1625
|
+
const files = await glob(assets);
|
|
1626
|
+
await Promise.all(files.map((file) => {
|
|
1627
|
+
const { base: filename } = parse(file);
|
|
1628
|
+
return octokit.rest.repos.uploadReleaseAsset({
|
|
1629
|
+
owner,
|
|
1630
|
+
repo,
|
|
1631
|
+
release_id,
|
|
1632
|
+
name: filename,
|
|
1633
|
+
data: readFileSync(file, "utf-8")
|
|
1634
|
+
});
|
|
1635
|
+
}));
|
|
1636
|
+
}
|
|
1637
|
+
async getRelease(options) {
|
|
1638
|
+
const { octokit } = this;
|
|
1639
|
+
const { data } = await tryCall(() => octokit.rest.repos.getReleaseByTag({ ...options }), { data: void 0 });
|
|
1640
|
+
return data;
|
|
1641
|
+
}
|
|
1642
|
+
async listPullRequest(options) {
|
|
1643
|
+
const { octokit } = this;
|
|
1644
|
+
const { owner, repo, from } = options;
|
|
1645
|
+
const { data } = await tryCall(() => octokit.rest.pulls.list({
|
|
1646
|
+
owner,
|
|
1647
|
+
repo,
|
|
1648
|
+
head: `${owner}:${from}`,
|
|
1649
|
+
state: "open"
|
|
1650
|
+
}), { data: [] });
|
|
1651
|
+
return data;
|
|
1652
|
+
}
|
|
1653
|
+
async getDefaultBranch(owner, repo) {
|
|
1654
|
+
const { octokit } = this;
|
|
1655
|
+
const { data: { default_branch } } = await tryCall(() => octokit.rest.repos.get({
|
|
1656
|
+
owner,
|
|
1657
|
+
repo
|
|
1658
|
+
}), { data: { default_branch: "master" } });
|
|
1659
|
+
return default_branch;
|
|
1660
|
+
}
|
|
1661
|
+
async getPullRequestData(project, from, to) {
|
|
1662
|
+
return {
|
|
1663
|
+
title: (await project.getCommitMessage()).split("\n")[0],
|
|
1664
|
+
body: [project.versionUpdates.map(({ name, notes }) => `# ${name}${newline(2)}${notes.trim()}`).join(newline(2)), dedent(`
|
|
1665
|
+
<!--
|
|
1666
|
+
Release pull request metadata.
|
|
1667
|
+
release-pull-request: true
|
|
1668
|
+
release-branch-from: ${from}
|
|
1669
|
+
release-branch-to: ${to}
|
|
1670
|
+
-->
|
|
1671
|
+
`)].join(newline(2))
|
|
1672
|
+
};
|
|
1673
|
+
}
|
|
1674
|
+
};
|
|
1675
|
+
//#endregion
|
|
1676
|
+
//#region src/project-github-action/publish.ts
|
|
1677
|
+
const runBuildCommands = async (commands, options, cwd) => {
|
|
1678
|
+
const { dryRun, env = process.env, logger } = options;
|
|
1679
|
+
for (const command of commands) {
|
|
1680
|
+
logger?.info(`Running build command: ${command}`);
|
|
1681
|
+
if (!dryRun) await execAsync(command, {
|
|
1682
|
+
cwd,
|
|
1683
|
+
env,
|
|
1684
|
+
error: "log"
|
|
1685
|
+
});
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
const publish = async (project, options = {}) => {
|
|
1689
|
+
const { manifest, manifest: { projectDir } } = project;
|
|
1690
|
+
const { dryRun, logger, tag } = options;
|
|
1691
|
+
const publishTag = isFunction(tag) ? tag(await manifest.getVersion(), await manifest.getPrereleaseVersion()) : tag ?? "latest";
|
|
1692
|
+
const pkg = await manifest.readManifest();
|
|
1693
|
+
const version = await manifest.getVersion();
|
|
1694
|
+
const validVersion = valid(version);
|
|
1695
|
+
if (!validVersion) throwError(`Invalid GitHub Action version: ${version}`);
|
|
1696
|
+
const { build = [], files = pkg.files = [] } = options;
|
|
1697
|
+
const tagPrefix = await project.getTagPrefix();
|
|
1698
|
+
const withTag = (version, tag) => {
|
|
1699
|
+
return equals(tag, "latest") ? version : `${version}-${tag}`;
|
|
1700
|
+
};
|
|
1701
|
+
const majorBranch = withTag(`${tagPrefix}${major(validVersion)}`, publishTag);
|
|
1702
|
+
const versionTag = project.toTag(tagPrefix, validVersion);
|
|
1703
|
+
const buildBranch = publishTag;
|
|
1704
|
+
const buildCommands = Array.isArray(build) ? build : [build];
|
|
1705
|
+
const originalBranch = await gitBranchCurrent();
|
|
1706
|
+
const releaseCommit = await gitRevParse(["--verify", "HEAD"]);
|
|
1707
|
+
logger?.info(`Publishing GitHub Action ${versionTag}...`);
|
|
1708
|
+
logger?.verbose(`Build branch: ${buildBranch}`);
|
|
1709
|
+
logger?.verbose(`Major branch: ${majorBranch}`);
|
|
1710
|
+
let shouldRestoreBranch = false;
|
|
1711
|
+
try {
|
|
1712
|
+
if (!dryRun) {
|
|
1713
|
+
if (await gitRevParse(["--is-shallow-repository"]) === "true") {
|
|
1714
|
+
logger?.info("Fetching git history...");
|
|
1715
|
+
await gitFetch([
|
|
1716
|
+
"--deepen=1",
|
|
1717
|
+
"--",
|
|
1718
|
+
"origin",
|
|
1719
|
+
originalBranch
|
|
1720
|
+
]);
|
|
1721
|
+
}
|
|
1722
|
+
await gitCheckoutBranchForce(buildBranch, releaseCommit);
|
|
1723
|
+
shouldRestoreBranch = originalBranch !== buildBranch;
|
|
1724
|
+
}
|
|
1725
|
+
await runBuildCommands(buildCommands, options, projectDir);
|
|
1726
|
+
if (!dryRun && files.length) {
|
|
1727
|
+
logger?.info("Staging GitHub Action files...");
|
|
1728
|
+
await gitAdd([
|
|
1729
|
+
"--force",
|
|
1730
|
+
"--",
|
|
1731
|
+
...files
|
|
1732
|
+
]);
|
|
1733
|
+
if (await gitDiff(["--cached", "--name-only"])) await gitCommitAmendNoEdit(["--no-verify"]);
|
|
1734
|
+
}
|
|
1735
|
+
if (!dryRun) {
|
|
1736
|
+
logger?.info("Creating GitHub Action release refs...");
|
|
1737
|
+
await gitBranch([
|
|
1738
|
+
"-f",
|
|
1739
|
+
"--",
|
|
1740
|
+
majorBranch,
|
|
1741
|
+
"HEAD"
|
|
1742
|
+
]);
|
|
1743
|
+
await gitTag([
|
|
1744
|
+
"-f",
|
|
1745
|
+
"--",
|
|
1746
|
+
versionTag,
|
|
1747
|
+
"HEAD"
|
|
1748
|
+
]);
|
|
1749
|
+
logger?.info("Pushing GitHub Action release refs...");
|
|
1750
|
+
await gitPush([
|
|
1751
|
+
"--force",
|
|
1752
|
+
"--no-verify",
|
|
1753
|
+
"origin",
|
|
1754
|
+
"--",
|
|
1755
|
+
buildBranch
|
|
1756
|
+
]);
|
|
1757
|
+
await gitPush([
|
|
1758
|
+
"--force",
|
|
1759
|
+
"--no-verify",
|
|
1760
|
+
"origin",
|
|
1761
|
+
"--",
|
|
1762
|
+
majorBranch
|
|
1763
|
+
]);
|
|
1764
|
+
await gitPush([
|
|
1765
|
+
"--force",
|
|
1766
|
+
"--no-verify",
|
|
1767
|
+
"origin",
|
|
1768
|
+
"--",
|
|
1769
|
+
versionTag
|
|
1770
|
+
]);
|
|
1771
|
+
}
|
|
1772
|
+
} finally {
|
|
1773
|
+
if (shouldRestoreBranch) await gitCheckout([originalBranch]);
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
//#endregion
|
|
1777
|
+
//#region src/project-github-action/project.ts
|
|
1778
|
+
/** Node.js GitHub Actions 项目,将版本引用发布到 Git 分支和标签 */
|
|
1779
|
+
var NodeGithubActionProject = class extends PackageJsonProject {
|
|
1780
|
+
/**
|
|
1781
|
+
* 获取项目最近一次发布的标签。
|
|
1782
|
+
* GitHub Action 发布标签指向发布分支中的打包提交,无法从源分支到达。因此需要遍历所有引用,并通过标签的父提交判断它是否属于当前分支。
|
|
1783
|
+
*/
|
|
1784
|
+
async getLastReleaseTag(options = {}) {
|
|
1785
|
+
const { tagPrefix: override } = options;
|
|
1786
|
+
const tags = getSemverTags({
|
|
1787
|
+
prefix: await this.getTagPrefix(override),
|
|
1788
|
+
all: true
|
|
1789
|
+
});
|
|
1790
|
+
for await (const tag of tags) if (await this.isReachableFromHead(tag) || await this.isReachableFromHead(`${tag}^`)) return tag;
|
|
1791
|
+
}
|
|
1792
|
+
async publish(options = {}) {
|
|
1793
|
+
const tagsMap = await this.getDefaultPublishTag();
|
|
1794
|
+
const name = await this.manifest.getName();
|
|
1795
|
+
const tag = tagsMap.get(name);
|
|
1796
|
+
await publish(this, {
|
|
1797
|
+
tag,
|
|
1798
|
+
...options
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
async publishCheck(options) {
|
|
1802
|
+
const { logger } = options;
|
|
1803
|
+
logger?.verbose("Checking GitHub repository configuration...");
|
|
1804
|
+
const remoteUrl = await gitConfigGet("remote.origin.url");
|
|
1805
|
+
const useGithub = isNotEmpty(parseGitHubRepo(remoteUrl));
|
|
1806
|
+
assert(useGithub, "Git remote \"origin\" is not a GitHub repository");
|
|
1807
|
+
}
|
|
1808
|
+
constructor(options = {}) {
|
|
1809
|
+
super(options);
|
|
1810
|
+
}
|
|
1811
|
+
async isReachableFromHead(ref) {
|
|
1812
|
+
return tryCall(() => runGit([
|
|
1813
|
+
"merge-base",
|
|
1814
|
+
"--is-ancestor",
|
|
1815
|
+
ref,
|
|
1816
|
+
"HEAD"
|
|
1817
|
+
]), false);
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
//#endregion
|
|
1821
|
+
//#region src/project-npm/check.ts
|
|
1822
|
+
const check$1 = async (project, options = {}) => {
|
|
1823
|
+
const { logger } = options;
|
|
1824
|
+
logger?.verbose("Checking npm registry...");
|
|
1825
|
+
const pinged = await pingRegistry();
|
|
1826
|
+
assert(pinged, "Unable to connect to the npm registry");
|
|
1827
|
+
logger?.verbose("Checking npm authentication...");
|
|
1828
|
+
const username = await getAuthenticatedUser();
|
|
1829
|
+
assert(username, "Not authenticated with npm");
|
|
1830
|
+
const name = await project.manifest.getName();
|
|
1831
|
+
if (await getPublishedVersion(name)) {
|
|
1832
|
+
logger?.verbose(`Checking npm write access for "${name}"...`);
|
|
1833
|
+
const hasAccess = await hasWriteAccess(name, username);
|
|
1834
|
+
assert(hasAccess, `No write access to npm package "${name}"`);
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
//#endregion
|
|
1838
|
+
//#region src/project-npm/cleanup.ts
|
|
1839
|
+
const PUBLISH_CONFIG_FIELDS$1 = [
|
|
1840
|
+
"bin",
|
|
1841
|
+
"main",
|
|
1842
|
+
"exports",
|
|
1843
|
+
"types",
|
|
1844
|
+
"typings",
|
|
1845
|
+
"module",
|
|
1846
|
+
"browser",
|
|
1847
|
+
"esnext",
|
|
1848
|
+
"es2015",
|
|
1849
|
+
"unpkg",
|
|
1850
|
+
"umd:main",
|
|
1851
|
+
"typesVersions",
|
|
1852
|
+
"cpu",
|
|
1853
|
+
"os",
|
|
1854
|
+
"engines"
|
|
1855
|
+
];
|
|
1856
|
+
const IGNORE_FIELDS$1 = [
|
|
1857
|
+
"babel",
|
|
1858
|
+
"browserslist",
|
|
1859
|
+
"c8",
|
|
1860
|
+
"commitlint",
|
|
1861
|
+
"devDependencies",
|
|
1862
|
+
"eslintConfig",
|
|
1863
|
+
"eslintIgnore",
|
|
1864
|
+
"husky",
|
|
1865
|
+
"jest",
|
|
1866
|
+
"lint-staged",
|
|
1867
|
+
"nano-staged",
|
|
1868
|
+
"pre-commit",
|
|
1869
|
+
"prettier",
|
|
1870
|
+
"pwmetrics",
|
|
1871
|
+
"remarkConfig",
|
|
1872
|
+
"renovate",
|
|
1873
|
+
"resolutions",
|
|
1874
|
+
"sharec",
|
|
1875
|
+
"simple-git-hooks",
|
|
1876
|
+
"simple-pre-commit",
|
|
1877
|
+
"size-limit",
|
|
1878
|
+
"typeCoverage",
|
|
1879
|
+
"yaspeller",
|
|
1880
|
+
"pnpm",
|
|
1881
|
+
"packageManager"
|
|
1882
|
+
];
|
|
1883
|
+
const KEEP_SCRIPT_NAMES$1 = [
|
|
1884
|
+
"preinstall",
|
|
1885
|
+
"install",
|
|
1886
|
+
"postinstall"
|
|
1887
|
+
];
|
|
1888
|
+
const IGNORE_FILES$1 = [
|
|
1889
|
+
".DS_Store",
|
|
1890
|
+
".git",
|
|
1891
|
+
".vscode",
|
|
1892
|
+
".idea",
|
|
1893
|
+
"node_modules"
|
|
1894
|
+
];
|
|
1895
|
+
const cleanPackageJSON$1 = async (dir, options) => {
|
|
1896
|
+
const file = join(dir, "package.json");
|
|
1897
|
+
const json = await readJsonFile(file);
|
|
1898
|
+
const { ignoreFields = [], keepScripts = [] } = options ?? {};
|
|
1899
|
+
const mergePublishConfig = (json) => {
|
|
1900
|
+
if (!json.publishConfig) return json;
|
|
1901
|
+
const publishConfig = { ...json.publishConfig };
|
|
1902
|
+
PUBLISH_CONFIG_FIELDS$1.forEach((field) => {
|
|
1903
|
+
if (publishConfig[field]) {
|
|
1904
|
+
json[field] = publishConfig[field];
|
|
1905
|
+
delete publishConfig[field];
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
return {
|
|
1909
|
+
...json,
|
|
1910
|
+
publishConfig
|
|
1911
|
+
};
|
|
1912
|
+
};
|
|
1913
|
+
const removeIgnoredFields = (json) => {
|
|
1914
|
+
const fields = [...ignoreFields, ...IGNORE_FIELDS$1];
|
|
1915
|
+
const [matched, unmatched] = partition(fields, (i) => i.includes("."));
|
|
1916
|
+
matched.forEach((field) => unset(json, field));
|
|
1917
|
+
return omit(json, unmatched);
|
|
1918
|
+
};
|
|
1919
|
+
const cleanScripts = (json) => {
|
|
1920
|
+
if (!json.scripts) return json;
|
|
1921
|
+
const keep = [...keepScripts, ...KEEP_SCRIPT_NAMES$1];
|
|
1922
|
+
const excludes = Object.keys(json.scripts).filter((k) => !keep.includes(k));
|
|
1923
|
+
json.scripts = omit(json.scripts, excludes);
|
|
1924
|
+
return json;
|
|
1925
|
+
};
|
|
1926
|
+
const removeEmptyObjects = (json) => {
|
|
1927
|
+
const excludes = Object.entries(json).filter(([k, v]) => isEmptyObject(v)).map(([k, v]) => k);
|
|
1928
|
+
return omit(json, excludes);
|
|
1929
|
+
};
|
|
1930
|
+
const cleaned = removeEmptyObjects(cleanScripts(removeIgnoredFields(structuredClone(mergePublishConfig(json)))));
|
|
1931
|
+
await writeJsonFile(file, cleaned);
|
|
1932
|
+
};
|
|
1933
|
+
const cleanReadme$1 = async (dir, homepage) => {
|
|
1934
|
+
const file = join(dir, "README.md");
|
|
1935
|
+
homepage && await editFile(file, (str) => {
|
|
1936
|
+
return str.split(/^##\s+\S.*$/m)[0] + `## Docs
|
|
1937
|
+
|
|
1938
|
+
Read full docs **[here](${homepage})**.`;
|
|
179
1939
|
});
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
};
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
//#region src/npm/publish.ts
|
|
220
|
-
const publishTagToNpm = async (options) => {
|
|
221
|
-
const { gitTag, defaultPackage, tagSeparator = "@", getPkgDir = () => ".", provenance } = options;
|
|
222
|
-
const tag = process.argv.slice(2)[0] || gitTag;
|
|
223
|
-
if (!tag) throw new Error(`❌ No git tag specified.`);
|
|
224
|
-
console.log(`\n📦 Start publishing from git tag: ${cyan(tag)}`);
|
|
225
|
-
let pkgName = defaultPackage;
|
|
226
|
-
let version;
|
|
227
|
-
if (tag.includes(tagSeparator)) {
|
|
228
|
-
[pkgName, version] = tag.split(tagSeparator);
|
|
229
|
-
console.log(`🔍 Parsed tag → package: "${yellow(pkgName)}", version: "${green(version)}"`);
|
|
1940
|
+
};
|
|
1941
|
+
//#endregion
|
|
1942
|
+
//#region src/project-npm/publish.ts
|
|
1943
|
+
const publish$2 = async (project, options = {}) => {
|
|
1944
|
+
const { manifest, manifest: { projectDir } } = project;
|
|
1945
|
+
const { access, tag, otp, env = process.env, dryRun, logger, cleanPublish } = options;
|
|
1946
|
+
const publishTag = isFunction(tag) ? tag(await manifest.getVersion(), await manifest.getPrereleaseVersion()) : tag;
|
|
1947
|
+
const args = [
|
|
1948
|
+
"publish",
|
|
1949
|
+
...when(access, "--access", access),
|
|
1950
|
+
...when(publishTag, "--tag", publishTag),
|
|
1951
|
+
...when(dryRun, "--dry-run"),
|
|
1952
|
+
...when(otp, "--otp", otp)
|
|
1953
|
+
];
|
|
1954
|
+
if (cleanPublish) {
|
|
1955
|
+
const { ignoreFields, keepScripts, cleanReadme: shouldCleanReadme } = options;
|
|
1956
|
+
const parent = resolve(projectDir);
|
|
1957
|
+
const temp = await createTempDir(parent);
|
|
1958
|
+
await copyDir(parent, temp.path, { ignore: [(name) => {
|
|
1959
|
+
return [temp.name, ...IGNORE_FILES$1].map((pattern) => createMatcher(pattern)).some((matcher) => matcher(name));
|
|
1960
|
+
}] });
|
|
1961
|
+
await cleanPackageJSON$1(temp.path, {
|
|
1962
|
+
ignoreFields,
|
|
1963
|
+
keepScripts
|
|
1964
|
+
});
|
|
1965
|
+
if (shouldCleanReadme) {
|
|
1966
|
+
const info = await project.getHostedGitInfo();
|
|
1967
|
+
if (!Object.values(info).every(isEmpty)) {
|
|
1968
|
+
const { host, prefix, owner, repo } = info;
|
|
1969
|
+
await cleanReadme$1(temp.path, `${host}/${prefix}${owner}/${repo}#readme`);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
logger?.verbose(`Publishing "${await manifest.getName()}" with command: npm ${args.join(" ")}`);
|
|
1973
|
+
await execAsync("npm", args, {
|
|
1974
|
+
cwd: temp.path,
|
|
1975
|
+
env,
|
|
1976
|
+
error: "log"
|
|
1977
|
+
});
|
|
1978
|
+
await temp.remove();
|
|
230
1979
|
} else {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
1980
|
+
logger?.verbose(`Publishing "${await manifest.getName()}" with command: npm ${args.join(" ")}`);
|
|
1981
|
+
await execAsync("npm", args, {
|
|
1982
|
+
cwd: projectDir,
|
|
1983
|
+
env,
|
|
1984
|
+
error: "log"
|
|
1985
|
+
});
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
//#endregion
|
|
1989
|
+
//#region src/project-npm/project.ts
|
|
1990
|
+
/** 一个基于 npm 的项目,使用 package.json 进行配置 */
|
|
1991
|
+
var NpmProject = class extends PackageJsonProject {
|
|
1992
|
+
async publish(options = {}) {
|
|
1993
|
+
await publish$2(this, { ...options });
|
|
1994
|
+
}
|
|
1995
|
+
async publishCheck(options = {}) {
|
|
1996
|
+
await check$1(this, options);
|
|
1997
|
+
}
|
|
1998
|
+
};
|
|
1999
|
+
//#endregion
|
|
2000
|
+
//#region src/project-npm/workspacesProject.ts
|
|
2001
|
+
const getPackagesGlobPatterns$1 = async (manifest) => {
|
|
2002
|
+
const workspaces = (await manifest).workspaces;
|
|
2003
|
+
return Array.isArray(workspaces) ? workspaces : workspaces?.packages ?? [];
|
|
2004
|
+
};
|
|
2005
|
+
async function* getProjects$1(options) {
|
|
2006
|
+
const { manifest, manifest: { projectDir } } = options;
|
|
2007
|
+
const packages = await getPackagesGlobPatterns$1(manifest.readManifest());
|
|
2008
|
+
for (const pkg of packages) {
|
|
2009
|
+
const matched = globSync(normalizePath(join(pkg, PackageJsonManifest.Filename)), {
|
|
2010
|
+
cwd: projectDir,
|
|
2011
|
+
onlyFiles: true,
|
|
2012
|
+
absolute: true,
|
|
2013
|
+
ignore: NpmWorkspacesProject.GlobIgnore
|
|
2014
|
+
});
|
|
2015
|
+
for (const filepath of matched) yield filepath;
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
/** 一个基于 npm workspaces 的 monorepo 项目,使用 package.json 的 workspaces 进行配置 */
|
|
2019
|
+
var NpmWorkspacesProject = class extends PackageJsonMonorepoProject {
|
|
2020
|
+
static GlobIgnore = ["**/node_modules/**"];
|
|
2021
|
+
constructor(options) {
|
|
2022
|
+
super({
|
|
2023
|
+
...options,
|
|
2024
|
+
getProjects: getProjects$1
|
|
2025
|
+
});
|
|
2026
|
+
}
|
|
2027
|
+
async publish(options = {}) {
|
|
2028
|
+
const onceCall = onceAsync(this.getDefaultPublishTag.bind(this));
|
|
2029
|
+
for await (const project of this.getProjects()) if (project.hasBump) {
|
|
2030
|
+
const opts = this.isFixedMode ? {} : await (async () => {
|
|
2031
|
+
const tagsMap = await onceCall();
|
|
2032
|
+
const name = await project.manifest.getName();
|
|
2033
|
+
return { tag: tagsMap.get(name) };
|
|
2034
|
+
})();
|
|
2035
|
+
await publish$2(project, {
|
|
2036
|
+
...opts,
|
|
2037
|
+
...options
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
async publishCheck(options = {}) {
|
|
2042
|
+
await check$1(this, options);
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
//#endregion
|
|
2046
|
+
//#region src/project-pnpm/check.ts
|
|
2047
|
+
const check = async (project, options = {}) => {
|
|
2048
|
+
const { logger } = options;
|
|
2049
|
+
logger?.verbose("Checking npm registry...");
|
|
2050
|
+
const pinged = await pingRegistry();
|
|
2051
|
+
assert(pinged, "Unable to connect to the npm registry");
|
|
2052
|
+
logger?.verbose("Checking npm authentication...");
|
|
2053
|
+
const username = await getAuthenticatedUser();
|
|
2054
|
+
assert(username, "Not authenticated with npm");
|
|
2055
|
+
const name = await project.manifest.getName();
|
|
2056
|
+
if (await getPublishedVersion(name)) {
|
|
2057
|
+
logger?.verbose(`Checking npm write access for "${name}"...`);
|
|
2058
|
+
const hasAccess = await hasWriteAccess(name, username);
|
|
2059
|
+
assert(hasAccess, `No write access to npm package "${name}"`);
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
//#endregion
|
|
2063
|
+
//#region src/project-pnpm/cleanup.ts
|
|
2064
|
+
const PUBLISH_CONFIG_FIELDS = [
|
|
2065
|
+
"bin",
|
|
2066
|
+
"main",
|
|
2067
|
+
"exports",
|
|
2068
|
+
"types",
|
|
2069
|
+
"typings",
|
|
2070
|
+
"module",
|
|
2071
|
+
"browser",
|
|
2072
|
+
"esnext",
|
|
2073
|
+
"es2015",
|
|
2074
|
+
"unpkg",
|
|
2075
|
+
"umd:main",
|
|
2076
|
+
"typesVersions",
|
|
2077
|
+
"cpu",
|
|
2078
|
+
"os",
|
|
2079
|
+
"engines"
|
|
2080
|
+
];
|
|
2081
|
+
const IGNORE_FIELDS = [
|
|
2082
|
+
"babel",
|
|
2083
|
+
"browserslist",
|
|
2084
|
+
"c8",
|
|
2085
|
+
"commitlint",
|
|
2086
|
+
"devDependencies",
|
|
2087
|
+
"eslintConfig",
|
|
2088
|
+
"eslintIgnore",
|
|
2089
|
+
"husky",
|
|
2090
|
+
"jest",
|
|
2091
|
+
"lint-staged",
|
|
2092
|
+
"nano-staged",
|
|
2093
|
+
"pre-commit",
|
|
2094
|
+
"prettier",
|
|
2095
|
+
"pwmetrics",
|
|
2096
|
+
"remarkConfig",
|
|
2097
|
+
"renovate",
|
|
2098
|
+
"resolutions",
|
|
2099
|
+
"sharec",
|
|
2100
|
+
"simple-git-hooks",
|
|
2101
|
+
"simple-pre-commit",
|
|
2102
|
+
"size-limit",
|
|
2103
|
+
"typeCoverage",
|
|
2104
|
+
"yaspeller",
|
|
2105
|
+
"pnpm",
|
|
2106
|
+
"packageManager"
|
|
2107
|
+
];
|
|
2108
|
+
const KEEP_SCRIPT_NAMES = [
|
|
2109
|
+
"preinstall",
|
|
2110
|
+
"install",
|
|
2111
|
+
"postinstall"
|
|
2112
|
+
];
|
|
2113
|
+
const IGNORE_FILES = [
|
|
2114
|
+
".DS_Store",
|
|
2115
|
+
".git",
|
|
2116
|
+
".vscode",
|
|
2117
|
+
".idea",
|
|
2118
|
+
"node_modules"
|
|
2119
|
+
];
|
|
2120
|
+
const cleanPackageJSON = async (dir, options) => {
|
|
2121
|
+
const file = join(dir, "package.json");
|
|
2122
|
+
const json = await readJsonFile(file);
|
|
2123
|
+
const { ignoreFields = [], keepScripts = [] } = options ?? {};
|
|
2124
|
+
const mergePublishConfig = (json) => {
|
|
2125
|
+
if (!json.publishConfig) return json;
|
|
2126
|
+
const publishConfig = { ...json.publishConfig };
|
|
2127
|
+
PUBLISH_CONFIG_FIELDS.forEach((field) => {
|
|
2128
|
+
if (publishConfig[field]) {
|
|
2129
|
+
json[field] = publishConfig[field];
|
|
2130
|
+
delete publishConfig[field];
|
|
2131
|
+
}
|
|
2132
|
+
});
|
|
2133
|
+
return {
|
|
2134
|
+
...json,
|
|
2135
|
+
publishConfig
|
|
2136
|
+
};
|
|
2137
|
+
};
|
|
2138
|
+
const removeIgnoredFields = (json) => {
|
|
2139
|
+
const fields = [...ignoreFields, ...IGNORE_FIELDS];
|
|
2140
|
+
const [matched, unmatched] = partition(fields, (i) => i.includes("."));
|
|
2141
|
+
matched.forEach((field) => unset(json, field));
|
|
2142
|
+
return omit(json, unmatched);
|
|
2143
|
+
};
|
|
2144
|
+
const cleanScripts = (json) => {
|
|
2145
|
+
if (!json.scripts) return json;
|
|
2146
|
+
const keep = [...keepScripts, ...KEEP_SCRIPT_NAMES];
|
|
2147
|
+
const excludes = Object.keys(json.scripts).filter((k) => !keep.includes(k));
|
|
2148
|
+
json.scripts = omit(json.scripts, excludes);
|
|
2149
|
+
return json;
|
|
2150
|
+
};
|
|
2151
|
+
const removeEmptyObjects = (json) => {
|
|
2152
|
+
const excludes = Object.entries(json).filter(([k, v]) => isEmptyObject(v)).map(([k, v]) => k);
|
|
2153
|
+
return omit(json, excludes);
|
|
2154
|
+
};
|
|
2155
|
+
const cleaned = removeEmptyObjects(cleanScripts(removeIgnoredFields(structuredClone(mergePublishConfig(json)))));
|
|
2156
|
+
await writeJsonFile(file, cleaned);
|
|
2157
|
+
};
|
|
2158
|
+
const cleanReadme = async (dir, homepage) => {
|
|
2159
|
+
const file = join(dir, "README.md");
|
|
2160
|
+
homepage && await editFile(file, (str) => {
|
|
2161
|
+
return str.split(/^##\s+\S.*$/m)[0] + `## Docs
|
|
2162
|
+
|
|
2163
|
+
Read full docs **[here](${homepage})**.`;
|
|
251
2164
|
});
|
|
252
|
-
console.log(`🎉 Successfully published ${red`${pkg.name}@${version}`} with dist-tag "${blue(publishTag)}"\n`);
|
|
253
2165
|
};
|
|
254
2166
|
//#endregion
|
|
255
|
-
|
|
2167
|
+
//#region src/project-pnpm/publish.ts
|
|
2168
|
+
const publish$1 = async (project, options = {}) => {
|
|
2169
|
+
const { manifest, manifest: { projectDir } } = project;
|
|
2170
|
+
const { access, tag, otp, env = process.env, dryRun, logger, gitChecks, cleanPublish } = options;
|
|
2171
|
+
const publishTag = isFunction(tag) ? tag(await manifest.getVersion(), await manifest.getPrereleaseVersion()) : tag;
|
|
2172
|
+
const args = [
|
|
2173
|
+
"publish",
|
|
2174
|
+
...when(access, "--access", access),
|
|
2175
|
+
...when(publishTag, "--tag", publishTag),
|
|
2176
|
+
...when(dryRun, "--dry-run"),
|
|
2177
|
+
...when(otp, "--otp", otp),
|
|
2178
|
+
...unless(gitChecks, "--no-git-checks")
|
|
2179
|
+
];
|
|
2180
|
+
if (cleanPublish) {
|
|
2181
|
+
const { ignoreFields, keepScripts, cleanReadme: shouldCleanReadme } = options;
|
|
2182
|
+
const parent = resolve(projectDir);
|
|
2183
|
+
const temp = await createTempDir(parent);
|
|
2184
|
+
await copyDir(parent, temp.path, { ignore: [(name) => {
|
|
2185
|
+
return [temp.name, ...IGNORE_FILES].map((pattern) => createMatcher(pattern)).some((matcher) => matcher(name));
|
|
2186
|
+
}] });
|
|
2187
|
+
await cleanPackageJSON(temp.path, {
|
|
2188
|
+
ignoreFields,
|
|
2189
|
+
keepScripts
|
|
2190
|
+
});
|
|
2191
|
+
if (shouldCleanReadme) {
|
|
2192
|
+
const info = await project.getHostedGitInfo();
|
|
2193
|
+
if (!Object.values(info).every(isEmpty)) {
|
|
2194
|
+
const { host, prefix, owner, repo } = info;
|
|
2195
|
+
await cleanReadme(temp.path, `${host}/${prefix}${owner}/${repo}#readme`);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
logger?.verbose(`Publishing "${await manifest.getName()}" with command: pnpm ${args.join(" ")}`);
|
|
2199
|
+
await execAsync("pnpm", args, {
|
|
2200
|
+
cwd: temp.path,
|
|
2201
|
+
env,
|
|
2202
|
+
error: "log"
|
|
2203
|
+
});
|
|
2204
|
+
await temp.remove();
|
|
2205
|
+
} else {
|
|
2206
|
+
logger?.verbose(`Publishing "${await manifest.getName()}" with command: pnpm ${args.join(" ")}`);
|
|
2207
|
+
await execAsync("pnpm", args, {
|
|
2208
|
+
cwd: projectDir,
|
|
2209
|
+
env,
|
|
2210
|
+
error: "log"
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
};
|
|
2214
|
+
//#endregion
|
|
2215
|
+
//#region src/project-pnpm/project.ts
|
|
2216
|
+
/** 一个基于 pnpm 的项目,使用 package.json 进行配置 */
|
|
2217
|
+
var PnpmProject = class extends PackageJsonProject {
|
|
2218
|
+
async publish(options = {}) {
|
|
2219
|
+
await publish$1(this, { ...options });
|
|
2220
|
+
}
|
|
2221
|
+
async publishCheck(options = {}) {
|
|
2222
|
+
await check(this, options);
|
|
2223
|
+
}
|
|
2224
|
+
};
|
|
2225
|
+
//#endregion
|
|
2226
|
+
//#region src/project-pnpm/workspacesProject.ts
|
|
2227
|
+
const getPackagesGlobPatterns = async (filepath) => {
|
|
2228
|
+
const { packages = [] } = parse$1(readFileSync(filepath, "utf8"));
|
|
2229
|
+
return packages;
|
|
2230
|
+
};
|
|
2231
|
+
const getProjects = async function* (options) {
|
|
2232
|
+
const { manifest: { projectDir } } = options;
|
|
2233
|
+
const workspaceFile = join(projectDir, PnpmWorkspacesProject.WorkspaceFile);
|
|
2234
|
+
const packages = await getPackagesGlobPatterns(workspaceFile);
|
|
2235
|
+
for (const pkg of packages) {
|
|
2236
|
+
const matched = globSync(normalizePath(join(pkg, PackageJsonManifest.Filename)), {
|
|
2237
|
+
cwd: projectDir,
|
|
2238
|
+
onlyFiles: true,
|
|
2239
|
+
absolute: true,
|
|
2240
|
+
ignore: PnpmWorkspacesProject.GlobIgnore
|
|
2241
|
+
});
|
|
2242
|
+
for (const filepath of matched) yield filepath;
|
|
2243
|
+
}
|
|
2244
|
+
};
|
|
2245
|
+
/** 一个基于 pnpm 工作区的单体仓库项目,使用 package.json 和 pnpm-workspace.yaml 进行配置 */
|
|
2246
|
+
var PnpmWorkspacesProject = class extends PackageJsonMonorepoProject {
|
|
2247
|
+
static WorkspaceFile = "pnpm-workspace.yaml";
|
|
2248
|
+
static GlobIgnore = ["**/node_modules/**"];
|
|
2249
|
+
constructor(options) {
|
|
2250
|
+
super({
|
|
2251
|
+
...options,
|
|
2252
|
+
getProjects
|
|
2253
|
+
});
|
|
2254
|
+
}
|
|
2255
|
+
async publish(options = {}) {
|
|
2256
|
+
const onceCall = onceAsync(this.getDefaultPublishTag.bind(this));
|
|
2257
|
+
for await (const project of this.getProjects()) if (project.hasBump) {
|
|
2258
|
+
const opts = this.isFixedMode ? {} : await (async () => {
|
|
2259
|
+
const tagsMap = await onceCall();
|
|
2260
|
+
const name = await project.manifest.getName();
|
|
2261
|
+
return { tag: tagsMap.get(name) };
|
|
2262
|
+
})();
|
|
2263
|
+
await publish$1(project, {
|
|
2264
|
+
...opts,
|
|
2265
|
+
...options
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
async publishCheck(options = {}) {
|
|
2270
|
+
await check$1(this, options);
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
//#endregion
|
|
2274
|
+
//#region src/releaser-github-action/comment.ts
|
|
2275
|
+
const ALLOWED_AUTHOR_ASSOCIATIONS = [
|
|
2276
|
+
"OWNER",
|
|
2277
|
+
"COLLABORATOR",
|
|
2278
|
+
"MEMBER"
|
|
2279
|
+
];
|
|
2280
|
+
const JSON_START = "```json";
|
|
2281
|
+
const JSON_START_OFFSET = 7;
|
|
2282
|
+
const JSON_END = "```";
|
|
2283
|
+
/** 所有支持的拉取请求评论命令 */
|
|
2284
|
+
const COMMANDS = ["!release/set-options", "!release/set-preamble"];
|
|
2285
|
+
/**
|
|
2286
|
+
* 判断正文是否以命令开头,并且命令后紧跟空白字符或正文结束。
|
|
2287
|
+
* 这样可以避免将更长的标记(例如 `!release/set-options-foo`)误判为匹配的命令
|
|
2288
|
+
*/
|
|
2289
|
+
const startsWithCommand = (body, command) => {
|
|
2290
|
+
if (!body.startsWith(command)) return false;
|
|
2291
|
+
const nextChar = body[command.length];
|
|
2292
|
+
return isUndefined(nextChar) || /\s/.test(nextChar);
|
|
2293
|
+
};
|
|
2294
|
+
/** 评论内容是否是支持的命令评论之一 */
|
|
2295
|
+
const isCommandComment = (body) => isString(body) && COMMANDS.some((command) => startsWithCommand(body, command));
|
|
2296
|
+
/** 解析 releaser 参数 */
|
|
2297
|
+
const parseSetOptionsComment = (comment) => {
|
|
2298
|
+
if (ALLOWED_AUTHOR_ASSOCIATIONS.includes(comment.author_association)) {
|
|
2299
|
+
const { body } = comment;
|
|
2300
|
+
if (body && startsWithCommand(body, "!release/set-options")) {
|
|
2301
|
+
const start = body.indexOf(JSON_START);
|
|
2302
|
+
const end = isIndex(start) ? body.indexOf(JSON_END, start + JSON_START_OFFSET) : -1;
|
|
2303
|
+
if (!isIndex(start) || !isIndex(end)) return;
|
|
2304
|
+
return body.substring(start + JSON_START_OFFSET, end).trim();
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
};
|
|
2308
|
+
/** 解析日志前言 */
|
|
2309
|
+
function parseSetPreambleComment(comment) {
|
|
2310
|
+
if (ALLOWED_AUTHOR_ASSOCIATIONS.includes(comment.author_association)) {
|
|
2311
|
+
const { body } = comment;
|
|
2312
|
+
if (body && startsWithCommand(body, "!release/set-preamble")) {
|
|
2313
|
+
const newlineIndex = body.indexOf("\n");
|
|
2314
|
+
const firstLine = isIndex(newlineIndex) ? body.slice(0, newlineIndex) : body;
|
|
2315
|
+
const secondLine = isIndex(newlineIndex) ? body.slice(newlineIndex + 1).trim() : "";
|
|
2316
|
+
const name = firstLine.slice(21).replace(/`/g, "").trim().split(/\s+/)[0];
|
|
2317
|
+
const preamble = secondLine;
|
|
2318
|
+
if (preamble) return {
|
|
2319
|
+
name,
|
|
2320
|
+
preamble
|
|
2321
|
+
};
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
//#endregion
|
|
2327
|
+
//#region src/releaser-github-action/conditions.ts
|
|
2328
|
+
const ifReleaseCommit = async (releaser) => {
|
|
2329
|
+
const { project, options: { dryRun } } = releaser;
|
|
2330
|
+
const logger = releaser.logger.createChild("if-release-commit");
|
|
2331
|
+
if (!(await gitLog(["-1", "--pretty=%B"]))?.startsWith("chore(release):")) {
|
|
2332
|
+
logger.verbose("Skipping non-release commit.");
|
|
2333
|
+
return false;
|
|
2334
|
+
}
|
|
2335
|
+
logger.verbose("Checking existing release tags...");
|
|
2336
|
+
if (!dryRun) await gitFetch(["--tags"]);
|
|
2337
|
+
const tags = await project.getTags();
|
|
2338
|
+
const canRelease = tags.length > 0;
|
|
2339
|
+
if (canRelease) {
|
|
2340
|
+
logger.verbose(`Found release tags:`);
|
|
2341
|
+
for (const tag of tags) logger.verbose(`- ${tag}`);
|
|
2342
|
+
} else logger.verbose(`No release tags found.`);
|
|
2343
|
+
return canRelease;
|
|
2344
|
+
};
|
|
2345
|
+
/**
|
|
2346
|
+
* 检测当前事件是否为针对处于打开状态的发布拉取请求的受支持命令评论:
|
|
2347
|
+
* (`!release/set-options` 或 `!release/set-preamble`)
|
|
2348
|
+
*
|
|
2349
|
+
* @returns 如果是此类评论,则返回 `目标分支`;如果是评论事件但不是命令评论,则返回 `false`;如果根本不是评论事件,则返回 `null`
|
|
2350
|
+
*/
|
|
2351
|
+
const ifCommandComment = () => {
|
|
2352
|
+
const { eventName, payload: { issue, comment } } = context;
|
|
2353
|
+
if (eventName !== "issue_comment") return null;
|
|
2354
|
+
const isPullRequest = issue?.pull_request;
|
|
2355
|
+
const issueAuthor = (issue?.user)?.login;
|
|
2356
|
+
const issueBody = issue?.body;
|
|
2357
|
+
const issueState = issue?.state;
|
|
2358
|
+
const commentBody = comment?.body;
|
|
2359
|
+
if (isPullRequest && issueAuthor === "github-actions[bot]" && issueState === "open" && issueBody?.includes("release-pull-request: true") && isCommandComment(commentBody)) {
|
|
2360
|
+
const matches = issueBody.match(/release-branch-to:\s*(\S+)/);
|
|
2361
|
+
return matches ? matches[1] : false;
|
|
2362
|
+
}
|
|
2363
|
+
return false;
|
|
2364
|
+
};
|
|
2365
|
+
//#endregion
|
|
2366
|
+
//#region src/releaser-github-action/options.ts
|
|
2367
|
+
const omitEmpty = (object) => {
|
|
2368
|
+
const result = {};
|
|
2369
|
+
for (const [key, value] of Object.entries(object)) if (isBoolean(value) || Boolean(value)) result[key] = value;
|
|
2370
|
+
return result;
|
|
2371
|
+
};
|
|
2372
|
+
const getOptionalBooleanInput = (name) => getInput(name) ? getBooleanInput(name) : void 0;
|
|
2373
|
+
const getOptionalJsonInput = (name) => {
|
|
2374
|
+
const value = getInput(name);
|
|
2375
|
+
return value ? JSON.parse(value) : void 0;
|
|
2376
|
+
};
|
|
2377
|
+
const getInputOptions = () => ({
|
|
2378
|
+
checkout: omitEmpty({ branch: getInput("checkout-branch") }),
|
|
2379
|
+
bump: omitEmpty({
|
|
2380
|
+
version: getInput("bump-version"),
|
|
2381
|
+
as: getInput("bump-as"),
|
|
2382
|
+
prerelease: getInput("bump-prerelease"),
|
|
2383
|
+
snapshot: getInput("bump-snapshot"),
|
|
2384
|
+
firstRelease: getOptionalBooleanInput("bump-first-release"),
|
|
2385
|
+
byProject: getOptionalJsonInput("bump-by-project")
|
|
2386
|
+
}),
|
|
2387
|
+
maintenanceBranch: omitEmpty({ enabled: getOptionalBooleanInput("maintenance-branch-enabled") }),
|
|
2388
|
+
publish: omitEmpty({
|
|
2389
|
+
access: getInput("publish-access"),
|
|
2390
|
+
tag: getInput("publish-tag")
|
|
2391
|
+
})
|
|
2392
|
+
});
|
|
2393
|
+
const getInputSkippedSteps = () => ({
|
|
2394
|
+
bump: getOptionalBooleanInput("bump-skip"),
|
|
2395
|
+
publish: getOptionalBooleanInput("publish-skip")
|
|
2396
|
+
});
|
|
2397
|
+
//#endregion
|
|
2398
|
+
//#region src/releaser-github-action/releaser.ts
|
|
2399
|
+
var ReleaserGithubAction = class extends Releaser {
|
|
2400
|
+
setUser(options = {}) {
|
|
2401
|
+
return super.setUser({
|
|
2402
|
+
username: "github-actions[bot]",
|
|
2403
|
+
email: "github-actions[bot]@users.noreply.github.com",
|
|
2404
|
+
...options
|
|
2405
|
+
});
|
|
2406
|
+
}
|
|
2407
|
+
checkout(options = {}) {
|
|
2408
|
+
return super.checkout({
|
|
2409
|
+
fetch: true,
|
|
2410
|
+
force: true,
|
|
2411
|
+
...options
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
tag(options = {}) {
|
|
2415
|
+
return super.tag({
|
|
2416
|
+
fetch: true,
|
|
2417
|
+
...options
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
push(options = {}) {
|
|
2421
|
+
return super.push({
|
|
2422
|
+
force: true,
|
|
2423
|
+
...options
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
constructor(options) {
|
|
2427
|
+
super({
|
|
2428
|
+
hosting: new GithubHosting({ octokit: options.octokit }),
|
|
2429
|
+
...options
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
fetchOptions() {
|
|
2433
|
+
return this.enqueue(async () => {
|
|
2434
|
+
const { state: { headBranch }, stepsOptions: { bump: bumpOpts } } = this;
|
|
2435
|
+
if (!headBranch) throwError("Head branch is not set. Please call `checkout` before `fetchParams`.");
|
|
2436
|
+
const { project, hosting } = this;
|
|
2437
|
+
const logger = this.logger.createChild("fetch-options");
|
|
2438
|
+
logger.info("Fetching options from pull request comments...");
|
|
2439
|
+
const { octokit } = hosting;
|
|
2440
|
+
const [owner, repo] = await hosting.getRepositoryId(project);
|
|
2441
|
+
const { data: [pr] } = await octokit.rest.pulls.list({
|
|
2442
|
+
owner,
|
|
2443
|
+
repo,
|
|
2444
|
+
head: `${owner}:${headBranch}`,
|
|
2445
|
+
state: "open"
|
|
2446
|
+
});
|
|
2447
|
+
if (!pr) {
|
|
2448
|
+
logger.info("No open pull request found.");
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
const { data: comments } = await octokit.rest.issues.listComments({
|
|
2452
|
+
owner,
|
|
2453
|
+
repo,
|
|
2454
|
+
issue_number: pr.number
|
|
2455
|
+
});
|
|
2456
|
+
let optionsApplied = false;
|
|
2457
|
+
let preamble;
|
|
2458
|
+
const preambleByName = /* @__PURE__ */ new Map();
|
|
2459
|
+
for (let i = comments.length, comment; i >= 0; i--) {
|
|
2460
|
+
comment = comments[i];
|
|
2461
|
+
if (!optionsApplied) {
|
|
2462
|
+
const json = parseSetOptionsComment(comment);
|
|
2463
|
+
if (json) {
|
|
2464
|
+
try {
|
|
2465
|
+
const options = JSON.parse(json);
|
|
2466
|
+
logger.verbose("Found set-options comment:");
|
|
2467
|
+
logger.verbose(json);
|
|
2468
|
+
this.setOptions(options);
|
|
2469
|
+
optionsApplied = true;
|
|
2470
|
+
} catch {
|
|
2471
|
+
logger.verbose("Failed to parse parameters comment:");
|
|
2472
|
+
logger.verbose(json);
|
|
2473
|
+
}
|
|
2474
|
+
continue;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
const setPreamble = parseSetPreambleComment(comment);
|
|
2478
|
+
if (setPreamble) {
|
|
2479
|
+
const { name, preamble: markdown } = setPreamble;
|
|
2480
|
+
if (name) {
|
|
2481
|
+
if (!preambleByName.has(name)) preambleByName.set(name, markdown);
|
|
2482
|
+
} else if (isUndefined(preamble)) preamble = markdown;
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
if (isEmpty(preamble) && isEmpty(preambleByName)) return;
|
|
2486
|
+
const bump = { ...bumpOpts };
|
|
2487
|
+
if (isNotEmpty(preamble)) {
|
|
2488
|
+
logger.verbose("Found global set-preamble comment.");
|
|
2489
|
+
bump.preamble = preamble;
|
|
2490
|
+
}
|
|
2491
|
+
if (isNotEmpty(preambleByName)) for (const [name, markdown] of preambleByName) (bump.byProject ??= {})[name] = {
|
|
2492
|
+
...bump.byProject?.[name],
|
|
2493
|
+
preamble: markdown
|
|
2494
|
+
};
|
|
2495
|
+
this.setOptions({ bump });
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
fetch() {
|
|
2499
|
+
return this.enqueue(async () => {
|
|
2500
|
+
const { logger, options: { dryRun } } = this;
|
|
2501
|
+
logger.info("fetch", "Fetching all commits and tags from the remote repository...");
|
|
2502
|
+
if (!dryRun) {
|
|
2503
|
+
const unshallow = await gitRevParse(["--is-shallow-repository"]) === "true" ? ["--unshallow"] : [];
|
|
2504
|
+
await gitFetch([
|
|
2505
|
+
"--prune",
|
|
2506
|
+
"--tags",
|
|
2507
|
+
...unshallow,
|
|
2508
|
+
"--",
|
|
2509
|
+
"origin",
|
|
2510
|
+
"HEAD"
|
|
2511
|
+
]);
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
async runPullRequestAction() {
|
|
2516
|
+
await this.setUser().checkout().fetchOptions().bump().commit().push().pullRequest().run();
|
|
2517
|
+
}
|
|
2518
|
+
async runReleaseAction(check = true) {
|
|
2519
|
+
await this.setUser().maintenanceBranch().tag().push().publish().release().run(check ? ifReleaseCommit : void 0);
|
|
2520
|
+
}
|
|
2521
|
+
async runSnapshotAction(snapshotTag) {
|
|
2522
|
+
if (!snapshotTag) throwError("Snapshot tag is required.");
|
|
2523
|
+
await this.fetch().bump({
|
|
2524
|
+
snapshot: snapshotTag,
|
|
2525
|
+
skipChangelog: true
|
|
2526
|
+
}).publish({ tag: snapshotTag }).revert().run();
|
|
2527
|
+
}
|
|
2528
|
+
async runAction() {
|
|
2529
|
+
const logger = this.logger.createChild("run");
|
|
2530
|
+
logger.info("Detecting action...");
|
|
2531
|
+
const commandComment = ifCommandComment();
|
|
2532
|
+
let isReleaseCommit = false;
|
|
2533
|
+
if (commandComment === false) {
|
|
2534
|
+
logger.info("Action triggered by a pull request comment.");
|
|
2535
|
+
logger.info("No command comment found in a comment. Stopping action.");
|
|
2536
|
+
return;
|
|
2537
|
+
}
|
|
2538
|
+
if (!isNull(commandComment)) {
|
|
2539
|
+
logger.info("Action triggered by a pull request comment.");
|
|
2540
|
+
if (await gitBranchCurrent() !== commandComment) this.checkout({
|
|
2541
|
+
branch: commandComment,
|
|
2542
|
+
fetch: true,
|
|
2543
|
+
force: false
|
|
2544
|
+
});
|
|
2545
|
+
} else isReleaseCommit = await ifReleaseCommit(this);
|
|
2546
|
+
if (isReleaseCommit) {
|
|
2547
|
+
logger.info("Running release action...");
|
|
2548
|
+
await this.runReleaseAction(false);
|
|
2549
|
+
} else {
|
|
2550
|
+
logger.info("Running pull request action...");
|
|
2551
|
+
await this.runPullRequestAction();
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
//#endregion
|
|
2556
|
+
//#region package.json
|
|
2557
|
+
var package_default = {
|
|
2558
|
+
name: "@peiyanlu/release",
|
|
2559
|
+
version: "2.0.0",
|
|
2560
|
+
description: "Tool to automate version bumps, changelogs, and releases.",
|
|
2561
|
+
license: "MIT",
|
|
2562
|
+
type: "module",
|
|
2563
|
+
"private": false,
|
|
2564
|
+
bin: { "release": "index.js" },
|
|
2565
|
+
exports: {
|
|
2566
|
+
".": {
|
|
2567
|
+
"types": "./dist/index.d.mts",
|
|
2568
|
+
"default": "./dist/index.mjs"
|
|
2569
|
+
},
|
|
2570
|
+
"./package.json": "./package.json"
|
|
2571
|
+
},
|
|
2572
|
+
scripts: {
|
|
2573
|
+
"dev": "tsdown -w",
|
|
2574
|
+
"build": "tsdown",
|
|
2575
|
+
"prepublishOnly": "tsdown",
|
|
2576
|
+
"release": "node index.js",
|
|
2577
|
+
"release:dry": "node index.js -C -n",
|
|
2578
|
+
"test": "vitest run",
|
|
2579
|
+
"test:cov": "vitest run --coverage"
|
|
2580
|
+
},
|
|
2581
|
+
dependencies: {
|
|
2582
|
+
"@actions/core": "^3.0.1",
|
|
2583
|
+
"@actions/github": "^9.1.1",
|
|
2584
|
+
"@clack/prompts": "^1.7.0",
|
|
2585
|
+
"@conventional-changelog/template": "^1.2.1",
|
|
2586
|
+
"@octokit/rest": "^22.0.1",
|
|
2587
|
+
"@peiyanlu/cli-utils": "^0.0.15",
|
|
2588
|
+
"@peiyanlu/node-utils": "^0.0.4",
|
|
2589
|
+
"@peiyanlu/ts-utils": "^0.0.12",
|
|
2590
|
+
"ansis": "^4.3.1",
|
|
2591
|
+
"commander": "^15.0.0",
|
|
2592
|
+
"conventional-changelog": "^8.1.0",
|
|
2593
|
+
"conventional-changelog-conventionalcommits": "^10.2.1",
|
|
2594
|
+
"conventional-recommended-bump": "^12.1.0",
|
|
2595
|
+
"open": "^11.0.0",
|
|
2596
|
+
"publint": "^0.3.22",
|
|
2597
|
+
"semver": "^7.8.5",
|
|
2598
|
+
"tinyglobby": "^0.2.17",
|
|
2599
|
+
"yaml": "^2.9.0",
|
|
2600
|
+
"handlebars": "^4.7.9"
|
|
2601
|
+
},
|
|
2602
|
+
devDependencies: {
|
|
2603
|
+
"@peiyanlu/test-tools": "^0.0.7",
|
|
2604
|
+
"@types/node": "^24.13.3",
|
|
2605
|
+
"@types/semver": "^7.7.1",
|
|
2606
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
2607
|
+
"tsdown": "^0.23.1",
|
|
2608
|
+
"typescript": "^6.0.3",
|
|
2609
|
+
"vitest": "^4.1.10"
|
|
2610
|
+
},
|
|
2611
|
+
author: {
|
|
2612
|
+
"name": "YanluPei",
|
|
2613
|
+
"email": "peiyanlu@vip.qq.com"
|
|
2614
|
+
},
|
|
2615
|
+
engines: { "node": ">=20" },
|
|
2616
|
+
keywords: ["typescript", "cli"],
|
|
2617
|
+
files: ["dist", "template"],
|
|
2618
|
+
publishConfig: {
|
|
2619
|
+
"access": "public",
|
|
2620
|
+
"registry": "https://registry.npmjs.org"
|
|
2621
|
+
},
|
|
2622
|
+
repository: {
|
|
2623
|
+
"type": "git",
|
|
2624
|
+
"url": "git+https://github.com/peiyanlu/release.git"
|
|
2625
|
+
},
|
|
2626
|
+
bugs: { "url": "https://github.com/peiyanlu/release/issues" },
|
|
2627
|
+
homepage: "https://github.com/peiyanlu/release#readme"
|
|
2628
|
+
};
|
|
2629
|
+
//#endregion
|
|
2630
|
+
//#region src/releaser-cli/prompts.ts
|
|
2631
|
+
const abortTask = (msg) => {
|
|
2632
|
+
cancel(`${red("✕")} ${msg ?? "Release cancelled."}`);
|
|
2633
|
+
process.exit(0);
|
|
2634
|
+
};
|
|
2635
|
+
const singlePrompt = async (fn, abortMsg) => {
|
|
2636
|
+
const value = await fn();
|
|
2637
|
+
if (isCancel(value)) abortTask(abortMsg);
|
|
2638
|
+
return value;
|
|
2639
|
+
};
|
|
2640
|
+
const question = (msg, dryRun) => {
|
|
2641
|
+
return `${(dryRun ? yellow : red)("?")} ${msg}`;
|
|
2642
|
+
};
|
|
2643
|
+
const getVersionHint = async (project, options = {}) => await project.getNextVersion(options) ?? "No version change";
|
|
2644
|
+
const createVersionOptions = async (project, options = {}, current, tagPrefix) => {
|
|
2645
|
+
const firstRelease = await project.isFirstRelease({ tagPrefix });
|
|
2646
|
+
if (firstRelease) return [
|
|
2647
|
+
{
|
|
2648
|
+
label: "Initial release",
|
|
2649
|
+
value: "initial",
|
|
2650
|
+
hint: `Release ${current}`,
|
|
2651
|
+
bump: {
|
|
2652
|
+
version: current,
|
|
2653
|
+
firstRelease
|
|
2654
|
+
}
|
|
2655
|
+
},
|
|
2656
|
+
{
|
|
2657
|
+
label: "Custom",
|
|
2658
|
+
value: "custom",
|
|
2659
|
+
hint: "Specify an exact version"
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
label: "As-is",
|
|
2663
|
+
value: "ignore",
|
|
2664
|
+
hint: `Do not update the version ${current}`,
|
|
2665
|
+
bump: { version: current }
|
|
2666
|
+
}
|
|
2667
|
+
];
|
|
2668
|
+
const fromPreRelease = isPrerelease(current);
|
|
2669
|
+
const candidates = [
|
|
2670
|
+
{
|
|
2671
|
+
label: fromPreRelease ? "Release" : "Patch",
|
|
2672
|
+
value: "patch",
|
|
2673
|
+
hint: await getVersionHint(project, {
|
|
2674
|
+
...options,
|
|
2675
|
+
tagPrefix,
|
|
2676
|
+
as: "patch"
|
|
2677
|
+
}),
|
|
2678
|
+
bump: { as: "patch" }
|
|
2679
|
+
},
|
|
2680
|
+
{
|
|
2681
|
+
label: "Minor",
|
|
2682
|
+
value: "minor",
|
|
2683
|
+
hint: await getVersionHint(project, {
|
|
2684
|
+
...options,
|
|
2685
|
+
tagPrefix,
|
|
2686
|
+
as: "minor"
|
|
2687
|
+
}),
|
|
2688
|
+
bump: { as: "minor" }
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
label: "Major",
|
|
2692
|
+
value: "major",
|
|
2693
|
+
hint: await getVersionHint(project, {
|
|
2694
|
+
...options,
|
|
2695
|
+
tagPrefix,
|
|
2696
|
+
as: "major"
|
|
2697
|
+
}),
|
|
2698
|
+
bump: { as: "major" }
|
|
2699
|
+
}
|
|
2700
|
+
];
|
|
2701
|
+
if (fromPreRelease) candidates.unshift({
|
|
2702
|
+
label: "Pre-release",
|
|
2703
|
+
value: "prerelease",
|
|
2704
|
+
hint: await getVersionHint(project, {
|
|
2705
|
+
...options,
|
|
2706
|
+
tagPrefix,
|
|
2707
|
+
as: "prerelease"
|
|
2708
|
+
}),
|
|
2709
|
+
bump: { as: "prerelease" }
|
|
2710
|
+
});
|
|
2711
|
+
else for (const as of [
|
|
2712
|
+
"patch",
|
|
2713
|
+
"minor",
|
|
2714
|
+
"major"
|
|
2715
|
+
]) candidates.push({
|
|
2716
|
+
label: `Pre-${as[0].toUpperCase()}${as.slice(1)}`,
|
|
2717
|
+
value: `pre${as}`,
|
|
2718
|
+
hint: await getVersionHint(project, {
|
|
2719
|
+
...options,
|
|
2720
|
+
tagPrefix,
|
|
2721
|
+
as,
|
|
2722
|
+
prerelease: "beta"
|
|
2723
|
+
}),
|
|
2724
|
+
bump: {
|
|
2725
|
+
as,
|
|
2726
|
+
prerelease: "beta"
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
candidates.push({
|
|
2730
|
+
label: "Custom",
|
|
2731
|
+
value: "custom",
|
|
2732
|
+
hint: "Specify an exact version"
|
|
2733
|
+
}, {
|
|
2734
|
+
label: "As-is",
|
|
2735
|
+
value: "ignore",
|
|
2736
|
+
hint: `Do not update the version ${current}`,
|
|
2737
|
+
bump: { version: current }
|
|
2738
|
+
});
|
|
2739
|
+
return candidates;
|
|
2740
|
+
};
|
|
2741
|
+
const runBumpPrompts = async (project, options = {}) => {
|
|
2742
|
+
const { bump: bumpOpts = {}, run = {} } = options;
|
|
2743
|
+
const bump = { ...bumpOpts };
|
|
2744
|
+
const { dryRun = false, ci = false } = run;
|
|
2745
|
+
if (bump.version || bump.as || bump.snapshot) return bump;
|
|
2746
|
+
const runVersionPrompts = async (project, tagPrefix) => {
|
|
2747
|
+
const promptsRes = {};
|
|
2748
|
+
const inferred = await project.inferReleaseType({
|
|
2749
|
+
...bump,
|
|
2750
|
+
tagPrefix
|
|
2751
|
+
});
|
|
2752
|
+
const name = await project.manifest.getName();
|
|
2753
|
+
const current = await project.manifest.getVersion();
|
|
2754
|
+
let useCustom = false;
|
|
2755
|
+
let customVersion = current;
|
|
2756
|
+
if (project.isMonorepoMode() && project.isFixedMode) {
|
|
2757
|
+
const { version: maxVersion, packages } = await project.getMaxVersion();
|
|
2758
|
+
if (lt(current, maxVersion)) {
|
|
2759
|
+
useCustom = true;
|
|
2760
|
+
customVersion = inc(maxVersion, inferred ?? "patch");
|
|
2761
|
+
note(`
|
|
2762
|
+
Current version: ${current}
|
|
2763
|
+
Highest package version: ${maxVersion}
|
|
2764
|
+
Packages at this version:\n${packages.map((name) => `- ${name}`).join("\n")}
|
|
2765
|
+
|
|
2766
|
+
Minimum release version: ${maxVersion}
|
|
2767
|
+
Suggested release version: ${customVersion} (used in CI)
|
|
2768
|
+
`.trim(), "Monorepo version mismatch", { format(line) {
|
|
2769
|
+
return line.trim();
|
|
2770
|
+
} });
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
const versionOptions = await createVersionOptions(project, bump, current, tagPrefix);
|
|
2774
|
+
const find = (val) => versionOptions.find((option) => option.value === val);
|
|
2775
|
+
const fromPreRelease = isPrerelease(current);
|
|
2776
|
+
const initialValue = useCustom ? "custom" : inferred && find(inferred) ? inferred : fromPreRelease && find("prerelease") ? "prerelease" : "patch";
|
|
2777
|
+
const type = ci ? initialValue : await singlePrompt(() => select({
|
|
2778
|
+
message: question(`Select "${cyan`${name}`}" release version type:`, dryRun),
|
|
2779
|
+
options: versionOptions,
|
|
2780
|
+
initialValue
|
|
2781
|
+
}));
|
|
2782
|
+
if (equals(type, "custom")) promptsRes.version = ci ? customVersion : await singlePrompt(() => text({
|
|
2783
|
+
message: question("Enter release version:", dryRun),
|
|
2784
|
+
initialValue: customVersion,
|
|
2785
|
+
placeholder: customVersion,
|
|
2786
|
+
validate: (val) => {
|
|
2787
|
+
if (isUndefined(val) || !isValidVersion(val)) return "Invalid version.";
|
|
2788
|
+
}
|
|
2789
|
+
}));
|
|
2790
|
+
else {
|
|
2791
|
+
const selected = find(type);
|
|
2792
|
+
Object.assign(promptsRes, selected?.bump);
|
|
2793
|
+
}
|
|
2794
|
+
return promptsRes;
|
|
2795
|
+
};
|
|
2796
|
+
if (project.isMonorepoMode() && project.isIndependentMode) for await (const subProject of project.getProjects()) {
|
|
2797
|
+
const name = await subProject.manifest.getName();
|
|
2798
|
+
const scope = await project.getScope(name);
|
|
2799
|
+
const tagPrefix = await project.getTagPrefix(scope);
|
|
2800
|
+
const monoBump = bump;
|
|
2801
|
+
(monoBump.byProject ??= {})[name] = {
|
|
2802
|
+
...monoBump.byProject?.[name],
|
|
2803
|
+
...await runVersionPrompts(subProject, tagPrefix)
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
else {
|
|
2807
|
+
const result = await runVersionPrompts(project);
|
|
2808
|
+
Object.assign(bump, result);
|
|
2809
|
+
}
|
|
2810
|
+
return bump;
|
|
2811
|
+
};
|
|
2812
|
+
const getCliPromptOptions = async (options = {}) => {
|
|
2813
|
+
const { dryRun = false, ci = false } = options;
|
|
2814
|
+
const shouldPrompt = (v) => !ci && (isUndefined(v) || dryRun);
|
|
2815
|
+
const res = await group({
|
|
2816
|
+
commit: () => options.skipGit ? Promise.resolve(false) : shouldPrompt(options.skipGit) ? confirm({ message: question("Commit release changes?", dryRun) }) : Promise.resolve(true),
|
|
2817
|
+
tag: () => options.skipGit ? Promise.resolve(false) : shouldPrompt(options.skipGit) ? confirm({ message: question("Create release tags?", dryRun) }) : Promise.resolve(true),
|
|
2818
|
+
push: () => options.skipGit ? Promise.resolve(false) : shouldPrompt(options.skipGit) ? confirm({ message: question("Push release changes?", dryRun) }) : Promise.resolve(true),
|
|
2819
|
+
publish: () => options.skipPublish ? Promise.resolve(false) : shouldPrompt(options.skipPublish) ? confirm({ message: question("Publish project?", dryRun) }) : Promise.resolve(true),
|
|
2820
|
+
release: () => options.skipRelease || options.skipGit ? Promise.resolve(false) : shouldPrompt(options.skipRelease) ? confirm({ message: question("Create repository release?", dryRun) }) : Promise.resolve(true)
|
|
2821
|
+
}, { onCancel: () => {
|
|
2822
|
+
abortTask();
|
|
2823
|
+
} });
|
|
2824
|
+
return {
|
|
2825
|
+
commit: !res.commit,
|
|
2826
|
+
tag: !res.tag,
|
|
2827
|
+
push: !res.push,
|
|
2828
|
+
publish: !res.publish,
|
|
2829
|
+
release: !res.release
|
|
2830
|
+
};
|
|
2831
|
+
};
|
|
2832
|
+
const runConfirmPrompts = async (ci = false, dryRun = false) => {
|
|
2833
|
+
if (!(ci ? true : await singlePrompt(() => confirm({ message: question(red("Confirm start workflow?"), dryRun) })))) abortTask();
|
|
2834
|
+
};
|
|
2835
|
+
//#endregion
|
|
2836
|
+
//#region src/releaser-cli/releaser.ts
|
|
2837
|
+
var ReleaserCli = class extends Releaser {
|
|
2838
|
+
constructor(options) {
|
|
2839
|
+
const { silent, dryRun, verbose } = options;
|
|
2840
|
+
super({
|
|
2841
|
+
logger: new Logger({
|
|
2842
|
+
silent,
|
|
2843
|
+
dryRun,
|
|
2844
|
+
verbose,
|
|
2845
|
+
colors: [
|
|
2846
|
+
blue,
|
|
2847
|
+
blueBright,
|
|
2848
|
+
cyan,
|
|
2849
|
+
cyanBright,
|
|
2850
|
+
green,
|
|
2851
|
+
greenBright,
|
|
2852
|
+
magenta,
|
|
2853
|
+
magentaBright,
|
|
2854
|
+
red,
|
|
2855
|
+
redBright
|
|
2856
|
+
].sort(() => Math.random() - .5),
|
|
2857
|
+
printer: ({ dryRun, stage, message, color }) => {
|
|
2858
|
+
const msg = isString(message) ? message : JSON.stringify(message, null, 2);
|
|
2859
|
+
log.message(dim`${dryRun ? yellow`[dry-run]` : ""}${color(`[${stage}]`)}: ${msg}`);
|
|
2860
|
+
}
|
|
2861
|
+
}),
|
|
2862
|
+
...options
|
|
2863
|
+
});
|
|
2864
|
+
}
|
|
2865
|
+
tag(options = {}) {
|
|
2866
|
+
return super.tag({
|
|
2867
|
+
fetch: true,
|
|
2868
|
+
...options
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
push(options = {}) {
|
|
2872
|
+
return super.push({
|
|
2873
|
+
force: true,
|
|
2874
|
+
...options
|
|
2875
|
+
});
|
|
2876
|
+
}
|
|
2877
|
+
async run(...args) {
|
|
2878
|
+
await super.run(...args);
|
|
2879
|
+
this.end();
|
|
2880
|
+
}
|
|
2881
|
+
setCliOptions(options) {
|
|
2882
|
+
return this.enqueue(async () => {
|
|
2883
|
+
const { ci, dryRun, releaseType: as } = options;
|
|
2884
|
+
const { project, stepsOptions: { bump: bumpOpts } } = this;
|
|
2885
|
+
const bump = await runBumpPrompts(project, {
|
|
2886
|
+
bump: {
|
|
2887
|
+
...bumpOpts,
|
|
2888
|
+
as
|
|
2889
|
+
},
|
|
2890
|
+
run: options
|
|
2891
|
+
});
|
|
2892
|
+
this.setOptions({ bump }).setSkippedSteps(await getCliPromptOptions(options));
|
|
2893
|
+
await runConfirmPrompts(ci, dryRun);
|
|
2894
|
+
});
|
|
2895
|
+
}
|
|
2896
|
+
setLegacyOptions() {
|
|
2897
|
+
return this.enqueue(async () => {
|
|
2898
|
+
const logger = this.logger.createChild("legacy-options");
|
|
2899
|
+
const { config, file } = await loadConfig(process.cwd());
|
|
2900
|
+
const legacySteps = omit(config, [
|
|
2901
|
+
"project",
|
|
2902
|
+
"hosting",
|
|
2903
|
+
"releaser"
|
|
2904
|
+
]);
|
|
2905
|
+
if (file) {
|
|
2906
|
+
logger.verbose(`Legacy config file: ${file}`);
|
|
2907
|
+
logger.verbose("Legacy config for steps:");
|
|
2908
|
+
logger.verbose(legacySteps);
|
|
2909
|
+
}
|
|
2910
|
+
this.setOptions(legacySteps);
|
|
2911
|
+
});
|
|
2912
|
+
}
|
|
2913
|
+
check() {
|
|
2914
|
+
return this.enqueue(async () => {
|
|
2915
|
+
const { project, hosting } = this;
|
|
2916
|
+
const logger = this.logger.createChild("check");
|
|
2917
|
+
logger.info("Checking release environment...");
|
|
2918
|
+
if (![
|
|
2919
|
+
"commit",
|
|
2920
|
+
"tag",
|
|
2921
|
+
"push"
|
|
2922
|
+
].every((k) => this.shouldSkip(k))) {
|
|
2923
|
+
logger.verbose("Checking Git repository...");
|
|
2924
|
+
const isRepo = await isGitRepo();
|
|
2925
|
+
assert(isRepo, "Current directory is not a Git repository");
|
|
2926
|
+
logger.verbose("Checking working tree...");
|
|
2927
|
+
const isClean = await isWorkingTreeClean();
|
|
2928
|
+
assert(isClean, "Git working tree is not clean");
|
|
2929
|
+
logger.verbose("Checking remote repository...");
|
|
2930
|
+
const hasRemote = await gitConfigGet("remote.origin.url");
|
|
2931
|
+
assert(hasRemote, "Git remote \"origin\" is not configured");
|
|
2932
|
+
}
|
|
2933
|
+
if (!this.shouldSkip("publish")) await project.publishCheck({ logger });
|
|
2934
|
+
if (!this.shouldSkip("release")) await hosting?.releaseCheck({ logger });
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
start() {
|
|
2938
|
+
return this.enqueue(async () => {
|
|
2939
|
+
const { name, version } = package_default;
|
|
2940
|
+
console.log(`${blue`i`} ${name} v${version}\n`);
|
|
2941
|
+
intro("🚀 Starting release");
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
end() {
|
|
2945
|
+
const { options: { dryRun = false } } = this;
|
|
2946
|
+
outro(`🎉 ${dryRun ? "Dry run complete" : "Release complete"}`);
|
|
2947
|
+
process.exit(0);
|
|
2948
|
+
}
|
|
2949
|
+
async runAction(options) {
|
|
2950
|
+
await this.start().setLegacyOptions().setCliOptions(options).check().bump().commit().tag().push().publish().release().run();
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
//#endregion
|
|
2954
|
+
export { Changelog, ChildLogger, ComposedProjectManifest, DEFAULT_CHANGELOG_FILE, DEFAULT_PRE_PUBLISH_TAG, DEFAULT_PUBLISH_TAG, DEFAULT_TAG_PREFIX, GitRepositoryHosting, GithubHosting, Logger, MonorepoProject, NodeGithubActionProject, NpmProject, NpmWorkspacesProject, Octokit, PackageJsonManifest, PackageJsonMonorepoProject, PackageJsonProject, PnpmProject, PnpmWorkspacesProject, Project, ProjectManifest, Releaser, ReleaserCli, ReleaserGithubAction, defaultTypes, defineConfig, extractLastRelease, findFile, getInputOptions, getInputSkippedSteps, headerPartial, ifCommandComment, ifReleaseCommit, loadConfig, loadJavaScriptFile, preamblePartial, publish, template };
|