@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/cli.mjs CHANGED
@@ -1,1488 +1,926 @@
1
- import { bold, compareUrl, each, heading, link, list, newline, segments, small, words } from "@conventional-changelog/template";
2
- import { bumpPackageVersion, coloredStatus, eol, execAsync, getAuthenticatedUser, getGithubReleaseUrl, getGithubUrl, getLatestTag, getLogSince, getPackageInfo, getPackageUrl, getPublishedVersion, getRemoteList, getRemoteNames, getShortStatus, gitAddAll, gitAddSync, gitAddTracked, gitBranchCurrent, gitCommitMessage, gitPushBranch, gitPushTag, gitResetHardSync, gitTagAnnotated, gitTagDeleteSync, hasWriteAccess, isGitRepo, isOtpError, isPrerelease, isValidVersion, isWorkingTreeClean, parseGitHubRepo, parseVersion, pingRegistry, publishPackage, registryArg, resolveChangelogRange, resolvePublishTag, runNpm } from "@peiyanlu/cli-utils";
3
- import { copyDir, createMatcher, createTempDir, editFile, normalizePath, readJsonFile, readJsonFileSync, writeJsonFile } from "@peiyanlu/node-utils";
4
- import { castArray, dedent, isBoolean, isEmpty, isEmptyObject, isFunction, isNotEmpty, isNumber, isPlainObject, isUndefined, isZero, nonNullable, omit, partition, sleep, unset } from "@peiyanlu/ts-utils";
5
- import { dim, green, red, rgb, underline, yellow } from "ansis";
1
+ import path, { join, relative, resolve } from "node:path";
2
+ import { fileURLToPath, pathToFileURL } from "node:url";
3
+ import { cancel, confirm, group, intro, isCancel, log, note, outro, select, text } from "@clack/prompts";
4
+ import { newline } from "@conventional-changelog/template";
5
+ import { gitAdd, gitAddSync, gitBranch, gitBranchCurrent, gitCheckoutBranch, gitCheckoutBranchForce, gitCommitMessage, gitConfigGet, gitConfigSet, gitFetch, gitLsRemote, gitPush, gitPushBranch, gitRevParse, gitTag, isGitRepo, isPrerelease, isValidVersion, isWorkingTreeClean } from "@peiyanlu/cli-utils";
6
+ import { readJsonFileSync } from "@peiyanlu/node-utils";
7
+ import { assert, equals, isString, isUndefined, omit, throwError } from "@peiyanlu/ts-utils";
8
+ import { blue, blueBright, cyan, cyanBright, dim, green, greenBright, magenta, magentaBright, red, redBright, yellow } from "ansis";
6
9
  import { program } from "commander";
7
- import { createReadStream, createWriteStream, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
8
- import { basename, dirname, join, resolve } from "node:path";
9
- import { cancel, confirm, group, intro, isCancel, log, outro, select, tasks, text } from "@clack/prompts";
10
- import { publint } from "publint";
11
- import { formatMessage } from "publint/utils";
12
- import { inc, neq } from "semver";
13
- import { pathToFileURL } from "node:url";
14
- import { inspect } from "node:util";
15
- import { ConventionalChangelog } from "conventional-changelog";
16
- import createPreset from "conventional-changelog-conventionalcommits";
17
- import { Bumper } from "conventional-recommended-bump";
18
- import { writeFile } from "node:fs/promises";
19
- import { finished } from "node:stream/promises";
20
- import { Octokit } from "@octokit/rest";
21
- import { contentType } from "mime-types";
22
- import open from "open";
23
- import { glob, globSync } from "tinyglobby";
24
- import { parse } from "yaml";
25
- //#region package.json
26
- var name = "@peiyanlu/release";
27
- var version = "1.3.0";
28
- var description = "Release project tool.";
29
- //#endregion
30
- //#region src/config.ts
31
- const findConfigFile = (cwd = process.cwd()) => {
32
- for (const ext of [
33
- "ts",
34
- "mts",
35
- "js",
36
- "mjs"
37
- ]) {
38
- const path = resolve(cwd, `release.config.${ext}`);
39
- if (existsSync(path)) return path;
40
- }
41
- };
42
- const loadConfig = async (file) => {
10
+ import Handlebars from "handlebars";
11
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { mkdir } from "node:fs/promises";
13
+ import "conventional-changelog";
14
+ import "conventional-changelog-conventionalcommits";
15
+ import "conventional-recommended-bump";
16
+ import { inc, lt } from "semver";
17
+ import "@octokit/rest";
18
+ import "open";
19
+ import "tinyglobby";
20
+ import "yaml";
21
+ import "@actions/github";
22
+ import "@actions/core";
23
+ // node_modules/.pnpm/tsdown@0.23.4_publint@0.3.25_typescript@6.0.3/node_modules/tsdown/esm-shims.js
24
+ const getFilename = () => fileURLToPath(import.meta.url);
25
+ const getDirname = () => path.dirname(getFilename());
26
+ const __dirname = /* @__PURE__ */ getDirname();
27
+ // src/config/index.ts
28
+ const loadJavaScriptFile = async (file) => {
43
29
  const { href } = pathToFileURL(file);
44
30
  const mod = await import(href);
45
- const temp = mod.default ?? mod;
31
+ const temp = "default" in mod ? mod.default : mod;
46
32
  return typeof temp === "function" ? temp() : temp;
47
33
  };
48
- const resolveConfig = async (cwd) => {
49
- const configFile = findConfigFile(cwd);
50
- if (!configFile) return {
51
- configFile: "",
34
+ const findFile = (cwd, filename, extensions) => {
35
+ for (const ext of extensions) {
36
+ const path = resolve(cwd, `${filename}.${ext}`);
37
+ if (existsSync(path)) return path;
38
+ }
39
+ };
40
+ const loadConfig = async (cwd = process.cwd()) => {
41
+ const file = findFile(cwd, "release.config", [
42
+ "ts",
43
+ "mts",
44
+ "js",
45
+ "mjs"
46
+ ]);
47
+ if (!file) return {
48
+ file: "",
52
49
  config: {}
53
50
  };
54
51
  return {
55
- configFile,
56
- config: await loadConfig(configFile)
52
+ file,
53
+ config: await loadJavaScriptFile(file)
57
54
  };
58
55
  };
59
- const mergeConfig = (defaults, overrides) => {
60
- const result = {};
61
- for (const key of Object.keys(defaults)) {
62
- const defaultValue = defaults[key];
63
- const overrideValue = overrides[key];
64
- if (isUndefined(overrideValue)) if (isFunction(defaultValue)) result[key] = defaultValue;
65
- else if (isPlainObject(defaultValue)) result[key] = mergeConfig(defaultValue, {});
66
- else result[key] = structuredClone(defaultValue);
67
- else if (isPlainObject(defaultValue) && isPlainObject(overrideValue)) result[key] = mergeConfig(defaultValue, overrideValue);
68
- else if (isFunction(overrideValue)) result[key] = overrideValue;
69
- else result[key] = structuredClone(overrideValue);
56
+ // src/core/utils.ts
57
+ const when = (condition, ...value) => condition ? [...value] : [];
58
+ // src/core/logger.ts
59
+ const defaultPrinter = ({ dryRun, stage, message }) => {
60
+ console.log(`${dryRun ? `[dry-run]` : ""}[${stage}]:`, message);
61
+ };
62
+ /** 一个子消息记录器,绑定到特定阶段 */
63
+ var ChildLogger = class {
64
+ parent;
65
+ stage;
66
+ /**
67
+ * 创建子日志记录器。
68
+ * @param parent - 要继承其选项的父日志记录器。
69
+ * @param stage - 此日志记录器所属的处理阶段。
70
+ */
71
+ constructor(parent, stage) {
72
+ this.parent = parent;
73
+ this.stage = stage;
74
+ }
75
+ /**
76
+ * 记录一条普通信息。
77
+ * @param message - 要记录的信息。
78
+ */
79
+ info(message) {
80
+ this.parent.info(this.stage, message);
81
+ }
82
+ /**
83
+ * 记录一条详细信息。
84
+ * @param message - 要记录的信息。
85
+ */
86
+ verbose(message) {
87
+ this.parent.verbose(this.stage, message);
70
88
  }
71
- return result;
72
89
  };
73
- //#endregion
74
- //#region src/messages.ts
75
- const n = (b) => b ? "(dry run)" : "";
76
- const t = () => green(`${Math.floor(process.uptime())}s`);
77
- const MSG = {
78
- INTRO: (dryRun) => `🚀 Starting release ${n(dryRun)}`,
79
- INTRO_PREPARE: (dryRun) => `🚀 Starting prepare release ${n(dryRun)}`,
80
- OUTRO: (dryRun) => `🎉 Release finished successfully in ${t()} ${n(dryRun)}`,
81
- OUTRO_PREPARE: (dryRun) => `🎉 Prepare release finished successfully in ${t()} ${n(dryRun)}`,
82
- LOG: {
83
- SHOW_VERSION: (version) => `🎉 Released ${green(version)}`,
84
- SHOW_CHANGELOG: "🎉 Changelog collected",
85
- CHANGELOG_EMPTY: "No commits found since last release",
86
- CHANGES_EMPTY: "Working tree clean. No files changed",
87
- ONLY_CHANGELOG: "🎉 Changelog generation completed"
88
- },
89
- CHECK: {
90
- GIT: {
91
- CHECKING: "Checking Git repository",
92
- CHECKED: (name, url) => `Git remote resolved: ${url ? `${dim(name)} → ${underline(dim(url))}` : "not found"}`
93
- },
94
- NPM: {
95
- CHECKING: "Checking npm registry",
96
- CHECKED: (registry, msg) => `NPM registry: ${underline(dim(registry))} ${dim(msg)}`
97
- },
98
- GITHUB: {
99
- CHECKING: "Checking github repository",
100
- CHECKED: (repository) => `GitHub repository: ${underline(dim(repository))}`
90
+ /** 消息记录器 */
91
+ var Logger = class {
92
+ options;
93
+ printer;
94
+ colors;
95
+ colorMap = /* @__PURE__ */ new Map();
96
+ constructor(options = {}) {
97
+ this.options = options;
98
+ const { colors = [], printer = defaultPrinter } = options;
99
+ this.colors = colors;
100
+ this.printer = printer;
101
+ }
102
+ info(stage, message) {
103
+ const { silent, dryRun } = this.options;
104
+ if (!silent) {
105
+ const color = this.getColor(stage);
106
+ this.printer({
107
+ dryRun,
108
+ stage,
109
+ message,
110
+ color
111
+ });
101
112
  }
102
- },
103
- TASK: {
104
- VERSION: {
105
- START: "Bumping version",
106
- END: (to) => `Version bumped: ${underline(dim(to))}`
107
- },
108
- CHANGELOG: {
109
- START: "Generating changelog",
110
- END: `Changelog generated`
111
- },
112
- NPM: {
113
- START: "Publishing npm package",
114
- END: (url) => url ? `Published npm package: ${underline(dim(url))}` : "NPM publish skipped"
115
- },
116
- GIT: {
117
- START: "Creating git commit, tag & push",
118
- END: (push) => push ? "Git commit, tag & push completed" : "Git commit, tag & push skipped"
119
- },
120
- GITHUB: {
121
- START: "Creating GitHub release",
122
- END: (url) => url ? `Created GitHub release: ${underline(dim(url))}` : "GitHub release skipped"
113
+ }
114
+ verbose(stage, message) {
115
+ const { silent, verbose, dryRun } = this.options;
116
+ if (!silent && (verbose || dryRun)) {
117
+ const color = this.getColor(stage);
118
+ this.printer({
119
+ dryRun,
120
+ stage,
121
+ message,
122
+ color
123
+ });
123
124
  }
124
- },
125
- PROMPT: {
126
- SELECT_PACKAGE: "Select package release:",
127
- SELECT_VERSION: "Select version bump:",
128
- INPUT_VERSION: "Enter custom version:",
129
- NPM_PUBLISH: (pkg) => `Publish to npm ${underline(dim`(${pkg})`)}?`,
130
- NPM_OTP: "Enter npm one-time password (OTP):",
131
- GITHUB_RELEASE: (title) => `Create a new GitHub release ${underline(dim`(${title})`)}?`,
132
- GIT_COMMIT: (msg) => `Create git commit ${underline(dim`(${msg})`)}?`,
133
- GIT_TAG: (msg) => `Create git tag ${underline(dim`(${msg})`)}?`,
134
- GIT_PUSH: "Push commits and tags to remote?"
135
- },
136
- ERROR: {
137
- GIT_WORKDIR: `[git] Working tree must be clean`,
138
- GIT_REGISTRY: (name) => `[git] Repository not found for package "${dim(name)}"`,
139
- GIT_REMOTE: (name) => `[git] Remote "${dim(name)}" not found`,
140
- GITHUB_TAG_EXIT: (tag) => `[github] Release for tag "${tag}" already exists and is not a draft`,
141
- GITHUB_TOKEN: (tokenRef) => `[github] Missing GitHub token. Please set ${yellow(tokenRef ?? "GITHUB_TOKEN")}`,
142
- GITHUB_AUTH: "[github] Invalid GitHub token or insufficient permissions",
143
- GITHUB_USER: "[github] User does not have permission to create releases",
144
- NPM_REGISTRY: (registry) => `[npm] Unable to reach npm registry ${underline(dim(registry))}`,
145
- NPM_AUTH: `[npm] Not authenticated with npm. Please run ${yellow("npm login")} and try again`,
146
- NPM_USER: (user, name) => `[npm] User ${dim(user)} is not a collaborator of ${dim(name)}`
147
- },
148
- ABORT: {
149
- CANCEL: "Operation cancelled",
150
- MONOREPO_CI_NO_PACKAGE: "CI mode requires a target package in monorepo. Please specify it via \"--package <pkg>\"",
151
- MONOREPO_NO_PACKAGES: "Monorepo detected, but no packages found. Please configure \"config.packages\""
152
- },
153
- INFO: {
154
- TOOL: (name, version) => `${name} ${dim(`v${version}`)}`,
155
- CONFIG: (path) => `config file: ${underline(dim(path || "not found"))}`
156
125
  }
157
- };
158
- //#endregion
159
- //#region src/git/commit.ts
160
- const gitCheck = async (ctx, config) => {
161
- const { pkg: { name } } = ctx;
162
- const { git: { requireRepository, requireRemote, requireCleanWorkingTree } } = config;
163
- if (!await isGitRepo()) if (requireRepository) throw new Error(MSG.ERROR.GIT_REGISTRY(name));
164
- else {
165
- ctx.noGit = true;
166
- return;
126
+ createChild(stage) {
127
+ return new ChildLogger(this, stage);
167
128
  }
168
- if (requireCleanWorkingTree && !await isWorkingTreeClean()) throw new Error(MSG.ERROR.GIT_WORKDIR);
169
- const remote = (await getRemoteList())[0];
170
- const { name: remoteName = "origin", url: remoteUrl } = remote ?? {};
171
- if (!remote || !remoteUrl) {
172
- if (requireRemote) throw new Error(MSG.ERROR.GIT_REMOTE(remoteName));
129
+ getColor(stage) {
130
+ const cached = this.colorMap.get(stage);
131
+ if (cached) return cached;
132
+ const index = this.colorMap.size % this.colors.length;
133
+ const color = this.colors[index] ?? ((text) => text);
134
+ this.colorMap.set(stage, color);
135
+ return color;
173
136
  }
174
- Object.assign(ctx.git, {
175
- remoteName,
176
- remoteUrl
177
- });
178
137
  };
179
- const commitAndTag = async (ctx, config) => {
180
- const { git: { currentTag, commitMessage, tagMessage }, dryRun } = ctx;
181
- const { git: { commit, tag, push, addUntrackedFiles, commitArgs, tagArgs, pushArgs } } = config;
182
- if (!commit) return;
183
- const args = dryRun ? ["--dry-run"] : [];
184
- addUntrackedFiles ? await gitAddAll(args) : await gitAddTracked(args);
185
- await gitCommitMessage(commitMessage, [...commitArgs, ...args]);
186
- if (!dryRun) Object.assign(ctx.git, { isCommitted: true });
187
- if (tag) {
188
- await gitTagAnnotated(currentTag, tagMessage, [...tagArgs, ...args]);
189
- if (!dryRun) Object.assign(ctx.git, { isTagged: true });
138
+ // src/core/releaser.ts
139
+ var Releaser = class {
140
+ options;
141
+ project;
142
+ hosting;
143
+ logger;
144
+ stepsOptions = {};
145
+ skippedSteps = {};
146
+ state = {};
147
+ queue = [];
148
+ constructor(options) {
149
+ this.options = options;
150
+ const { project, hosting, logger, ...log } = options;
151
+ this.project = project;
152
+ this.hosting = hosting;
153
+ this.logger = logger ?? new Logger(log);
190
154
  }
191
- if (push) {
192
- const remotes = await getRemoteNames();
193
- for (const remoteName of remotes) {
194
- await gitPushTag(remoteName, currentTag, args);
195
- await gitPushBranch(remoteName, await gitBranchCurrent(), [...pushArgs, ...args]);
196
- }
197
- if (!dryRun) Object.assign(ctx.git, { isPushed: true });
155
+ enqueue(fn) {
156
+ this.queue.push(fn);
157
+ return this;
198
158
  }
199
- };
200
- const gitRollback = (ctx) => {
201
- const { git: { currentTag, isCommitted, isTagged, isPushed } } = ctx;
202
- if (isPushed) return;
203
- if (isTagged) gitTagDeleteSync(currentTag);
204
- gitResetHardSync(isCommitted ? 1 : 0);
205
- };
206
- //#endregion
207
- //#region src/utils.ts
208
- const info = (msg) => {
209
- console.log(`${rgb(33, 91, 184)(`i`)} ${msg}`);
210
- };
211
- const msg = (prefix, msg) => {
212
- log.message(`${rgb(33, 91, 184)(`[${prefix}]`)} ${dim(msg)}`);
213
- };
214
- const question = (msg, type) => {
215
- return `${{
216
- package: yellow,
217
- version: green,
218
- git: rgb(220, 94, 62),
219
- npm: rgb(186, 70, 61),
220
- github: rgb(64, 110, 228)
221
- }[type]`?`} ${msg}`;
222
- };
223
- const success = (msg, dryRun) => {
224
- return `${(dryRun ? rgb(123, 115, 66) : green)("✔")} ${msg}`;
225
- };
226
- const runLifeCycleHook = async (hooks, key, dryRun) => {
227
- const handler = hooks[key];
228
- if (typeof handler === "function") {
229
- if (!dryRun) await handler(log);
230
- log.success(success(`${dim`run`} ${inspect(handler)}`, dryRun));
231
- return;
159
+ setOptions(options) {
160
+ const logger = this.logger.createChild("set-options");
161
+ logger.verbose("Setting options for steps:");
162
+ logger.verbose(options);
163
+ for (const [key, value] of Object.entries(options)) Object.assign(this.stepsOptions[key] ??= {}, value);
164
+ logger.verbose("Final options for steps:");
165
+ logger.verbose(this.stepsOptions);
166
+ return this;
232
167
  }
233
- return Promise.allSettled(castArray(handler).filter(Boolean).map(async (hook) => {
234
- if (!dryRun) await execAsync(hook).catch(log.error);
235
- log.success(success(hook.replace(/^echo$/, dim("echo")), dryRun));
236
- }));
237
- };
238
- const diff = (from, to, separator = ".") => {
239
- const a = from.split(separator);
240
- return to.split(separator).map((v, i) => v === a[i] ? dim(v) : green(v)).join(separator);
241
- };
242
- const formatTemplate = async (ctx, config) => {
243
- const { pkg: { next }, selectedPkg } = ctx;
244
- const { git: { commitMessage: cm, tagMessage: tm }, github: { releaseName: rm }, toTag } = config;
245
- const f = (s, v) => s.replace("${tag}", v);
246
- const latestTag = await getLatestTag() ?? "";
247
- const currentTag = toTag(selectedPkg, next);
248
- const commitMessage = f(cm, currentTag);
249
- const tagMessage = f(tm, currentTag);
250
- const releaseName = f(rm, currentTag);
251
- Object.assign(ctx.git, {
252
- latestTag,
253
- currentTag,
254
- commitMessage,
255
- tagMessage
256
- });
257
- Object.assign(ctx.github, { releaseName });
258
- };
259
- const abortTask = (msg) => {
260
- cancel(`${red("✕")} ${msg}`);
261
- process.exit(0);
262
- };
263
- const abortSinglePrompt = (value) => {
264
- if (isCancel(value)) abortTask(MSG.ABORT.CANCEL);
265
- };
266
- const abortGroupPrompt = (ctx) => {
267
- ctx && gitRollback(ctx);
268
- abortTask(MSG.ABORT.CANCEL);
269
- };
270
- const abortOnError = (err, ctx, exit = true) => {
271
- ctx && gitRollback(ctx);
272
- const msg = err.message.replace(/^\[(github|npm|git)]/, (a) => red(a));
273
- log.message(`\n${msg}\n`);
274
- exit && process.exit(1);
275
- };
276
- const taskEnd = (msg) => {
277
- outro(msg);
278
- process.exit(0);
279
- };
280
- //#endregion
281
- //#region src/version/prompts.ts
282
- const createVersionMap = (current) => ({
283
- patch: inc(current, "patch"),
284
- minor: inc(current, "minor"),
285
- major: inc(current, "major"),
286
- release: inc(current, "release"),
287
- prerelease: inc(current, "prerelease"),
288
- prepatch: inc(current, "prepatch", "beta"),
289
- preminor: inc(current, "preminor", "beta"),
290
- premajor: inc(current, "premajor", "beta")
291
- });
292
- const option = (label, value, hint) => ({
293
- label,
294
- value,
295
- hint: hint ?? value
296
- });
297
- const runVersionPrompts = async (ctx, config, inferred) => {
298
- const { pkg: { current, fromPreRelease } } = ctx;
299
- const selectVersion = async () => {
300
- const versions = createVersionMap(current);
301
- const options = [
302
- ...fromPreRelease ? [option("Pre-Release", "prerelease", versions.prerelease)] : [],
303
- option("Patch", "patch", versions.patch),
304
- option("Minor", "minor", versions.minor),
305
- option("Major", "major", versions.major),
306
- ...!fromPreRelease ? [
307
- option("Pre-Patch", "prepatch", versions.prepatch),
308
- option("Pre-Minor", "preminor", versions.preminor),
309
- option("Pre-Major", "premajor", versions.premajor)
310
- ] : [],
311
- option("As-Is", "ignore", current),
312
- option("Custom", "custom", "custom specified")
313
- ];
314
- const type = await select({
315
- message: question(MSG.PROMPT.SELECT_VERSION, "version"),
316
- options,
317
- initialValue: inferred ?? (fromPreRelease ? "prerelease" : "patch")
168
+ setUser(options) {
169
+ return this.enqueue(async () => {
170
+ const { options: { dryRun }, stepsOptions: { setUser } } = this;
171
+ const { username, email } = {
172
+ ...setUser,
173
+ ...options
174
+ };
175
+ const logger = this.logger.createChild("set-user");
176
+ if (this.shouldSkip("setUser")) {
177
+ logger.info("Skipping set git user.");
178
+ return;
179
+ }
180
+ logger.info("Setting git user...");
181
+ if (!username && !email) {
182
+ logger.info("No git user configuration provided.");
183
+ return;
184
+ }
185
+ if (username) {
186
+ logger.verbose(`Setting git user.name to "${username}".`);
187
+ if (!dryRun) await gitConfigSet("user.name", username);
188
+ }
189
+ if (email) {
190
+ logger.verbose(`Setting git user.email to "${email}".`);
191
+ if (!dryRun) await gitConfigSet("user.email", email);
192
+ }
318
193
  });
319
- abortSinglePrompt(type);
320
- return type;
321
- };
322
- const inputVersion = async () => {
323
- const version = await text({
324
- message: question(MSG.PROMPT.INPUT_VERSION, "version"),
325
- initialValue: current,
326
- validate: (val) => {
327
- if (isUndefined(val) || !isValidVersion(val)) return "Invalid version";
194
+ }
195
+ setSkippedSteps(options) {
196
+ const logger = this.logger.createChild("set-skipped-steps");
197
+ logger.verbose("Setting skipped for steps:");
198
+ logger.verbose(options);
199
+ Object.assign(this.skippedSteps, options);
200
+ logger.verbose("Final skipped for steps:");
201
+ logger.verbose(this.skippedSteps);
202
+ return this;
203
+ }
204
+ checkout(options) {
205
+ return this.enqueue(async () => {
206
+ const { options: { dryRun }, stepsOptions: { checkout } } = this;
207
+ const { branch: headBranch = "release-branch", fetch, force } = {
208
+ ...checkout,
209
+ ...options
210
+ };
211
+ const logger = this.logger.createChild("checkout");
212
+ if (this.shouldSkip("checkout")) {
213
+ logger.info("Skipping checkout.");
214
+ return;
215
+ }
216
+ logger.info(`Checking out branch "${headBranch}"...`);
217
+ this.state.headBranch = headBranch;
218
+ this.state.baseBranch = await this.getBaseBranch();
219
+ if (fetch) {
220
+ logger.verbose(`Fetching all commits and tags from the remote repository...`);
221
+ if (!dryRun) await gitFetch([
222
+ "--prune",
223
+ "--unshallow",
224
+ "--tags",
225
+ "--",
226
+ "origin",
227
+ "HEAD"
228
+ ]);
229
+ }
230
+ if (force) {
231
+ logger.verbose(`Force checking out branch "${headBranch}"...`);
232
+ if (!dryRun) await gitCheckoutBranchForce(headBranch);
233
+ } else {
234
+ logger.verbose(`Checking out branch "${headBranch}"...`);
235
+ if (!dryRun) await gitCheckoutBranch(headBranch);
328
236
  }
329
237
  });
330
- abortSinglePrompt(version);
331
- return version;
332
- };
333
- const resolveNextVersion = async (type, current) => {
334
- if (type === "ignore") return current;
335
- if (type === "custom") return inputVersion();
336
- return createVersionMap(current)[type];
337
- };
338
- return await resolveNextVersion(await selectVersion(), current);
339
- };
340
- //#endregion
341
- //#region src/defaults.ts
342
- const createDefaultContext = () => {
343
- return {
344
- isCI: false,
345
- release: false,
346
- showRelease: false,
347
- showChangelog: false,
348
- onlyChangelog: false,
349
- dryRun: false,
350
- noGit: false,
351
- noNpm: false,
352
- noGitHub: false,
353
- increment: "",
354
- isIncrement: false,
355
- configFileExists: false,
356
- selectedPkg: "",
357
- pkg: {
358
- current: "",
359
- next: "",
360
- name: "",
361
- isPrivate: false,
362
- fromPreRelease: false,
363
- toPreRelease: false,
364
- publishConfig: {
365
- access: "public",
366
- registry: "https://registry.npmjs.org"
367
- },
368
- preId: "beta",
369
- preBase: "0",
370
- scope: void 0
371
- },
372
- npm: {
373
- username: "",
374
- otp: "",
375
- tag: "latest"
376
- },
377
- git: {
378
- remoteName: "origin",
379
- remoteUrl: "",
380
- latestTag: "",
381
- previousTag: "",
382
- currentTag: "",
383
- isCommitted: false,
384
- isTagged: false,
385
- isPushed: false,
386
- commitMessage: "",
387
- tagMessage: ""
388
- },
389
- github: {
390
- url: "",
391
- username: "",
392
- repo: "",
393
- changelog: "",
394
- owner: "",
395
- isWeb: false,
396
- isReleased: false,
397
- releaseId: 0,
398
- releaseUrl: "",
399
- uploadUrl: "",
400
- discussionUrl: "",
401
- token: "",
402
- releaseName: ""
403
- }
404
- };
405
- };
406
- const createDefaultConfig = (isCI) => {
407
- return {
408
- hooks: {
409
- "before:bump": "",
410
- "after:bump": "",
411
- "before:publish": "",
412
- "after:publish": "",
413
- "before:push": "",
414
- "after:push": "",
415
- "before:release": "",
416
- "after:release": ""
417
- },
418
- isMonorepo: false,
419
- packages: [],
420
- getPkgDir(pkg) {
421
- return ".";
422
- },
423
- toTag(pkg, version) {
424
- return `v${version}`;
425
- },
426
- changelog: {
427
- tagPrefix(pkg) {
428
- return "";
429
- },
430
- releaseCount: 1,
431
- includeHidden: false,
432
- transformTypes: (t) => t
433
- },
434
- skipGit: false,
435
- skipNpm: false,
436
- skipGithub: false,
437
- git: {
438
- commit: isCI,
439
- tag: isCI,
440
- push: isCI,
441
- commitMessage: "chore(release): ${tag}",
442
- commitArgs: [],
443
- tagMessage: "${tag}",
444
- tagArgs: [],
445
- pushArgs: [],
446
- addUntrackedFiles: false,
447
- requireRemote: true,
448
- requireRepository: true,
449
- requireCleanWorkingTree: true
450
- },
451
- npm: {
452
- publish: isCI,
453
- publishArgs: [],
454
- skipChecks: false,
455
- cleanup: {
456
- packageJson: true,
457
- readme: false,
458
- removeTempDir: true
238
+ }
239
+ bump(options) {
240
+ return this.enqueue(async () => {
241
+ const { project, options: { dryRun }, stepsOptions: { bump } } = this;
242
+ const logger = this.logger.createChild("bump");
243
+ if (this.shouldSkip("bump")) {
244
+ logger.info("Skipping bump.");
245
+ return;
459
246
  }
460
- },
461
- github: {
462
- release: isCI,
463
- releaseName: "${tag}",
464
- autoGenerate: false,
465
- prerelease: false,
466
- draft: false,
467
- tokenRef: "GITHUB_TOKEN",
468
- assets: [],
469
- skipChecks: false,
470
- shortNotes: false
471
- }
472
- };
473
- };
474
- //#endregion
475
- //#region src/git/changetype.ts
476
- const defaultTypes = [
477
- {
478
- type: "feat",
479
- section: "✨ 新功能",
480
- effect: "bump",
481
- description: "新增功能"
482
- },
483
- {
484
- type: "feature",
485
- section: "✨ 新功能",
486
- effect: "bump",
487
- description: "新增功能"
488
- },
489
- {
490
- type: "fix",
491
- section: "🐛 Bug 修复",
492
- effect: "bump",
493
- description: "修复 bug"
494
- },
495
- {
496
- type: "perf",
497
- section: "⚡ 性能优化",
498
- effect: "bump",
499
- description: "提升性能"
500
- },
501
- {
502
- type: "revert",
503
- section: "⏪ 回退",
504
- effect: "bump",
505
- description: "回退到之前版本"
506
- },
507
- {
508
- type: "docs",
509
- section: "📝 文档",
510
- effect: "changelog",
511
- description: "文档更新"
512
- },
513
- {
514
- type: "style",
515
- section: "💄 样式调整",
516
- effect: "hidden",
517
- description: "样式或格式修改"
518
- },
519
- {
520
- type: "chore",
521
- section: "🧹 其他更新",
522
- effect: "hidden",
523
- description: "非功能性更改"
524
- },
525
- {
526
- type: "refactor",
527
- section: "♻️ 代码重构",
528
- effect: "changelog",
529
- description: "重构代码"
530
- },
531
- {
532
- type: "test",
533
- section: "🧪 测试",
534
- effect: "hidden",
535
- description: "测试更新"
536
- },
537
- {
538
- type: "build",
539
- section: "🏗️ 构建系统",
540
- effect: "hidden",
541
- description: "构建工具和系统修改"
542
- },
543
- {
544
- type: "ci",
545
- section: "🤖 CI",
546
- effect: "hidden",
547
- description: "持续集成配置"
548
- },
549
- {
550
- type: "security",
551
- section: "🛡️ 安全修复",
552
- effect: "changelog",
553
- description: "安全性更新"
554
- },
555
- {
556
- type: "hotfix",
557
- section: "🚑 热修复",
558
- effect: "changelog",
559
- description: "紧急修复"
560
- },
561
- {
562
- type: "i18n",
563
- section: "🌐 国际化",
564
- effect: "changelog",
565
- description: "国际化相关更新"
566
- },
567
- {
568
- type: "ux",
569
- section: "🎨 用户体验",
570
- effect: "changelog",
571
- description: "用户体验改进"
572
- },
573
- {
574
- type: "deps",
575
- section: "📦 依赖更新",
576
- effect: "changelog",
577
- description: "依赖版本变更"
578
- },
579
- {
580
- type: "tool",
581
- section: "🛠️ 工具链",
582
- effect: "hidden",
583
- description: "工具链变更"
584
- },
585
- {
586
- type: "config",
587
- section: "⚙️ 配置",
588
- effect: "hidden",
589
- description: "配置文件更新"
247
+ logger.info("Bumping version...");
248
+ const done = await project.bump({
249
+ dryRun,
250
+ logger,
251
+ ...bump,
252
+ ...options
253
+ });
254
+ this.state.bump = done;
255
+ if (!done) logger.info("No version changes detected.");
256
+ });
590
257
  }
591
- ];
592
- //#endregion
593
- //#region src/git/templates.ts
594
- const isLikelyHash = (value) => /^[0-9a-f]{7,40}$/i.test(value);
595
- function headerPartial(context) {
596
- const { isPatch, linkCompare, version, title, date, previousTag } = context;
597
- const fullTitle = words(linkCompare && !isLikelyHash(previousTag) ? link(version, compareUrl(context)) : version, title && `"${title}"`, date && `(${date})`);
598
- return heading(2, isPatch ? small(fullTitle) : fullTitle);
599
- }
600
- function preamblePartial(contest) {
601
- const { preamble, commitGroups, noteGroups } = contest;
602
- return segments(preamble, isEmpty(preamble) && isEmpty(commitGroups) && isEmpty(noteGroups) && "Version bump without any changes.");
603
- }
604
- function template(context) {
605
- const { headerPartial, preamblePartial, commitPartial, footerPartial, noteGroups, commitGroups } = context;
606
- return segments(headerPartial(context), preamblePartial(context), each(noteGroups, (group) => segments(heading(3, words("⚠", group.title)), list(group.notes, (note) => {
607
- 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());
608
- })), newline(2)), each(commitGroups, (group) => segments(group.title && heading(3, group.title), list(group.commits, (commit) => commitPartial(context, commit))), newline(2)), footerPartial(context));
609
- }
610
- //#endregion
611
- //#region src/git/changelog.ts
612
- const parsePreset = (options = {}) => {
613
- const { includeHidden, transformTypes, scope } = options;
614
- let types = [...defaultTypes];
615
- if (includeHidden) types.forEach((type) => {
616
- if (type.effect === "hidden") type.effect = "changelog";
617
- });
618
- types = transformTypes?.(types) ?? types;
619
- const preset = createPreset({
620
- types,
621
- scope
622
- });
623
- preset.writer ??= {};
624
- preset.writer.preamblePartial = preamblePartial;
625
- preset.writer.headerPartial = headerPartial;
626
- preset.writer.template = template;
627
- return preset;
628
- };
629
- const inferReleaseType = async (options) => {
630
- const preset = parsePreset(options);
631
- const res = await new Bumper().config(preset).bump(preset.whatBump);
632
- if (!("releaseType" in res)) return void 0;
633
- const { releaseType, reason } = res;
634
- return options?.json ? {
635
- releaseType,
636
- reason
637
- } : releaseType;
638
- };
639
- const createGenerator = async (options) => {
640
- const { getPkgDir, tagPrefix, releaseCount = 1, includeHidden, transformTypes, scope } = options;
641
- const pkgDir = getPkgDir();
642
- return new ConventionalChangelog().readPackage(`${pkgDir}/package.json`).config(parsePreset({
643
- includeHidden,
644
- transformTypes,
645
- scope
646
- })).options({ releaseCount }).commits({ path: pkgDir }).tags({ prefix: tagPrefix });
647
- };
648
- const getChangelog = async (options) => {
649
- const generator = await createGenerator(options);
650
- let changelog = "";
651
- for await (const chunk of generator.write()) changelog += chunk;
652
- return changelog.trimEnd();
653
- };
654
- const generateChangelog = async (options) => {
655
- const { getPkgDir, releaseCount = 1 } = options;
656
- const infile = join(getPkgDir(), "CHANGELOG.md");
657
- if (!existsSync(infile)) await writeFile(infile, "");
658
- const exist = readFileSync(infile, "utf-8");
659
- const writeStream = createWriteStream(infile);
660
- const generator = await createGenerator(options);
661
- for await (const chunk of generator.write()) writeStream.write(chunk);
662
- const override = isZero(releaseCount);
663
- if (!(isZero(exist.trim().length) || override)) {
664
- writeStream.write(eol());
665
- writeStream.write(exist);
258
+ commit(options) {
259
+ return this.enqueue(async () => {
260
+ const { project, options: { dryRun }, state, stepsOptions: { commit } } = this;
261
+ const { sign, allowEmpty } = {
262
+ ...commit,
263
+ ...options
264
+ };
265
+ const logger = this.logger.createChild("commit");
266
+ if (this.shouldSkip("commit")) {
267
+ logger.info("Skipping commit.");
268
+ return;
269
+ }
270
+ logger.info(`Committing changes...`);
271
+ if (!state.bump) {
272
+ logger.info("No changes to commit.");
273
+ return;
274
+ }
275
+ const files = project.changedFiles;
276
+ const message = await project.getCommitMessage();
277
+ logger.verbose(`Files to commit:`);
278
+ files.forEach((file) => {
279
+ logger.verbose(`- ${file}`);
280
+ });
281
+ logger.verbose(message.includes("\n") ? `Commit message:\n\n${message}` : `Commit message: ${message}`);
282
+ if (!dryRun) {
283
+ await gitAdd(["--", ...files]);
284
+ await gitCommitMessage(message, [
285
+ ...when(sign, "--gpg-sign"),
286
+ ...when(allowEmpty, "--allow-empty"),
287
+ "--no-verify"
288
+ ]);
289
+ }
290
+ });
666
291
  }
667
- writeStream.end();
668
- await finished(writeStream);
669
- };
670
- //#endregion
671
- //#region src/git/prompts.ts
672
- const runGitPrompts = async (ctx, config) => {
673
- const { git: { currentTag, commitMessage }, configFileExists: cfe, dryRun, isCI } = ctx;
674
- const { git: { commit, tag, push } } = config;
675
- const logs = [];
676
- const shouldPrompt = (val) => isUndefined(val) || dryRun && !isCI;
677
- const { log: _, ...reset } = await group({
678
- commit: () => {
679
- return shouldPrompt(commit) ? confirm({ message: question(MSG.PROMPT.GIT_COMMIT(commitMessage), "git") }) : (() => {
680
- logs.push(yellow`git.commit`);
681
- return Promise.resolve(commit);
682
- })();
683
- },
684
- tag: ({ results: { commit } }) => {
685
- return shouldPrompt(tag) ? commit ? confirm({ message: question(MSG.PROMPT.GIT_TAG(currentTag), "git") }) : (() => {
686
- return Promise.resolve(false);
687
- })() : (() => {
688
- logs.push(yellow`git.tag`);
689
- return Promise.resolve(tag);
690
- })();
691
- },
692
- push: ({ results: { commit, tag } }) => {
693
- return shouldPrompt(push) ? commit || tag ? confirm({ message: question(MSG.PROMPT.GIT_PUSH, "git") }) : (() => {
694
- return Promise.resolve(false);
695
- })() : (() => {
696
- logs.push(yellow`git.push`);
697
- return Promise.resolve(push);
698
- })();
699
- },
700
- log: () => {
701
- if (logs.length) msg("GIT", `Using ${cfe ? "user" : "default"} config: ${logs.join(", ")}`);
702
- return Promise.resolve();
703
- }
704
- }, { onCancel: ({ results }) => {
705
- abortGroupPrompt(ctx);
706
- } });
707
- Object.assign(config.git, reset);
708
- };
709
- //#endregion
710
- //#region src/github/prompts.ts
711
- const runGithubPrompts = async (ctx, config) => {
712
- const { configFileExists: cfe, dryRun, github: { releaseName }, isCI } = ctx;
713
- const { github: { release } } = config;
714
- const logs = [];
715
- const shouldPrompt = (val) => isUndefined(val) || dryRun && !isCI;
716
- const res = await group({
717
- release: () => shouldPrompt(release) ? confirm({ message: question(MSG.PROMPT.GITHUB_RELEASE(releaseName), "github") }) : (() => {
718
- logs.push(yellow`github.release`);
719
- return Promise.resolve(release);
720
- })(),
721
- log: () => {
722
- if (logs.length) msg("GITHUB", `Using ${cfe ? "user" : "default"} config: ${logs.join(", ")}`);
723
- return Promise.resolve();
724
- }
725
- }, { onCancel: ({ results }) => {
726
- abortGroupPrompt(ctx);
727
- } });
728
- Object.assign(config.github, { release: res.release });
729
- return res;
730
- };
731
- //#endregion
732
- //#region src/github/release.ts
733
- const truncateBody = (body) => {
734
- if (body && body.length >= 124e3) return body.substring(0, 124e3) + "...";
735
- return body;
736
- };
737
- const getReleaseByTagSafe = async (octokit, options) => {
738
- try {
739
- const { data } = await octokit.repos.getReleaseByTag({ ...options });
740
- return data;
741
- } catch (e) {
742
- if (e?.status === 404) return null;
743
- throw e;
292
+ revert() {
293
+ return this.enqueue(async () => {
294
+ const { project, logger, options: { dryRun } } = this;
295
+ logger.info("revert", "Reverting version updates...");
296
+ if (!await project.revert({
297
+ dryRun,
298
+ logger: logger.createChild("revert")
299
+ })) {
300
+ logger.info("revert", "No version updates to revert.");
301
+ return;
302
+ }
303
+ this.state.bump = false;
304
+ });
744
305
  }
745
- };
746
- const createCRelease = async (octokit, options) => {
747
- const { owner, repo, tag, name, body, draft, prerelease, generateReleaseNotes, makeLatest, targetCommitish, discussionCategoryName } = options;
748
- const { data } = await octokit.repos.createRelease({
749
- owner,
750
- repo,
751
- tag_name: tag,
752
- name,
753
- body,
754
- draft,
755
- prerelease,
756
- generate_release_notes: generateReleaseNotes,
757
- make_latest: makeLatest,
758
- target_commitish: targetCommitish,
759
- discussion_category_name: discussionCategoryName
760
- });
761
- return data;
762
- };
763
- const updateCRelease = async (octokit, options) => {
764
- const { owner, repo, tag, name, body, draft, prerelease, generateReleaseNotes, makeLatest, targetCommitish, discussionCategoryName, releaseId } = options;
765
- const { data } = await octokit.repos.updateRelease({
766
- owner,
767
- repo,
768
- release_id: releaseId,
769
- tag_name: tag,
770
- name,
771
- body,
772
- draft,
773
- prerelease,
774
- generate_release_notes: generateReleaseNotes,
775
- make_latest: makeLatest,
776
- target_commitish: targetCommitish,
777
- discussion_category_name: discussionCategoryName
778
- });
779
- return data;
780
- };
781
- const createCReleaseByTag = async (octokit, options) => {
782
- const existing = await getReleaseByTagSafe(octokit, options);
783
- if (!existing) return createCRelease(octokit, { ...options });
784
- const { draft, id: releaseId } = existing;
785
- if (!draft) throw new Error(MSG.ERROR.GITHUB_TAG_EXIT(options.tag));
786
- return updateCRelease(octokit, {
787
- releaseId,
788
- ...options
789
- });
790
- };
791
- const uploadAssets = async (octokit, options) => {
792
- const { owner, repo, name, uploadUrl, releaseId, assets } = options;
793
- const files = await glob(assets);
794
- await Promise.all(files.map((file) => {
795
- return octokit.repos.uploadReleaseAsset({
796
- owner,
797
- repo,
798
- name,
799
- url: uploadUrl,
800
- release_id: releaseId,
801
- data: createReadStream(file),
802
- headers: {
803
- "content-type": contentType(file) || "application/octet-stream",
804
- "content-length": statSync(file).size
306
+ maintenanceBranch(options) {
307
+ return this.enqueue(async () => {
308
+ const { project, options: { dryRun }, stepsOptions: { maintenanceBranch } } = this;
309
+ const { enabled, force } = {
310
+ ...maintenanceBranch,
311
+ ...options
312
+ };
313
+ const logger = this.logger.createChild("maintenance-branch");
314
+ if (this.shouldSkip("maintenanceBranch")) {
315
+ logger.info("Skipping create maintenance branches.");
316
+ return;
317
+ }
318
+ logger.info("Creating maintenance branches...");
319
+ if (!enabled) {
320
+ logger.info("Maintenance branch creation is disabled.");
321
+ return;
322
+ }
323
+ logger.verbose("Fetching fresh tags from the remote repository...");
324
+ if (!dryRun) await gitFetch(["--tags"]);
325
+ const branches = await project.getMaintenanceBranches();
326
+ if (!branches.length) {
327
+ logger.info("No maintenance branches to create.");
328
+ return;
329
+ }
330
+ logger.verbose("Branches to create:");
331
+ for (const { from, to } of branches) {
332
+ logger.verbose(`- ${to} from ${from}`);
333
+ if (dryRun) continue;
334
+ if ((await gitRevParse(["--verify", `refs/heads/${to}`]) || await gitLsRemote([
335
+ "--heads",
336
+ "origin",
337
+ to
338
+ ])) && !force) {
339
+ logger.info(`Branch ${to} already exists, skipping.`);
340
+ continue;
341
+ }
342
+ const args = when(force, "--force");
343
+ await gitBranch([
344
+ ...args,
345
+ "--",
346
+ to,
347
+ from
348
+ ]);
349
+ await gitPushBranch("origin", to, ["--no-verify", ...args]);
805
350
  }
806
351
  });
807
- }));
808
- };
809
- const createWebRelease = async (options) => {
810
- const { owner, repo, tag, title, body, prerelease } = options;
811
- const url = new URL(`${getGithubUrl(owner, repo)}/releases/new`);
812
- Object.entries({
813
- tag,
814
- title,
815
- body,
816
- prerelease
817
- }).forEach(([key, value]) => {
818
- url.searchParams.set(key, value);
819
- });
820
- await open(url.toString());
821
- await sleep(1e3 * 1.5);
822
- };
823
- const isGitHubActions = Boolean(process.env.GITHUB_ACTIONS);
824
- const githubCheck = async (ctx, config) => {
825
- const { git: { remoteUrl } } = ctx;
826
- const { github: { tokenRef, skipChecks } } = config;
827
- if (!remoteUrl || isEmpty(parseGitHubRepo(remoteUrl))) {
828
- ctx.noGitHub = true;
829
- return;
830
352
  }
831
- const [owner, repo] = parseGitHubRepo(remoteUrl);
832
- const url = getGithubUrl(owner, repo);
833
- Object.assign(ctx.github, {
834
- owner,
835
- repo,
836
- url
837
- });
838
- const token = tokenRef ? process.env[tokenRef] : process.env.GITHUB_TOKEN;
839
- if (!token) {
840
- Object.assign(ctx.github, { isWeb: true });
841
- if (isGitHubActions) throw new Error(MSG.ERROR.GITHUB_TOKEN(tokenRef));
842
- return;
353
+ tag(options) {
354
+ return this.enqueue(async () => {
355
+ const { project, options: { dryRun }, stepsOptions: { tag } } = this;
356
+ const { fetch, sign } = {
357
+ ...tag,
358
+ ...options
359
+ };
360
+ const logger = this.logger.createChild("tag");
361
+ if (this.shouldSkip("tag")) {
362
+ logger.info("Skipping tag.");
363
+ return;
364
+ }
365
+ logger.info("Tagging version...");
366
+ if (fetch) {
367
+ logger.verbose("Fetching fresh tags from the remote repository...");
368
+ if (!dryRun) await gitFetch(["--tags"]);
369
+ }
370
+ const tags = await project.getTags();
371
+ this.state.tags = tags.length > 0;
372
+ if (!this.state.tags) {
373
+ logger.info("No version changes detected.");
374
+ return;
375
+ }
376
+ logger.verbose(`Tags to create:`);
377
+ for (const tag of tags) {
378
+ logger.verbose(`- ${tag}`);
379
+ if (!dryRun) await gitTag([
380
+ sign ? "--sign" : "--annotate",
381
+ "--message",
382
+ tag,
383
+ tag
384
+ ]);
385
+ }
386
+ });
843
387
  }
844
- Object.assign(ctx.github, { token });
845
- if (!isGitHubActions && !skipChecks) {
846
- const octokit = new Octokit({ auth: token });
847
- let username = "";
848
- try {
849
- const { data: { login } } = await octokit.users.getAuthenticated();
850
- username = login;
851
- } catch (e) {
852
- throw new Error(MSG.ERROR.GITHUB_AUTH);
853
- }
854
- try {
855
- await octokit.repos.checkCollaborator({
856
- owner,
857
- repo,
858
- username
388
+ push(options) {
389
+ return this.enqueue(async () => {
390
+ const { options: { dryRun }, state, stepsOptions: { push } } = this;
391
+ const { force } = {
392
+ ...push,
393
+ ...options
394
+ };
395
+ const logger = this.logger.createChild("push");
396
+ if (this.shouldSkip("push")) {
397
+ logger.info("Skipping push.");
398
+ return;
399
+ }
400
+ const branch = await gitBranchCurrent();
401
+ logger.info(`Pushing changes to "${branch}"...`);
402
+ if (!state.bump && !state.tags) {
403
+ logger.info("Nothing to push.");
404
+ return;
405
+ }
406
+ if (!dryRun) {
407
+ if (state.bump) await gitPushBranch("origin", branch, [...when(force, "--force"), "--no-verify"]);
408
+ if (state.tags) await gitPush([
409
+ "origin",
410
+ "--tags",
411
+ "--no-verify"
412
+ ]);
413
+ }
414
+ });
415
+ }
416
+ publish(options) {
417
+ return this.enqueue(async () => {
418
+ const { project, options: { dryRun }, stepsOptions: { publish } } = this;
419
+ const logger = this.logger.createChild("publish");
420
+ if (this.shouldSkip("publish")) {
421
+ logger.info("Skipping publish.");
422
+ return;
423
+ }
424
+ logger.info("Publishing...");
425
+ const tagsMap = await project.getDefaultPublishTag();
426
+ const name = await project.manifest.getName();
427
+ const tag = tagsMap.get(name);
428
+ await project.publish({
429
+ dryRun,
430
+ logger,
431
+ tag,
432
+ ...publish,
433
+ ...options
859
434
  });
860
- } catch (e) {
861
- throw new Error(MSG.ERROR.GITHUB_USER);
862
- }
435
+ });
863
436
  }
864
- };
865
- const createRelease = async (ctx, config) => {
866
- const { pkg: { name, toPreRelease }, git: { currentTag }, isCI, dryRun } = ctx;
867
- const { github: { isWeb, token, owner, repo, changelog, releaseName } } = ctx;
868
- const { github: { release, assets, prerelease, autoGenerate, draft } } = config;
869
- if (!release || dryRun) return;
870
- const octokit = new Octokit({ auth: token });
871
- if (isWeb) await createWebRelease({
872
- owner,
873
- repo,
874
- tag: currentTag,
875
- title: releaseName,
876
- body: truncateBody(changelog),
877
- prerelease: String(toPreRelease || prerelease)
878
- });
879
- else if (isCI) {
880
- const res = await createCReleaseByTag(octokit, {
881
- owner,
882
- repo,
883
- tag: currentTag,
884
- name: releaseName,
885
- body: autoGenerate ? "" : truncateBody(changelog),
886
- draft,
887
- prerelease: toPreRelease || prerelease,
888
- generateReleaseNotes: autoGenerate,
889
- makeLatest: "true"
437
+ release(option) {
438
+ const { hosting } = this;
439
+ if (!hosting) throwError("Hosting service is not configured. Please set the hosting option.");
440
+ return this.enqueue(async () => {
441
+ const { project, options: { dryRun }, stepsOptions: { release } } = this;
442
+ const logger = this.logger.createChild("release");
443
+ if (this.shouldSkip("release")) {
444
+ logger.info("Skipping release.");
445
+ return;
446
+ }
447
+ logger.info("Creating release...");
448
+ await hosting.createRelease({
449
+ project,
450
+ dryRun,
451
+ logger,
452
+ ...release,
453
+ ...option
454
+ });
890
455
  });
891
- if (!res) return;
892
- if (assets.length) {
893
- const { id: releaseId, upload_url: uploadUrl } = res;
894
- await uploadAssets(octokit, {
895
- owner,
896
- repo,
897
- name,
898
- uploadUrl,
899
- releaseId,
900
- assets
456
+ }
457
+ pullRequest(option) {
458
+ const { hosting } = this;
459
+ if (!hosting) throwError("Hosting service is not configured. Please set the hosting option.");
460
+ return this.enqueue(async () => {
461
+ const { project, state, options: { dryRun }, stepsOptions: { pullRequest } } = this;
462
+ const logger = this.logger.createChild("pull-request");
463
+ if (this.shouldSkip("pullRequest")) {
464
+ logger.info("Skipping pull request.");
465
+ return;
466
+ }
467
+ logger.info("Creating pull request...");
468
+ if (!state.bump) {
469
+ logger.info("No version changes detected, skipping pull request creation.");
470
+ return;
471
+ }
472
+ if (!state.headBranch) {
473
+ logger.info("Head branch is not set, skipping pull request creation.");
474
+ return;
475
+ }
476
+ await hosting.createPullRequest({
477
+ project,
478
+ dryRun,
479
+ logger,
480
+ from: state.headBranch,
481
+ to: await this.getBaseBranch(),
482
+ ...pullRequest,
483
+ ...option
901
484
  });
902
- }
485
+ });
903
486
  }
904
- };
905
- //#endregion
906
- //#region src/npm/prompts.ts
907
- const runNpmPublishPrompts = async (ctx, config) => {
908
- const { pkg: { name, next }, configFileExists: cfe, dryRun, isCI } = ctx;
909
- const { npm: { publish } } = config;
910
- const logs = [];
911
- const target = `${name}@${next}`;
912
- const shouldPrompt = (val) => isUndefined(val) || dryRun && !isCI;
913
- const res = await group({
914
- publish: () => shouldPrompt(publish) ? confirm({ message: question(MSG.PROMPT.NPM_PUBLISH(target), "npm") }) : (() => {
915
- logs.push(yellow`npm.publish`);
916
- return Promise.resolve(publish);
917
- })(),
918
- log: () => {
919
- if (logs.length) msg("NPM", `Using ${cfe ? "user" : "default"} config: ${logs.join(", ")}`);
920
- return Promise.resolve();
487
+ async run(condition) {
488
+ let shouldRun = true;
489
+ if (condition) {
490
+ const logger = this.logger.createChild("run");
491
+ logger.info("Checking condition...");
492
+ shouldRun = await condition(this);
493
+ if (shouldRun) logger.info("Condition passed, running release steps...");
494
+ else logger.info("Condition failed, skipping release steps.");
921
495
  }
922
- }, { onCancel: async ({ results }) => {
923
- abortGroupPrompt(ctx);
924
- } });
925
- Object.assign(config.npm, { publish: res.publish });
926
- return res;
496
+ if (shouldRun) for (const fn of this.queue) await fn();
497
+ const logger = this.logger.createChild("done");
498
+ this.queue.length = 0;
499
+ logger.info(`Done!`);
500
+ }
501
+ shouldSkip(step) {
502
+ return this.skippedSteps[step] === true;
503
+ }
504
+ async getBaseBranch() {
505
+ const { state: { baseBranch } } = this;
506
+ if (baseBranch) return baseBranch;
507
+ return gitBranchCurrent();
508
+ }
927
509
  };
928
- const runNpmOptPrompts = async (ctx, config) => {
929
- const res = await group({ otp: () => text({ message: MSG.PROMPT.NPM_OTP }) }, { onCancel: ({ results }) => {
930
- abortGroupPrompt(ctx);
931
- } });
932
- Object.assign(ctx.npm, { otp: res.otp });
933
- return res;
510
+ // src/releaser-cli/prompts.ts
511
+ const abortTask = (msg) => {
512
+ cancel(`${red("✕")} ${msg ?? "Release cancelled."}`);
513
+ process.exit(0);
934
514
  };
935
- //#endregion
936
- //#region src/npm/cleanup.ts
937
- const PUBLISH_CONFIG_FIELDS = [
938
- "bin",
939
- "main",
940
- "exports",
941
- "types",
942
- "typings",
943
- "module",
944
- "browser",
945
- "esnext",
946
- "es2015",
947
- "unpkg",
948
- "umd:main",
949
- "typesVersions",
950
- "cpu",
951
- "os",
952
- "engines"
953
- ];
954
- const IGNORE_FIELDS = [
955
- "babel",
956
- "browserslist",
957
- "c8",
958
- "commitlint",
959
- "devDependencies",
960
- "eslintConfig",
961
- "eslintIgnore",
962
- "husky",
963
- "jest",
964
- "lint-staged",
965
- "nano-staged",
966
- "pre-commit",
967
- "prettier",
968
- "pwmetrics",
969
- "remarkConfig",
970
- "renovate",
971
- "resolutions",
972
- "sharec",
973
- "simple-git-hooks",
974
- "simple-pre-commit",
975
- "size-limit",
976
- "typeCoverage",
977
- "yaspeller",
978
- "pnpm",
979
- "packageManager"
980
- ];
981
- const KEEP_SCRIPT_NAMES = [
982
- "preinstall",
983
- "install",
984
- "postinstall"
985
- ];
986
- const IGNORE_FILES = [
987
- ".DS_Store",
988
- ".git",
989
- ".vscode",
990
- ".idea",
991
- "node_modules"
992
- ];
993
- const clearPackageJSON = async (dir, options) => {
994
- const file = join(dir, "package.json");
995
- const json = await readJsonFile(file);
996
- const { ignoreFields = [], keepScripts = [] } = options ?? {};
997
- const mergePublishConfig = (json) => {
998
- if (!json.publishConfig) return json;
999
- const publishConfig = { ...json.publishConfig };
1000
- PUBLISH_CONFIG_FIELDS.forEach((field) => {
1001
- if (publishConfig[field]) {
1002
- json[field] = publishConfig[field];
1003
- delete publishConfig[field];
515
+ const singlePrompt = async (fn, abortMsg) => {
516
+ const value = await fn();
517
+ if (isCancel(value)) abortTask(abortMsg);
518
+ return value;
519
+ };
520
+ const question = (msg, dryRun) => {
521
+ return `${(dryRun ? yellow : red)("?")} ${msg}`;
522
+ };
523
+ const getVersionHint = async (project, options = {}) => await project.getNextVersion(options) ?? "No version change";
524
+ const createVersionOptions = async (project, options = {}, current, tagPrefix) => {
525
+ const firstRelease = await project.isFirstRelease({ tagPrefix });
526
+ if (firstRelease) return [
527
+ {
528
+ label: "Initial release",
529
+ value: "initial",
530
+ hint: `Release ${current}`,
531
+ bump: {
532
+ version: current,
533
+ firstRelease
1004
534
  }
1005
- });
1006
- return {
1007
- ...json,
1008
- publishConfig
1009
- };
1010
- };
1011
- const removeIgnoredFields = (json) => {
1012
- const [matched, unmatched] = partition([...ignoreFields, ...IGNORE_FIELDS], (i) => i.includes("."));
1013
- matched.forEach((field) => unset(json, field));
1014
- return omit(json, unmatched);
1015
- };
1016
- const cleanScripts = (json) => {
1017
- if (!json.scripts) return json;
1018
- const keep = [...keepScripts, ...KEEP_SCRIPT_NAMES];
1019
- const excludes = Object.keys(json.scripts).filter((k) => !keep.includes(k));
1020
- json.scripts = omit(json.scripts, excludes);
1021
- return json;
1022
- };
1023
- const removeEmptyObjects = (json) => {
1024
- return omit(json, Object.entries(json).filter(([k, v]) => isEmptyObject(v)).map(([k, v]) => k));
1025
- };
1026
- await writeJsonFile(file, removeEmptyObjects(cleanScripts(removeIgnoredFields(structuredClone(mergePublishConfig(json))))));
1027
- };
1028
- const cleanReadme = async (dir, homepage) => {
1029
- const file = join(dir, "README.md");
1030
- homepage && await editFile(file, (str) => {
1031
- return str.split(/^##\s+\S.*$/m)[0] + `## Docs
1032
-
1033
- Read full docs **[here](${homepage})**.`;
1034
- });
1035
- };
1036
- //#endregion
1037
- //#region src/npm/publish.ts
1038
- const canPublish = async (registry, cwd) => {
1039
- const res = await runNpm([
1040
- "publish",
1041
- "--dry-run",
1042
- "--access",
1043
- "public",
1044
- ...registryArg(registry)
1045
- ], {
1046
- error: "throw",
1047
- cwd
1048
- }).catch((err) => err);
1049
- const allowedErr = (err) => {
1050
- return [/previously published versions/i, /cannot publish over/i].some((reg) => reg.test(err.message));
1051
- };
1052
- if (!(res instanceof Error) || allowedErr(res)) return true;
1053
- throw res;
1054
- };
1055
- const npmCheck = async (ctx, config) => {
1056
- const { pkg: { name, current, publishConfig: { registry } }, selectedPkg } = ctx;
1057
- const { npm: { skipChecks }, getPkgDir } = config;
1058
- const tag = await resolvePublishTag(name, current);
1059
- Object.assign(ctx.npm, { tag });
1060
- if (skipChecks) return "";
1061
- const [pinged, username] = await Promise.all([
1062
- pingRegistry(registry),
1063
- getAuthenticatedUser(registry),
1064
- canPublish(registry, getPkgDir(selectedPkg))
1065
- ]);
1066
- if (!pinged) throw new Error(MSG.ERROR.NPM_REGISTRY(registry));
1067
- if (!username) throw new Error(MSG.ERROR.NPM_AUTH);
1068
- Object.assign(ctx.npm, { username });
1069
- const latest = await getPublishedVersion(`${name}@${tag}`);
1070
- if (latest) {
1071
- if (!await hasWriteAccess(name, username, registry)) throw new Error(MSG.ERROR.NPM_USER(username, name));
1072
- }
1073
- return latest ? `(${tag} → ${latest})` : `(${tag} → no published version)`;
1074
- };
1075
- const publishNpm = async (ctx, config) => {
1076
- const { dryRun, selectedPkg, npm: { otp, tag }, github: { url } } = ctx;
1077
- const { npm: { publish, publishArgs, cleanup: { packageJson, readme, removeTempDir } }, getPkgDir } = config;
1078
- if (!publish) return;
1079
- const otpArgs = otp ? ["--otp", otp] : [];
1080
- const dryRunArg = dryRun ? ["--dry-run"] : [];
1081
- const args = [
1082
- ...otpArgs,
1083
- ...publishArgs,
1084
- ...dryRunArg
535
+ },
536
+ {
537
+ label: "Custom",
538
+ value: "custom",
539
+ hint: "Specify an exact version",
540
+ bump: { firstRelease }
541
+ },
542
+ {
543
+ label: "As-is",
544
+ value: "ignore",
545
+ hint: `Do not update the version ${current}`,
546
+ bump: { version: current }
547
+ }
1085
548
  ];
1086
- if (packageJson || readme) {
1087
- const parent = resolve(getPkgDir(selectedPkg));
1088
- const temp = await createTempDir(parent);
1089
- await copyDir(parent, temp.path, { ignore: [(name) => {
1090
- return [temp.name, ...IGNORE_FILES].map((pattern) => createMatcher(pattern)).some((matcher) => matcher(name));
1091
- }] });
1092
- const options = typeof packageJson === "boolean" ? void 0 : packageJson;
1093
- packageJson && await clearPackageJSON(temp.path, options);
1094
- readme && await cleanReadme(temp.path, `${url}#readme`);
1095
- await publishPackage({
1096
- tag,
1097
- args,
1098
- cwd: temp.path
1099
- });
1100
- removeTempDir && await temp.remove();
1101
- } else await publishPackage({
1102
- tag,
1103
- args,
1104
- cwd: getPkgDir(selectedPkg)
549
+ const fromPreRelease = isPrerelease(current);
550
+ const candidates = [
551
+ {
552
+ label: fromPreRelease ? "Release" : "Patch",
553
+ value: "patch",
554
+ hint: await getVersionHint(project, {
555
+ ...options,
556
+ tagPrefix,
557
+ as: "patch"
558
+ }),
559
+ bump: { as: "patch" }
560
+ },
561
+ {
562
+ label: "Minor",
563
+ value: "minor",
564
+ hint: await getVersionHint(project, {
565
+ ...options,
566
+ tagPrefix,
567
+ as: "minor"
568
+ }),
569
+ bump: { as: "minor" }
570
+ },
571
+ {
572
+ label: "Major",
573
+ value: "major",
574
+ hint: await getVersionHint(project, {
575
+ ...options,
576
+ tagPrefix,
577
+ as: "major"
578
+ }),
579
+ bump: { as: "major" }
580
+ }
581
+ ];
582
+ if (fromPreRelease) candidates.unshift({
583
+ label: "Pre-release",
584
+ value: "prerelease",
585
+ hint: await getVersionHint(project, {
586
+ ...options,
587
+ tagPrefix,
588
+ as: "prerelease"
589
+ }),
590
+ bump: { as: "prerelease" }
1105
591
  });
1106
- };
1107
- //#endregion
1108
- //#region src/action.ts
1109
- var Action = class {
1110
- async handleRelease(cmdArgs, options) {
1111
- const { ctx, config } = await this.createContext(cmdArgs, options);
1112
- await this.checkTask(ctx, config);
1113
- await this.bumpTask(ctx, config);
1114
- await this.changelogTask(ctx, config);
1115
- await this.gitTask(ctx, config);
1116
- await this.npmTask(ctx, config);
1117
- await this.githubTask(ctx, config);
1118
- if (ctx.dryRun) gitRollback(ctx);
1119
- taskEnd(MSG.OUTRO(ctx.dryRun));
1120
- }
1121
- async handlePrepareRelease(cmdArgs, options) {
1122
- const { ctx, config } = await this.createContext(cmdArgs, options);
1123
- await this.checkTask(ctx, config);
1124
- await this.bumpTask(ctx, config);
1125
- await this.changelogTask(ctx, config);
1126
- await this.gitTask(ctx, config);
1127
- if (ctx.dryRun) gitRollback(ctx);
1128
- taskEnd(MSG.OUTRO_PREPARE(ctx.dryRun));
1129
- }
1130
- async createContext(cmdArgs, options) {
1131
- const { configFile, config } = await this.createConfig(options);
1132
- info(MSG.INFO.TOOL(name, version));
1133
- info(MSG.INFO.CONFIG(configFile));
1134
- console.log();
1135
- const { otp, package: defPkg, prepare, showChangelog, showRelease, ci, dryRun, onlyChangelog } = options;
1136
- intro(prepare ? MSG.INTRO_PREPARE(dryRun) : MSG.INTRO(dryRun));
1137
- const { isMonorepo, packages, getPkgDir, skipGit, skipNpm, skipGithub } = config;
1138
- if (isMonorepo) {
1139
- if (ci) {
1140
- if (!defPkg) abortTask(MSG.ABORT.MONOREPO_CI_NO_PACKAGE);
1141
- } else if (packages.length < 1) abortTask(MSG.ABORT.MONOREPO_NO_PACKAGES);
592
+ else for (const as of [
593
+ "patch",
594
+ "minor",
595
+ "major"
596
+ ]) candidates.push({
597
+ label: `Pre-${as[0].toUpperCase()}${as.slice(1)}`,
598
+ value: `pre${as}`,
599
+ hint: await getVersionHint(project, {
600
+ ...options,
601
+ tagPrefix,
602
+ as,
603
+ prerelease: "beta"
604
+ }),
605
+ bump: {
606
+ as,
607
+ prerelease: "beta"
1142
608
  }
1143
- const selectPackage = async () => {
1144
- const pkg = await select({
1145
- message: question(MSG.PROMPT.SELECT_PACKAGE, "package"),
1146
- options: packages.map((pkg) => ({
1147
- label: pkg,
1148
- value: pkg
1149
- }))
1150
- });
1151
- abortSinglePrompt(pkg);
1152
- return pkg;
1153
- };
1154
- const selectedPkg = !isMonorepo ? "" : ci ? defPkg : packages.length === 1 ? packages[0] : await selectPackage();
1155
- const { pkg, pkgDir } = getPackageInfo(selectedPkg, getPkgDir);
1156
- const { version: pkgVersion, private: pkgPrivate = false, name: pkgName, publishConfig = {} } = pkg;
1157
- const nextVersion = inc(pkgVersion, cmdArgs) ?? "";
1158
- const ctx = mergeConfig(createDefaultContext(), {
1159
- selectedPkg,
1160
- configFileExists: isNotEmpty(configFile),
1161
- dryRun,
1162
- showRelease,
1163
- showChangelog,
1164
- onlyChangelog,
1165
- npm: { otp: String(otp) },
1166
- increment: cmdArgs,
1167
- isIncrement: true,
1168
- isCI: [
1169
- ci,
1170
- showChangelog,
1171
- showRelease,
1172
- process.env.GITHUB_ACTIONS,
1173
- process.env.CI
1174
- ].some(Boolean),
1175
- pkg: {
1176
- name: pkgName,
1177
- isPrivate: pkgPrivate,
1178
- fromPreRelease: isPrerelease(pkgVersion),
1179
- current: pkgVersion,
1180
- next: nextVersion,
1181
- toPreRelease: false,
1182
- publishConfig: { ...publishConfig },
1183
- scope: isEmpty(selectedPkg) ? void 0 : selectedPkg
1184
- },
1185
- noGit: skipGit,
1186
- noNpm: pkgPrivate || skipNpm,
1187
- noGitHub: skipGit || skipGithub
609
+ });
610
+ candidates.push({
611
+ label: "Custom",
612
+ value: "custom",
613
+ hint: "Specify an exact version"
614
+ }, {
615
+ label: "As-is",
616
+ value: "ignore",
617
+ hint: `Do not update the version ${current}`,
618
+ bump: { version: current }
619
+ });
620
+ return candidates;
621
+ };
622
+ const runBumpPrompts = async (project, options = {}) => {
623
+ const { bump: bumpOpts = {}, run = {} } = options;
624
+ const bump = { ...bumpOpts };
625
+ const { dryRun = false, ci = false } = run;
626
+ if (bump.version || bump.as || bump.snapshot) return bump;
627
+ const runVersionPrompts = async (project, tagPrefix) => {
628
+ const promptsRes = {};
629
+ const inferred = await project.inferReleaseType({
630
+ ...bump,
631
+ tagPrefix
1188
632
  });
1189
- const { messages } = await publint({ pkgDir });
1190
- for (const message of messages) {
1191
- const formated = formatMessage(message, pkg);
1192
- formated && msg("PKG", formated);
633
+ const name = await project.manifest.getName();
634
+ const current = await project.manifest.getVersion();
635
+ let useCustom = false;
636
+ let customVersion = current;
637
+ if (project.isMonorepoMode() && project.isFixedMode) {
638
+ const { version: maxVersion, packages } = await project.getMaxVersion();
639
+ if (lt(current, maxVersion)) {
640
+ useCustom = true;
641
+ customVersion = inc(maxVersion, inferred ?? "patch");
642
+ note(`
643
+ Current version: ${current}
644
+ Highest package version: ${maxVersion}
645
+ Packages at this version:\n${packages.map((name) => `- ${name}`).join("\n")}
646
+
647
+ Minimum release version: ${maxVersion}
648
+ Suggested release version: ${customVersion} (used in CI)
649
+ `.trim(), "Monorepo version mismatch", { format(line) {
650
+ return line.trim();
651
+ } });
652
+ }
1193
653
  }
1194
- return {
1195
- ctx,
1196
- config
654
+ const versionOptions = await createVersionOptions(project, bump, current, tagPrefix);
655
+ const find = (val) => versionOptions.find((option) => option.value === val);
656
+ const fromPreRelease = isPrerelease(current);
657
+ const initialValue = useCustom ? "custom" : inferred && find(inferred) ? inferred : fromPreRelease && find("prerelease") ? "prerelease" : "patch";
658
+ const type = ci ? initialValue : await singlePrompt(() => select({
659
+ message: question(`Select "${cyan`${name}`}" release version type:`, dryRun),
660
+ options: versionOptions,
661
+ initialValue
662
+ }));
663
+ if (equals(type, "custom")) promptsRes.version = ci ? customVersion : await singlePrompt(() => text({
664
+ message: question("Enter release version:", dryRun),
665
+ initialValue: customVersion,
666
+ placeholder: customVersion,
667
+ validate: (val) => {
668
+ if (isUndefined(val) || !isValidVersion(val)) return "Invalid version.";
669
+ }
670
+ }));
671
+ const selected = find(type);
672
+ Object.assign(promptsRes, selected?.bump);
673
+ return promptsRes;
674
+ };
675
+ if (project.isMonorepoMode() && project.isIndependentMode) for await (const subProject of project.getProjects()) {
676
+ const name = await subProject.manifest.getName();
677
+ const scope = await project.getScope(name);
678
+ const tagPrefix = await project.getTagPrefix(scope);
679
+ const monoBump = bump;
680
+ (monoBump.byProject ??= {})[name] = {
681
+ ...monoBump.byProject?.[name],
682
+ ...await runVersionPrompts(subProject, tagPrefix)
1197
683
  };
1198
684
  }
1199
- async createConfig(options) {
1200
- const { ci, requireCleanWorkingTree, skipGit, skipNpm, skipGithub, isMonorepo, includeHidden, releaseCount } = options;
1201
- const toFalse = (value) => {
1202
- return isBoolean(value) && !value ? value : void 0;
1203
- };
1204
- const toNumber = (value) => {
1205
- return isNumber(value) ? value : void 0;
1206
- };
1207
- const defaultConfig = createDefaultConfig(ci ? true : void 0);
1208
- const { configFile, config: local } = await this.resolveUserConfig(process.cwd());
1209
- return {
1210
- configFile,
1211
- config: mergeConfig(mergeConfig(defaultConfig, local), {
1212
- changelog: {
1213
- releaseCount: toNumber(releaseCount),
1214
- includeHidden
1215
- },
1216
- git: { requireCleanWorkingTree: toFalse(requireCleanWorkingTree) },
1217
- skipGit,
1218
- skipNpm,
1219
- skipGithub,
1220
- isMonorepo
1221
- })
1222
- };
685
+ else {
686
+ const result = await runVersionPrompts(project);
687
+ Object.assign(bump, result);
1223
688
  }
1224
- async resolveUserConfig(cwd = process.cwd()) {
1225
- return resolveConfig(cwd);
689
+ return bump;
690
+ };
691
+ const getCliPromptOptions = async (options = {}) => {
692
+ const { dryRun = false, ci = false } = options;
693
+ const shouldPrompt = (v) => !ci && (isUndefined(v) || dryRun);
694
+ const res = await group({
695
+ commit: () => options.skipGit ? Promise.resolve(false) : shouldPrompt(options.skipGit) ? confirm({ message: question("Commit release changes?", dryRun) }) : Promise.resolve(true),
696
+ tag: ({ results: { commit } }) => !commit ? Promise.resolve(false) : shouldPrompt(options.skipGit) ? confirm({ message: question("Create release tags?", dryRun) }) : Promise.resolve(true),
697
+ push: ({ results: { commit } }) => !commit ? Promise.resolve(false) : shouldPrompt(options.skipGit) ? confirm({ message: question("Push release changes?", dryRun) }) : Promise.resolve(true),
698
+ publish: () => options.skipPublish ? Promise.resolve(false) : shouldPrompt(options.skipPublish) ? confirm({ message: question("Publish project?", dryRun) }) : Promise.resolve(true),
699
+ release: () => options.skipRelease ? Promise.resolve(false) : shouldPrompt(options.skipRelease) ? confirm({ message: question("Create repository release?", dryRun) }) : Promise.resolve(true)
700
+ }, { onCancel: () => {
701
+ abortTask();
702
+ } });
703
+ return {
704
+ commit: !res.commit,
705
+ tag: !res.tag,
706
+ push: !res.push,
707
+ publish: !res.publish,
708
+ release: !res.release
709
+ };
710
+ };
711
+ const runConfirmPrompts = async (ci = false, dryRun = false) => {
712
+ if (!(ci ? true : await singlePrompt(() => confirm({ message: question(red("Confirm start workflow?"), dryRun) })))) abortTask();
713
+ };
714
+ // src/releaser-cli/releaser.ts
715
+ const pkgJson$1 = readJsonFileSync(join(__dirname, "..", "package.json"));
716
+ var ReleaserCli = class extends Releaser {
717
+ constructor(options) {
718
+ const { silent, dryRun, verbose } = options;
719
+ super({
720
+ logger: new Logger({
721
+ silent,
722
+ dryRun,
723
+ verbose,
724
+ colors: [
725
+ blue,
726
+ blueBright,
727
+ cyan,
728
+ cyanBright,
729
+ green,
730
+ greenBright,
731
+ magenta,
732
+ magentaBright,
733
+ red,
734
+ redBright
735
+ ].sort(() => Math.random() - .5),
736
+ printer: ({ dryRun, stage, message, color }) => {
737
+ const msg = isString(message) ? message : JSON.stringify(message, null, 2);
738
+ log.message(dim`${dryRun ? yellow`[dry-run]` : ""}${color(`[${stage}]`)}: ${msg}`);
739
+ }
740
+ }),
741
+ ...options
742
+ });
1226
743
  }
1227
- async printChangelog(ctx, config) {
1228
- const { showChangelog, selectedPkg, noGit, isIncrement, pkg: { scope } } = ctx;
1229
- const { isMonorepo, getPkgDir, changelog: { tagPrefix, releaseCount, includeHidden, transformTypes } } = config;
1230
- const { github: { shortNotes } } = config;
1231
- if (noGit) return;
1232
- const match = isMonorepo ? `${tagPrefix?.(selectedPkg)}*` : "*";
1233
- const { from, to } = isZero(releaseCount) ? {
1234
- from: "",
1235
- to: "HEAD"
1236
- } : await resolveChangelogRange(isIncrement, match);
1237
- const commits = await getLogSince(from, to, getPkgDir(selectedPkg));
1238
- const changelog = await getChangelog({
1239
- getPkgDir: () => getPkgDir(selectedPkg),
1240
- tagPrefix: tagPrefix?.(selectedPkg),
1241
- scope: isEmpty(nonNullable([scope])) ? void 0 : nonNullable([scope]),
1242
- releaseCount,
1243
- includeHidden,
1244
- transformTypes
744
+ tag(options = {}) {
745
+ return super.tag({
746
+ fetch: true,
747
+ ...options
1245
748
  });
1246
- const short = `Please refer to [CHANGELOG.md](${getPkgDir(selectedPkg)}/CHANGELOG.md) for details.`;
1247
- Object.assign(ctx.github, { changelog: shortNotes ? short : changelog });
1248
- if (commits) {
1249
- msg("GIT", `Changelog(${from}...${to}):${eol(2)}` + changelog);
1250
- msg("GIT", `Commits(${from}...${to}):${eol(2)}` + commits);
1251
- if (showChangelog) taskEnd(MSG.LOG.SHOW_CHANGELOG);
1252
- } else msg("GIT", MSG.LOG.CHANGELOG_EMPTY);
1253
749
  }
1254
- async checkTask(ctx, config) {
1255
- const { dryRun, onlyChangelog } = ctx;
1256
- await tasks([
1257
- {
1258
- title: MSG.CHECK.GIT.CHECKING,
1259
- task: async () => {
1260
- await gitCheck(ctx, config);
1261
- const { git: { remoteName, remoteUrl } } = ctx;
1262
- return success(MSG.CHECK.GIT.CHECKED(remoteName, remoteUrl), dryRun);
1263
- },
1264
- enabled: !ctx.noGit
1265
- },
1266
- {
1267
- title: MSG.CHECK.NPM.CHECKING,
1268
- task: async () => {
1269
- const msg = await npmCheck(ctx, config);
1270
- const { pkg: { publishConfig: { registry } } } = ctx;
1271
- return success(MSG.CHECK.NPM.CHECKED(registry, msg), dryRun);
1272
- },
1273
- enabled: !(ctx.noNpm || onlyChangelog)
1274
- },
1275
- {
1276
- title: MSG.CHECK.GITHUB.CHECKING,
1277
- task: async () => {
1278
- await githubCheck(ctx, config);
1279
- const { github: { owner, repo } } = ctx;
1280
- const repository = getGithubUrl(owner, repo);
1281
- return success(MSG.CHECK.GITHUB.CHECKED(repository), dryRun);
1282
- },
1283
- enabled: !(ctx.noGitHub || onlyChangelog)
1284
- }
1285
- ]).catch(abortOnError);
750
+ push(options = {}) {
751
+ return super.push({
752
+ force: true,
753
+ ...options
754
+ });
1286
755
  }
1287
- async bumpTask(ctx, config) {
1288
- const { pkg: { current }, dryRun, isCI, showRelease, selectedPkg, onlyChangelog } = ctx;
1289
- const { hooks, getPkgDir, changelog: { includeHidden, transformTypes } } = config;
1290
- if (onlyChangelog) return;
1291
- const need = (ctx) => {
1292
- const { pkg: { next }, isIncrement } = ctx;
1293
- return !next && isIncrement;
1294
- };
1295
- if (need(ctx)) {
1296
- const res = await inferReleaseType({
1297
- includeHidden,
1298
- transformTypes,
1299
- json: true
756
+ async run(...args) {
757
+ await super.run(...args);
758
+ this.end();
759
+ }
760
+ setCliOptions(options) {
761
+ return this.enqueue(async () => {
762
+ const { ci, dryRun, releaseType: as, skipBump } = options;
763
+ const { project, stepsOptions: { bump: bumpOpts } } = this;
764
+ const version = await project.manifest.getVersion();
765
+ const bump = skipBump ? { version } : await runBumpPrompts(project, {
766
+ bump: {
767
+ ...bumpOpts,
768
+ as
769
+ },
770
+ run: options
1300
771
  });
1301
- if (res) msg("BUMP", res.reason);
1302
- const inferred = res?.releaseType;
1303
- const nextVersion = (isCI ? inc(current, inferred ?? "patch") : void 0) || await runVersionPrompts(ctx, config, inferred);
1304
- ctx.isIncrement = neq(current, nextVersion);
1305
- const { version: next, isPrerelease: toPreRelease, preId, preBase } = parseVersion(nextVersion);
1306
- Object.assign(ctx.pkg, {
1307
- next,
1308
- toPreRelease,
1309
- preId,
1310
- preBase
772
+ const skippedSteps = await getCliPromptOptions(options);
773
+ await runConfirmPrompts(ci, dryRun);
774
+ this.setOptions({ bump }).setSkippedSteps({
775
+ bump: version === bump.version && !bump.firstRelease,
776
+ ...skippedSteps
1311
777
  });
1312
- }
1313
- const { pkg: { next } } = ctx;
1314
- if (showRelease) taskEnd(MSG.LOG.SHOW_VERSION(next));
1315
- await runLifeCycleHook(hooks, "before:bump", dryRun);
1316
- await tasks([{
1317
- title: MSG.TASK.VERSION.START,
1318
- task: async () => {
1319
- await bumpPackageVersion(next, [], getPkgDir(selectedPkg));
1320
- await formatTemplate(ctx, config);
1321
- const to = `(${current}...${diff(current, next)})`;
1322
- return success(MSG.TASK.VERSION.END(to), dryRun);
1323
- },
1324
- enabled: true
1325
- }]).catch((err) => abortOnError(err, ctx));
1326
- await runLifeCycleHook(hooks, "after:bump", dryRun);
1327
- await this.printChangelog(ctx, config);
1328
- }
1329
- async changelogTask(ctx, config) {
1330
- const { isIncrement, dryRun, selectedPkg, noGit, onlyChangelog, pkg: { scope } } = ctx;
1331
- const { getPkgDir, changelog: { tagPrefix, releaseCount, includeHidden, transformTypes } } = config;
1332
- if (noGit) return;
1333
- await tasks([{
1334
- title: MSG.TASK.CHANGELOG.START,
1335
- task: async () => {
1336
- await generateChangelog({
1337
- getPkgDir: () => getPkgDir(selectedPkg),
1338
- tagPrefix: tagPrefix?.(selectedPkg),
1339
- scope: isEmpty(nonNullable([scope])) ? void 0 : nonNullable([scope]),
1340
- releaseCount,
1341
- includeHidden,
1342
- transformTypes
1343
- });
1344
- await gitAddAll();
1345
- return success(MSG.TASK.CHANGELOG.END, dryRun);
1346
- },
1347
- enabled: isIncrement
1348
- }]).catch((err) => abortOnError(err, ctx));
1349
- const changeset = await getShortStatus();
1350
- if (changeset) msg("GIT", `Changes:${eol(2)}` + coloredStatus(changeset));
1351
- else msg("GIT", MSG.LOG.CHANGES_EMPTY);
1352
- if (onlyChangelog) taskEnd(MSG.LOG.ONLY_CHANGELOG);
778
+ });
1353
779
  }
1354
- async gitTask(ctx, config) {
1355
- const { noGit, dryRun } = ctx;
1356
- const { hooks } = config;
1357
- if (noGit) return;
1358
- await runGitPrompts(ctx, config);
1359
- await runLifeCycleHook(hooks, "before:push", dryRun);
1360
- await tasks([{
1361
- title: MSG.TASK.GIT.START,
1362
- task: async () => {
1363
- await commitAndTag(ctx, config);
1364
- const { git: { push } } = config;
1365
- return success(MSG.TASK.GIT.END(Boolean(push)), dryRun);
780
+ setLegacyOptions() {
781
+ return this.enqueue(async () => {
782
+ const logger = this.logger.createChild("legacy-options");
783
+ const { config, file } = await loadConfig(process.cwd());
784
+ const legacySteps = omit(config, [
785
+ "project",
786
+ "hosting",
787
+ "releaser"
788
+ ]);
789
+ if (file) {
790
+ logger.verbose(`Legacy config file: ${file}`);
791
+ logger.verbose("Legacy config for steps:");
792
+ logger.verbose(legacySteps);
1366
793
  }
1367
- }]).catch((err) => abortOnError(err, ctx));
1368
- await runLifeCycleHook(hooks, "after:push", dryRun);
794
+ this.setOptions(legacySteps);
795
+ });
1369
796
  }
1370
- async npmTask(ctx, config) {
1371
- const { noNpm, dryRun, isCI, pkg: { name, next } } = ctx;
1372
- const { hooks } = config;
1373
- if (noNpm) return;
1374
- await runNpmPublishPrompts(ctx, config);
1375
- await runLifeCycleHook(hooks, "before:publish", dryRun);
1376
- await tasks([{
1377
- title: MSG.TASK.NPM.START,
1378
- task: async () => {
1379
- await publishNpm(ctx, config).catch(async (err) => {
1380
- if (isOtpError(err) && !isCI) {
1381
- await runNpmOptPrompts(ctx, config);
1382
- await publishNpm(ctx, config);
1383
- } else throw err;
1384
- });
1385
- const { npm: { publish } } = config;
1386
- const url = publish ? getPackageUrl(name, next) : void 0;
1387
- return success(MSG.TASK.NPM.END(url), dryRun);
1388
- }
1389
- }]).catch((err) => abortOnError(err, ctx, false));
1390
- await runLifeCycleHook(hooks, "after:publish", dryRun);
797
+ start() {
798
+ return this.enqueue(async () => {
799
+ const { name, version } = pkgJson$1;
800
+ console.log(`${blue`i`} ${name} v${version}\n`);
801
+ intro("🚀 Starting release");
802
+ });
1391
803
  }
1392
- async githubTask(ctx, config) {
1393
- const { noGitHub, dryRun } = ctx;
1394
- const { hooks } = config;
1395
- if (noGitHub) return;
1396
- await runGithubPrompts(ctx, config);
1397
- await runLifeCycleHook(hooks, "before:release", dryRun);
1398
- await tasks([{
1399
- title: MSG.TASK.GITHUB.START,
1400
- task: async () => {
1401
- await createRelease(ctx, config);
1402
- const { github: { owner, repo }, git: { currentTag } } = ctx;
1403
- const { github: { release } } = config;
1404
- const url = release ? getGithubReleaseUrl(owner, repo, currentTag) : void 0;
1405
- return success(MSG.TASK.GITHUB.END(url), dryRun);
1406
- }
1407
- }]).catch((err) => abortOnError(err, ctx, false));
1408
- await runLifeCycleHook(hooks, "after:release", dryRun);
804
+ end() {
805
+ const { options: { dryRun = false } } = this;
806
+ outro(`🎉 ${dryRun ? "Dry run complete" : "Release complete"}`);
807
+ process.exit(0);
808
+ }
809
+ async runAction(options) {
810
+ await this.start().setLegacyOptions().setCliOptions(options).bump().commit().tag().push().publish().release().run();
1409
811
  }
1410
812
  };
1411
- //#endregion
1412
- //#region src/monorepo/packages.ts
1413
- const scanWorkspacePackages = (cwd) => {
1414
- const workspaceFile = join(cwd, "pnpm-workspace.yaml");
1415
- if (!existsSync(workspaceFile)) return [];
1416
- const { packages = [] } = parse(readFileSync(workspaceFile, "utf8"));
1417
- return globSync(packages.map((pattern) => normalizePath(join(pattern, "package.json"))), {
1418
- cwd,
1419
- absolute: false
813
+ // src/cli.ts
814
+ const pkgJson = readJsonFileSync(join(__dirname, "..", "package.json"));
815
+ program.name("release").description(pkgJson.description).version(pkgJson.version, "-v, --version", "Print the tool version and exit.").usage("[release-type] [options]").argument("[release-type]", `Release type. One of 'major', 'minor', 'patch', or 'prerelease'.`).enablePositionalOptions().option("-n, --dry-run", "Run in dry mode.").option("--ci", "Enable CI mode.").option("--skip-bump", "Skip updating the project version.").option("--skip-git", "Skip Git commit, tag, and push operations. Git history is still read.").option("--skip-publish", "Skip publishing the project.").option("--skip-release", "Skip creating a release on the repository hosting service.").helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
816
+ const { config: { project, hosting, releaser } } = await loadConfig(process.cwd());
817
+ const { dryRun } = options;
818
+ assert(await isGitRepo(), "Current directory is not a Git repository");
819
+ assert(await isWorkingTreeClean(), "Git working tree is not clean");
820
+ assert(await gitConfigGet("remote.origin.url"), "Git remote \"origin\" is not configured");
821
+ assert(project, "Project not found");
822
+ assert(hosting, "GitRepositoryHosting not found");
823
+ await new ReleaserCli({
824
+ ...releaser,
825
+ project,
826
+ hosting,
827
+ dryRun
828
+ }).runAction({
829
+ releaseType,
830
+ ...options
1420
831
  });
1421
- };
1422
- const getPackages = (cwd) => {
1423
- return scanWorkspacePackages(cwd).reduce(((acc, file) => {
1424
- const workspace = dirname(dirname(file));
1425
- (acc[workspace] ??= []).push(basename(dirname(file)));
1426
- return acc;
1427
- }), {});
1428
- };
1429
- const monoConfig = (packages = [], workspace = "packages") => {
1430
- const subs = isEmpty(packages) ? "[]" : `[ ${[...packages].map((k) => `'${k}'`).join(", ")} ]`;
1431
- return dedent(`
1432
- import { defineConfig } from '@peiyanlu/release'
1433
- ${newline(1)}
1434
- export default defineConfig({
1435
- isMonorepo: true,
1436
- packages: ${subs},
1437
- getPkgDir: (pkg) => \`${workspace}/\${ pkg }\`,
1438
- toTag: (pkg, version) => \`\${ pkg }@\${ version }\`,
1439
- changelog: {
1440
- tagPrefix: (pkg) => \`\${ pkg }@\`,
1441
- },
1442
- })
1443
- `);
1444
- };
1445
- const generateMonoConfig = (cwd = process.cwd()) => {
1446
- const entries = Object.entries(getPackages(cwd));
1447
- const [workspace, packages] = entries.length === 1 ? entries[0] : [];
1448
- return monoConfig(packages, workspace);
1449
- };
1450
- //#endregion
1451
- //#region src/cli.ts
1452
- program.name("release").description(description).version(version, "-v, --version", "Print the tool version and exit.").usage("[release-type] [options]").argument("[release-type]", "Version bump type: patch | minor | major").enablePositionalOptions().option("-n, --dry-run", "Run in dry mode", false).option("-p, --package <pkg>", "Specify package name (Mono-repo CI only).", "").option("--otp <code>", "One-time password for npm publish.", "").option("--prepare", "Prepare a release.", false).option("--ci", "Enable CI mode.", false).option("--show-changelog", "Print changelog and exit.", false).option("--show-release", "Print release version and exit.", false).option("--only-changelog", "Only update the changelog.", false).option("-r, --release-count <count>", "Release count for release.", (val) => Number(val)).option("--include-hidden", "Include hidden commit types in the changelog.").option("-C, --no-require-clean-working-tree", "Allow releasing with uncommitted changes.").option("--skip-git", "Skip all Git-related checks and operations.").option("--skip-npm", "Skip all npm-related checks and operations.").option("--skip-github", "Skip all GitHub-related checks and operations.").option("-m, --is-monorepo", "Mono-repo project").helpOption("-h, --help", "Display help information.").action(async (releaseType, options) => {
1453
- const { prepare } = options;
1454
- const action = new Action();
1455
- prepare ? await action.handlePrepareRelease(releaseType, options) : await action.handleRelease(releaseType, options);
1456
832
  });
1457
- program.command("init").description("Create a release configuration file").option("-f, --force", "Overwrite existing config file", false).option("-m, --monorepo", "Mono-repo project", false).option("-a, --add", "Automatically stage the config file.", false).action(async (options) => {
1458
- const { force = false, monorepo = false, add = false } = options;
833
+ program.command("init").description("Initialize release configuration for your project.").option("--npm", "Use npm as the package manager", false).option("--pnpm", "Use pnpm as the package manager", true).option("--trusted", "Use npm Trusted Publishing", true).option("--monorepo", "Configure a monorepo", false).option("--fixed", "Use fixed versioning for the monorepo", false).option("--github-workflow", "Create a GitHub Actions release workflow", true).option("--force", "Overwrite existing configuration or workflow files", false).option("--add", "Stage generated files with Git", true).action(async (options, command) => {
834
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
835
+ const wasProvided = (name) => "cli" === command.getOptionValueSource(name);
836
+ const ask = async (name, prompt, value, provided = wasProvided(name)) => {
837
+ if (!interactive || provided) return value;
838
+ const answer = await prompt();
839
+ if (isCancel(answer)) {
840
+ cancel("Initialization cancelled.");
841
+ process.exit(0);
842
+ }
843
+ return answer;
844
+ };
1459
845
  const cwd = process.cwd();
1460
- const { type } = readJsonFileSync(join(cwd, "package.json"));
1461
- const isESM = type === "module";
1462
- const isTS = existsSync(join(cwd, "tsconfig.json"));
1463
- const infile = `release.config.${isESM ? "" : "m"}${isTS ? "ts" : "js"}`;
1464
- const configFile = join(cwd, infile);
1465
- if (existsSync(configFile) && !force) {
1466
- console.error(`${red`Error:`} ${yellow(infile)} already exists.\nUse ${green`--force`} to overwrite.`);
1467
- process.exit(1);
846
+ const tplDir = resolve(__dirname, "..", "template");
847
+ const detectedPackageManager = existsSync(join(cwd, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(cwd, "package-lock.json")) ? "npm" : "none";
848
+ const defaultPackageManager = options.npm ? "npm" : options.pnpm ? "pnpm" : detectedPackageManager;
849
+ if (interactive) intro("🚀 Initialize release config");
850
+ const packageManager = await ask("npm", () => select({
851
+ message: "Package manager?",
852
+ options: [
853
+ {
854
+ label: "npm",
855
+ value: "npm"
856
+ },
857
+ {
858
+ label: "pnpm",
859
+ value: "pnpm"
860
+ },
861
+ {
862
+ label: "None",
863
+ value: "none"
864
+ }
865
+ ],
866
+ initialValue: defaultPackageManager
867
+ }), defaultPackageManager, wasProvided("npm") || wasProvided("pnpm"));
868
+ const npm = packageManager === "npm";
869
+ const pnpm = packageManager === "pnpm";
870
+ const monorepo = await ask("monorepo", () => confirm({
871
+ message: "Monorepo?",
872
+ initialValue: options.monorepo
873
+ }), options.monorepo);
874
+ const fixed = monorepo ? await ask("fixed", () => confirm({
875
+ message: "Fixed versioning?",
876
+ initialValue: options.fixed
877
+ }), options.fixed) : false;
878
+ const githubWorkflow = await ask("githubWorkflow", () => confirm({
879
+ message: "GitHub Actions workflow?",
880
+ initialValue: options.githubWorkflow
881
+ }), options.githubWorkflow);
882
+ const trusted = githubWorkflow ? await ask("trusted", () => confirm({
883
+ message: "npm Trusted Publishing?",
884
+ initialValue: options.trusted
885
+ }), options.trusted) : false;
886
+ const add = await ask("add", () => confirm({
887
+ message: "Stage files with Git?",
888
+ initialValue: options.add
889
+ }), options.add);
890
+ const pkg = readJsonFileSync(join(cwd, "package.json"));
891
+ const isTS = existsSync(join(cwd, "tsconfig.json")) || Object.keys({
892
+ ...pkg.dependencies,
893
+ ...pkg.devDependencies
894
+ }).includes("typescript");
895
+ const configFile = join(cwd, `release.config.${isTS ? "ts" : "js"}`);
896
+ const workflowFile = join(cwd, ".github/workflows", "release.yml");
897
+ const existingFiles = [...existsSync(configFile) ? [configFile] : [], ...githubWorkflow && existsSync(workflowFile) ? [workflowFile] : []];
898
+ let force = options.force;
899
+ if (existingFiles.length && !force) {
900
+ const files = existingFiles.map((p) => relative(cwd, p)).join(" | ");
901
+ force = await ask("force", () => confirm({
902
+ message: `Overwrite files? ${dim(files)}`,
903
+ initialValue: false
904
+ }), false);
905
+ if (!force) throwError(`Existing files: ${files}`);
1468
906
  }
1469
- const config = () => dedent(`
1470
- import { defineConfig } from '@peiyanlu/release'
1471
- ${newline(1)}
1472
- export default defineConfig({})
1473
- `);
1474
- const content = monorepo ? generateMonoConfig(process.cwd()) : config();
1475
- writeFileSync(configFile, content + newline(), "utf-8");
1476
- console.log(`Wrote to ${underline(dim(configFile))}`);
1477
- if (add) gitAddSync([infile]);
1478
- console.log(`\n${content}`);
907
+ const template = readFileSync(join(tplDir, `release.config.hbs`), "utf-8");
908
+ const configContent = Handlebars.compile(template)({
909
+ monorepo,
910
+ fixed,
911
+ npm,
912
+ pnpm,
913
+ npmOrPnpm: npm || pnpm
914
+ });
915
+ writeFileSync(configFile, configContent.trimEnd() + newline(), "utf-8");
916
+ if (githubWorkflow) {
917
+ await mkdir(".github/workflows", { recursive: true });
918
+ const template = readFileSync(join(tplDir, `release.workflow.hbs`), "utf-8");
919
+ const workflowContent = Handlebars.compile(template)({ trusted });
920
+ writeFileSync(workflowFile, workflowContent.trimEnd() + newline(), "utf-8");
921
+ }
922
+ if (add) gitAddSync(githubWorkflow ? [configFile, workflowFile] : [configFile]);
923
+ if (interactive) outro("✨ Release config ready");
1479
924
  });
1480
- program.addHelpText("afterAll", "\nCASE: release -C -r0 --include-hidden --only-changelog\n\nThanks for using!");
1481
925
  program.parse(process.argv);
1482
- program.on("command:*", () => {
1483
- console.error(`\n${red`Error`} Invalid command: ${red`%s`}`, program.args.join(" "));
1484
- console.log(`See ${red`--help`} for a list of available commands.\n`);
1485
- process.exit(1);
1486
- });
1487
- //#endregion
1488
926
  export {};