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