rylease 3.0.0 → 3.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,4928 +1 @@
1
- import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.mjs";
2
- import { createRequire } from "node:module";
3
- import { access, glob, mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
- import { basename, dirname, isAbsolute, join, normalize, posix, relative, resolve, sep } from "node:path";
5
- import * as z from "zod";
6
- import { pathToFileURL } from "node:url";
7
- import { performance } from "node:perf_hooks";
8
- import { execFile } from "node:child_process";
9
- import { createHash } from "node:crypto";
10
- import { promisify } from "node:util";
11
- import { crc32, deflateRaw } from "node:zlib";
12
- import { homedir, tmpdir } from "node:os";
13
- //#region src/core/options.ts
14
- /** Options that don't match a step's schema; the message lists every problem. */
15
- var OptionsError = class extends Error {
16
- issues;
17
- constructor(issues) {
18
- super(issues.join("\n"));
19
- this.name = "OptionsError";
20
- this.issues = issues;
21
- }
22
- };
23
- /**
24
- * Validates options against a shape, applying defaults. Unknown keys are
25
- * rejected. Each problem is reported with `path` prepended, e.g.
26
- * `conventional/determine-kind.types.docs: Invalid option: ...`.
27
- */
28
- function parseOptions(shape, value, path) {
29
- const result = z.strictObject(shape).safeParse(value ?? {});
30
- if (result.success) return result.data;
31
- throw new OptionsError(result.error.issues.map((issue) => `${joinPath(path, issue.path)}: ${issue.message}`));
32
- }
33
- function joinPath(path, keys = []) {
34
- return keys.reduce((text, key) => typeof key === "number" ? `${text}[${key}]` : /^[\w-]+$/.test(String(key)) ? `${text}.${String(key)}` : `${text}[${JSON.stringify(String(key))}]`, path);
35
- }
36
- //#endregion
37
- //#region src/core/factory.ts
38
- /** Makes a factory that validates its options on every call. */
39
- function makeFactory(kind, definition) {
40
- const shape = definition.options ?? {};
41
- const factory = (options) => definition.create(parseOptions(shape, options, definition.name));
42
- return Object.defineProperties(factory, {
43
- kind: {
44
- value: kind,
45
- enumerable: true
46
- },
47
- name: {
48
- value: definition.name,
49
- enumerable: true
50
- },
51
- description: {
52
- value: definition.description,
53
- enumerable: true
54
- },
55
- options: {
56
- value: shape,
57
- enumerable: true
58
- }
59
- });
60
- }
61
- /** Whether a value is a factory, of any kind or of the given one. */
62
- function isFactory(value, kind) {
63
- if (typeof value !== "function") return false;
64
- const factory = value;
65
- return typeof factory.kind === "string" && (kind === void 0 || factory.kind === kind) && typeof factory.name === "string";
66
- }
67
- //#endregion
68
- //#region src/core/components.ts
69
- /** Defines a source of components, with options as a zod shape. */
70
- function defineComponents(definition) {
71
- return makeFactory("components", definition);
72
- }
73
- /**
74
- * Checks components and fills in their defaults, ordered so that each comes
75
- * after the components it depends on.
76
- */
77
- function resolveComponents(components, defaults) {
78
- const issues = [];
79
- const names = /* @__PURE__ */ new Set();
80
- for (const [index, component] of components.entries()) {
81
- if (typeof component.name !== "string" || component.name === "") issues.push(`components[${index}].name: expected a name`);
82
- else if (names.has(component.name)) issues.push(`components[${index}].name: '${component.name}' is listed twice`);
83
- names.add(component.name);
84
- }
85
- const paths = components.map((component, index) => {
86
- const path = normalize(component.path ?? ".").replaceAll(sep, "/");
87
- if (isAbsolute(path) || path === ".." || path.startsWith("../")) issues.push(`components[${index}].path: '${component.path}' must be inside the project`);
88
- return path.replace(/\/$/, "");
89
- });
90
- for (const [index, component] of components.entries()) for (const dependency of component.dependsOn ?? []) if (!names.has(dependency)) issues.push(`components[${index}].dependsOn: '${dependency}' isn't a component`);
91
- const several = components.length > 1;
92
- const tags = components.map((component) => {
93
- return (component.tag ?? defaults.tag ?? (several ? "{name}@{version}" : "v{version}")).replaceAll("{name}", component.name);
94
- });
95
- for (const [index, tag] of tags.entries()) {
96
- const at = components[index]?.tag === void 0 ? "tag" : `components[${index}].tag`;
97
- const unknown = [...tag.matchAll(/\{(\w+)\}/g)].map(([, name]) => name).filter((name) => name !== "version");
98
- if (!tag.includes("{version}")) issues.push(`${at}: '${tag}' has no {version} placeholder`);
99
- else if (unknown.length > 0) issues.push(`${at}: unknown placeholder {${unknown[0]}} in '${tag}'; use {version} or {name}`);
100
- }
101
- const seen = /* @__PURE__ */ new Map();
102
- for (const [index, tag] of tags.entries()) {
103
- const other = seen.get(tag);
104
- const name = components[index]?.name ?? "";
105
- if (other !== void 0) issues.push(`components: '${other}' and '${name}' share the tag '${tag}'; add {name} to it`);
106
- seen.set(tag, name);
107
- }
108
- if (issues.length > 0) throw new OptionsError(issues);
109
- return dependencyOrder(components.map((component, index) => {
110
- const path = paths[index] ?? ".";
111
- const nested = paths.filter((other, i) => i !== index && isInside(other, path));
112
- const whole = path === "." && nested.length === 0;
113
- return {
114
- info: {
115
- name: component.name,
116
- path,
117
- dependsOn: [...component.dependsOn ?? []],
118
- pathspec: whole ? [] : [path, ...nested.map((other) => `:(exclude)${other}`)]
119
- },
120
- scheme: component.scheme ?? defaults.scheme,
121
- history: component.history ?? defaults.history,
122
- tag: tags[index] ?? ""
123
- };
124
- }));
125
- }
126
- /** Whether `path` is strictly inside `parent`, both relative to the project. */
127
- function isInside(path, parent) {
128
- if (path === parent) return false;
129
- if (parent === ".") return path !== ".";
130
- const between = relative(parent, path);
131
- return between !== "" && !between.startsWith("..") && !isAbsolute(between);
132
- }
133
- /** Dependencies first, otherwise in the order given. */
134
- function dependencyOrder(components) {
135
- const ordered = [];
136
- const done = /* @__PURE__ */ new Set();
137
- let remaining = components;
138
- while (remaining.length > 0) {
139
- const ready = remaining.filter(({ info }) => info.dependsOn.every((name) => done.has(name)));
140
- if (ready.length === 0) throw new OptionsError([`components: ${remaining.map(({ info }) => info.name).join(", ")} depend on each other in a cycle`]);
141
- for (const component of ready) {
142
- ordered.push(component);
143
- done.add(component.info.name);
144
- }
145
- remaining = remaining.filter((component) => !ready.includes(component));
146
- }
147
- return ordered;
148
- }
149
- //#endregion
150
- //#region src/utils/files.ts
151
- /**
152
- * A path relative to a component's directory: where it is, and how it's
153
- * written relative to the project, e.g. in `files` and reasons.
154
- */
155
- function inComponent(env, component, path) {
156
- return {
157
- file: resolve(env.cwd, component.path, path),
158
- relative: posix.normalize(posix.join(component.path, path.replaceAll("\\", "/")))
159
- };
160
- }
161
- /**
162
- * Resolves globs, relative to `cwd` unless absolute, to the absolute paths of
163
- * the files they match, sorted; a directory stands for every file in it.
164
- * Every pattern must match something, so a typo fails instead of uploading
165
- * nothing.
166
- */
167
- async function resolveFiles(cwd, patterns) {
168
- const files = /* @__PURE__ */ new Set();
169
- for (const pattern of patterns) {
170
- const normalized = pattern.replaceAll("\\", "/");
171
- let matched = false;
172
- for await (const match of glob(normalized, { cwd })) {
173
- const path = isAbsolute(match) ? match : resolve(cwd, match);
174
- for (const file of await filesIn(path)) {
175
- files.add(file);
176
- matched = true;
177
- }
178
- }
179
- if (!matched) throw new Error(`no files match '${pattern}'`);
180
- }
181
- return [...files].sort();
182
- }
183
- async function filesIn(path) {
184
- if (!(await stat(path)).isDirectory()) return [path];
185
- return (await readdir(path, {
186
- recursive: true,
187
- withFileTypes: true
188
- })).filter((entry) => entry.isFile()).map((entry) => join(entry.parentPath, entry.name));
189
- }
190
- /** `1.2 MB`, `340 kB`, `12 B`. */
191
- function formatSize(bytes) {
192
- const units = [
193
- "B",
194
- "kB",
195
- "MB",
196
- "GB"
197
- ];
198
- let size = bytes;
199
- let unit = 0;
200
- while (size >= 1e3 && unit < units.length - 1) {
201
- size /= 1e3;
202
- unit++;
203
- }
204
- return `${unit === 0 ? size : size.toFixed(1)} ${units[unit]}`;
205
- }
206
- async function loadJson(path) {
207
- return JSON.parse(await readFile(path, "utf8"));
208
- }
209
- //#endregion
210
- //#region src/components/npm/read.ts
211
- /**
212
- * The `packages:` list of a pnpm-workspace.yaml, in block style; negated
213
- * patterns are left out.
214
- */
215
- function pnpmPackages(text) {
216
- const patterns = [];
217
- let inPackages = false;
218
- for (const line of text.split(/\r?\n/)) {
219
- if (/^packages:\s*$/.test(line)) {
220
- inPackages = true;
221
- continue;
222
- }
223
- if (!inPackages) continue;
224
- const item = /^\s+-\s+(.+?)\s*$/.exec(line);
225
- if (item?.[1]) {
226
- const pattern = /^(['"])(.*)\1$/.exec(item[1])?.[2] ?? item[1].replace(/\s+#.*$/, "");
227
- if (!pattern.startsWith("!")) patterns.push(pattern);
228
- } else if (/^\S/.test(line)) inPackages = false;
229
- }
230
- return patterns;
231
- }
232
- /** Globs of the workspace's packages, from pnpm-workspace.yaml or package.json. */
233
- async function getPackages(cwd) {
234
- const pnpm = await readFile(join(cwd, "pnpm-workspace.yaml"), "utf8").catch(() => void 0);
235
- if (pnpm !== void 0) {
236
- const patterns = pnpmPackages(pnpm);
237
- if (patterns.length > 0) return patterns;
238
- }
239
- const workspaces = (await loadJson(join(cwd, "package.json")).catch(() => ({}))).workspaces;
240
- const list = Array.isArray(workspaces) ? workspaces : workspaces?.packages;
241
- if (!Array.isArray(list) || list.length === 0) throw new Error("no workspaces: list them in package.json `workspaces`, or in pnpm-workspace.yaml `packages`");
242
- return list.filter((item) => typeof item === "string");
243
- }
244
- //#endregion
245
- //#region src/components/npm/workspaces.ts
246
- const DEPENDENCY_FIELDS$1 = [
247
- "dependencies",
248
- "devDependencies",
249
- "peerDependencies",
250
- "optionalDependencies"
251
- ];
252
- /**
253
- * The packages of an npm, Yarn or pnpm workspace, as components named after
254
- * them, each depending on the workspace packages it lists as dependencies.
255
- */
256
- const npmWorkspaces = defineComponents({
257
- name: "npm/workspaces",
258
- description: "Each package of the npm, Yarn or pnpm workspace, named after it and depending on the workspace packages it depends on.",
259
- options: {
260
- private: z.boolean().default(false).describe("Include packages marked `private`."),
261
- exclude: z.array(z.string()).default([]).describe("Packages to leave out, by name.")
262
- },
263
- create: (options) => ({
264
- name: "npm/workspaces",
265
- resolve: async (env) => {
266
- const patterns = await getPackages(env.cwd);
267
- const found = [];
268
- for (const pattern of patterns) for await (const match of glob(`${pattern.replace(/\/$/, "")}/package.json`, { cwd: env.cwd })) {
269
- const path = match.replaceAll("\\", "/").replace(/\/?package\.json$/, "") || ".";
270
- if (path.includes("node_modules")) continue;
271
- const json = JSON.parse(await readFile(join(env.cwd, match), "utf8"));
272
- if (typeof json.name !== "string") continue;
273
- if (json.private === true && !options.private) continue;
274
- if (options.exclude.includes(json.name)) continue;
275
- if (!found.some((other) => other.path === path)) found.push({
276
- name: json.name,
277
- path,
278
- json
279
- });
280
- }
281
- if (found.length === 0) throw new Error(`no workspace packages in ${env.cwd}`);
282
- found.sort((a, b) => a.path.localeCompare(b.path));
283
- const names = new Set(found.map(({ name }) => name));
284
- return found.map(({ name, path, json }) => {
285
- const dependsOn = /* @__PURE__ */ new Set();
286
- for (const field of DEPENDENCY_FIELDS$1) {
287
- const deps = json[field];
288
- if (typeof deps !== "object" || deps === null) continue;
289
- for (const dep of Object.keys(deps)) if (names.has(dep) && dep !== name) dependsOn.add(dep);
290
- }
291
- return {
292
- name,
293
- path,
294
- dependsOn: [...dependsOn]
295
- };
296
- });
297
- }
298
- })
299
- });
300
- //#endregion
301
- //#region src/components/index.ts
302
- var components_exports = /* @__PURE__ */ __exportAll({ npmWorkspaces: () => npmWorkspaces });
303
- //#endregion
304
- //#region src/utils/text.ts
305
- function plural(count, noun, nouns = `${noun}s`) {
306
- return `${count} ${count === 1 ? noun : nouns}`;
307
- }
308
- /** `a`, `a and b`, `a, b and c`. */
309
- function joinList(parts) {
310
- return parts.length > 1 ? `${parts.slice(0, -1).join(", ")} and ${parts.at(-1)}` : parts[0] ?? "";
311
- }
312
- //#endregion
313
- //#region src/utils/git.ts
314
- /** Runs `git` in `cwd` and resolves with its trimmed stdout. */
315
- function git(cwd, ...args) {
316
- return new Promise((resolve, reject) => {
317
- execFile("git", args, {
318
- cwd,
319
- maxBuffer: 67108864
320
- }, (error, stdout, stderr) => {
321
- if (error) {
322
- const detail = stderr.trim() || error.message;
323
- reject(/* @__PURE__ */ new Error(`git ${args.join(" ")} failed: ${detail}`));
324
- } else resolve(stdout.trim());
325
- });
326
- });
327
- }
328
- /** Like {@link git}, but resolves with `undefined` instead of rejecting. */
329
- async function tryGit(cwd, ...args) {
330
- try {
331
- return await git(cwd, ...args);
332
- } catch {
333
- return;
334
- }
335
- }
336
- /** `GITHUB_REPOSITORY`, or `owner/repo` from the `origin` remote. */
337
- async function detectRepo(cwd) {
338
- if (process.env.GITHUB_REPOSITORY) return process.env.GITHUB_REPOSITORY;
339
- const url = await tryGit(cwd, "remote", "get-url", "origin");
340
- return url ? parseGitHubRepo(url) : void 0;
341
- }
342
- /** `git@github.com:o/r.git`, `https://github.com/o/r` and similar → `o/r`. */
343
- function parseGitHubRepo(url) {
344
- return /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(url.trim())?.[1];
345
- }
346
- /** Rejects shallow clones, whose history would silently miss commits. */
347
- async function assertFullHistory(cwd) {
348
- if (await git(cwd, "rev-parse", "--is-shallow-repository") === "true") throw new Error("the repository is a shallow clone; fetch full history (e.g. `fetch-depth: 0`)");
349
- }
350
- /**
351
- * `git log` of the commits since `ref` (or all of them), touching a
352
- * component's pathspec. A component that's the whole project still only
353
- * counts the commits of the working directory, when that's a subdirectory.
354
- */
355
- async function logSince(cwd, options) {
356
- const range = options.ref ? `${options.ref}..HEAD` : "HEAD";
357
- let pathspec = options.pathspec;
358
- if (pathspec.length === 0) {
359
- if (await git(cwd, "rev-parse", "--show-prefix")) pathspec = ["."];
360
- }
361
- return git(cwd, "log", `--format=${options.format}`, range, ...pathspec.length > 0 ? ["--", ...pathspec] : []);
362
- }
363
- /**
364
- * The oldest commit of the latest run of commits in which `file` had
365
- * `version`, as `read` finds it in the file's content at each commit; that is,
366
- * the commit that set the version. `undefined` outside a git repository, or
367
- * when no commit has it.
368
- */
369
- async function findVersionCommit(cwd, file, version, read) {
370
- const log = await tryGit(cwd, "log", "--format=%H", "--", file);
371
- if (log === void 0) return void 0;
372
- await assertFullHistory(cwd);
373
- let found;
374
- for (const sha of log.split("\n").filter(Boolean)) {
375
- const content = await tryGit(cwd, "show", `${sha}:./${file}`);
376
- if (content === void 0 || read(content) !== version) break;
377
- found = sha;
378
- }
379
- return found;
380
- }
381
- const BOT_IDENTITY = [
382
- "-c",
383
- "user.name=github-actions[bot]",
384
- "-c",
385
- "user.email=41898282+github-actions[bot]@users.noreply.github.com"
386
- ];
387
- /**
388
- * Arguments that set the CI's identity when none is set, for commands that
389
- * commit or tag: the bot's in GitHub Actions, the user who started the
390
- * pipeline's in GitLab CI.
391
- */
392
- async function identity(cwd) {
393
- if (await tryGit(cwd, "config", "user.email")) return [];
394
- if (process.env.GITHUB_ACTIONS === "true") return BOT_IDENTITY;
395
- const { GITLAB_CI, GITLAB_USER_NAME, GITLAB_USER_EMAIL } = process.env;
396
- if (GITLAB_CI === "true" && GITLAB_USER_NAME && GITLAB_USER_EMAIL) return [
397
- "-c",
398
- `user.name=${GITLAB_USER_NAME}`,
399
- "-c",
400
- `user.email=${GITLAB_USER_EMAIL}`
401
- ];
402
- return [];
403
- }
404
- //#endregion
405
- //#region src/utils/version.ts
406
- const PATTERN$1 = /^v?(\d+(?:\.\d+)*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
407
- /** Parses a version, or returns `undefined` if it isn't one, e.g. `next`. */
408
- function parseVersion(version) {
409
- const match = PATTERN$1.exec(version.trim());
410
- if (!match) return void 0;
411
- const [, release = "", prerelease, build] = match;
412
- return {
413
- release: release.split("."),
414
- ...prerelease !== void 0 && { prerelease },
415
- ...build !== void 0 && { build }
416
- };
417
- }
418
- /**
419
- * Orders versions by precedence: number by number, a missing number counting
420
- * as 0 (`1.2` = `1.2.0`), then by pre-release as in semver. Build metadata is
421
- * ignored.
422
- */
423
- function compareVersions(a, b) {
424
- for (let i = 0; i < Math.max(a.release.length, b.release.length); i++) {
425
- const order = compareNumbers(a.release[i] ?? "0", b.release[i] ?? "0");
426
- if (order !== 0) return order;
427
- }
428
- return comparePrerelease(a.prerelease, b.prerelease);
429
- }
430
- /** The index of the first number that differs, or `undefined` if none does. */
431
- function firstDifference(a, b) {
432
- for (let i = 0; i < Math.max(a.release.length, b.release.length); i++) if (compareNumbers(a.release[i] ?? "0", b.release[i] ?? "0") !== 0) return i;
433
- }
434
- /** Compares digit strings by value, however long, ignoring leading zeros. */
435
- function compareNumbers(a, b) {
436
- const x = BigInt(a);
437
- const y = BigInt(b);
438
- return x < y ? -1 : x > y ? 1 : 0;
439
- }
440
- function comparePrerelease(a, b) {
441
- if (a === b) return 0;
442
- if (a === void 0) return 1;
443
- if (b === void 0) return -1;
444
- const left = a.split(".");
445
- const right = b.split(".");
446
- for (let i = 0; i < Math.max(left.length, right.length); i++) {
447
- const x = left[i];
448
- const y = right[i];
449
- if (x === void 0) return -1;
450
- if (y === void 0) return 1;
451
- const order = /^\d+$/.test(x) && /^\d+$/.test(y) ? compareNumbers(x, y) : x < y ? -1 : x > y ? 1 : 0;
452
- if (order !== 0) return order;
453
- }
454
- return 0;
455
- }
456
- //#endregion
457
- //#region src/utils/template.ts
458
- const PLACEHOLDER = /\{(\w+)\}/g;
459
- /** Replaces `{name}` placeholders, e.g. `v{version}` → `v1.2.3`. */
460
- function fill(template, values) {
461
- return template.replace(PLACEHOLDER, (_, name) => {
462
- const value = values[name];
463
- if (value === void 0) {
464
- const known = Object.keys(values).map((key) => `{${key}}`);
465
- throw new Error(`unknown placeholder {${name}} in '${template}'; use ${known.join(" or ")}`);
466
- }
467
- return value;
468
- });
469
- }
470
- /**
471
- * Matches text produced by `fill(template, { [name]: ... })`, capturing the
472
- * value of `name`, e.g. `v{version}` → `/^v(.+)$/`.
473
- */
474
- function templatePattern(template, name) {
475
- let found = false;
476
- const source = template.split(PLACEHOLDER).map((part, index) => {
477
- if (index % 2 === 0) return part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
478
- if (part !== name) throw new Error(`unknown placeholder {${part}} in '${template}'`);
479
- found = true;
480
- return "(.+)";
481
- }).join("");
482
- if (!found) throw new Error(`'${template}' has no {${name}} placeholder`);
483
- return new RegExp(`^${source}$`);
484
- }
485
- const VERSION_PLACEHOLDERS = "`{version}`, `{tag}` and `{name}` (the component's) are filled in, and the scheme's own, e.g. `{major}`, `{minor}` and `{patch}`";
486
- /**
487
- * Generic placeholders of a version, in any scheme, under the scheme's own:
488
- * `2026.01.3` gives `{major}` 2026, `{minor}` 01 and `{patch}` 3. A part the
489
- * version doesn't have is left out, so using it fails.
490
- */
491
- function versionValues(version) {
492
- const [major, minor, patch] = parseVersion(version)?.release ?? [];
493
- return Object.fromEntries(Object.entries({
494
- version,
495
- major,
496
- minor,
497
- patch
498
- }).filter((entry) => entry[1] !== void 0));
499
- }
500
- //#endregion
501
- //#region src/core/channels.ts
502
- const DEFAULT_FORMATS = {
503
- prerelease: "{version}-{channel}.{n}",
504
- build: "{version}+{channel}.{n}"
505
- };
506
- /** Checks channels, so a typo fails before anything runs. */
507
- function checkChannels(channels) {
508
- const issues = [];
509
- for (const [name, channel] of Object.entries(channels)) {
510
- const at = `channels.${name}`;
511
- if (!/^[0-9A-Za-z-]+$/.test(name)) issues.push(`${at}: names may only have letters, digits and '-'`);
512
- if (channel.type !== void 0 && channel.type !== "prerelease" && channel.type !== "build") issues.push(`${at}.type: expected prerelease or build`);
513
- const format = channel.format;
514
- if (format !== void 0) {
515
- for (const name of ["{version}", "{n}"]) if (!format.includes(name)) issues.push(`${at}.format: '${format}' has no ${name} placeholder`);
516
- const unknown = [...format.matchAll(/\{(\w+)\}/g)].map(([, name]) => name).find((name) => ![
517
- "version",
518
- "n",
519
- "channel"
520
- ].includes(name ?? ""));
521
- if (unknown) issues.push(`${at}.format: unknown placeholder {${unknown}}; use {version}, {n} or {channel}`);
522
- }
523
- }
524
- if (issues.length > 0) throw new OptionsError(issues);
525
- }
526
- /**
527
- * The channel a run releases on: the one asked for, or the first whose
528
- * branches match; none means a stable release.
529
- */
530
- function selectChannel(channels, branch, asked) {
531
- if (asked !== void 0 && asked !== "") {
532
- const channel = channels[asked];
533
- if (!channel) {
534
- const known = Object.keys(channels);
535
- throw new OptionsError([`channel: '${asked}' isn't a channel${known.length > 0 ? `; use ${known.join(", ")}` : "; define it in channels"}`]);
536
- }
537
- return {
538
- name: asked,
539
- type: channel.type ?? "prerelease"
540
- };
541
- }
542
- for (const [name, channel] of Object.entries(channels)) if (channel.branches?.some((pattern) => matchesGlob(branch, pattern))) return {
543
- name,
544
- type: channel.type ?? "prerelease"
545
- };
546
- }
547
- /** Every channel, with its format for the scheme. */
548
- function resolveChannels(channels, scheme) {
549
- return Object.entries(channels).map(([name, channel]) => {
550
- const type = channel.type ?? "prerelease";
551
- return {
552
- name,
553
- type,
554
- format: channel.format ?? scheme.formats?.[type] ?? DEFAULT_FORMATS[type]
555
- };
556
- });
557
- }
558
- /**
559
- * Classifies a version: on one of the channels (`2.0.0-next.3`), stable in
560
- * the scheme (`2.0.0`), or neither.
561
- */
562
- function classify$1(version, scheme, channels) {
563
- for (const channel of channels) {
564
- const match = channelPattern(channel).exec(version);
565
- const target = match?.groups?.version;
566
- const n = match?.groups?.n;
567
- if (target !== void 0 && n !== void 0 && scheme.matches(target)) return {
568
- type: channel.type,
569
- channel: channel.name,
570
- target,
571
- n: Number(n)
572
- };
573
- }
574
- return scheme.matches(version) ? {
575
- type: "stable",
576
- target: version
577
- } : { type: "foreign" };
578
- }
579
- /** A version on a channel. */
580
- function channelVersion(channel, target, n) {
581
- return fill(channel.format, {
582
- version: target,
583
- channel: channel.name,
584
- n: String(n)
585
- });
586
- }
587
- const RANK$4 = {
588
- prerelease: -1,
589
- stable: 0,
590
- build: 1
591
- };
592
- /**
593
- * Orders classified versions: by target in the scheme, then a prerelease
594
- * before the stable version before a build, then by counter.
595
- */
596
- function compareClassified(a, b, scheme) {
597
- if (a.type === "foreign" || b.type === "foreign") return 0;
598
- const order = scheme.compare(a.target, b.target);
599
- if (order !== 0) return order;
600
- if (a.type !== b.type) return RANK$4[a.type] - RANK$4[b.type];
601
- return a.type === "stable" || b.type === "stable" ? 0 : a.n - b.n;
602
- }
603
- function channelPattern(channel) {
604
- const source = channel.format.split(/(\{\w+\})/).map((part) => {
605
- if (part === "{version}") return "(?<version>.+?)";
606
- if (part === "{n}") return "(?<n>\\d+)";
607
- return (part === "{channel}" ? channel.name : part).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
608
- }).join("");
609
- return new RegExp(`^${source}$`);
610
- }
611
- /** `*` matches within a path segment, `**` across them. */
612
- function matchesGlob(text, pattern) {
613
- const source = pattern.split(/(\*\*|\*)/).map((part) => part === "**" ? ".*" : part === "*" ? "[^/]*" : part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("");
614
- return new RegExp(`^${source}$`).test(text);
615
- }
616
- //#endregion
617
- //#region src/core/facts.ts
618
- /** The facts about `changes`, which hold only as long as the changes do. */
619
- const CHANGE_FIELDS = [
620
- "title",
621
- "description",
622
- "labels",
623
- "link"
624
- ];
625
- function isChangeFact(fact) {
626
- return fact.startsWith("changes.");
627
- }
628
- /**
629
- * Throws unless every change has what the facts promise, e.g. `labels` for
630
- * `changes.labels`. Facts that aren't about changes aren't checked here.
631
- */
632
- function checkChanges(changes, facts) {
633
- for (const fact of facts) {
634
- const field = CHANGE_FIELDS.find((name) => fact === `changes.${name}`);
635
- if (field === void 0) continue;
636
- const index = changes.findIndex((change) => change[field] === void 0);
637
- if (index !== -1) throw new TypeError(`invalid output: it provides ${fact}, but change ${index} (${changes[index]?.reason}) has no '${field}'`);
638
- }
639
- }
640
- /** Steps that provide each fact, by name, as they're defined; for error messages. */
641
- const providers = /* @__PURE__ */ new Map();
642
- function registerProvider(name, facts) {
643
- for (const fact of facts) {
644
- const names = providers.get(fact) ?? /* @__PURE__ */ new Set();
645
- names.add(name);
646
- providers.set(fact, names);
647
- }
648
- }
649
- /** The defined steps that provide a fact, e.g. to suggest one. */
650
- function providersOf(fact) {
651
- return [...providers.get(fact) ?? []];
652
- }
653
- //#endregion
654
- //#region src/core/reporter.ts
655
- const ANSI = {
656
- bold: [1, 22],
657
- dim: [2, 22],
658
- red: [31, 39],
659
- green: [32, 39],
660
- yellow: [33, 39],
661
- magenta: [35, 39],
662
- cyan: [36, 39]
663
- };
664
- /** Aligned, colored output: one row per step with phase, name, duration and reason. */
665
- function prettyReporter(options = {}) {
666
- const write = options.write ?? ((text) => void process.stdout.write(text));
667
- const color = options.color ?? (process.env.FORCE_COLOR !== void 0 || process.stdout.isTTY === true && process.env.NO_COLOR === void 0);
668
- const paint = (style, text) => color ? `\x1b[${ANSI[style][0]}m${text}\x1b[${ANSI[style][1]}m` : text;
669
- let phaseWidth = 0;
670
- let nameWidth = 0;
671
- let componentWidth = 0;
672
- let dryRun = false;
673
- return {
674
- start(env, steps, components) {
675
- dryRun = env.dryRun;
676
- phaseWidth = Math.max(0, ...steps.map((step) => step.phase.length));
677
- nameWidth = Math.max(0, ...steps.map((step) => step.name.length));
678
- componentWidth = components.length > 1 ? Math.max(...components.map((component) => component.name.length)) : 0;
679
- const mode = env.dryRun ? paint("yellow", "dry run") : "release";
680
- write(`\n${paint("bold", "rylease")} ${mode} ${paint("dim", "·")} ${paint("cyan", env.branch)} ${paint("dim", `· ${env.cwd}`)}\n\n`);
681
- },
682
- step(record) {
683
- const icon = {
684
- ok: paint("green", "✔"),
685
- skipped: paint("dim", "○"),
686
- failed: paint("red", "✖")
687
- }[record.status];
688
- const phase = paint("magenta", record.phase.padEnd(phaseWidth));
689
- const name = paint("bold", record.name.padEnd(nameWidth));
690
- const component = componentWidth > 0 ? `${paint("cyan", (record.component ?? "*").padEnd(componentWidth))} ` : "";
691
- const duration = paint("dim", (record.status === "skipped" ? "-" : formatDuration(record.durationMs)).padStart(7));
692
- const reason = record.status === "ok" ? record.reason : paint(record.status === "failed" ? "red" : "dim", record.reason);
693
- write(` ${icon} ${component}${phase} ${name} ${duration} ${reason}\n`);
694
- },
695
- finish(report) {
696
- const line = (component) => {
697
- const from = component.previousVersion ?? "nothing";
698
- const action = component.status === "pending" ? paint("yellow", dryRun ? "would be proposed" : "proposed") : dryRun ? paint("yellow", "would release") : "released";
699
- if (component.status !== "released" && component.status !== "pending") return `${paint("bold", from)} ${paint("dim", "·")} nothing to release`;
700
- const to = paint("green", paint("bold", component.version ?? "?"));
701
- return from === component.version ? `${to} ${paint("dim", "·")} ${action}` : `${paint("bold", from)} → ${to} ${paint("dim", `(${component.kind})`)} ${paint("dim", "·")} ${action}`;
702
- };
703
- let summary;
704
- if (report.status === "failed") summary = paint("red", "release failed");
705
- else if (report.components.length === 1 && report.components[0]) summary = line(report.components[0]);
706
- else {
707
- const width = Math.max(...report.components.map((component) => component.name.length));
708
- summary = report.components.map((component) => `${paint("cyan", component.name.padEnd(width))} ${line(component)}`).join("\n ");
709
- }
710
- const deferred = report.deferred === void 0 ? "" : `\n ${paint("dim", report.deferred)}`;
711
- write(`\n ${summary}${deferred}\n\n`);
712
- }
713
- };
714
- }
715
- function defaultReporter() {
716
- return prettyReporter();
717
- }
718
- function formatDuration(ms) {
719
- if (ms < 1) return "<1ms";
720
- if (ms < 1e3) return `${Math.round(ms)}ms`;
721
- return `${(ms / 1e3).toFixed(2)}s`;
722
- }
723
- //#endregion
724
- //#region src/core/types.ts
725
- /** Every phase, in the order the engine runs its steps. */
726
- const PHASES = [
727
- "read-changes",
728
- "determine-kind",
729
- "determine-notes",
730
- "write",
731
- "stage",
732
- "release"
733
- ];
734
- /**
735
- * How much a release changes, whatever the version scheme: the scheme decides
736
- * what each kind bumps, e.g. semver's `MAJOR.minor.patch`.
737
- */
738
- const KINDS = [
739
- "breaking",
740
- "feature",
741
- "fix",
742
- "none"
743
- ];
744
- //#endregion
745
- //#region src/core/engine.ts
746
- var ReleaseError = class extends Error {
747
- report;
748
- constructor(message, report, options) {
749
- super(message, options);
750
- this.name = "ReleaseError";
751
- this.report = report;
752
- }
753
- };
754
- function phaseIndex(phase) {
755
- return PHASES.indexOf(phase);
756
- }
757
- const KIND_PHASE = phaseIndex("determine-kind");
758
- /** Phases that must have at least one step for a release to be possible. */
759
- const REQUIRED_PHASES = PHASES.filter((phase) => phase !== "write" && phase !== "stage" && phase !== "release");
760
- /** What each phase produces, where later phases read it. */
761
- const PHASE_VALUES = {
762
- "read-changes": "changes",
763
- "determine-kind": "kind",
764
- "determine-notes": "notes",
765
- write: "releases",
766
- stage: "releases",
767
- release: "releases"
768
- };
769
- const CHANGE_FACTS = [
770
- "changes.title",
771
- "changes.description",
772
- "changes.labels",
773
- "changes.link"
774
- ];
775
- /** Stable sort of steps by phase; steps of the same phase keep their order. */
776
- function sortSteps(steps) {
777
- return [...steps].sort((a, b) => phaseIndex(a.phase) - phaseIndex(b.phase));
778
- }
779
- async function runRelease(setup, options = {}) {
780
- const cwd = options.cwd ?? process.cwd();
781
- const env = {
782
- cwd,
783
- branch: options.branch ?? await detectBranch(cwd),
784
- dryRun: options.dryRun ?? false
785
- };
786
- const reporter = options.reporter === void 0 ? defaultReporter() : options.reporter;
787
- const given = options.release === void 0 ? [] : [options.release].flat();
788
- const ordered = sortSteps(setup.steps);
789
- const records = [];
790
- let states = [];
791
- let deferred;
792
- const report = (status, error) => {
793
- const components = states.map((state) => ({
794
- name: state.component.info.name,
795
- path: state.component.info.path,
796
- status: state.kind === "none" || !state.live ? "unchanged" : status === "failed" ? "failed" : status,
797
- dependencies: state.dependencies,
798
- releases: state.releases,
799
- ...definedOnly({
800
- previousVersion: state.previousVersion,
801
- previousRef: state.previousRef,
802
- previousTag: state.previousTag,
803
- kind: state.kind,
804
- version: state.version,
805
- tag: state.tag,
806
- notes: state.notes
807
- })
808
- }));
809
- const only = states.length === 1 ? states[0] : void 0;
810
- return {
811
- env,
812
- status,
813
- records,
814
- components,
815
- releases: only?.releases ?? [],
816
- ...definedOnly({
817
- previousVersion: only?.previousVersion,
818
- previousRef: only?.previousRef,
819
- kind: only?.kind,
820
- version: only?.version,
821
- notes: only?.notes,
822
- deferred,
823
- error
824
- })
825
- };
826
- };
827
- const record = (entry) => {
828
- records.push(entry);
829
- reporter?.step?.(entry);
830
- };
831
- const fail = (error, message = errorMessage(error)) => {
832
- const result = report("failed", error);
833
- reporter?.finish?.(result);
834
- throw new ReleaseError(message, result, { cause: error });
835
- };
836
- let resolved;
837
- try {
838
- const channels = setup.channels ?? {};
839
- checkChannels(channels);
840
- const channel = selectChannel(channels, env.branch, options.channel);
841
- if (channel) env.channel = channel;
842
- resolved = await resolveSetup(setup, env);
843
- } catch (error) {
844
- reporter?.start?.(env, ordered, []);
845
- return fail(error);
846
- }
847
- reporter?.start?.(env, [...resolved.flatMap((component) => [...historiesOf(component).map(({ name }) => ({
848
- phase: "read-version",
849
- name
850
- })), {
851
- phase: "determine-version",
852
- name: component.scheme.name
853
- }]), ...ordered], resolved.map(({ info }) => info));
854
- states = resolved.map((component) => newState(component, setup.channels ?? {}, env));
855
- const missing = given.length > 0 ? [] : REQUIRED_PHASES.filter((phase) => !ordered.some((step) => step.phase === phase));
856
- if (missing.length > 0) fail(/* @__PURE__ */ new Error(`no step provided for: ${missing.join(", ")}`));
857
- if (given.length > 0) {
858
- for (const state of states) state.live = false;
859
- for (const release of given) {
860
- const target = givenTarget(states, release.component);
861
- if (typeof target === "string") fail(new Error(target));
862
- const state = target;
863
- setGiven(state, release, `releasing the given ${release.version}`);
864
- if (release.previousVersion !== void 0) state.facts.add("previousVersion");
865
- }
866
- } else {
867
- for (const state of states) {
868
- const start = performance.now();
869
- const { scheme } = state.component;
870
- try {
871
- const found = await findPrevious(state, env);
872
- const previous = found.found;
873
- const kind = previous?.version === void 0 ? void 0 : classify$1(previous.version, scheme, state.channels);
874
- const foreign = kind?.type === "foreign";
875
- Object.assign(state, {
876
- previousVersion: foreign ? void 0 : previous?.version,
877
- previousRef: previous?.ref,
878
- previousTag: previous?.tag,
879
- stable: previous?.stable?.version ?? (kind?.type === "stable" ? previous?.version : void 0)
880
- });
881
- if (state.previousVersion !== void 0) state.facts.add("previousVersion");
882
- record({
883
- phase: "read-version",
884
- name: found.name,
885
- component: state.component.info.name,
886
- status: "ok",
887
- durationMs: performance.now() - start,
888
- reason: previous ? `${previous.reason}${foreign ? `; ${previous.version} isn't ${scheme.name}, so this release is its first` : ""}` : "no previous release; every change counts",
889
- output: previous ?? null
890
- });
891
- if (previous?.pending) {
892
- const pending = previous.pending;
893
- setGiven(state, {
894
- version: pending.version,
895
- ...definedOnly({
896
- notes: pending.notes,
897
- kind: pending.kind,
898
- previousVersion: pending.previousVersion
899
- })
900
- }, `releasing ${pending.version}, ${pending.reason}`);
901
- }
902
- } catch (error) {
903
- record({
904
- phase: "read-version",
905
- name: historiesOf(state.component).map(({ name }) => name).join(", "),
906
- component: state.component.info.name,
907
- status: "failed",
908
- durationMs: performance.now() - start,
909
- reason: errorMessage(error),
910
- error
911
- });
912
- fail(error);
913
- }
914
- }
915
- if (states.some((state) => state.given !== void 0)) for (const state of states.filter((state) => state.given === void 0)) state.live = false;
916
- }
917
- const publishing = states.find((state) => state.given !== void 0)?.given;
918
- const problems = preflight(ordered, states, publishing !== void 0);
919
- if (problems.length > 0) fail(new Error(problems.join("\n")));
920
- let bumped = publishing !== void 0;
921
- if (bumped) setDependencies(states);
922
- const skipRest = (from, reason) => {
923
- for (const step of ordered.slice(from)) record({
924
- phase: step.phase,
925
- name: step.name,
926
- status: "skipped",
927
- durationMs: 0,
928
- reason
929
- });
930
- };
931
- const finish = (status) => {
932
- const result = report(status);
933
- reporter?.finish?.(result);
934
- return result;
935
- };
936
- for (const [index, step] of ordered.entries()) {
937
- if (publishing !== void 0 && step.phase !== "release") {
938
- record({
939
- phase: step.phase,
940
- name: step.name,
941
- status: "skipped",
942
- durationMs: 0,
943
- reason: publishing
944
- });
945
- continue;
946
- }
947
- if (!bumped && phaseIndex(step.phase) > KIND_PHASE) {
948
- bumped = true;
949
- await bump(states, env, record, fail);
950
- setDependencies(states);
951
- }
952
- const live = states.filter((state) => state.live);
953
- if (live.length === 0) {
954
- skipRest(index, "no release-worthy changes");
955
- return finish("unchanged");
956
- }
957
- if (deferred !== void 0 && step.phase === "release") {
958
- skipRest(index, deferred);
959
- return finish("pending");
960
- }
961
- const targets = step.scope === "all" ? [void 0] : live;
962
- for (const target of targets) {
963
- const start = performance.now();
964
- const group = target ? [target] : live;
965
- const component = target?.component.info.name;
966
- try {
967
- for (const state of group) assertEarlierPhases(state, step.phase);
968
- const input = target ? {
969
- ...values(target),
970
- ...shared(env, setup.context, records)
971
- } : {
972
- components: live.map(values),
973
- ...shared(env, setup.context, records)
974
- };
975
- const condition = step.if;
976
- if (condition && !condition(input)) {
977
- const source = condition.source;
978
- record({
979
- phase: step.phase,
980
- name: step.name,
981
- ...component !== void 0 && { component },
982
- status: "skipped",
983
- durationMs: performance.now() - start,
984
- reason: source ? `\`${source}\` is false` : "its condition is false"
985
- });
986
- continue;
987
- }
988
- for (const state of group) assertNeeds(step, state);
989
- const output = await step.run(input);
990
- const applied = apply(step, group, target === void 0, output);
991
- deferred ??= applied.defer;
992
- record({
993
- phase: step.phase,
994
- name: step.name,
995
- ...component !== void 0 && { component },
996
- status: "ok",
997
- durationMs: performance.now() - start,
998
- reason: applied.reason,
999
- output
1000
- });
1001
- } catch (error) {
1002
- const message = errorMessage(error);
1003
- record({
1004
- phase: step.phase,
1005
- name: step.name,
1006
- ...component !== void 0 && { component },
1007
- status: "failed",
1008
- durationMs: performance.now() - start,
1009
- reason: message,
1010
- error
1011
- });
1012
- skipRest(index + 1, `'${step.phase}' step '${step.name}' failed`);
1013
- fail(error, `${step.phase} step '${step.name}'${component !== void 0 && states.length > 1 ? ` (${component})` : ""} failed: ${message}`);
1014
- }
1015
- }
1016
- }
1017
- if (!bumped) {
1018
- await bump(states, env, record, fail);
1019
- setDependencies(states);
1020
- }
1021
- if (!states.some((state) => state.live)) return finish("unchanged");
1022
- return finish(deferred !== void 0 ? "pending" : "released");
1023
- }
1024
- async function resolveSetup(setup, env) {
1025
- const source = setup.components;
1026
- const components = source === void 0 ? [{ name: "root" }] : "resolve" in source ? await source.resolve(env) : source;
1027
- const { history } = setup;
1028
- return resolveComponents(components, {
1029
- scheme: setup.scheme,
1030
- history: Array.isArray(history) ? chain(history) : history,
1031
- tag: setup.tag
1032
- });
1033
- }
1034
- /** Histories tried in turn; the first that finds a release wins. */
1035
- function chain(histories) {
1036
- const [only] = histories;
1037
- if (histories.length === 1 && only) return only;
1038
- return Object.assign({
1039
- name: histories.map(({ name }) => name).join(", "),
1040
- find: async () => void 0
1041
- }, { chain: histories });
1042
- }
1043
- function historiesOf(component) {
1044
- const history = component.history;
1045
- return history.chain ?? [history];
1046
- }
1047
- /** What a history is asked, for a component. */
1048
- function historyInput(state, env) {
1049
- return {
1050
- env,
1051
- component: state.component.info,
1052
- scheme: state.component.scheme,
1053
- tag: state.component.tag,
1054
- channel: state.channel,
1055
- channels: state.channels
1056
- };
1057
- }
1058
- async function findPrevious(state, env) {
1059
- const histories = historiesOf(state.component);
1060
- for (const history of histories) {
1061
- const found = await history.find(historyInput(state, env));
1062
- if (found) return {
1063
- name: history.name,
1064
- found
1065
- };
1066
- }
1067
- return {
1068
- name: histories.map(({ name }) => name).join(", "),
1069
- found: void 0
1070
- };
1071
- }
1072
- function newState(component, channels, env) {
1073
- const resolved = resolveChannels(channels, component.scheme);
1074
- return {
1075
- component,
1076
- previousVersion: void 0,
1077
- previousRef: void 0,
1078
- previousTag: void 0,
1079
- changes: void 0,
1080
- kind: void 0,
1081
- exact: void 0,
1082
- version: void 0,
1083
- tag: void 0,
1084
- placeholders: void 0,
1085
- notes: void 0,
1086
- dependencies: [],
1087
- releases: [],
1088
- facts: new Set(component.scheme.traits ?? []),
1089
- live: true,
1090
- stable: void 0,
1091
- channel: resolved.find(({ name }) => name === env.channel?.name),
1092
- channels: resolved,
1093
- given: void 0
1094
- };
1095
- }
1096
- /** Makes a component release a version worked out before, e.g. by a job. */
1097
- function setGiven(state, release, reason) {
1098
- const previousVersion = release.previousVersion ?? release.version;
1099
- state.live = true;
1100
- state.given = reason;
1101
- state.previousVersion = previousVersion;
1102
- state.changes = [];
1103
- state.kind = release.kind ?? difference(previousVersion, release.version);
1104
- state.notes = release.notes ?? "";
1105
- const kind = classify$1(release.version, state.component.scheme, state.channels);
1106
- setVersion(state, release.version, kind.type === "foreign" ? release.version : kind.target);
1107
- for (const fact of CHANGE_FACTS) state.facts.add(fact);
1108
- }
1109
- /** Each live component's dependencies released along with it. */
1110
- function setDependencies(states) {
1111
- for (const state of states.filter((state) => state.live)) state.dependencies = state.component.info.dependsOn.flatMap((name) => {
1112
- const other = states.find((s) => s.component.info.name === name);
1113
- return other?.live && other.version !== void 0 && other.kind && other.kind !== "none" ? [{
1114
- name,
1115
- version: other.version,
1116
- ...definedOnly({ previousVersion: other.previousVersion }),
1117
- kind: other.kind
1118
- }] : [];
1119
- });
1120
- }
1121
- function givenTarget(states, name) {
1122
- if (name === void 0) {
1123
- const [only] = states;
1124
- return states.length === 1 && only ? only : `there are several components (${names(states)}); say which the given release is for`;
1125
- }
1126
- return states.find((state) => state.component.info.name === name) ?? `'${name}' isn't a component; use ${names(states)}`;
1127
- }
1128
- function names(states) {
1129
- return states.map((state) => state.component.info.name).join(", ");
1130
- }
1131
- /**
1132
- * Sets the version, and what follows from it: the tag and placeholders. On a
1133
- * channel, the scheme's placeholders are its target's.
1134
- */
1135
- function setVersion(state, version, target = version) {
1136
- const { component } = state;
1137
- const tag = fill(component.tag, { version });
1138
- state.version = version;
1139
- state.tag = tag;
1140
- state.placeholders = {
1141
- ...versionValues(target),
1142
- ...component.scheme.matches(target) ? component.scheme.fields?.(target) : {},
1143
- version,
1144
- tag,
1145
- name: component.info.name,
1146
- ...state.channel && { channel: state.channel.name }
1147
- };
1148
- }
1149
- /**
1150
- * After the last `determine-kind` step: components with nothing to release
1151
- * drop out, and the others get their next version from their scheme, on
1152
- * their channel if there is one.
1153
- */
1154
- async function bump(states, env, record, fail) {
1155
- for (const state of states.filter((state) => state.live)) {
1156
- const start = performance.now();
1157
- const { scheme, info } = state.component;
1158
- const base = {
1159
- phase: "determine-version",
1160
- name: scheme.name,
1161
- component: info.name
1162
- };
1163
- if (state.kind === void 0) {
1164
- const error = /* @__PURE__ */ new Error("no 'determine-kind' step ran, so it has no kind");
1165
- record({
1166
- ...base,
1167
- status: "failed",
1168
- durationMs: 0,
1169
- reason: error.message,
1170
- error
1171
- });
1172
- fail(error);
1173
- }
1174
- if (state.kind === "none") {
1175
- state.live = false;
1176
- record({
1177
- ...base,
1178
- status: "skipped",
1179
- durationMs: 0,
1180
- reason: "no release-worthy changes"
1181
- });
1182
- continue;
1183
- }
1184
- const kind = state.kind;
1185
- const previous = state.previousVersion;
1186
- try {
1187
- const next = await nextVersion(state, kind, env);
1188
- const { version, target } = next;
1189
- if (!scheme.matches(target)) throw new Error(`${target} isn't a ${scheme.name} version`);
1190
- if (previous !== void 0) {
1191
- if (compareClassified(classify$1(version, scheme, state.channels), classify$1(previous, scheme, state.channels), scheme) <= 0) throw new Error(`${version} wouldn't come after ${previous}`);
1192
- }
1193
- setVersion(state, version, target);
1194
- const exists = historiesOf(state.component).find((h) => h.exists);
1195
- if (state.tag !== void 0 && await exists?.exists?.({
1196
- ...historyInput(state, env),
1197
- releaseTag: state.tag
1198
- })) throw new Error(`${state.tag} already exists; it was released from another branch`);
1199
- record({
1200
- ...base,
1201
- status: "ok",
1202
- durationMs: performance.now() - start,
1203
- reason: previous === void 0 ? `${version}: '${kind}', ${next.how}` : `${version}: '${kind}' from ${previous}, ${next.how}`,
1204
- output: {
1205
- version,
1206
- tag: state.tag
1207
- }
1208
- });
1209
- } catch (error) {
1210
- record({
1211
- ...base,
1212
- status: "failed",
1213
- durationMs: performance.now() - start,
1214
- reason: errorMessage(error),
1215
- error
1216
- });
1217
- fail(error);
1218
- }
1219
- }
1220
- }
1221
- /**
1222
- * The next version, and the scheme's version it stands for: on a
1223
- * prerelease channel, the next stable version's next prerelease (keeping a
1224
- * higher target that its channel already leads up to); on a build channel,
1225
- * the stable version's next build.
1226
- */
1227
- async function nextVersion(state, kind, env) {
1228
- const { scheme } = state.component;
1229
- const channel = state.channel;
1230
- let target;
1231
- let how;
1232
- if (channel?.type === "build") {
1233
- if (state.stable === void 0) throw new Error(`a build on '${channel.name}' needs a previous release to number builds of`);
1234
- target = state.stable;
1235
- how = `build of ${target}`;
1236
- } else if (state.exact !== void 0) {
1237
- target = state.exact;
1238
- how = "as determined";
1239
- } else {
1240
- const bumped = scheme.bump({
1241
- ...state.stable !== void 0 && { previous: state.stable },
1242
- kind,
1243
- branch: env.branch,
1244
- date: /* @__PURE__ */ new Date()
1245
- });
1246
- target = bumped.version;
1247
- how = bumped.reason;
1248
- }
1249
- if (!channel) return {
1250
- version: target,
1251
- target,
1252
- how
1253
- };
1254
- const previous = state.previousVersion === void 0 ? void 0 : classify$1(state.previousVersion, scheme, state.channels);
1255
- const onChannel = previous !== void 0 && previous.type !== "stable" && previous.type !== "foreign" && previous.channel === channel.name ? previous : void 0;
1256
- if (channel.type === "prerelease" && onChannel && scheme.compare(onChannel.target, target) > 0) {
1257
- target = onChannel.target;
1258
- how = `continues ${target}`;
1259
- }
1260
- const counted = await historiesOf(state.component).find((h) => h.count)?.count?.({
1261
- ...historyInput(state, env),
1262
- target
1263
- }) ?? 0;
1264
- const last = onChannel && scheme.compare(onChannel.target, target) === 0 ? onChannel.n : 0;
1265
- const n = Math.max(counted, last) + 1;
1266
- return {
1267
- version: channelVersion(channel, target, n),
1268
- target,
1269
- how: `${how}, ${channel.name} ${n}`
1270
- };
1271
- }
1272
- /**
1273
- * Checks, before anything runs, that every step's needs are met by the
1274
- * scheme and the steps before it, naming steps that would meet them.
1275
- */
1276
- function preflight(steps, states, given) {
1277
- const problems = /* @__PURE__ */ new Set();
1278
- for (const state of states) {
1279
- const facts = new Set(state.facts);
1280
- let source;
1281
- for (const step of steps) {
1282
- if (given && step.phase !== "release") continue;
1283
- for (const fact of step.needs ?? []) {
1284
- if (facts.has(fact)) continue;
1285
- const by = isChangeFact(fact) ? source === void 0 ? "no step" : source : fact === "previousVersion" ? void 0 : `the scheme ${state.component.scheme.name}`;
1286
- const providers = providersOf(fact).filter((name) => name !== source);
1287
- const prefix = states.length > 1 ? `${state.component.info.name}: ` : "";
1288
- problems.add(by === void 0 ? `${prefix}${step.name} needs a previous ${state.component.scheme.name} version, and there's none` : `${prefix}${step.name} needs ${fact}, which ${by} doesn't provide${providers.length > 0 ? `; ${joinOr(providers)} ${providers.length > 1 ? "do" : "does"}` : ""}`);
1289
- }
1290
- if (step.phase === "read-changes" && !step.if) {
1291
- for (const fact of [...facts]) if (isChangeFact(fact)) facts.delete(fact);
1292
- source = step.name;
1293
- }
1294
- for (const fact of step.provides ?? []) facts.add(fact);
1295
- }
1296
- }
1297
- return [...problems];
1298
- }
1299
- function joinOr(items) {
1300
- return items.length <= 1 ? items[0] ?? "" : `${items.slice(0, -1).join(", ")} or ${items.at(-1)}`;
1301
- }
1302
- function assertNeeds(step, state) {
1303
- const missing = (step.needs ?? []).filter((fact) => !state.facts.has(fact));
1304
- if (missing.length > 0) throw new Error(`it needs ${missing.join(", ")}, which no step that ran provides`);
1305
- }
1306
- function assertEarlierPhases(state, before) {
1307
- for (const phase of PHASES.slice(0, phaseIndex(before))) if (state[PHASE_VALUES[phase]] === void 0) throw new Error(`no '${phase}' step ran, so it has no ${PHASE_VALUES[phase]}`);
1308
- }
1309
- /** What a step receives for a component. */
1310
- function values(state) {
1311
- return {
1312
- component: state.component.info,
1313
- ...definedOnly({
1314
- previousVersion: state.previousVersion,
1315
- previousRef: state.previousRef,
1316
- previousTag: state.previousTag,
1317
- changes: state.changes,
1318
- kind: state.kind,
1319
- version: state.version,
1320
- tag: state.tag,
1321
- placeholders: state.placeholders,
1322
- notes: state.notes
1323
- }),
1324
- ...state.version !== void 0 && { dependencies: [...state.dependencies] },
1325
- releases: [...state.releases]
1326
- };
1327
- }
1328
- function shared(env, context, records) {
1329
- return {
1330
- env: { ...env },
1331
- context,
1332
- history: [...records]
1333
- };
1334
- }
1335
- /** Checks a step's output and applies it; resolves with its reason. */
1336
- function apply(step, group, all, value) {
1337
- const output = checkObject(value);
1338
- if (step.phase === "write" || step.phase === "stage" || step.phase === "release") {
1339
- checkRelease(output);
1340
- if (step.phase !== "release" && output.defer !== void 0) {
1341
- if (typeof output.defer !== "string" || output.defer.trim() === "") throw new TypeError("invalid output: 'defer' must be a non-empty string when present");
1342
- }
1343
- const result = {
1344
- name: step.name,
1345
- output: output.output,
1346
- files: output.files ?? []
1347
- };
1348
- for (const state of group) {
1349
- state.releases.push(result);
1350
- for (const fact of step.provides ?? []) state.facts.add(fact);
1351
- }
1352
- return {
1353
- reason: output.reason,
1354
- ...typeof output.defer === "string" && { defer: output.defer }
1355
- };
1356
- }
1357
- if (!all) {
1358
- const [state] = group;
1359
- if (state) applyTo(step, state, output);
1360
- return { reason: output.reason };
1361
- }
1362
- const results = output.results;
1363
- if (typeof results !== "object" || results === null || Array.isArray(results)) throw new TypeError("invalid output: 'results' must map components to their results");
1364
- for (const name of Object.keys(results)) if (!group.some((state) => state.component.info.name === name)) throw new TypeError(`invalid output: '${name}' isn't a component being released`);
1365
- for (const state of group) {
1366
- const own = results[state.component.info.name];
1367
- if (own !== void 0) applyTo(step, state, checkObject(own));
1368
- }
1369
- return { reason: output.reason };
1370
- }
1371
- function applyTo(step, state, output) {
1372
- switch (step.phase) {
1373
- case "read-changes": {
1374
- const changes = parseChanges(output);
1375
- checkChanges(changes, step.provides ?? []);
1376
- state.changes = changes;
1377
- for (const fact of [...state.facts]) if (isChangeFact(fact)) state.facts.delete(fact);
1378
- break;
1379
- }
1380
- case "determine-kind":
1381
- if (!KINDS.includes(output.kind)) throw new TypeError(`invalid output: 'kind' must be one of ${KINDS.join(", ")}`);
1382
- if (output.version !== void 0 && (typeof output.version !== "string" || output.version === "")) throw new TypeError("invalid output: 'version' must be a non-empty string when present");
1383
- state.kind = output.kind;
1384
- state.exact = output.version;
1385
- break;
1386
- case "determine-notes":
1387
- if (typeof output.notes !== "string") throw new TypeError("invalid output: 'notes' must be a string");
1388
- state.notes = output.notes;
1389
- }
1390
- for (const fact of step.provides ?? []) state.facts.add(fact);
1391
- }
1392
- /** Guards against untyped (e.g. plain JS) steps returning malformed output. */
1393
- function checkObject(value) {
1394
- if (typeof value !== "object" || value === null) throw new TypeError("invalid output: expected an object");
1395
- const output = value;
1396
- if (typeof output.reason !== "string" || output.reason.trim() === "") throw new TypeError("invalid output: 'reason' must be a non-empty string");
1397
- return output;
1398
- }
1399
- function parseChanges(output) {
1400
- const changes = output.changes;
1401
- if (!Array.isArray(changes) || !changes.every((change) => typeof change === "object" && change !== null && typeof change.reason === "string")) throw new TypeError("invalid output: 'changes' must be an array of changes, each with a 'reason'");
1402
- if (!changes.every(({ labels }) => labels === void 0 || Array.isArray(labels) && labels.every((label) => typeof label === "string"))) throw new TypeError("invalid output: a change's 'labels' must be an array of strings when present");
1403
- return changes;
1404
- }
1405
- function checkRelease(output) {
1406
- if (!("output" in output)) throw new TypeError("invalid output: 'output' is required (use null for none)");
1407
- if (output.files !== void 0 && !(Array.isArray(output.files) && output.files.every((file) => typeof file === "string"))) throw new TypeError("invalid output: 'files' must be an array of paths when present");
1408
- }
1409
- /**
1410
- * `breaking` or `feature` when the first or second number differs;
1411
- * otherwise, or when either isn't a version, `fix`.
1412
- */
1413
- function difference(from, to) {
1414
- const a = parseVersion(from);
1415
- const b = parseVersion(to);
1416
- const index = a && b ? firstDifference(a, b) : void 0;
1417
- return index === 0 ? "breaking" : index === 1 ? "feature" : "fix";
1418
- }
1419
- async function detectBranch(cwd) {
1420
- const fromCi = process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_REF_NAME;
1421
- if (fromCi) return fromCi;
1422
- return await tryGit(cwd, "rev-parse", "--abbrev-ref", "HEAD") ?? "unknown";
1423
- }
1424
- function errorMessage(error) {
1425
- return error instanceof Error ? error.message : String(error);
1426
- }
1427
- /** Drops `undefined` properties, for `exactOptionalPropertyTypes`. */
1428
- function definedOnly(object) {
1429
- return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== void 0));
1430
- }
1431
- //#endregion
1432
- //#region src/core/define.ts
1433
- /**
1434
- * Defines a release pipeline. Pass a custom context type to type the `context`
1435
- * every step receives: `defineRelease<{ slackTs?: string }>({ context: {}, steps })`.
1436
- * Its `scheme` and `history` are its own, or else the preset's its steps come from.
1437
- */
1438
- function defineRelease(config) {
1439
- const issues = [];
1440
- const scheme = config.scheme ?? fromPreset(config.steps, "scheme", issues);
1441
- const history = config.history ?? fromPreset(config.steps, "history", issues);
1442
- if (!scheme || !history) throw new OptionsError(issues);
1443
- return {
1444
- steps: config.steps,
1445
- scheme,
1446
- history,
1447
- run: (options) => runRelease({
1448
- steps: config.steps,
1449
- context: config.context,
1450
- scheme,
1451
- tag: config.tag,
1452
- history,
1453
- components: config.components,
1454
- channels: config.channels
1455
- }, options)
1456
- };
1457
- }
1458
- const EXAMPLES = {
1459
- scheme: "semver",
1460
- history: "git/tags"
1461
- };
1462
- /** The scheme or history provided by the one preset `steps` come from that has it. */
1463
- function fromPreset(steps, key, issues) {
1464
- const provided = /* @__PURE__ */ new Map();
1465
- for (const { preset } of steps) if (preset?.[key] && !provided.has(preset.name)) provided.set(preset.name, preset[key]);
1466
- const [only, ...others] = provided.values();
1467
- if (only && others.length === 0) return only;
1468
- issues.push(provided.size === 0 ? `${key}: expected one, e.g. ${EXAMPLES[key]}, or a preset that provides one` : `${key}: ${joinList([...provided.keys()])} each provide one; set it`);
1469
- }
1470
- /**
1471
- * Defines a reusable step whose options are declared as a zod shape. The
1472
- * result is a factory: call it with options to get a step. Its options are
1473
- * validated on every call, so they're just as safe when they come from a
1474
- * workflow file.
1475
- *
1476
- * ```ts
1477
- * export const header = defineStep({
1478
- * phase: "determine-notes",
1479
- * name: "header",
1480
- * options: { text: z.string().default("Released by rylease") },
1481
- * run: ({ notes, options }) => ({ notes: `> ${options.text}\n\n${notes}`, reason: "added header" }),
1482
- * });
1483
- * header({ text: "Hi" });
1484
- * ```
1485
- */
1486
- function defineStep(definition) {
1487
- const shape = definition.options ?? {};
1488
- const scope = definition.scope ?? "each";
1489
- const declared = definition.needs ?? [];
1490
- const needs = typeof declared === "function" ? [] : declared;
1491
- const provides = definition.provides ?? [];
1492
- for (const key of ["phase", "name"]) if (!(key in definition)) throw new OptionsError([`${definition.name}: ${key} is required`]);
1493
- registerProvider(definition.name, provides);
1494
- const factory = (options) => {
1495
- const parsed = parseOptions(shape, options, definition.name);
1496
- const run = definition.run;
1497
- const needs = typeof declared === "function" ? declared(parsed) : declared;
1498
- return {
1499
- phase: typeof definition.phase === "function" ? definition.phase(parsed) : definition.phase,
1500
- name: definition.name,
1501
- ...scope === "all" && { scope },
1502
- ...needs.length > 0 && { needs },
1503
- ...provides.length > 0 && { provides },
1504
- run: (input) => run({
1505
- ...input,
1506
- options: parsed
1507
- })
1508
- };
1509
- };
1510
- return Object.defineProperties(factory, {
1511
- kind: {
1512
- value: "step",
1513
- enumerable: true
1514
- },
1515
- phase: {
1516
- value: definition.phase,
1517
- enumerable: true
1518
- },
1519
- name: {
1520
- value: definition.name,
1521
- enumerable: true
1522
- },
1523
- description: {
1524
- value: definition.description,
1525
- enumerable: true
1526
- },
1527
- scope: {
1528
- value: scope,
1529
- enumerable: true
1530
- },
1531
- needs: {
1532
- value: needs,
1533
- enumerable: true
1534
- },
1535
- provides: {
1536
- value: provides,
1537
- enumerable: true
1538
- },
1539
- options: {
1540
- value: shape,
1541
- enumerable: true
1542
- }
1543
- });
1544
- }
1545
- /**
1546
- * Bundles step factories under one name. Calling the preset creates every
1547
- * step, passing each its options by name:
1548
- * `conventional({ "github/create-release": false, "conventional/determine-kind": { fallback: "none" } })`.
1549
- * `defaults` sets the preset's own options per step, under the caller's;
1550
- * `scheme` and `history` make the release's, unless it sets its own, e.g.
1551
- * `history: gitTags` for a preset that tags its releases.
1552
- */
1553
- function definePreset(definition) {
1554
- const names = definition.steps.map((step) => step.name);
1555
- const defaults = definition.defaults ?? {};
1556
- const preset = (options = {}) => {
1557
- const unknown = Object.keys(options).filter((key) => !names.includes(key));
1558
- if (unknown.length > 0) throw new OptionsError(unknown.map((key) => `${definition.name}: '${key}' isn't one of its steps (${names.join(", ")})`));
1559
- const made = {
1560
- name: definition.name,
1561
- ...definition.scheme && { scheme: definition.scheme() },
1562
- ...definition.history && { history: definition.history() }
1563
- };
1564
- return definition.steps.flatMap((factory) => {
1565
- const value = options[factory.name];
1566
- if (value === false) return [];
1567
- const base = defaults[factory.name];
1568
- const merged = base && (value == null || typeof value === "object") ? {
1569
- ...base,
1570
- ...value
1571
- } : value;
1572
- try {
1573
- return [{
1574
- ...factory(merged),
1575
- preset: made
1576
- }];
1577
- } catch (error) {
1578
- throw error instanceof OptionsError ? new OptionsError(error.issues.map((issue) => `${definition.name}.${issue}`)) : error;
1579
- }
1580
- });
1581
- };
1582
- return Object.defineProperties(preset, {
1583
- kind: {
1584
- value: "preset",
1585
- enumerable: true
1586
- },
1587
- name: {
1588
- value: definition.name,
1589
- enumerable: true
1590
- },
1591
- description: {
1592
- value: definition.description,
1593
- enumerable: true
1594
- },
1595
- steps: {
1596
- value: definition.steps,
1597
- enumerable: true
1598
- },
1599
- defaults: {
1600
- value: definition.defaults ?? {},
1601
- enumerable: true
1602
- },
1603
- scheme: {
1604
- value: definition.scheme,
1605
- enumerable: true
1606
- },
1607
- history: {
1608
- value: definition.history,
1609
- enumerable: true
1610
- }
1611
- });
1612
- }
1613
- /**
1614
- * Makes a step run only when `condition` holds, on top of any condition it
1615
- * already has: `when(steps.npmPublish(), ({ env }) => !env.dryRun)`.
1616
- */
1617
- function when(step, condition) {
1618
- const own = step.if;
1619
- const source = condition.source ?? "its condition";
1620
- const combined = own ? Object.assign((input) => condition(input) && own(input), { source: `${source} && ${own.source ?? "its own condition"}` }) : condition;
1621
- return {
1622
- ...step,
1623
- if: combined
1624
- };
1625
- }
1626
- //#endregion
1627
- //#region src/core/expression.ts
1628
- const NAMES = [
1629
- "dryRun",
1630
- "branch",
1631
- "cwd",
1632
- "channel",
1633
- "prerelease",
1634
- "component",
1635
- "components",
1636
- "previousVersion",
1637
- "previousRef",
1638
- "previousTag",
1639
- "changes",
1640
- "kind",
1641
- "version",
1642
- "tag",
1643
- "notes",
1644
- "dependencies",
1645
- "steps"
1646
- ];
1647
- const VALUES = [
1648
- "previousVersion",
1649
- "previousRef",
1650
- "previousTag",
1651
- "changes",
1652
- "kind",
1653
- "version",
1654
- "tag",
1655
- "notes",
1656
- "dependencies"
1657
- ];
1658
- /** Values of a component, as expressions read them. */
1659
- function valuesOf(results) {
1660
- const component = results.component;
1661
- return {
1662
- ...component && { component: {
1663
- name: component.name,
1664
- path: component.path
1665
- } },
1666
- ...Object.fromEntries(VALUES.flatMap((name) => results[name] === void 0 ? [] : [[name, results[name]]]))
1667
- };
1668
- }
1669
- /** What an expression reads, from what a step receives: for one component, or all. */
1670
- function expressionContext(input) {
1671
- const fields = input;
1672
- const own = fields.component;
1673
- const all = fields.components;
1674
- return {
1675
- dryRun: fields.env.dryRun,
1676
- branch: fields.env.branch,
1677
- cwd: fields.env.cwd,
1678
- channel: fields.env.channel?.name ?? null,
1679
- prerelease: fields.env.channel?.type === "prerelease",
1680
- ...all ? {
1681
- components: all.map(valuesOf),
1682
- ...all.length === 1 && all[0] ? valuesOf(all[0]) : {}
1683
- } : valuesOf(fields),
1684
- steps: Object.fromEntries(fields.history.filter((record) => record.phase !== "read-version" && record.phase !== "determine-version" && (record.component === void 0 || own === void 0 || record.component === own.name)).map((record) => [record.name, {
1685
- phase: record.phase,
1686
- status: record.status,
1687
- reason: record.reason,
1688
- output: record.phase === "release" ? record.output?.output : record.output
1689
- }]))
1690
- };
1691
- }
1692
- var ExpressionError = class extends Error {
1693
- name = "ExpressionError";
1694
- };
1695
- const FUNCTIONS = {
1696
- contains: (search, item) => Array.isArray(search) ? search.includes(item) : String(search ?? "").includes(String(item ?? "")),
1697
- startsWith: (text, prefix) => String(text ?? "").startsWith(String(prefix ?? "")),
1698
- endsWith: (text, suffix) => String(text ?? "").endsWith(String(suffix ?? ""))
1699
- };
1700
- const TOKEN = /\s*(?:(\d+(?:\.\d+)?)|'((?:[^']|'')*)'|([A-Za-z_][\w-]*)|(==|!=|<=|>=|&&|\|\||[!<>().[\],]))/y;
1701
- function tokenize(source) {
1702
- const tokens = [];
1703
- TOKEN.lastIndex = 0;
1704
- while (source.slice(TOKEN.lastIndex).trim() !== "") {
1705
- const at = TOKEN.lastIndex;
1706
- const match = TOKEN.exec(source);
1707
- if (!match) {
1708
- const rest = source.slice(at).trimStart();
1709
- const hint = rest[0] === "/" ? "; read names with '/' as steps['git/commit']" : "";
1710
- throw new ExpressionError(`unexpected '${rest[0]}' at ${source.length - rest.length + 1}${hint}`);
1711
- }
1712
- const [, number, string, name, op] = match;
1713
- if (number !== void 0) tokens.push({
1714
- kind: "number",
1715
- value: Number(number)
1716
- });
1717
- else if (string !== void 0) tokens.push({
1718
- kind: "string",
1719
- value: string.replaceAll("''", "'")
1720
- });
1721
- else if (name !== void 0) tokens.push({
1722
- kind: "name",
1723
- value: name
1724
- });
1725
- else tokens.push({
1726
- kind: "op",
1727
- value: op ?? ""
1728
- });
1729
- }
1730
- return tokens;
1731
- }
1732
- /**
1733
- * Compiles an `if:` expression, in the style of GitHub's: literals (`true`,
1734
- * `'text'`, `1`, `null`), names from {@link ExpressionContext} with `.x` and
1735
- * `['x']` access, `!`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `&&`, `||`,
1736
- * parentheses, and `contains`, `startsWith` and `endsWith`.
1737
- */
1738
- function compileExpression(source) {
1739
- const tokens = tokenize(source);
1740
- let position = 0;
1741
- const peek = () => tokens[position];
1742
- const isOp = (value) => {
1743
- const token = peek();
1744
- return token?.kind === "op" && token.value === value;
1745
- };
1746
- const expect = (value) => {
1747
- if (!isOp(value)) throw unexpected();
1748
- position++;
1749
- };
1750
- const unexpected = () => {
1751
- const token = peek();
1752
- return new ExpressionError(token ? `unexpected '${token.value}'` : "unexpected end");
1753
- };
1754
- const binary = (next, operators) => {
1755
- return () => {
1756
- let left = next();
1757
- for (;;) {
1758
- const token = peek();
1759
- const apply = token?.kind === "op" ? operators[token.value] : void 0;
1760
- if (!apply) return left;
1761
- position++;
1762
- const right = next();
1763
- const l = left;
1764
- left = (context) => apply(l(context), () => right(context));
1765
- }
1766
- };
1767
- };
1768
- const compare = (test) => (a, b) => {
1769
- const right = b();
1770
- return (typeof a === "number" || typeof a === "string") && typeof right === typeof a && test(a, right);
1771
- };
1772
- const primary = () => {
1773
- const token = peek();
1774
- if (!token) throw unexpected();
1775
- position++;
1776
- if (token.kind === "number" || token.kind === "string") return () => token.value;
1777
- if (token.kind === "op") {
1778
- if (token.value === "!") {
1779
- const operand = primary();
1780
- return (context) => !operand(context);
1781
- }
1782
- if (token.value === "(") {
1783
- const inner = or();
1784
- expect(")");
1785
- return inner;
1786
- }
1787
- position--;
1788
- throw unexpected();
1789
- }
1790
- const literals = {
1791
- true: true,
1792
- false: false,
1793
- null: null
1794
- };
1795
- if (token.value in literals) return () => literals[token.value];
1796
- if (isOp("(")) {
1797
- const fn = FUNCTIONS[token.value];
1798
- if (!fn) throw new ExpressionError(`unknown function '${token.value}'; use ${Object.keys(FUNCTIONS).join(", ")}`);
1799
- position++;
1800
- const args = [];
1801
- if (!isOp(")")) {
1802
- args.push(or());
1803
- while (isOp(",")) {
1804
- position++;
1805
- args.push(or());
1806
- }
1807
- }
1808
- expect(")");
1809
- return (context) => fn(...args.map((arg) => arg(context)));
1810
- }
1811
- if (!NAMES.includes(token.value)) throw new ExpressionError(`unknown name '${token.value}'; use ${NAMES.join(", ")}`);
1812
- const name = token.value;
1813
- let node = (context) => context[name];
1814
- for (;;) if (isOp(".")) {
1815
- position++;
1816
- const key = peek();
1817
- if (key?.kind !== "name") throw unexpected();
1818
- position++;
1819
- const object = node;
1820
- node = (context) => property(object(context), key.value);
1821
- } else if (isOp("[")) {
1822
- position++;
1823
- const key = or();
1824
- expect("]");
1825
- const object = node;
1826
- node = (context) => property(object(context), key(context));
1827
- } else return node;
1828
- };
1829
- const or = binary(binary(binary(binary(primary, {
1830
- "<": compare((a, b) => a < b),
1831
- "<=": compare((a, b) => a <= b),
1832
- ">": compare((a, b) => a > b),
1833
- ">=": compare((a, b) => a >= b)
1834
- }), {
1835
- "==": (a, b) => (a ?? null) === (b() ?? null),
1836
- "!=": (a, b) => (a ?? null) !== (b() ?? null)
1837
- }), { "&&": (a, b) => a ? b() : a }), { "||": (a, b) => a || b() });
1838
- if (tokens.length === 0) throw new ExpressionError("empty expression");
1839
- const root = or();
1840
- if (position < tokens.length) throw unexpected();
1841
- return root;
1842
- }
1843
- function property(object, key) {
1844
- if (typeof object !== "object" || object === null) return void 0;
1845
- const name = String(key);
1846
- return Object.hasOwn(object, name) ? object[name] : void 0;
1847
- }
1848
- //#endregion
1849
- //#region src/core/history.ts
1850
- /** Defines where previous releases are found, with options as a zod shape. */
1851
- function defineHistory(definition) {
1852
- return makeFactory("history", definition);
1853
- }
1854
- //#endregion
1855
- //#region src/steps/npm/package-json.ts
1856
- function versionOf(json, source) {
1857
- if (typeof json.version !== "string" || json.version === "") throw new Error(`${source} has no 'version'`);
1858
- return json.version;
1859
- }
1860
- const DEPENDENCY_FIELDS = [
1861
- "dependencies",
1862
- "devDependencies",
1863
- "peerDependencies",
1864
- "optionalDependencies"
1865
- ];
1866
- /**
1867
- * A package.json's text with a new `version`, and with dependencies on the
1868
- * given packages pointed at theirs, keeping each range's operator (`^`, `~`,
1869
- * `>=` or none). `workspace:`, `file:` and ranges like `1.x` are left for the
1870
- * package manager. Its indentation and trailing newline are kept.
1871
- */
1872
- function setPackageJsonVersion(text, version, packages) {
1873
- const json = JSON.parse(text);
1874
- json.version = version;
1875
- const changed = [];
1876
- for (const field of DEPENDENCY_FIELDS) {
1877
- const deps = json[field];
1878
- if (typeof deps !== "object" || deps === null) continue;
1879
- for (const [name, range] of Object.entries(deps)) {
1880
- const next = packages.get(name);
1881
- if (next === void 0 || typeof range !== "string") continue;
1882
- const updated = withOperator(range, next);
1883
- if (updated === void 0 || updated === range) continue;
1884
- deps[name] = updated;
1885
- changed.push(`${name}@${updated}`);
1886
- }
1887
- }
1888
- const indent = /^[ \t]+(?=")/m.exec(text)?.[0] ?? " ";
1889
- const newline = text.endsWith("\n") ? "\n" : "";
1890
- return {
1891
- text: JSON.stringify(json, null, indent) + newline,
1892
- dependencies: [...new Set(changed)]
1893
- };
1894
- }
1895
- /**
1896
- * A range pointed at a new version, keeping its operator, or `undefined`
1897
- * when it isn't a plain range on one version.
1898
- */
1899
- function withOperator(range, version) {
1900
- const match = /^(\^|~|>=|=)?v?\d+(?:\.\d+)*(?:[-+][0-9A-Za-z.+-]+)?$/.exec(range.trim());
1901
- return match ? `${match[1] ?? ""}${version}` : void 0;
1902
- }
1903
- //#endregion
1904
- //#region src/steps/fs/formats.ts
1905
- const FORMAT_NAMES = [
1906
- "plain",
1907
- "npm",
1908
- "cargo",
1909
- "python",
1910
- "helm",
1911
- "gradle",
1912
- "maven",
1913
- "pattern"
1914
- ];
1915
- /** The format by name; `pattern` needs its template, e.g. `VERSION = "{version}"`. */
1916
- function versionFormat(name, pattern) {
1917
- switch (name) {
1918
- case "plain": return plain;
1919
- case "npm": return npm;
1920
- case "cargo": return cargo;
1921
- case "python": return python;
1922
- case "helm": return helm;
1923
- case "gradle": return gradle;
1924
- case "maven": return maven;
1925
- case "pattern":
1926
- if (pattern === void 0) throw new Error("the pattern format needs a `pattern`");
1927
- return fromPattern(pattern);
1928
- }
1929
- }
1930
- const unchanged = (text) => ({
1931
- text,
1932
- dependencies: []
1933
- });
1934
- const plain = {
1935
- name: "plain",
1936
- path: "VERSION",
1937
- read: (text) => text.trim() || void 0,
1938
- write: (text, version) => unchanged(`${version}${text === "" || text.endsWith("\n") ? "\n" : ""}`)
1939
- };
1940
- const npm = {
1941
- name: "npm",
1942
- path: "package.json",
1943
- needs: ["version.semver"],
1944
- read: (text) => {
1945
- const json = parseJson(text);
1946
- return typeof json?.version === "string" && json.version !== "" ? json.version : void 0;
1947
- },
1948
- write: setPackageJsonVersion,
1949
- packageName: (text) => {
1950
- const name = parseJson(text)?.name;
1951
- return typeof name === "string" ? name : void 0;
1952
- }
1953
- };
1954
- function parseJson(text) {
1955
- try {
1956
- const json = JSON.parse(text);
1957
- return typeof json === "object" && json !== null ? json : void 0;
1958
- } catch {
1959
- return;
1960
- }
1961
- }
1962
- /** `key = "value"` in the first of the TOML tables that has it. */
1963
- function tomlKey(tables, key) {
1964
- const find = (text) => {
1965
- const lines = text.split("\n");
1966
- for (const table of tables) {
1967
- let inside = false;
1968
- for (const [index, line] of lines.entries()) {
1969
- const header = /^\s*\[([^[\]]+)\]\s*(#.*)?$/.exec(line);
1970
- if (header) {
1971
- inside = header[1]?.trim() === table;
1972
- continue;
1973
- }
1974
- if (/^\s*\[\[/.test(line)) {
1975
- inside = false;
1976
- continue;
1977
- }
1978
- const match = new RegExp(`^(\\s*${key}\\s*=\\s*)(["'])([^"']*)\\2(.*)$`).exec(line);
1979
- if (inside && match) return {
1980
- lines,
1981
- index,
1982
- match
1983
- };
1984
- }
1985
- }
1986
- };
1987
- return {
1988
- read: (text) => find(text)?.match[3],
1989
- write: (text, value) => {
1990
- const found = find(text);
1991
- if (!found) return void 0;
1992
- const [, before, quote, , after] = found.match;
1993
- found.lines[found.index] = `${before}${quote}${value}${quote}${after}`;
1994
- return found.lines.join("\n");
1995
- }
1996
- };
1997
- }
1998
- const CARGO_TABLES = [
1999
- "dependencies",
2000
- "dev-dependencies",
2001
- "build-dependencies",
2002
- "workspace.dependencies"
2003
- ];
2004
- const cargoVersion = tomlKey(["package", "workspace.package"], "version");
2005
- const cargoName = tomlKey(["package"], "name");
2006
- const cargo = {
2007
- name: "cargo",
2008
- path: "Cargo.toml",
2009
- needs: ["version.semver"],
2010
- read: cargoVersion.read,
2011
- write: (text, version, packages) => {
2012
- const written = cargoVersion.write(text, version);
2013
- if (written === void 0) throw new Error("it has no [package] version");
2014
- const dependencies = [];
2015
- let table = "";
2016
- return {
2017
- text: written.split("\n").map((line) => {
2018
- const header = /^\s*\[([^[\]]+)\]/.exec(line);
2019
- if (header) table = header[1]?.trim() ?? "";
2020
- if (!CARGO_TABLES.some((name) => table.endsWith(name))) return line;
2021
- const match = /^(\s*"?([\w-]+)"?\s*=\s*\{[^}]*\bversion\s*=\s*")([^"]*)(".*)$/.exec(line);
2022
- const next = match?.[2] ? packages.get(match[2]) : void 0;
2023
- const range = match?.[3];
2024
- if (!match || next === void 0 || range === void 0) return line;
2025
- const updated = withOperator(range, next);
2026
- if (updated === void 0 || updated === range) return line;
2027
- dependencies.push(`${match[2]}@${updated}`);
2028
- return `${match[1]}${updated}${match[4]}`;
2029
- }).join("\n"),
2030
- dependencies
2031
- };
2032
- },
2033
- packageName: cargoName.read
2034
- };
2035
- const pythonVersion = tomlKey(["project", "tool.poetry"], "version");
2036
- const python = {
2037
- name: "python",
2038
- path: "pyproject.toml",
2039
- read: pythonVersion.read,
2040
- write: (text, version) => {
2041
- const written = pythonVersion.write(text, version);
2042
- if (written === void 0) throw new Error("it has no [project] or [tool.poetry] version");
2043
- return unchanged(written);
2044
- }
2045
- };
2046
- /** A top-level `key: value` line, e.g. in YAML or a properties file. */
2047
- function lineKey(name, path, pattern) {
2048
- return {
2049
- name,
2050
- path,
2051
- read: (text) => pattern.exec(text)?.[3],
2052
- write: (text, version) => {
2053
- if (!pattern.test(text)) throw new Error(`it has no ${name} version`);
2054
- return unchanged(text.replace(pattern, (_, before, quote, __, after) => `${before}${quote}${version}${quote}${after}`));
2055
- }
2056
- };
2057
- }
2058
- const helm = lineKey("helm", "Chart.yaml", /^(version:\s*)(["']?)([^"'\s#]+)\2(.*)$/m);
2059
- const gradle = lineKey("gradle", "gradle.properties", /^(\s*version\s*[=:]\s*)()([^\s#]+)()$/m);
2060
- /** The project's own `<version>`: the one directly in `<project>`. */
2061
- function mavenVersion(text) {
2062
- const tags = /<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<(\/?)([\w.:-]+)[^>]*?(\/?)>/g;
2063
- let depth = 0;
2064
- let open;
2065
- for (const match of text.matchAll(tags)) {
2066
- const [whole, closing, name, selfClosing] = match;
2067
- if (name === void 0 || selfClosing) continue;
2068
- if (closing) {
2069
- depth--;
2070
- if (name === "version" && depth === 1 && open !== void 0) return {
2071
- start: open,
2072
- end: match.index
2073
- };
2074
- continue;
2075
- }
2076
- if (name === "version" && depth === 1) open = match.index + whole.length;
2077
- depth++;
2078
- }
2079
- }
2080
- const maven = {
2081
- name: "maven",
2082
- path: "pom.xml",
2083
- read: (text) => {
2084
- const found = mavenVersion(text);
2085
- return found ? text.slice(found.start, found.end).trim() : void 0;
2086
- },
2087
- write: (text, version) => {
2088
- const found = mavenVersion(text);
2089
- if (!found) throw new Error("it has no project <version>");
2090
- return unchanged(`${text.slice(0, found.start)}${version}${text.slice(found.end)}`);
2091
- }
2092
- };
2093
- /**
2094
- * Versions wherever a template matches, e.g. `__version__ = "{version}"`;
2095
- * every match is written.
2096
- */
2097
- function fromPattern(template) {
2098
- if (!template.includes("{version}")) throw new Error(`the pattern '${template}' has no {version} placeholder`);
2099
- const source = template.split("{version}").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("([^\\s\"'`,;<>]+)");
2100
- const pattern = new RegExp(source, "g");
2101
- return {
2102
- name: "pattern",
2103
- path: "",
2104
- read: (text) => new RegExp(source).exec(text)?.[1],
2105
- write: (text, version) => {
2106
- if (!new RegExp(source).test(text)) throw new Error(`nothing matches '${template}'`);
2107
- return unchanged(text.replace(pattern, () => template.replaceAll("{version}", version)));
2108
- }
2109
- };
2110
- }
2111
- //#endregion
2112
- //#region src/histories/fs/version-file.ts
2113
- /**
2114
- * Finds the previous release in the project's version file: the version in
2115
- * it, released at the commit that set it. For projects that kept their
2116
- * version in a file before tagging releases, like `package.json` or a
2117
- * `VERSION` file.
2118
- */
2119
- const fsVersionFile = defineHistory({
2120
- name: "fs/version-file",
2121
- description: "The version in the project's version file, in any format `fs/write-version` writes, released at the commit that set it.",
2122
- options: {
2123
- format: z.enum(FORMAT_NAMES).default("plain").describe("As for `fs/write-version`."),
2124
- path: z.string().optional().describe("Relative to the component's directory; defaults to the format's file."),
2125
- pattern: z.string().optional().describe("With the `pattern` format, where the version is.")
2126
- },
2127
- create: (options) => {
2128
- const format = versionFormat(options.format, options.pattern);
2129
- const path = options.path ?? format.path;
2130
- if (path === "") throw new Error("the pattern format needs a `path`");
2131
- return {
2132
- name: "fs/version-file",
2133
- find: async ({ env, component }) => {
2134
- const dir = join(env.cwd, component.path);
2135
- const shown = join(component.path, path).replaceAll("\\", "/");
2136
- const version = format.read(await readFile(join(dir, path), "utf8"));
2137
- if (version === void 0) throw new Error(`${shown} has no version`);
2138
- const ref = await findVersionCommit(dir, path, version, format.read);
2139
- return {
2140
- version,
2141
- ...ref && { ref },
2142
- reason: ref ? `${version} in ${shown}, set in ${ref.slice(0, 7)}` : `${version} in ${shown}, not in git history`
2143
- };
2144
- }
2145
- };
2146
- }
2147
- });
2148
- //#endregion
2149
- //#region src/histories/git/git-tags.ts
2150
- const FIELD = "";
2151
- /**
2152
- * Finds the previous release by its tag: the nearest on HEAD. Of the release
2153
- * tags merged into HEAD, only those that no other one comes after count, so
2154
- * a `1.x` branch finds its own latest release, not `main`'s; of those, the
2155
- * highest version in the scheme wins. When the scheme changed since, the
2156
- * nearest release still marks where changes start, but there's no previous
2157
- * version to bump: the release is the scheme's first.
2158
- *
2159
- * A stable release only counts stable releases. On a channel, its own
2160
- * releases count too, and the nearest stable release is what's bumped.
2161
- */
2162
- const gitTags = defineHistory({
2163
- name: "git/tags",
2164
- description: "The nearest release tag on HEAD: the highest version among the tags no other release tag comes after. On a channel, its own releases count too.",
2165
- create: () => ({
2166
- name: "git/tags",
2167
- find: async (input) => {
2168
- const cwd = input.env.cwd;
2169
- if (await tryGit(cwd, "rev-parse", "--verify", "--quiet", "HEAD") === void 0) return;
2170
- await assertFullHistory(cwd);
2171
- const tags = await listTags(input, true);
2172
- const channel = input.channel?.name;
2173
- const counts = (tag) => tag.kind.type === "stable" || tag.kind.type === "foreign" || tag.kind.channel === channel;
2174
- const best = await nearest(cwd, tags.filter(counts), input);
2175
- if (!best) return void 0;
2176
- const at = `${best.name} at ${best.commit.slice(0, 7)}`;
2177
- if (best.kind.type === "foreign") return {
2178
- ref: best.commit,
2179
- tag: best.name,
2180
- reason: `${at}, nearest on HEAD; not ${input.scheme.name}, so this release is its first`
2181
- };
2182
- const stable = best.kind.type === "stable" ? void 0 : await nearest(cwd, tags.filter((tag) => tag.kind.type === "stable"), input);
2183
- return {
2184
- version: best.version,
2185
- ref: best.commit,
2186
- tag: best.name,
2187
- reason: `${at}, nearest on HEAD${stable ? `; ${stable.version} before it` : ""}`,
2188
- ...stable && { stable: {
2189
- version: stable.version,
2190
- ref: stable.commit,
2191
- tag: stable.name
2192
- } }
2193
- };
2194
- },
2195
- count: async (input) => {
2196
- const channel = input.channel?.name;
2197
- let highest = 0;
2198
- for (const { kind } of await listTags(input, false)) if (kind.type !== "stable" && kind.type !== "foreign" && kind.channel === channel && input.scheme.compare(kind.target, input.target) === 0) highest = Math.max(highest, kind.n);
2199
- return highest;
2200
- },
2201
- exists: async ({ env, releaseTag }) => await tryGit(env.cwd, "rev-parse", "--verify", "--quiet", `refs/tags/${releaseTag}`) !== void 0
2202
- })
2203
- });
2204
- /** Release tags, of HEAD or of the whole repository, classified. */
2205
- async function listTags(input, merged) {
2206
- const pattern = templatePattern(input.tag, "version");
2207
- const listing = await git(input.env.cwd, "for-each-ref", ...merged ? ["--merged=HEAD"] : [], `--format=%(refname:strip=2)${FIELD}%(objectname)${FIELD}%(*objectname)${FIELD}%(creatordate:unix)`, "refs/tags");
2208
- const tags = [];
2209
- for (const line of listing.split("\n").filter(Boolean)) {
2210
- const [name = "", object = "", peeled = "", date = "0"] = line.split(FIELD);
2211
- const version = pattern.exec(name)?.[1];
2212
- if (version === void 0) continue;
2213
- const kind = classify$1(version, input.scheme, input.channels);
2214
- if (kind.type === "foreign" && parseVersion(version) === void 0) continue;
2215
- tags.push({
2216
- name,
2217
- version,
2218
- commit: peeled || object,
2219
- date: Number(date),
2220
- kind
2221
- });
2222
- }
2223
- return tags;
2224
- }
2225
- /**
2226
- * The nearest of the tags: of those no other one comes after, the highest
2227
- * in the scheme, or failing that, the latest of another scheme.
2228
- */
2229
- async function nearest(cwd, tags, input) {
2230
- if (tags.length === 0) return void 0;
2231
- const commits = [...new Set(tags.map((tag) => tag.commit))];
2232
- const frontier = new Set(commits.length === 1 ? commits : (await git(cwd, "merge-base", "--independent", ...commits)).split("\n"));
2233
- const near = tags.filter((tag) => frontier.has(tag.commit));
2234
- const ours = near.filter((tag) => tag.kind.type !== "foreign");
2235
- return ours.length > 0 ? ours.reduce((a, b) => compareClassified(b.kind, a.kind, input.scheme) > 0 ? b : a) : near.reduce((a, b) => b.date > a.date ? b : a);
2236
- }
2237
- //#endregion
2238
- //#region src/utils/retry.ts
2239
- /** Waits; replaceable in tests. */
2240
- const clock = { sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)) };
2241
- /** Calls `run` until it resolves, waiting longer before each retry. */
2242
- async function retry(run, options) {
2243
- const delayMs = options.delayMs ?? ((retry) => retry * 1e3);
2244
- for (let attempt = 1;; attempt++) try {
2245
- return await run(attempt);
2246
- } catch (error) {
2247
- if (attempt >= options.attempts || !(options.retryIf?.(error) ?? true)) throw error;
2248
- await clock.sleep(delayMs(attempt));
2249
- }
2250
- }
2251
- /**
2252
- * Polls `check` until it resolves with a value, e.g. until something another
2253
- * job created becomes visible; resolves with `undefined` if it never does.
2254
- */
2255
- async function waitFor(check, options) {
2256
- try {
2257
- return await retry(async () => {
2258
- const value = await check();
2259
- if (value === void 0) throw new NotYet();
2260
- return value;
2261
- }, {
2262
- ...options,
2263
- retryIf: (error) => error instanceof NotYet
2264
- });
2265
- } catch (error) {
2266
- if (error instanceof NotYet) return void 0;
2267
- throw error;
2268
- }
2269
- }
2270
- var NotYet = class extends Error {};
2271
- //#endregion
2272
- //#region src/steps/github/api.ts
2273
- /** The token option, `GITHUB_TOKEN` or `GH_TOKEN`. */
2274
- function githubToken(token) {
2275
- const found = token ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
2276
- if (!found) throw new Error("no token; set GITHUB_TOKEN or GH_TOKEN");
2277
- return found;
2278
- }
2279
- var GitHubError = class extends Error {
2280
- status;
2281
- constructor(status, message) {
2282
- super(message);
2283
- this.name = "GitHubError";
2284
- this.status = status;
2285
- }
2286
- };
2287
- /** Statuses worth another try: rate limits and server errors. */
2288
- function isTransient(error) {
2289
- if (error instanceof GitHubError) return error.status === 429 || error.status >= 500;
2290
- return error instanceof TypeError;
2291
- }
2292
- /** A GitHub REST client for one repository that retries transient failures. */
2293
- function githubClient(repo, token) {
2294
- const apiUrl = process.env.GITHUB_API_URL ?? "https://api.github.com";
2295
- return {
2296
- repo,
2297
- request: (method, path, options = {}) => retry(async () => {
2298
- const url = /^https?:/.test(path) ? path : `${apiUrl}/repos/${repo}${path}`;
2299
- const response = await fetch(url, {
2300
- method,
2301
- headers: {
2302
- accept: "application/vnd.github+json",
2303
- authorization: `Bearer ${token}`,
2304
- "x-github-api-version": "2022-11-28",
2305
- ...options.body !== void 0 && { "content-type": options.contentType ?? "application/json" }
2306
- },
2307
- ...options.body !== void 0 && { body: options.contentType === void 0 ? JSON.stringify(options.body) : options.body }
2308
- });
2309
- const text = await response.text();
2310
- if (!response.ok && !options.allow?.includes(response.status)) {
2311
- const wait = Number(response.headers.get("retry-after"));
2312
- if (response.status === 429 && wait > 0) await clock.sleep(Math.min(wait, 60) * 1e3);
2313
- throw new GitHubError(response.status, `GitHub responded ${response.status} to ${method} ${url}: ${text}`);
2314
- }
2315
- return {
2316
- status: response.status,
2317
- data: text ? JSON.parse(text) : null
2318
- };
2319
- }, {
2320
- attempts: options.attempts ?? 5,
2321
- retryIf: isTransient
2322
- })
2323
- };
2324
- }
2325
- //#endregion
2326
- //#region src/steps/github/release.ts
2327
- /** The `repo` option, or the repository being released. */
2328
- async function releaseRepo(cwd, repo) {
2329
- const found = repo ?? await detectRepo(cwd);
2330
- if (!found) throw new Error("couldn't determine the GitHub repository; set `repo` or GITHUB_REPOSITORY");
2331
- return found;
2332
- }
2333
- /** Options of the steps that create a release, or find one. */
2334
- const releaseOptions = {
2335
- title: z.string().default("{tag}").describe("`{tag}`, `{version}`, `{name}` and the scheme's placeholders are filled in."),
2336
- draft: z.boolean().default(false),
2337
- prerelease: z.union([z.boolean(), z.literal("auto")]).default("auto").describe("`auto` marks releases on a prerelease channel."),
2338
- repo: z.string().optional().describe("`owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote."),
2339
- token: z.string().optional().describe("Defaults to `GITHUB_TOKEN` or `GH_TOKEN`.")
2340
- };
2341
- /** The release of a tag, drafts included, or `undefined`. */
2342
- async function findRelease(client, tag) {
2343
- const { status, data } = await client.request("GET", `/releases/tags/${encodeURIComponent(tag)}`, { allow: [404] });
2344
- if (status !== 404) return data;
2345
- const { data: releases } = await client.request("GET", "/releases?per_page=100");
2346
- return releases.find((release) => release.tag_name === tag);
2347
- }
2348
- /**
2349
- * Finds the release of a tag, or creates it. Other jobs may be doing the same
2350
- * at once: when one of them wins, this waits until its release is visible and
2351
- * uses that.
2352
- */
2353
- async function ensureRelease(client, release) {
2354
- const existing = await findRelease(client, release.tag);
2355
- if (existing) return {
2356
- release: existing,
2357
- created: false
2358
- };
2359
- try {
2360
- const { data } = await client.request("POST", "/releases", { body: {
2361
- tag_name: release.tag,
2362
- target_commitish: release.target,
2363
- name: release.title,
2364
- body: release.notes,
2365
- draft: release.draft,
2366
- prerelease: release.prerelease
2367
- } });
2368
- return {
2369
- release: data,
2370
- created: true
2371
- };
2372
- } catch (error) {
2373
- if (!(error instanceof GitHubError && error.status === 422)) throw error;
2374
- const created = await waitFor(() => findRelease(client, release.tag), { attempts: 5 });
2375
- if (!created) throw error;
2376
- return {
2377
- release: created,
2378
- created: false
2379
- };
2380
- }
2381
- }
2382
- /** The name GitHub stores an asset under, e.g. `My App.zip` → `My.App.zip`. */
2383
- function assetName(file) {
2384
- return file.replace(/[^\w.+@~-]/g, ".");
2385
- }
2386
- async function listAssets(client, release) {
2387
- const assets = [];
2388
- for (let page = 1;; page++) {
2389
- const { data } = await client.request("GET", `/releases/${release.id}/assets?per_page=100&page=${page}`);
2390
- assets.push(...data);
2391
- if (data.length < 100) return assets;
2392
- }
2393
- }
2394
- /**
2395
- * Uploads a file as an asset of the release, retrying failed uploads. An
2396
- * asset of the same name is replaced if `clobber` is set, or if it's what's
2397
- * left of an unfinished upload.
2398
- */
2399
- async function uploadAsset(client, release, asset, clobber) {
2400
- const name = assetName(asset.name);
2401
- return retry(async () => {
2402
- const existing = (await listAssets(client, release)).find((other) => other.name === name);
2403
- if (existing) {
2404
- if (existing.state === "uploaded" && !clobber) throw new Error(`release ${release.tag_name} already has ${name}; set clobber to replace it`);
2405
- await client.request("DELETE", `/releases/assets/${existing.id}`, { allow: [404] });
2406
- }
2407
- const url = new URL(release.upload_url.replace(/\{.*\}$/, ""));
2408
- url.searchParams.set("name", asset.name);
2409
- const { data } = await client.request("POST", url.href, {
2410
- body: asset.content,
2411
- contentType: "application/octet-stream",
2412
- attempts: 1
2413
- });
2414
- return data;
2415
- }, {
2416
- attempts: 5,
2417
- retryIf: isTransient
2418
- });
2419
- }
2420
- /** Whether a release is a prerelease, by the `prerelease` option. */
2421
- function isPrerelease(option, env) {
2422
- return option === "auto" ? env.channel?.type === "prerelease" : option;
2423
- }
2424
- //#endregion
2425
- //#region src/steps/github/release-pr.ts
2426
- /** The branch prefix of release pull requests, and their label until released. */
2427
- const RELEASE_BRANCH = "rylease/";
2428
- const PENDING_LABEL = "rylease: pending";
2429
- const MARKER = /<!-- rylease (\{[\s\S]*?\}) -->\s*$/;
2430
- /** A pull request's body: each release's notes, then the manifest, hidden. */
2431
- function releaseBody(manifest) {
2432
- return `${Object.values(manifest.components).map(({ tag, notes }) => `## ${tag}\n\n${notes.trim() || "_No notes._"}`).join("\n\n")}\n\n<!-- rylease ${JSON.stringify(manifest)} -->\n`;
2433
- }
2434
- /** The manifest in a pull request's body, if it has a valid one. */
2435
- function readManifest(body) {
2436
- const json = MARKER.exec(body ?? "")?.[1];
2437
- if (json === void 0) return void 0;
2438
- try {
2439
- const manifest = JSON.parse(json);
2440
- return typeof manifest.components === "object" ? manifest : void 0;
2441
- } catch {
2442
- return;
2443
- }
2444
- }
2445
- //#endregion
2446
- //#region src/histories/github/release-pr.ts
2447
- /**
2448
- * Finds a release that a merged release pull request proposed: when HEAD is
2449
- * its merge, the release steps run for the versions it proposed, with its
2450
- * notes. Components it doesn't name, or whose tag exists already, are found
2451
- * by their tags, as with `git/tags`.
2452
- */
2453
- const githubReleasePr = defineHistory({
2454
- name: "github/release-pr",
2455
- description: "A release proposed by a release pull request from `github/open-release-pr`, once it's merged; otherwise the nearest release tag on HEAD, as `git/tags`.",
2456
- options: {
2457
- label: z.string().default(PENDING_LABEL).describe("The label of release pull requests not yet released."),
2458
- repo: z.string().optional().describe("`owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote."),
2459
- token: z.string().optional().describe("Defaults to `GITHUB_TOKEN` or `GH_TOKEN`.")
2460
- },
2461
- create: (options) => {
2462
- const tags = gitTags();
2463
- let merged;
2464
- const find = async (cwd) => {
2465
- const head = await tryGit(cwd, "rev-parse", "HEAD");
2466
- if (head === void 0) return void 0;
2467
- const { data } = await githubClient(await releaseRepo(cwd, options.repo), githubToken(options.token)).request("GET", `/commits/${head}/pulls`);
2468
- for (const pull of data) {
2469
- const manifest = readManifest(pull.body);
2470
- if (pull.merged_at && pull.head.ref.startsWith("rylease/") && pull.labels.some((label) => label.name === options.label) && manifest) return {
2471
- number: pull.number,
2472
- manifest
2473
- };
2474
- }
2475
- };
2476
- return {
2477
- name: "github/release-pr",
2478
- find: async (input) => {
2479
- merged ??= find(input.env.cwd);
2480
- const pull = await merged;
2481
- const entry = pull?.manifest.components[input.component.name];
2482
- const released = entry === void 0 || await tryGit(input.env.cwd, "rev-parse", "--verify", "--quiet", `refs/tags/${entry.tag}`) !== void 0;
2483
- if (!pull || !entry || released) return tags.find(input);
2484
- const head = await git(input.env.cwd, "rev-parse", "HEAD");
2485
- return {
2486
- ...entry.previousVersion !== void 0 && { version: entry.previousVersion },
2487
- reason: `#${pull.number} merged at ${head.slice(0, 7)}`,
2488
- pending: {
2489
- version: entry.version,
2490
- notes: entry.notes,
2491
- kind: entry.kind === "none" ? "fix" : entry.kind,
2492
- ...entry.previousVersion !== void 0 && { previousVersion: entry.previousVersion },
2493
- reason: `proposed in #${pull.number}`
2494
- }
2495
- };
2496
- },
2497
- count: (input) => tags.count?.(input) ?? Promise.resolve(0),
2498
- exists: (input) => tags.exists?.(input) ?? Promise.resolve(false)
2499
- };
2500
- }
2501
- });
2502
- //#endregion
2503
- //#region src/histories/index.ts
2504
- var histories_exports = /* @__PURE__ */ __exportAll({
2505
- fsVersionFile: () => fsVersionFile,
2506
- gitTags: () => gitTags,
2507
- githubReleasePr: () => githubReleasePr
2508
- });
2509
- //#endregion
2510
- //#region src/steps/channels.ts
2511
- /** The `channels` option of steps that a channel's releases skip by default. */
2512
- const channelsOption = z.boolean().default(false).describe("Also run for releases on a channel, e.g. prereleases; by default only stable releases do.");
2513
- /**
2514
- * For a step that leaves channel releases to the stable release, e.g. so a
2515
- * prerelease doesn't take the changelog's entries: its output when it's
2516
- * skipped, or `undefined` when it runs.
2517
- */
2518
- function skipOnChannel(env, channels, what) {
2519
- if (!env.channel || channels) return void 0;
2520
- return {
2521
- output: null,
2522
- reason: `left ${what} for the stable release, as this is on ${env.channel.name}`
2523
- };
2524
- }
2525
- //#endregion
2526
- //#region src/steps/change-files/parse.ts
2527
- /**
2528
- * Parses a change file:
2529
- *
2530
- * ```md
2531
- * ---
2532
- * "@x/core": feature
2533
- * cli: fix
2534
- * ---
2535
- * Add dark mode.
2536
- *
2537
- * More about it.
2538
- * ```
2539
- */
2540
- function parseChangeFile(text, path) {
2541
- const lines = text.replace(/^/, "").split(/\r?\n/);
2542
- if (lines[0]?.trim() !== "---") throw new Error(`${path} must start with '---', then kinds by component`);
2543
- const end = lines.findIndex((line, i) => i > 0 && line.trim() === "---");
2544
- if (end === -1) throw new Error(`${path} has no closing '---'`);
2545
- const kinds = {};
2546
- for (const [i, line] of lines.slice(1, end).entries()) {
2547
- if (line.trim() === "" || line.trim().startsWith("#")) continue;
2548
- const match = /^\s*(?:"([^"]+)"|'([^']+)'|([^:\s]+))\s*:\s*(\S+)\s*$/.exec(line);
2549
- const name = match?.[1] ?? match?.[2] ?? match?.[3];
2550
- const kind = match?.[4];
2551
- if (name === void 0 || kind === void 0) throw new Error(`${path} line ${i + 2}: expected 'component: kind'`);
2552
- if (!KINDS.includes(kind)) throw new Error(`${path} line ${i + 2}: '${kind}' isn't a kind; use ${KINDS.join(", ")}`);
2553
- kinds[name] = kind;
2554
- }
2555
- const [title = "", ...rest] = lines.slice(end + 1).join("\n").trim().split("\n");
2556
- if (title.trim() === "") throw new Error(`${path} says nothing after its kinds`);
2557
- const description = rest.join("\n").trim();
2558
- return {
2559
- kinds,
2560
- title: title.trim(),
2561
- ...description && { description }
2562
- };
2563
- }
2564
- //#endregion
2565
- //#region src/steps/change-files/files.ts
2566
- const directoryOption = z.string().default(".changes").describe("Where change files are kept, relative to the project.");
2567
- /** Every change file in the directory, parsed, by its path in the project. */
2568
- async function readChangeFiles(cwd, directory) {
2569
- const names = await readdir(join(cwd, directory)).catch((error) => {
2570
- if (error.code === "ENOENT") return [];
2571
- throw error;
2572
- });
2573
- const files = [];
2574
- for (const name of names.filter((name) => name.endsWith(".md")).sort()) {
2575
- if (name.toLowerCase() === "readme.md") continue;
2576
- const path = posix.join(directory.replaceAll("\\", "/"), name);
2577
- files.push({
2578
- path,
2579
- file: parseChangeFile(await readFile(join(cwd, path), "utf8"), path)
2580
- });
2581
- }
2582
- return files;
2583
- }
2584
- /**
2585
- * The kind a change file gives a component: its own, or `kind` when the
2586
- * component is the only one.
2587
- */
2588
- function kindFor(file, component, only) {
2589
- return file.kinds[component] ?? (only ? file.kinds.kind : void 0);
2590
- }
2591
- //#endregion
2592
- //#region src/steps/change-files/clean.ts
2593
- /**
2594
- * Deletes the change files a release consumed, those naming a component
2595
- * that's released, and reports them for `git/commit` to commit their
2596
- * deletion. Releases on a channel leave them for the stable release.
2597
- */
2598
- const changeFilesClean = defineStep({
2599
- phase: "write",
2600
- name: "change-files/clean",
2601
- scope: "all",
2602
- description: "Deletes the change files of the components being released, for `git/commit` to commit; releases on a channel leave them for the stable release.",
2603
- options: {
2604
- directory: directoryOption,
2605
- channels: channelsOption
2606
- },
2607
- run: async ({ env, components, options }) => {
2608
- const skipped = skipOnChannel(env, options.channels, "the change files");
2609
- if (skipped) return skipped;
2610
- const only = components.length === 1 && components[0]?.component.path === ".";
2611
- const released = components.map(({ component }) => component.name);
2612
- const files = (await readChangeFiles(env.cwd, options.directory)).filter(({ file }) => released.some((name) => kindFor(file, name, only) !== void 0)).map(({ path }) => path);
2613
- if (files.length === 0) return {
2614
- output: { deleted: [] },
2615
- reason: "no change files to delete"
2616
- };
2617
- if (!env.dryRun) for (const path of files) await rm(join(env.cwd, path));
2618
- return {
2619
- output: { deleted: env.dryRun ? [] : files },
2620
- files,
2621
- reason: `${env.dryRun ? "would delete" : "deleted"} ${files.length > 3 ? plural(files.length, "change file") : joinList(files)}`
2622
- };
2623
- }
2624
- });
2625
- //#endregion
2626
- //#region src/steps/change-files/read.ts
2627
- /**
2628
- * Collects the change files in `.changes/`: each says what changed, and
2629
- * how much for each component it names. A change's label is its kind, for
2630
- * `labels/determine-kind` and `labels/determine-notes`.
2631
- */
2632
- const changeFilesRead = defineStep({
2633
- phase: "read-changes",
2634
- name: "change-files/read",
2635
- scope: "all",
2636
- provides: ["changes.title", "changes.labels"],
2637
- description: "Collects the change files in `.changes/`, each naming the components it changes and how much, e.g. `core: feature`; each change is labeled with its kind.",
2638
- options: { directory: directoryOption },
2639
- run: async ({ env, components, options }) => {
2640
- const files = await readChangeFiles(env.cwd, options.directory);
2641
- const names = new Set(components.map(({ component }) => component.name));
2642
- const only = components.length === 1 && components[0]?.component.path === ".";
2643
- for (const { path, file } of files) for (const name of Object.keys(file.kinds)) {
2644
- if (names.has(name) || only && name === "kind") continue;
2645
- throw new Error(`${path} names '${name}', which isn't a component; use ${only ? "kind" : [...names].join(", ")}`);
2646
- }
2647
- const results = {};
2648
- for (const { component } of components) {
2649
- const changes = files.flatMap(({ path, file }) => {
2650
- const kind = kindFor(file, component.name, only);
2651
- return kind === void 0 ? [] : [{
2652
- title: file.title,
2653
- ...file.description && { description: file.description },
2654
- labels: [kind],
2655
- reason: path
2656
- }];
2657
- });
2658
- results[component.name] = {
2659
- changes,
2660
- reason: `${plural(changes.length, "change file")} in ${options.directory}`
2661
- };
2662
- }
2663
- return {
2664
- results,
2665
- reason: `${plural(files.length, "change file")} in ${options.directory}`
2666
- };
2667
- }
2668
- });
2669
- //#endregion
2670
- //#region src/steps/components/dependents.ts
2671
- const RANK$3 = {
2672
- none: 0,
2673
- fix: 1,
2674
- feature: 2,
2675
- breaking: 3
2676
- };
2677
- /**
2678
- * Releases the components that depend on a released component, so their
2679
- * releases pick up the new version, e.g. with `npm/write-version` updating
2680
- * their dependency on it. Dependents of dependents follow.
2681
- */
2682
- const componentsDependents = defineStep({
2683
- phase: "determine-kind",
2684
- name: "components/dependents",
2685
- scope: "all",
2686
- description: "Releases each component that depends on a released one, as at least a `fix`; list it after the steps that determine kinds.",
2687
- options: {
2688
- kind: z.enum([
2689
- "breaking",
2690
- "feature",
2691
- "fix"
2692
- ]).default("fix").describe("The least kind of a dependent's release."),
2693
- on: z.array(z.enum([
2694
- "breaking",
2695
- "feature",
2696
- "fix"
2697
- ])).default([
2698
- "breaking",
2699
- "feature",
2700
- "fix"
2701
- ]).describe("The kinds of a dependency's release that release its dependents.")
2702
- },
2703
- run: ({ components, options }) => {
2704
- const kinds = new Map(components.map(({ component, kind }) => [component.name, kind ?? "none"]));
2705
- const results = {};
2706
- for (const { component } of components) {
2707
- const released = component.dependsOn.filter((name) => {
2708
- const kind = kinds.get(name) ?? "none";
2709
- return kind !== "none" && options.on.includes(kind);
2710
- });
2711
- const current = kinds.get(component.name) ?? "none";
2712
- if (released.length === 0 || RANK$3[current] >= RANK$3[options.kind]) continue;
2713
- kinds.set(component.name, options.kind);
2714
- results[component.name] = {
2715
- kind: options.kind,
2716
- reason: `depends on ${joinList(released)}, released too`
2717
- };
2718
- }
2719
- const raised = Object.keys(results);
2720
- return {
2721
- results,
2722
- reason: raised.length > 0 ? `${plural(raised.length, "dependent")} released: ${joinList(raised)}` : "no dependents of released components"
2723
- };
2724
- }
2725
- });
2726
- //#endregion
2727
- //#region src/steps/conventional/classify.ts
2728
- /** A conventional commit header, e.g. `feat(api)!: drop node 18`. */
2729
- const HEADER = /^([a-z][\w-]*)(?:\(([^()]*)\))?(!)?: (.+)$/;
2730
- const BREAKING = /\bBREAKING(?:[ -]CHANGES?)?\b/;
2731
- /**
2732
- * Splits a commit message into conventional sections. Every line that looks
2733
- * like a conventional header starts a new section, so squashed messages with
2734
- * several entries yield several sections. Types must start lowercase, so
2735
- * footers like `Refs: #12` aren't mistaken for headers. Text before the first
2736
- * header (e.g. a merge commit's subject) is ignored.
2737
- */
2738
- function parseSections(message) {
2739
- const sections = [];
2740
- for (const line of message.split(/\r?\n/)) if (HEADER.test(line.trim())) sections.push({
2741
- title: line.trim(),
2742
- lines: []
2743
- });
2744
- else sections.at(-1)?.lines.push(line);
2745
- return sections.map(({ title, lines }) => {
2746
- const description = lines.join("\n").trim();
2747
- return description ? {
2748
- title,
2749
- description
2750
- } : { title };
2751
- });
2752
- }
2753
- function parseTitle(title) {
2754
- const match = HEADER.exec(title.trim());
2755
- if (!match) return void 0;
2756
- const [, type = "", scope, bang, subject = ""] = match;
2757
- return {
2758
- type,
2759
- ...scope ? { scope } : {},
2760
- breaking: bang === "!",
2761
- subject
2762
- };
2763
- }
2764
- /** Whether a text mentions a breaking change (`BREAKING CHANGE:`, `BREAKING-CHANGE`, ...). */
2765
- function mentionsBreaking(text) {
2766
- return text !== void 0 && BREAKING.test(text);
2767
- }
2768
- /** `!` or `BREAKING` → breaking; otherwise by the rules (by default `feat` → feature, the rest → fix). */
2769
- function classify(change, rules = {
2770
- types: { feat: "feature" },
2771
- fallback: "fix"
2772
- }) {
2773
- const source = `(${change.reason})`;
2774
- const title = change.title === void 0 ? void 0 : parseTitle(change.title);
2775
- const prefix = title ? `${title.type}${title.scope ? `(${title.scope})` : ""}${title.breaking ? "!" : ""}` : void 0;
2776
- if (title?.breaking) return {
2777
- kind: "breaking",
2778
- reason: `'${prefix}' in title ${source}`
2779
- };
2780
- if (mentionsBreaking(change.title)) return {
2781
- kind: "breaking",
2782
- reason: `'BREAKING' in title ${source}`
2783
- };
2784
- if (mentionsBreaking(change.description)) return {
2785
- kind: "breaking",
2786
- reason: `'BREAKING' in description ${source}`
2787
- };
2788
- if (!title) return {
2789
- kind: rules.fallback,
2790
- reason: `untyped change ${source}`
2791
- };
2792
- return {
2793
- kind: typeKey(title, rules.types) ?? rules.fallback,
2794
- reason: `'${prefix}' in title ${source}`
2795
- };
2796
- }
2797
- /** Looks up `type(scope)`, then `type`, in a map keyed by either. */
2798
- function typeKey(title, map) {
2799
- return (title.scope ? map[`${title.type}(${title.scope})`] : void 0) ?? map[title.type];
2800
- }
2801
- //#endregion
2802
- //#region src/steps/conventional/determine-kind.ts
2803
- const RANK$2 = {
2804
- none: 0,
2805
- fix: 1,
2806
- feature: 2,
2807
- breaking: 3
2808
- };
2809
- /**
2810
- * `!` or `BREAKING` → breaking; otherwise each change's kind comes from its
2811
- * conventional type, and the highest kind wins.
2812
- */
2813
- const conventionalDetermineKind = defineStep({
2814
- phase: "determine-kind",
2815
- name: "conventional/determine-kind",
2816
- description: "`!` or `BREAKING` → breaking; otherwise by conventional type (`feat` → feature, anything else → fix). The highest kind wins.",
2817
- options: {
2818
- types: z.record(z.string(), z.enum(KINDS)).default({}).describe("Kinds by `type` or `type(scope)`, on top of `feat: feature`, e.g. `{ docs: none, chore(deps): fix }`."),
2819
- fallback: z.enum(KINDS).default("fix").describe("The kind of any other change, untyped ones included.")
2820
- },
2821
- run: ({ changes, options }) => {
2822
- const rules = {
2823
- types: {
2824
- feat: "feature",
2825
- ...options.types
2826
- },
2827
- fallback: options.fallback
2828
- };
2829
- let best;
2830
- for (const change of changes) {
2831
- const candidate = classify(change, rules);
2832
- if (!best || RANK$2[candidate.kind] > RANK$2[best.kind]) best = candidate;
2833
- if (best.kind === "breaking") break;
2834
- }
2835
- return best ?? {
2836
- kind: "none",
2837
- reason: "no changes"
2838
- };
2839
- }
2840
- });
2841
- //#endregion
2842
- //#region src/steps/notes.ts
2843
- /** The `dependencies` option of the steps that build notes. */
2844
- const dependenciesOption = z.union([z.string(), z.literal(false)]).default("Dependencies").describe("The last section, for the components it depends on that were released along with it; `false` leaves it out.");
2845
- /** A section listing released dependencies, e.g. `- core 1.2.0 → 2.0.0`. */
2846
- function dependencySection(title, dependencies) {
2847
- if (title === false || dependencies.length === 0) return [];
2848
- return [{
2849
- title,
2850
- entries: dependencies.map(({ name, version, previousVersion }) => previousVersion === void 0 ? `- ${name} ${version}` : `- ${name} ${previousVersion} → ${version}`)
2851
- }];
2852
- }
2853
- /** Notes from the sections that have entries, and their reason. */
2854
- function joinSections(kind, sections) {
2855
- const filled = sections.filter(({ entries }) => entries.length > 0);
2856
- return {
2857
- notes: filled.map(({ title, entries }) => `### ${title}\n\n${entries.join("\n")}`).join("\n\n"),
2858
- reason: `${kind} release; ${filled.map(({ title, entries }) => `${title.toLowerCase()}: ${entries.length}`).join(", ") || "no entries"}`
2859
- };
2860
- }
2861
- //#endregion
2862
- //#region src/steps/conventional/determine-notes.ts
2863
- /**
2864
- * A `### Title` section for breaking changes, then one per group of
2865
- * conventional types, then the rest.
2866
- */
2867
- const conventionalBuildNotes = defineStep({
2868
- phase: "determine-notes",
2869
- name: "conventional/determine-notes",
2870
- description: "`### Breaking Changes`, `### Features`, `### Bug Fixes`, `### Performance Improvements` and `### Reverts` sections, then `### Other` for everything else.",
2871
- options: {
2872
- sections: z.array(z.strictObject({
2873
- title: z.string(),
2874
- types: z.array(z.string()).describe("`type` or `type(scope)`")
2875
- })).default([
2876
- {
2877
- title: "Features",
2878
- types: ["feat"]
2879
- },
2880
- {
2881
- title: "Bug Fixes",
2882
- types: ["fix"]
2883
- },
2884
- {
2885
- title: "Performance Improvements",
2886
- types: ["perf"]
2887
- },
2888
- {
2889
- title: "Reverts",
2890
- types: ["revert"]
2891
- }
2892
- ]).describe("Sections in order; a change goes in the first that lists its type."),
2893
- breaking: z.union([z.string(), z.literal(false)]).default("Breaking Changes").describe("The first section, for breaking changes of any type; `false` marks them **(breaking)** in their own section instead."),
2894
- other: z.union([z.string(), z.literal(false)]).default("Other").describe("The section for remaining changes; `false` leaves them out."),
2895
- links: z.boolean().default(true).describe("Link each change to its source, e.g. its commit."),
2896
- dependencies: dependenciesOption
2897
- },
2898
- run: ({ changes, kind, dependencies, options }) => {
2899
- const section = (title, types = []) => ({
2900
- title,
2901
- types,
2902
- entries: []
2903
- });
2904
- const breaking = options.breaking === false ? void 0 : section(options.breaking);
2905
- const other = options.other === false ? void 0 : section(options.other);
2906
- const typed = options.sections.map(({ title, types }) => section(title, types));
2907
- for (const change of changes) {
2908
- const title = change.title ? parseTitle(change.title) : void 0;
2909
- const isBreaking = title?.breaking || mentionsBreaking(change.title) || mentionsBreaking(change.description);
2910
- const target = (isBreaking ? breaking : void 0) ?? (title && typed.find((s) => matches(title, s.types))) ?? other;
2911
- if (!target) continue;
2912
- const entry = format$1(change, title, {
2913
- marker: isBreaking && target !== breaking,
2914
- link: options.links
2915
- });
2916
- if (entry !== void 0) target.entries.push(entry);
2917
- }
2918
- return joinSections(kind, [...[
2919
- breaking,
2920
- ...typed,
2921
- other
2922
- ].filter((s) => s !== void 0), ...dependencySection(options.dependencies, dependencies)]);
2923
- }
2924
- });
2925
- function matches(title, types) {
2926
- return types.includes(title.type) || title.scope !== void 0 && types.includes(`${title.type}(${title.scope})`);
2927
- }
2928
- function format$1(change, parsed, style) {
2929
- const text = parsed ? `${parsed.scope ? `**${parsed.scope}:** ` : ""}${parsed.subject}` : (change.title ?? change.description?.split("\n")[0])?.trim();
2930
- if (!text) return void 0;
2931
- return `- ${text}${style.marker ? " **(breaking)**" : ""}${style.link && change.link ? ` ([${change.link.label}](${change.link.url}))` : ""}`;
2932
- }
2933
- //#endregion
2934
- //#region src/steps/conventional/read-commits.ts
2935
- const COMMIT_SEPARATOR = "";
2936
- const FIELD_SEPARATOR = "";
2937
- /**
2938
- * Collects every conventional section (`feat: ...`, `fix(x)!: ...`) in the
2939
- * commits since the previous release, or in all history without one. Only
2940
- * commits touching the component count; for the whole project, when the
2941
- * working directory is a subdirectory of the repository, only those touching
2942
- * it. Each change links to its commit when the repository is known: on
2943
- * GitHub, or in GitLab CI, on GitLab.
2944
- */
2945
- const conventionalReadCommits = defineStep({
2946
- phase: "read-changes",
2947
- name: "conventional/read-commits",
2948
- description: "Collects each conventional section (`feat:`, `fix(x)!:` …) of every commit touching the component since the previous release, or in all history without one.",
2949
- provides: ["changes.title"],
2950
- run: async ({ env, component, previousRef }) => {
2951
- const cwd = env.cwd;
2952
- await assertFullHistory(cwd);
2953
- const log = await logSince(cwd, {
2954
- ref: previousRef,
2955
- pathspec: component.pathspec,
2956
- format: `%H${FIELD_SEPARATOR}%B${COMMIT_SEPARATOR}`
2957
- });
2958
- const commitUrl = await commitUrlOf(cwd);
2959
- const changes = [];
2960
- let commits = 0;
2961
- for (const entry of log.split(COMMIT_SEPARATOR)) {
2962
- const [sha, message = ""] = entry.trim().split(FIELD_SEPARATOR);
2963
- if (!sha) continue;
2964
- commits++;
2965
- const short = sha.slice(0, 7);
2966
- const link = commitUrl ? {
2967
- label: short,
2968
- url: commitUrl(sha)
2969
- } : void 0;
2970
- for (const section of parseSections(message)) changes.push({
2971
- ...section,
2972
- reason: `commit ${short}`,
2973
- ...link && { link }
2974
- });
2975
- }
2976
- const since = previousRef ? `since ${previousRef.slice(0, 7)}` : "in all history";
2977
- return {
2978
- changes,
2979
- reason: `${plural(changes.length, "change")} in ${plural(commits, "commit")} ${since}`
2980
- };
2981
- }
2982
- });
2983
- /** Links to commits on GitHub, or in GitLab CI on GitLab, when the repository is known. */
2984
- async function commitUrlOf(cwd) {
2985
- const repo = await detectRepo(cwd);
2986
- if (repo) {
2987
- const server = process.env.GITHUB_SERVER_URL || "https://github.com";
2988
- return (sha) => `${server}/${repo}/commit/${sha}`;
2989
- }
2990
- const project = process.env.CI_PROJECT_URL;
2991
- return project ? (sha) => `${project}/-/commit/${sha}` : void 0;
2992
- }
2993
- //#endregion
2994
- //#region src/utils/exec.ts
2995
- /** Runs a command and resolves with its trimmed stdout; fails with its stderr. */
2996
- function exec(command, args, options) {
2997
- return new Promise((resolve, reject) => {
2998
- execFile(command, args, {
2999
- ...options,
3000
- maxBuffer: 67108864
3001
- }, (error, stdout, stderr) => {
3002
- if (error) {
3003
- const detail = stderr.trim() || error.message;
3004
- reject(/* @__PURE__ */ new Error(`${command} ${args.join(" ")} failed: ${detail}`));
3005
- } else resolve(stdout.trim());
3006
- });
3007
- });
3008
- }
3009
- //#endregion
3010
- //#region src/steps/exec/run.ts
3011
- /**
3012
- * Runs a step's command for a component, or in a dry run its `dryRun`
3013
- * command if it has one; resolves with what it ran, and its output.
3014
- */
3015
- async function runCommand(input, options) {
3016
- const { env, component, placeholders } = input;
3017
- const command = env.dryRun && options.dryRun !== void 0 ? options.dryRun : options.command;
3018
- const argv = typeof command === "string" ? [
3019
- "sh",
3020
- "-c",
3021
- fill(command, placeholders)
3022
- ] : command.map((part) => fill(part, placeholders));
3023
- const shown = typeof command === "string" ? argv[2] ?? "" : argv.join(" ");
3024
- if (env.dryRun && options.dryRun === void 0) return {
3025
- shown,
3026
- stdout: null
3027
- };
3028
- const [program = "", ...args] = argv;
3029
- return {
3030
- shown,
3031
- stdout: await exec(program, args, {
3032
- cwd: inComponent(env, component, options.cwd).file,
3033
- env: {
3034
- ...process.env,
3035
- ...Object.fromEntries(Object.entries(options.env).map(([key, value]) => [key, fill(value, placeholders)]))
3036
- }
3037
- })
3038
- };
3039
- }
3040
- /** The last lines of a command's output, for its reason. */
3041
- function tail(stdout, lines = 1) {
3042
- return stdout.split("\n").filter((line) => line.trim()).slice(-lines).join(" ").slice(0, 200);
3043
- }
3044
- //#endregion
3045
- //#region src/steps/exec/command.ts
3046
- /**
3047
- * Runs a command for each component in the phase it's given: in `write`,
3048
- * e.g. `cargo update --workspace` to update a lockfile; in `release`, e.g.
3049
- * `cargo publish` or `docker push app:{version}`.
3050
- */
3051
- const execCommand = defineStep({
3052
- phase: (options) => options.phase,
3053
- name: "exec/command",
3054
- description: "Runs a command for each component in its `phase`: `write` e.g. to update a lockfile, listing what it changes as `files`, or `release` e.g. `cargo publish` or `docker push app:{version}`.",
3055
- options: {
3056
- phase: z.enum([
3057
- "write",
3058
- "stage",
3059
- "release"
3060
- ]).describe("The phase it runs in."),
3061
- command: z.union([z.string(), z.array(z.string()).min(1)]).describe("`{placeholders}` are filled in. Text runs in `sh -c`; a list runs as it is, without a shell."),
3062
- dryRun: z.union([z.string(), z.array(z.string()).min(1)]).optional().describe("Runs instead in a dry run, e.g. `cargo publish --dry-run`; without it, a dry run only says what it would run."),
3063
- cwd: z.string().default(".").describe("Where it runs, relative to the component's directory."),
3064
- env: z.record(z.string(), z.string()).default({}).describe("More environment variables, with `{placeholders}` filled in."),
3065
- files: z.array(z.string()).default([]).describe("What it changes, relative to the component's directory, for `git/commit`.")
3066
- },
3067
- run: async (input) => {
3068
- const { env, component, options } = input;
3069
- const files = options.files.map((file) => inComponent(env, component, file).relative);
3070
- const { shown, stdout } = await runCommand(input, options);
3071
- if (stdout === null) return {
3072
- output: {
3073
- command: shown,
3074
- stdout: null
3075
- },
3076
- files,
3077
- reason: `would run ${shown}`
3078
- };
3079
- const last = tail(stdout);
3080
- return {
3081
- output: {
3082
- command: shown,
3083
- stdout
3084
- },
3085
- files,
3086
- reason: `ran ${shown}${last ? `: ${last}` : ""}`
3087
- };
3088
- }
3089
- });
3090
- //#endregion
3091
- //#region src/steps/fs/write-changes.ts
3092
- /** Writes release notes to a file, creating it if needed. */
3093
- const fsWriteChanges = defineStep({
3094
- phase: "write",
3095
- name: "fs/write-changes",
3096
- description: "Writes a heading and the notes to a file.",
3097
- options: {
3098
- path: z.string().default("CHANGELOG.md").describe("Relative to the component's directory."),
3099
- heading: z.string().default("## {tag} ({date})").describe(`${VERSION_PLACEHOLDERS}, and \`{date}\` (YYYY-MM-DD).`),
3100
- mode: z.enum([
3101
- "prepend",
3102
- "append",
3103
- "replace"
3104
- ]).default("prepend"),
3105
- channels: channelsOption
3106
- },
3107
- run: async ({ env, component, placeholders, notes, options }) => {
3108
- const { heading, mode } = options;
3109
- const { file, relative: path } = inComponent(env, component, options.path);
3110
- const skipped = skipOnChannel(env, options.channels, path);
3111
- if (skipped) return skipped;
3112
- const existing = await readFile(file, "utf8").catch((error) => {
3113
- if (error.code === "ENOENT") return void 0;
3114
- throw error;
3115
- });
3116
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
3117
- const entry = `${fill(heading, {
3118
- ...placeholders,
3119
- date
3120
- })}\n\n${notes}`.trimEnd();
3121
- const action = existing === void 0 ? "create" : mode;
3122
- if (env.dryRun) return {
3123
- output: {
3124
- path,
3125
- entry,
3126
- written: false
3127
- },
3128
- files: [path],
3129
- reason: `would ${action} ${path}`
3130
- };
3131
- let content = [entry];
3132
- if (mode !== "replace") content = [
3133
- ...mode === "prepend" ? [entry] : [],
3134
- ...existing?.trim() ? [existing.trim()] : [],
3135
- ...mode === "append" ? [entry] : []
3136
- ];
3137
- await writeFile(file, `${content.join("\n\n")}\n`);
3138
- return {
3139
- output: {
3140
- path,
3141
- entry,
3142
- written: true
3143
- },
3144
- files: [path],
3145
- reason: `${existing === void 0 ? "created" : `${mode}${mode.endsWith("e") ? "d" : "ed"}`} ${path}`
3146
- };
3147
- }
3148
- });
3149
- //#endregion
3150
- //#region src/core/scheme.ts
3151
- /**
3152
- * Defines a version scheme whose options are declared as a zod shape. The
3153
- * result is a factory, like a step's: `calver({ format: "YY.MINOR.MICRO" })`.
3154
- */
3155
- function defineScheme(definition) {
3156
- return makeFactory("scheme", definition);
3157
- }
3158
- //#endregion
3159
- //#region src/schemes/semver.ts
3160
- const PATTERN = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z.-]+))?$/;
3161
- /** Whether a version is valid semver: `MAJOR.MINOR.PATCH`, without leading zeros. */
3162
- function isSemver(version) {
3163
- return PATTERN.test(version.trim());
3164
- }
3165
- /**
3166
- * Whether a version is a stable semantic version: one without a pre-release
3167
- * part, which the scheme leaves to channels.
3168
- */
3169
- function isStable(version) {
3170
- const match = PATTERN.exec(version.trim());
3171
- return match !== null && match[4] === void 0;
3172
- }
3173
- function parseSemver(version) {
3174
- const match = PATTERN.exec(version.trim());
3175
- if (!match) throw new Error(`'${version}' is not a valid semantic version`);
3176
- const [, major, minor, patch, prerelease, build] = match;
3177
- return {
3178
- major: Number(major),
3179
- minor: Number(minor),
3180
- patch: Number(patch),
3181
- ...prerelease !== void 0 && { prerelease },
3182
- ...build !== void 0 && { build }
3183
- };
3184
- }
3185
- function formatSemver(version) {
3186
- let text = `${version.major}.${version.minor}.${version.patch}`;
3187
- if (version.prerelease !== void 0) text += `-${version.prerelease}`;
3188
- if (version.build !== void 0) text += `+${version.build}`;
3189
- return text;
3190
- }
3191
- /** Bumps a version: `breaking` → major, `feature` → minor, `fix` → patch. */
3192
- function bumpSemver(version, kind) {
3193
- const { major, minor, patch } = version;
3194
- switch (kind) {
3195
- case "breaking": return {
3196
- major: major + 1,
3197
- minor: 0,
3198
- patch: 0
3199
- };
3200
- case "feature": return {
3201
- major,
3202
- minor: minor + 1,
3203
- patch: 0
3204
- };
3205
- case "fix": return {
3206
- major,
3207
- minor,
3208
- patch: patch + 1
3209
- };
3210
- }
3211
- }
3212
- const POSITION = {
3213
- breaking: "major",
3214
- feature: "minor",
3215
- fix: "patch"
3216
- };
3217
- /**
3218
- * `MAJOR.minor.patch`: breaking changes bump the major number, features the
3219
- * minor and fixes the patch. Before 1.0.0, bumps shift down a level by
3220
- * default (`0.MAJOR.minorOrPatch`).
3221
- */
3222
- const semver = defineScheme({
3223
- name: "semver",
3224
- description: "`MAJOR.minor.patch` by the kind: breaking, feature or fix; `0.MAJOR.minorOrPatch` before 1.0.0.",
3225
- options: {
3226
- shiftPreMajor: z.boolean().default(true).describe("Before 1.0.0, bump breaking changes as minor and features as patch."),
3227
- initial: z.string().default("0.1.0").refine(isSemver, "must be a semantic version, e.g. 1.0.0").describe("The first version, when there's no previous one.")
3228
- },
3229
- create: (options) => ({
3230
- name: "semver",
3231
- traits: ["version.semver"],
3232
- matches: isStable,
3233
- compare: (a, b) => compareVersions(parseVersion(a), parseVersion(b)),
3234
- bump: ({ previous, kind }) => {
3235
- if (previous === void 0) return {
3236
- version: options.initial,
3237
- reason: `first release`
3238
- };
3239
- const parsed = parseSemver(previous);
3240
- const shifted = parsed.major === 0 && options.shiftPreMajor ? kind === "breaking" ? "feature" : "fix" : kind;
3241
- return {
3242
- version: formatSemver(bumpSemver(parsed, shifted)),
3243
- reason: shifted === kind ? `bumps ${POSITION[kind]}` : `bumps ${POSITION[shifted]} (pre-1.0)`
3244
- };
3245
- },
3246
- fields: (version) => {
3247
- const { major, minor, patch, prerelease } = parseSemver(version);
3248
- return {
3249
- major: String(major),
3250
- minor: String(minor),
3251
- patch: String(patch),
3252
- ...prerelease !== void 0 && { prerelease }
3253
- };
3254
- }
3255
- })
3256
- });
3257
- //#endregion
3258
- //#region src/steps/fs/write-version.ts
3259
- /**
3260
- * Writes each component's new version to its version file, in the project's
3261
- * own format, keeping everything else as it was. With the `npm` and `cargo`
3262
- * formats, dependencies on the other components released with it are
3263
- * pointed at their new versions too, keeping each range's operator.
3264
- */
3265
- const fsWriteVersion = defineStep({
3266
- phase: "write",
3267
- name: "fs/write-version",
3268
- scope: "all",
3269
- description: "Writes each component's new version to its version file: `VERSION`, `package.json`, `Cargo.toml`, `pyproject.toml`, `Chart.yaml`, `gradle.properties`, `pom.xml`, or any file by pattern. With `npm` and `cargo`, it also updates dependencies on the other components released with it, and needs `version.semver`.",
3270
- options: {
3271
- format: z.enum(FORMAT_NAMES).default("plain").describe("`plain` (the whole file), `npm`, `cargo`, `python`, `helm`, `gradle`, `maven`, or `pattern`."),
3272
- path: z.union([z.string(), z.array(z.string()).min(1)]).optional().describe("Relative to each component's directory; globs, or a list, write several, e.g. `packages/*/package.json` for packages sharing a version. Defaults to the format's file."),
3273
- pattern: z.string().optional().describe("With the `pattern` format, where the version is, e.g. `__version__ = \"{version}\"`.")
3274
- },
3275
- needs: (options) => versionFormat(options.format, options.pattern ?? "{version}").needs ?? [],
3276
- run: async ({ env, components, options }) => {
3277
- const format = versionFormat(options.format, options.pattern);
3278
- const patterns = [options.path ?? format.path].flat();
3279
- if (patterns.includes("")) throw new Error("the pattern format needs a `path`");
3280
- if (format.needs?.includes("version.semver")) {
3281
- for (const { version } of components) if (!isSemver(version)) throw new Error(`${format.name} needs a semantic version, and '${version}' isn't one`);
3282
- }
3283
- const files = (await Promise.all(components.map(async ({ component, version }) => {
3284
- const dir = inComponent(env, component, ".").file;
3285
- const found = options.format === "plain" ? patterns.map((pattern) => inComponent(env, component, pattern).file) : await resolveFiles(dir, patterns);
3286
- return Promise.all(found.map(async (file) => ({
3287
- file,
3288
- version,
3289
- text: await readFile(file, "utf8").catch((error) => {
3290
- if (error.code === "ENOENT") return "";
3291
- throw error;
3292
- }),
3293
- relative: posix.normalize(relative(env.cwd, file).replaceAll("\\", "/"))
3294
- })));
3295
- }))).flat();
3296
- const packages = /* @__PURE__ */ new Map();
3297
- for (const { text, version } of files) {
3298
- const name = format.packageName?.(text);
3299
- if (name !== void 0) packages.set(name, version);
3300
- }
3301
- const written = [];
3302
- for (const { file, version, text, relative: path } of files) {
3303
- if (text === "" && options.format !== "plain") throw new Error(`${path} doesn't exist`);
3304
- let result;
3305
- try {
3306
- result = format.write(text, version, packages);
3307
- } catch (error) {
3308
- throw new Error(`can't write ${version} to ${path}: ${error instanceof Error ? error.message : String(error)}`);
3309
- }
3310
- if (!env.dryRun) await writeFile(file, result.text);
3311
- written.push({
3312
- path,
3313
- version,
3314
- dependencies: result.dependencies
3315
- });
3316
- }
3317
- const described = joinList(written.map(({ path, version, dependencies }) => `${version} to ${path}${dependencies.length > 0 ? ` (and ${joinList(dependencies)})` : ""}`));
3318
- return {
3319
- output: { written: env.dryRun ? [] : written },
3320
- files: written.map(({ path }) => path),
3321
- reason: `${env.dryRun ? "would write" : "wrote"} ${described}`
3322
- };
3323
- }
3324
- });
3325
- //#endregion
3326
- //#region src/steps/git/placeholders.ts
3327
- /**
3328
- * Placeholders for what's released as a whole: `{tags}`, every release's
3329
- * tag, and with one component, its own placeholders.
3330
- */
3331
- function releasedPlaceholders(components) {
3332
- const [only] = components;
3333
- return {
3334
- ...components.length === 1 && only ? only.placeholders : {},
3335
- tags: components.map(({ tag }) => tag).join(", ")
3336
- };
3337
- }
3338
- //#endregion
3339
- //#region src/steps/git/commit.ts
3340
- /**
3341
- * Commits the files that earlier `release` steps changed, for every
3342
- * component, in one commit. Without this the version bump never lands.
3343
- * Pushing is off by default, so `git/create-tag` can push the commit and its
3344
- * tags together.
3345
- */
3346
- const gitCommit = defineStep({
3347
- phase: "stage",
3348
- name: "git/commit",
3349
- scope: "all",
3350
- description: "Commits the files earlier release steps changed, for every component, in one commit.",
3351
- options: {
3352
- files: z.array(z.string()).default([]).describe("More files to commit, relative to the working directory."),
3353
- message: z.string().default("chore(release): {tags}").describe("The commit message; `{tags}` is filled in with every release's tag, and with one component, its placeholders too, e.g. `{version}`."),
3354
- push: z.union([z.string(), z.literal(false)]).default(false).describe("The remote to push the branch to; `false` leaves it to a later step, e.g. `git/create-tag`, so the commit and tag land together.")
3355
- },
3356
- run: async ({ env, components, options }) => {
3357
- const remote = options.push;
3358
- const message = fill(options.message, releasedPlaceholders(components));
3359
- const files = [.../* @__PURE__ */ new Set([...components.flatMap(({ releases }) => releases.flatMap((release) => release.files)), ...options.files])];
3360
- if (files.length === 0) throw new Error("no files to commit; no earlier step reported any");
3361
- const pushing = remote ? ` and push to ${remote}` : "";
3362
- if (env.dryRun) return {
3363
- output: {
3364
- commit: null,
3365
- pushed: false
3366
- },
3367
- reason: `would commit ${files.join(", ")}${pushing}`
3368
- };
3369
- const cwd = env.cwd;
3370
- await git(cwd, "add", "--all", "--", ...files);
3371
- if (await git(cwd, "diff", "--cached", "--name-only") === "") throw new Error("nothing to commit");
3372
- await git(cwd, ...await identity(cwd), "commit", "-m", message);
3373
- const commit = await git(cwd, "rev-parse", "HEAD");
3374
- if (remote) await git(cwd, "push", remote, "HEAD");
3375
- return {
3376
- output: {
3377
- commit,
3378
- pushed: remote !== false
3379
- },
3380
- reason: `committed ${commit.slice(0, 7)}${remote ? ` and pushed to ${remote}` : ""}`
3381
- };
3382
- }
3383
- });
3384
- //#endregion
3385
- //#region src/steps/git/create-tag.ts
3386
- /**
3387
- * Tags HEAD with every component's release, moves any floating tags to it,
3388
- * and pushes them along with the current branch in one atomic push, so a
3389
- * commit from `git/commit` and its tags land together or not at all.
3390
- */
3391
- const gitCreateTag = defineStep({
3392
- phase: "release",
3393
- name: "git/create-tag",
3394
- scope: "all",
3395
- description: "Tags HEAD with each release's tag and pushes them, along with the branch, in one atomic push.",
3396
- options: {
3397
- message: z.string().default("chore(release): {tag}").describe(`The tag message; ${VERSION_PLACEHOLDERS}.`),
3398
- move: z.array(z.string()).default([]).describe("Tags to create or move to each release, e.g. `v{major}`; force-pushed. Those with `{channel}` only move on a channel, e.g. to `next`, and the others only on stable releases."),
3399
- push: z.union([z.string(), z.literal(false)]).default(false).describe("The remote to push the tags and the current branch to; `false` skips pushing.")
3400
- },
3401
- run: async ({ env, components, options }) => {
3402
- const remote = options.push;
3403
- const tags = components.map(({ tag, placeholders }) => ({
3404
- tag,
3405
- message: fill(options.message, placeholders)
3406
- }));
3407
- const moved = [...new Set(components.flatMap(({ placeholders }) => options.move.filter((name) => name.includes("{channel}") === (env.channel !== void 0)).map((name) => fill(name, placeholders))))];
3408
- const names = joinList(tags.map(({ tag }) => tag));
3409
- if (env.dryRun) return {
3410
- output: {
3411
- tags: tags.map(({ tag }) => tag),
3412
- moved,
3413
- pushed: false
3414
- },
3415
- reason: `would ${joinList([
3416
- `tag ${names}`,
3417
- ...moved.length ? [`move ${moved.join(", ")}`] : [],
3418
- ...remote ? [`push to ${remote}`] : []
3419
- ])}`
3420
- };
3421
- const cwd = env.cwd;
3422
- const head = await git(cwd, "rev-parse", "HEAD");
3423
- const who = await identity(cwd);
3424
- for (const { tag, message } of tags) await git(cwd, ...who, "tag", "-a", tag, "-m", message);
3425
- for (const name of moved) await git(cwd, "tag", "-f", name, "HEAD");
3426
- if (remote) await git(cwd, "push", "--atomic", remote, ...await tryGit(cwd, "symbolic-ref", "-q", "HEAD") ? ["HEAD"] : [], ...tags.map(({ tag }) => `refs/tags/${tag}`), ...moved.map((name) => `+refs/tags/${name}`));
3427
- return {
3428
- output: {
3429
- tags: tags.map(({ tag }) => tag),
3430
- moved,
3431
- pushed: remote !== false
3432
- },
3433
- reason: joinList([
3434
- `tagged ${names} at ${head.slice(0, 7)}`,
3435
- ...moved.length ? [`moved ${moved.join(", ")}`] : [],
3436
- ...remote ? [`pushed to ${remote}`] : []
3437
- ])
3438
- };
3439
- }
3440
- });
3441
- //#endregion
3442
- //#region src/steps/github/create-release.ts
3443
- /**
3444
- * Creates a GitHub release for the version, with the notes as its body, or
3445
- * finds the one another job already created.
3446
- */
3447
- const githubCreateRelease = defineStep({
3448
- phase: "release",
3449
- name: "github/create-release",
3450
- description: "Creates a GitHub release of the tag with the notes, unless it already has one; the tag is created at HEAD if it doesn't exist. Needs a token with `contents: write`.",
3451
- options: releaseOptions,
3452
- run: async ({ env, tag, placeholders, notes, options }) => {
3453
- const title = fill(options.title, placeholders);
3454
- const repo = await releaseRepo(env.cwd, options.repo);
3455
- if (env.dryRun) return {
3456
- output: {
3457
- repo,
3458
- tag,
3459
- url: null
3460
- },
3461
- reason: `would create release ${tag} on ${repo}`
3462
- };
3463
- const { release, created } = await ensureRelease(githubClient(repo, githubToken(options.token)), {
3464
- tag,
3465
- target: await git(env.cwd, "rev-parse", "HEAD"),
3466
- title,
3467
- notes,
3468
- draft: options.draft,
3469
- prerelease: isPrerelease(options.prerelease, env)
3470
- });
3471
- return {
3472
- output: {
3473
- repo,
3474
- tag,
3475
- id: release.id,
3476
- url: release.html_url
3477
- },
3478
- reason: `${created ? "created" : "found existing"} release ${tag}: ${release.html_url}`
3479
- };
3480
- }
3481
- });
3482
- //#endregion
3483
- //#region src/steps/github/open-release-pr.ts
3484
- /**
3485
- * Proposes the release as a pull request instead of releasing it: commits
3486
- * what earlier `write` steps changed to a release branch, and opens a pull
3487
- * request for it, or updates the one that's open. The release steps wait
3488
- * until it merges; then the `github/release-pr` history finds it, and they
3489
- * run for what it proposed.
3490
- */
3491
- const githubOpenReleasePr = defineStep({
3492
- phase: "stage",
3493
- name: "github/open-release-pr",
3494
- scope: "all",
3495
- description: "Commits the prepared release to a branch and opens a pull request for it, or updates the open one; the release waits until it merges. Use with the `github/release-pr` history. List it after the `write` steps, in place of `git/commit`.",
3496
- options: {
3497
- branch: z.string().default(`${RELEASE_BRANCH}{branch}`).describe("The release branch, force-pushed; `{branch}` is the branch being released."),
3498
- title: z.string().default("chore(release): {tags}").describe("The commit message and title; `{tags}` lists every release's tag."),
3499
- labels: z.array(z.string()).default([PENDING_LABEL]).describe("Labels for the pull request; `github/release-pr` looks for the first."),
3500
- remote: z.string().default("origin").describe("Where to push the branch."),
3501
- repo: z.string().optional().describe("`owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote."),
3502
- token: z.string().optional().describe("Defaults to `GITHUB_TOKEN` or `GH_TOKEN`; needs `pull-requests: write`.")
3503
- },
3504
- run: async ({ env, components, options }) => {
3505
- const tags = components.map(({ tag }) => tag).join(", ");
3506
- const branch = fill(options.branch, { branch: env.branch });
3507
- const title = fill(options.title, { tags });
3508
- const files = [...new Set(components.flatMap(({ releases }) => releases.flatMap((release) => release.files)))];
3509
- const manifest = { components: Object.fromEntries(components.map((component) => [component.component.name, {
3510
- version: component.version,
3511
- tag: component.tag,
3512
- kind: component.kind,
3513
- ...component.previousVersion !== void 0 && { previousVersion: component.previousVersion },
3514
- notes: component.notes
3515
- }])) };
3516
- const repo = await releaseRepo(env.cwd, options.repo);
3517
- if (env.dryRun) return {
3518
- output: {
3519
- branch,
3520
- number: null,
3521
- url: null
3522
- },
3523
- files,
3524
- reason: `would push ${files.length > 0 ? joinList(files) : "nothing new"} to ${branch} and propose ${tags} on ${repo}`,
3525
- defer: `released when the pull request for ${tags} merges`
3526
- };
3527
- const client = githubClient(repo, githubToken(options.token));
3528
- const cwd = env.cwd;
3529
- const back = await tryGit(cwd, "symbolic-ref", "-q", "--short", "HEAD") ?? await git(cwd, "rev-parse", "HEAD");
3530
- await git(cwd, "checkout", "-q", "--detach");
3531
- try {
3532
- if (files.length > 0) await git(cwd, "add", "--all", "--", ...files);
3533
- await git(cwd, ...await identity(cwd), "commit", "-q", "--allow-empty", "-m", title);
3534
- await git(cwd, "push", "--force", options.remote, `HEAD:refs/heads/${branch}`);
3535
- } finally {
3536
- await git(cwd, "checkout", "-q", "--force", back);
3537
- }
3538
- const owner = repo.split("/")[0];
3539
- const { data: open } = await client.request("GET", `/pulls?state=open&head=${encodeURIComponent(`${owner}:${branch}`)}`);
3540
- const body = releaseBody(manifest);
3541
- let pull;
3542
- const [existing] = open;
3543
- if (existing) pull = (await client.request("PATCH", `/pulls/${existing.number}`, { body: {
3544
- title,
3545
- body
3546
- } })).data;
3547
- else pull = (await client.request("POST", "/pulls", { body: {
3548
- title,
3549
- body,
3550
- head: branch,
3551
- base: env.branch
3552
- } })).data;
3553
- if (options.labels.length > 0) await client.request("POST", `/issues/${pull.number}/labels`, { body: { labels: options.labels } });
3554
- return {
3555
- output: {
3556
- branch,
3557
- number: pull.number,
3558
- url: pull.html_url
3559
- },
3560
- files,
3561
- reason: `${existing ? "updated" : "opened"} #${pull.number} for ${tags}, with ${plural(files.length, "file")}: ${pull.html_url}`,
3562
- defer: `released when #${pull.number} merges`
3563
- };
3564
- }
3565
- });
3566
- //#endregion
3567
- //#region src/steps/github/read-pull-requests.ts
3568
- /** Commits looked up at once. */
3569
- const CONCURRENCY = 8;
3570
- /**
3571
- * Collects the pull requests merged into the branch through the commits
3572
- * touching the component since the previous release, or in all history
3573
- * without one, each with its labels. Commits that weren't merged through a
3574
- * pull request aren't counted.
3575
- */
3576
- const githubReadPullRequests = defineStep({
3577
- phase: "read-changes",
3578
- name: "github/read-pull-requests",
3579
- description: "Collects the pull requests merged into the branch by the commits touching the component since the previous release, with their labels. Needs a token that can read pull requests.",
3580
- provides: [
3581
- "changes.title",
3582
- "changes.labels",
3583
- "changes.link"
3584
- ],
3585
- options: {
3586
- repo: z.string().optional().describe("`owner/repo`; defaults to `GITHUB_REPOSITORY` or the `origin` remote."),
3587
- token: z.string().optional().describe("Defaults to `GITHUB_TOKEN` or `GH_TOKEN`.")
3588
- },
3589
- run: async ({ env, component, previousRef, options }) => {
3590
- const cwd = env.cwd;
3591
- await assertFullHistory(cwd);
3592
- const shas = (await logSince(cwd, {
3593
- ref: previousRef,
3594
- pathspec: component.pathspec,
3595
- format: "%H"
3596
- })).split("\n").filter(Boolean);
3597
- const client = githubClient(await releaseRepo(cwd, options.repo), githubToken(options.token));
3598
- const found = /* @__PURE__ */ new Map();
3599
- for (let i = 0; i < shas.length; i += CONCURRENCY) {
3600
- const batch = await Promise.all(shas.slice(i, i + CONCURRENCY).map(async (sha) => {
3601
- const { data } = await client.request("GET", `/commits/${sha}/pulls`);
3602
- return data;
3603
- }));
3604
- for (const pull of batch.flat()) if (pull.merged_at && pull.base.ref === env.branch) found.set(pull.number, pull);
3605
- }
3606
- const changes = [...found.values()].map((pull) => ({
3607
- title: pull.title,
3608
- ...pull.body?.trim() && { description: pull.body.trim() },
3609
- reason: `PR #${pull.number}`,
3610
- link: {
3611
- label: `#${pull.number}`,
3612
- url: pull.html_url
3613
- },
3614
- labels: pull.labels.map((label) => label.name)
3615
- }));
3616
- const since = previousRef ? `since ${previousRef.slice(0, 7)}` : "in all history";
3617
- return {
3618
- changes,
3619
- reason: `${plural(changes.length, "pull request")} in ${plural(shas.length, "commit")} ${since}`
3620
- };
3621
- }
3622
- });
3623
- //#endregion
3624
- //#region src/utils/zip.ts
3625
- const deflate = promisify(deflateRaw);
3626
- const LIMIT = 4294967295;
3627
- /**
3628
- * Zips files, deflated, into one buffer. Without ZIP64, the archive and each
3629
- * file must stay under 4 GiB.
3630
- */
3631
- async function zip(entries) {
3632
- const parts = [];
3633
- const central = [];
3634
- let offset = 0;
3635
- for (const entry of entries) {
3636
- const name = Buffer.from(entry.name.replaceAll("\\", "/"));
3637
- const data = await deflate(entry.content);
3638
- if (entry.content.length >= LIMIT || offset + data.length >= LIMIT) throw new Error(`'${entry.name}' is too large to zip (4 GiB at most)`);
3639
- const crc = crc32(entry.content);
3640
- const fields = (header, at) => {
3641
- header.writeUInt16LE(20, at);
3642
- header.writeUInt16LE(2048, at + 2);
3643
- header.writeUInt16LE(8, at + 4);
3644
- header.writeUInt16LE(0, at + 6);
3645
- header.writeUInt16LE(33, at + 8);
3646
- header.writeUInt32LE(crc, at + 10);
3647
- header.writeUInt32LE(data.length, at + 14);
3648
- header.writeUInt32LE(entry.content.length, at + 18);
3649
- header.writeUInt16LE(name.length, at + 22);
3650
- header.writeUInt16LE(0, at + 24);
3651
- };
3652
- const local = Buffer.alloc(30);
3653
- local.writeUInt32LE(67324752, 0);
3654
- fields(local, 4);
3655
- const record = Buffer.alloc(46);
3656
- record.writeUInt32LE(33639248, 0);
3657
- record.writeUInt16LE(20, 4);
3658
- fields(record, 6);
3659
- record.writeUInt32LE(offset, 42);
3660
- central.push(record, name);
3661
- parts.push(local, name, data);
3662
- offset += local.length + name.length + data.length;
3663
- }
3664
- const directory = Buffer.concat(central);
3665
- const end = Buffer.alloc(22);
3666
- end.writeUInt32LE(101010256, 0);
3667
- end.writeUInt16LE(entries.length, 8);
3668
- end.writeUInt16LE(entries.length, 10);
3669
- end.writeUInt32LE(directory.length, 12);
3670
- end.writeUInt32LE(offset, 16);
3671
- return Buffer.concat([
3672
- ...parts,
3673
- directory,
3674
- end
3675
- ]);
3676
- }
3677
- //#endregion
3678
- //#region src/steps/github/artifact.ts
3679
- const SERVICE = "twirp/github.actions.results.api.v1.ArtifactService";
3680
- /** Whether this is a job that can upload workflow artifacts. */
3681
- function canUploadArtifacts() {
3682
- return Boolean(process.env.ACTIONS_RESULTS_URL && process.env.ACTIONS_RUNTIME_TOKEN);
3683
- }
3684
- /** The job's artifact service, from what the runner gives JavaScript actions. */
3685
- function service() {
3686
- const url = process.env.ACTIONS_RESULTS_URL;
3687
- const token = process.env.ACTIONS_RUNTIME_TOKEN;
3688
- if (!url || !token) throw new Error("no artifact service; workflow artifacts can only be uploaded by the rylease action in a GitHub Actions job");
3689
- const [, run, job] = (JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString()).scp?.split(" ").find((scope) => scope.startsWith("Actions.Results:")))?.split(":") ?? [];
3690
- if (!run || !job) throw new Error("the runtime token names no job");
3691
- return {
3692
- ids: {
3693
- workflow_run_backend_id: run,
3694
- workflow_job_run_backend_id: job
3695
- },
3696
- call: async (method, body) => {
3697
- const response = await fetch(`${new URL(url).origin}/${SERVICE}/${method}`, {
3698
- method: "POST",
3699
- headers: {
3700
- authorization: `Bearer ${token}`,
3701
- "content-type": "application/json"
3702
- },
3703
- body: JSON.stringify(body)
3704
- });
3705
- const text = await response.text();
3706
- if (!response.ok) throw new Error(`artifact service responded ${response.status} to ${method}: ${text}`);
3707
- return JSON.parse(text);
3708
- }
3709
- };
3710
- }
3711
- /** Uploads a zip as a workflow artifact; resolves with its id. */
3712
- async function uploadArtifact(name, content, retentionDays) {
3713
- const { ids, call } = service();
3714
- const created = await call("CreateArtifact", {
3715
- ...ids,
3716
- name,
3717
- version: 4,
3718
- ...retentionDays !== void 0 && { expires_at: new Date(Date.now() + retentionDays * 864e5).toISOString() }
3719
- });
3720
- if (!created.ok) throw new Error(`couldn't create artifact '${name}'`);
3721
- await retry(async () => {
3722
- const response = await fetch(created.signed_upload_url, {
3723
- method: "PUT",
3724
- headers: {
3725
- "x-ms-blob-type": "BlockBlob",
3726
- "x-ms-version": "2020-10-02",
3727
- "content-type": "application/zip"
3728
- },
3729
- body: content
3730
- });
3731
- if (!response.ok) throw new Error(`artifact storage responded ${response.status}: ${await response.text()}`);
3732
- }, { attempts: 5 });
3733
- const finalized = await call("FinalizeArtifact", {
3734
- ...ids,
3735
- name,
3736
- size: String(content.length),
3737
- hash: `sha256:${createHash("sha256").update(content).digest("hex")}`
3738
- });
3739
- if (!finalized.ok) throw new Error(`couldn't finalize artifact '${name}'`);
3740
- return finalized.artifact_id;
3741
- }
3742
- /**
3743
- * Zips files as a workflow artifact, with paths relative to their closest
3744
- * common directory, as `actions/upload-artifact` does.
3745
- */
3746
- async function uploadFiles(name, files, retentionDays) {
3747
- const root = files.map(dirname).reduce(commonDirectory);
3748
- const entries = await Promise.all(files.map(async (file) => ({
3749
- name: relative(root, file).split(sep).join("/"),
3750
- content: await readFile(file)
3751
- })));
3752
- const sizes = await Promise.all(files.map(async (file) => (await stat(file)).size));
3753
- return {
3754
- id: await uploadArtifact(name, await zip(entries), retentionDays),
3755
- files: entries.map((entry) => entry.name),
3756
- size: sizes.reduce((sum, bytes) => sum + bytes, 0)
3757
- };
3758
- }
3759
- function commonDirectory(a, b) {
3760
- const left = a.split(sep);
3761
- const right = b.split(sep);
3762
- let length = 0;
3763
- while (length < left.length && left[length] === right[length]) length++;
3764
- return left.slice(0, length).join(sep) || sep;
3765
- }
3766
- //#endregion
3767
- //#region src/steps/github/upload-artifact.ts
3768
- /**
3769
- * Keeps files as an artifact of the workflow run; with `dryRunArtifact`, in
3770
- * dry runs too, e.g. to keep what a dry run built.
3771
- */
3772
- const githubUploadArtifact = defineStep({
3773
- phase: "release",
3774
- name: "github/upload-artifact",
3775
- description: "Uploads files as a workflow artifact. Only works in the GitHub Action.",
3776
- options: {
3777
- name: z.string().describe("The artifact's name, unique in the run; `{version}`, `{tag}` and the other placeholders are filled in."),
3778
- files: z.array(z.string()).min(1).describe("Paths, globs or directories, relative to the component's directory unless absolute; each must match."),
3779
- dryRunArtifact: z.boolean().default(false).describe("Upload in dry runs too."),
3780
- retentionDays: z.number().int().min(1).optional().describe("Defaults to the repository's setting.")
3781
- },
3782
- run: async ({ env, component, placeholders, options }) => {
3783
- const name = fill(options.name, placeholders);
3784
- const files = await resolveFiles(inComponent(env, component, ".").file, options.files);
3785
- const listed = `${plural(files.length, "file")} as artifact ${name}`;
3786
- if (env.dryRun && !(options.dryRunArtifact && canUploadArtifacts())) return {
3787
- output: {
3788
- name,
3789
- id: null,
3790
- files: []
3791
- },
3792
- reason: `would upload ${listed}${options.dryRunArtifact ? " (outside GitHub Actions)" : ""}`
3793
- };
3794
- const uploaded = await uploadFiles(name, files, options.retentionDays);
3795
- return {
3796
- output: {
3797
- name,
3798
- id: uploaded.id,
3799
- files: uploaded.files
3800
- },
3801
- reason: `uploaded ${plural(files.length, "file")} (${formatSize(uploaded.size)}) as artifact ${name}`
3802
- };
3803
- }
3804
- });
3805
- //#endregion
3806
- //#region src/steps/github/upload-release-assets.ts
3807
- /**
3808
- * Uploads files to the version's GitHub release, creating the release if no
3809
- * other step or job has yet, so that jobs building different files can each
3810
- * upload their own.
3811
- */
3812
- const githubUploadReleaseAssets = defineStep({
3813
- phase: "release",
3814
- name: "github/upload-release-assets",
3815
- description: "Uploads files to the GitHub release, creating it (with the notes) if no other job has yet.",
3816
- options: {
3817
- files: z.array(z.string()).min(1).describe("Paths or globs, relative to the component's directory unless absolute; each must match."),
3818
- clobber: z.boolean().default(true).describe("Replace assets of the same name."),
3819
- dryRunArtifact: z.boolean().default(false).describe("In dry runs, keep the files as a workflow artifact instead; only in the GitHub Action."),
3820
- artifactName: z.string().optional().describe("Unique in the run; `{version}`, `{tag}` and the other placeholders are filled in. Defaults to the asset's name, or for several, a name made from theirs."),
3821
- ...releaseOptions
3822
- },
3823
- run: async ({ env, component, tag, placeholders, notes, options }) => {
3824
- const title = fill(options.title, placeholders);
3825
- const repo = await releaseRepo(env.cwd, options.repo);
3826
- const files = await resolveFiles(inComponent(env, component, ".").file, options.files);
3827
- const sizes = await Promise.all(files.map(async (file) => (await stat(file)).size));
3828
- const listed = files.map((file, index) => `${assetName(basename(file))} (${formatSize(sizes[index] ?? 0)})`);
3829
- if (env.dryRun) {
3830
- const reason = `would upload ${joinList(listed)} to release ${tag} on ${repo}`;
3831
- if (!options.dryRunArtifact) return {
3832
- output: {
3833
- repo,
3834
- tag,
3835
- url: null,
3836
- assets: []
3837
- },
3838
- reason
3839
- };
3840
- const artifact = artifactNameOf(files, options.artifactName, placeholders);
3841
- if (!canUploadArtifacts()) return {
3842
- output: {
3843
- repo,
3844
- tag,
3845
- url: null,
3846
- assets: [],
3847
- artifact: null
3848
- },
3849
- reason: `${reason} (no artifact ${artifact} outside GitHub Actions)`
3850
- };
3851
- const { id } = await uploadFiles(artifact, files);
3852
- return {
3853
- output: {
3854
- repo,
3855
- tag,
3856
- url: null,
3857
- assets: [],
3858
- artifact: {
3859
- name: artifact,
3860
- id
3861
- }
3862
- },
3863
- reason: `${reason}; kept them as artifact ${artifact}`
3864
- };
3865
- }
3866
- const client = githubClient(repo, githubToken(options.token));
3867
- const { release, created } = await ensureRelease(client, {
3868
- tag,
3869
- target: await git(env.cwd, "rev-parse", "HEAD"),
3870
- title,
3871
- notes,
3872
- draft: options.draft,
3873
- prerelease: isPrerelease(options.prerelease, env)
3874
- });
3875
- const assets = [];
3876
- for (const file of files) {
3877
- const asset = await uploadAsset(client, release, {
3878
- name: basename(file),
3879
- content: await readFile(file)
3880
- }, options.clobber);
3881
- assets.push({
3882
- name: asset.name,
3883
- size: asset.size,
3884
- url: asset.browser_download_url
3885
- });
3886
- }
3887
- return {
3888
- output: {
3889
- repo,
3890
- tag,
3891
- url: release.html_url,
3892
- assets
3893
- },
3894
- reason: `uploaded ${joinList(listed)} to ${created ? "new" : "existing"} release ${tag}: ${release.html_url}`
3895
- };
3896
- }
3897
- });
3898
- function artifactNameOf(files, name, placeholders) {
3899
- if (name !== void 0) return fill(name, placeholders);
3900
- const assets = files.map((file) => assetName(basename(file)));
3901
- if (assets.length === 1 && assets[0]) return assets[0];
3902
- return `release-assets-${createHash("sha256").update(assets.join("\n")).digest("hex").slice(0, 8)}`;
3903
- }
3904
- //#endregion
3905
- //#region src/steps/gitlab/create-release.ts
3906
- var GitLabError = class extends Error {
3907
- status;
3908
- constructor(status, message) {
3909
- super(message);
3910
- this.name = "GitLabError";
3911
- this.status = status;
3912
- }
3913
- };
3914
- /**
3915
- * Creates a GitLab release for the version, with the notes as its
3916
- * description, or finds the one another job already created. In GitLab CI,
3917
- * the project, API and job token come from the job's environment.
3918
- */
3919
- const gitlabCreateRelease = defineStep({
3920
- phase: "release",
3921
- name: "gitlab/create-release",
3922
- description: "Creates a GitLab release of the tag with the notes, unless it already has one; the tag is created at HEAD if it doesn't exist.",
3923
- options: {
3924
- title: z.string().default("{tag}").describe("`{tag}`, `{version}`, `{name}` and the scheme's placeholders are filled in."),
3925
- project: z.string().optional().describe("The project's ID or path, e.g. `group/project`; defaults to `CI_PROJECT_ID`."),
3926
- token: z.string().optional().describe("Defaults to `GITLAB_TOKEN`, then the job's `CI_JOB_TOKEN`.")
3927
- },
3928
- run: async ({ env, tag, placeholders, notes, options }) => {
3929
- const title = fill(options.title, placeholders);
3930
- const project = options.project ?? process.env.CI_PROJECT_ID;
3931
- if (!project) throw new Error("couldn't determine the GitLab project; set `project` or CI_PROJECT_ID");
3932
- if (env.dryRun) return {
3933
- output: {
3934
- project,
3935
- tag,
3936
- url: null
3937
- },
3938
- reason: `would create release ${tag} on ${project}`
3939
- };
3940
- const request = gitlabClient(project, options.token);
3941
- let created = true;
3942
- let release;
3943
- try {
3944
- release = await request("POST", "/releases", {
3945
- tag_name: tag,
3946
- ref: await git(env.cwd, "rev-parse", "HEAD"),
3947
- name: title,
3948
- description: notes
3949
- });
3950
- } catch (error) {
3951
- if (!(error instanceof GitLabError && error.status === 409)) throw error;
3952
- created = false;
3953
- release = await request("GET", `/releases/${encodeURIComponent(tag)}`);
3954
- }
3955
- const url = release._links.self;
3956
- return {
3957
- output: {
3958
- project,
3959
- tag,
3960
- url
3961
- },
3962
- reason: `${created ? "created" : "found existing"} release ${tag}: ${url}`
3963
- };
3964
- }
3965
- });
3966
- /** Requests to a project's REST API, retrying rate limits and server errors. */
3967
- function gitlabClient(project, token) {
3968
- const api = process.env.CI_API_V4_URL || "https://gitlab.com/api/v4";
3969
- const auth = token ?? process.env.GITLAB_TOKEN;
3970
- const header = auth ? { "private-token": auth } : process.env.CI_JOB_TOKEN ? { "job-token": process.env.CI_JOB_TOKEN } : void 0;
3971
- if (!header) throw new Error("no token; set GITLAB_TOKEN, or run in GitLab CI");
3972
- return (method, path, body) => retry(async () => {
3973
- const url = `${api}/projects/${encodeURIComponent(project)}${path}`;
3974
- const response = await fetch(url, {
3975
- method,
3976
- headers: {
3977
- ...header,
3978
- ...body !== void 0 && { "content-type": "application/json" }
3979
- },
3980
- ...body !== void 0 && { body: JSON.stringify(body) }
3981
- });
3982
- const text = await response.text();
3983
- if (!response.ok) throw new GitLabError(response.status, `GitLab responded ${response.status} to ${method} ${url}: ${text}`);
3984
- return JSON.parse(text);
3985
- }, {
3986
- attempts: 5,
3987
- retryIf: (error) => error instanceof GitLabError ? error.status === 429 || error.status >= 500 : error instanceof TypeError
3988
- });
3989
- }
3990
- //#endregion
3991
- //#region src/steps/keep-a-changelog/changelog.ts
3992
- /** The `## [Unreleased]` heading of a Keep a Changelog file. */
3993
- const UNRELEASED = /^##\s+\[?unreleased\]?\s*$/i;
3994
- /** Finds the `## [Unreleased]` section of a changelog's lines. */
3995
- function findUnreleased(lines, path) {
3996
- const start = lines.findIndex((line) => UNRELEASED.test(line));
3997
- if (start === -1) throw new Error(`${path} has no '## [Unreleased]' section`);
3998
- const next = lines.findIndex((line, i) => i > start && /^##\s/.test(line));
3999
- return {
4000
- start,
4001
- end: next === -1 ? lines.length : next
4002
- };
4003
- }
4004
- //#endregion
4005
- //#region src/steps/keep-a-changelog/read-unreleased.ts
4006
- /**
4007
- * Collects the entries of the `## [Unreleased]` section of a changelog kept as
4008
- * on keepachangelog.com: each top-level bullet is a change, labeled with its
4009
- * `### Added`, `### Fixed` … heading, and the lines under it its description.
4010
- */
4011
- const keepAChangelogReadUnreleased = defineStep({
4012
- phase: "read-changes",
4013
- name: "keep-a-changelog/read-unreleased",
4014
- description: "Collects each entry under `## [Unreleased]` in a Keep a Changelog file, labeled with its `### Added`, `### Fixed` … heading.",
4015
- provides: ["changes.title", "changes.labels"],
4016
- options: { path: z.string().default("CHANGELOG.md").describe("Relative to the component's directory.") },
4017
- run: async ({ env, component, options }) => {
4018
- const { file, relative: path } = inComponent(env, component, options.path);
4019
- const lines = (await readFile(file, "utf8")).split(/\r?\n/);
4020
- const { start, end } = findUnreleased(lines, path);
4021
- const changes = [];
4022
- let category;
4023
- let current;
4024
- const finish = () => {
4025
- const description = current?.body.join("\n").trim();
4026
- if (current && description) current.change.description = description;
4027
- current = void 0;
4028
- };
4029
- for (let i = start + 1; i < end; i++) {
4030
- const line = lines[i] ?? "";
4031
- const heading = /^###\s+(.+?)\s*$/.exec(line);
4032
- const bullet = /^[-*+]\s+(.*)$/.exec(line);
4033
- if (heading) {
4034
- finish();
4035
- category = heading[1];
4036
- } else if (bullet) {
4037
- finish();
4038
- current = {
4039
- change: {
4040
- title: (bullet[1] ?? "").trim(),
4041
- reason: `${path} line ${i + 1}`,
4042
- labels: category ? [category] : []
4043
- },
4044
- body: []
4045
- };
4046
- changes.push(current.change);
4047
- } else if (current && (line.trim() === "" || /^\s/.test(line))) current.body.push(line.replace(/^ {1,2}|^\t/, ""));
4048
- else finish();
4049
- }
4050
- finish();
4051
- return {
4052
- changes,
4053
- reason: `${plural(changes.length, "entry", "entries")} under ${lines[start]?.trim()} in ${path}`
4054
- };
4055
- }
4056
- });
4057
- //#endregion
4058
- //#region src/steps/keep-a-changelog/write-release.ts
4059
- /** A `[unreleased]: <base>/compare/<tag>...HEAD` link definition. */
4060
- const UNRELEASED_LINK = /^\[unreleased\]:\s*(\S+)\/compare\/(\S+?)\.\.\.HEAD\s*$/i;
4061
- /**
4062
- * Releases the `## [Unreleased]` section of a Keep a Changelog file: its
4063
- * entries move under a heading for the version, and an empty
4064
- * `## [Unreleased]` takes its place. A `[unreleased]: …/compare/v1.0.0...HEAD`
4065
- * link moves on to the new tag, and the version gets its own compare link.
4066
- */
4067
- const keepAChangelogWriteRelease = defineStep({
4068
- phase: "write",
4069
- name: "keep-a-changelog/write-release",
4070
- description: "Moves the entries under `## [Unreleased]` to a heading for the version, and updates the compare links, if there are any.",
4071
- options: {
4072
- path: z.string().default("CHANGELOG.md").describe("Relative to the component's directory."),
4073
- heading: z.string().default("## [{version}] - {date}").describe(`${VERSION_PLACEHOLDERS}, and \`{date}\` (YYYY-MM-DD).`),
4074
- channels: channelsOption
4075
- },
4076
- run: async ({ env, component, version, tag, placeholders, options }) => {
4077
- const { file, relative: path } = inComponent(env, component, options.path);
4078
- const skipped = skipOnChannel(env, options.channels, path);
4079
- if (skipped) return skipped;
4080
- const text = await readFile(file, "utf8");
4081
- const lines = text.split(/\r?\n/);
4082
- const { start } = findUnreleased(lines, path);
4083
- const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
4084
- const heading = fill(options.heading, {
4085
- ...placeholders,
4086
- date
4087
- });
4088
- lines.splice(start + 1, 0, "", heading);
4089
- const link = lines.findIndex((line) => UNRELEASED_LINK.test(line));
4090
- const [, base, previous] = UNRELEASED_LINK.exec(lines[link] ?? "") ?? [];
4091
- if (base !== void 0 && previous !== void 0) lines.splice(link, 1, `[unreleased]: ${base}/compare/${tag}...HEAD`, `[${version}]: ${base}/compare/${previous}...${tag}`);
4092
- const updated = `${heading}${base ? " and its compare link" : ""}`;
4093
- if (env.dryRun) return {
4094
- output: {
4095
- path,
4096
- heading,
4097
- written: false
4098
- },
4099
- files: [path],
4100
- reason: `would add ${updated} to ${path}`
4101
- };
4102
- await writeFile(file, lines.join(text.includes("\r\n") ? "\r\n" : "\n"));
4103
- return {
4104
- output: {
4105
- path,
4106
- heading,
4107
- written: true
4108
- },
4109
- files: [path],
4110
- reason: `added ${updated} to ${path}`
4111
- };
4112
- }
4113
- });
4114
- //#endregion
4115
- //#region src/steps/labels/labels.ts
4116
- /** Labels compare case-insensitively, as GitHub's do. */
4117
- function hasLabel(change, labels) {
4118
- const wanted = labels.map((label) => label.toLowerCase());
4119
- return (change.labels ?? []).some((label) => wanted.includes(label.toLowerCase()));
4120
- }
4121
- /** Looks a label up in a map keyed by labels, ignoring case. */
4122
- function lookup(map, label) {
4123
- const key = Object.keys(map).find((other) => other.toLowerCase() === label.toLowerCase());
4124
- return key === void 0 ? void 0 : map[key];
4125
- }
4126
- //#endregion
4127
- //#region src/steps/labels/determine-kind.ts
4128
- const RANK$1 = {
4129
- none: 0,
4130
- fix: 1,
4131
- feature: 2,
4132
- breaking: 3
4133
- };
4134
- /**
4135
- * Each change's kind comes from its labels, e.g. a pull request's: the
4136
- * highest of them, or `fallback` when none is mapped. The highest kind wins.
4137
- */
4138
- const labelsDetermineKind = defineStep({
4139
- phase: "determine-kind",
4140
- name: "labels/determine-kind",
4141
- description: "By each change's labels, e.g. a pull request's (`breaking` → breaking, `enhancement` → feature, anything else → fix). The highest kind wins.",
4142
- needs: ["changes.labels"],
4143
- options: {
4144
- kinds: z.record(z.string(), z.enum(KINDS)).default({
4145
- breaking: "breaking",
4146
- "semver:major": "breaking",
4147
- feature: "feature",
4148
- enhancement: "feature",
4149
- "semver:minor": "feature"
4150
- }).describe("Kinds by label, ignoring case."),
4151
- fallback: z.enum(KINDS).default("fix").describe("The kind of a change without a label in `kinds`.")
4152
- },
4153
- run: ({ changes, options }) => {
4154
- let best;
4155
- for (const change of changes) {
4156
- let candidate = {
4157
- kind: options.fallback,
4158
- reason: `no label in kinds (${change.reason})`
4159
- };
4160
- for (const label of change.labels ?? []) {
4161
- const kind = lookup(options.kinds, label);
4162
- if (kind !== void 0 && RANK$1[kind] > RANK$1[candidate.kind]) candidate = {
4163
- kind,
4164
- reason: `'${label}' label (${change.reason})`
4165
- };
4166
- }
4167
- if (!best || RANK$1[candidate.kind] > RANK$1[best.kind]) best = candidate;
4168
- if (best.kind === "breaking") break;
4169
- }
4170
- return best ?? {
4171
- kind: "none",
4172
- reason: "no changes"
4173
- };
4174
- }
4175
- });
4176
- //#endregion
4177
- //#region src/steps/labels/determine-notes.ts
4178
- /**
4179
- * A `### Title` section per group of labels, like GitHub's own release notes
4180
- * categories, then the rest.
4181
- */
4182
- const labelsBuildNotes = defineStep({
4183
- phase: "determine-notes",
4184
- name: "labels/determine-notes",
4185
- description: "A `### Title` section per group of labels, e.g. `### Bug Fixes` for `bug`, then `### Other` for everything else.",
4186
- needs: ["changes.labels"],
4187
- options: {
4188
- sections: z.array(z.strictObject({
4189
- title: z.string(),
4190
- labels: z.array(z.string())
4191
- })).default([
4192
- {
4193
- title: "Breaking Changes",
4194
- labels: ["breaking", "semver:major"]
4195
- },
4196
- {
4197
- title: "Features",
4198
- labels: ["feature", "enhancement"]
4199
- },
4200
- {
4201
- title: "Bug Fixes",
4202
- labels: ["bug", "fix"]
4203
- }
4204
- ]).describe("Sections in order; a change goes in the first with one of its labels, ignoring case."),
4205
- other: z.union([z.string(), z.literal(false)]).default("Other").describe("The section for remaining changes; `false` leaves them out."),
4206
- exclude: z.array(z.string()).default(["skip-changelog"]).describe("Changes with any of these labels are left out."),
4207
- descriptions: z.boolean().default(false).describe("Write each change's description, indented, under it."),
4208
- links: z.boolean().default(true).describe("Link each change to its source, e.g. its pull request."),
4209
- dependencies: dependenciesOption
4210
- },
4211
- run: ({ changes, kind, dependencies, options }) => {
4212
- const sections = [...options.sections, ...options.other === false ? [] : [{ title: options.other }]].map((section) => ({
4213
- ...section,
4214
- entries: []
4215
- }));
4216
- for (const change of changes) {
4217
- if (hasLabel(change, options.exclude)) continue;
4218
- const target = sections.find((section) => !("labels" in section) || hasLabel(change, section.labels));
4219
- const entry = format(change, options);
4220
- if (target && entry !== void 0) target.entries.push(entry);
4221
- }
4222
- return joinSections(kind, [...sections, ...dependencySection(options.dependencies, dependencies)]);
4223
- }
4224
- });
4225
- function format(change, style) {
4226
- const lines = (change.title ?? change.description ?? "").trim().split("\n");
4227
- const text = lines[0]?.trim();
4228
- if (!text) return void 0;
4229
- const link = style.links && change.link ? ` ([${change.link.label}](${change.link.url}))` : "";
4230
- const details = (change.title === void 0 ? lines.slice(1) : [change.description ?? ""]).join("\n").trim();
4231
- return `- ${text}${link}${style.descriptions && details ? `\n${details.split("\n").map((line) => line.trim() ? ` ${line}` : "").join("\n")}` : ""}`;
4232
- }
4233
- //#endregion
4234
- //#region src/steps/npm/publish.ts
4235
- const DEFAULT_REGISTRY = "https://registry.npmjs.org/";
4236
- /**
4237
- * Publishes the package to npm, after earlier steps wrote the new version to
4238
- * `package.json`. Lifecycle scripts like `prepublishOnly` run as usual.
4239
- */
4240
- const npmPublish = defineStep({
4241
- phase: "release",
4242
- name: "npm/publish",
4243
- description: "Publishes the component's package to the npm registry with `npm` or `pnpm publish`; components are published dependencies first.",
4244
- needs: ["version.semver"],
4245
- options: {
4246
- path: z.string().default(".").describe("The package's directory, relative to the component's."),
4247
- client: z.enum(["npm", "pnpm"]).optional().describe("Defaults to the `packageManager` in package.json, then `pnpm` if there's a `pnpm-lock.yaml`, then `npm`."),
4248
- tag: z.string().optional().describe("The dist-tag; defaults to the channel's name on a channel, then `publishConfig.tag`, or `latest`."),
4249
- access: z.enum(["public", "restricted"]).optional().describe("Defaults to `publishConfig.access`."),
4250
- provenance: z.boolean().default(false).describe("Publishes with a provenance statement; needs `id-token: write`."),
4251
- registry: z.string().optional().describe("Defaults to `publishConfig.registry`, or the npm registry."),
4252
- token: z.string().optional().describe("Defaults to `NPM_TOKEN` or `NODE_AUTH_TOKEN`; without one, the client's own auth is used, e.g. trusted publishing.")
4253
- },
4254
- run: async ({ env, component, version, options }) => {
4255
- const { file: dir, relative } = inComponent(env, component, options.path);
4256
- const manifest = posix.join(relative, "package.json");
4257
- const json = await loadJson(join(dir, "package.json"));
4258
- const name = typeof json.name === "string" ? json.name : void 0;
4259
- if (!name) throw new Error(`${manifest} has no 'name'`);
4260
- if (json.private === true) throw new Error(`${manifest} is private; npm won't publish it`);
4261
- const publishConfig = json.publishConfig ?? {};
4262
- const registry = options.registry ?? (typeof publishConfig.registry === "string" ? publishConfig.registry : DEFAULT_REGISTRY);
4263
- const client = options.client ?? await detectClient(dir, json);
4264
- const tag = options.tag ?? env.channel?.name ?? (typeof publishConfig.tag === "string" ? publishConfig.tag : "latest");
4265
- const target = `${name}@${version} to ${registry} as ${tag}`;
4266
- const output = {
4267
- name,
4268
- version,
4269
- client,
4270
- tag,
4271
- registry
4272
- };
4273
- if (env.dryRun) return {
4274
- output: {
4275
- ...output,
4276
- published: false
4277
- },
4278
- reason: `would ${client} publish ${target}`
4279
- };
4280
- const current = versionOf(json, manifest);
4281
- if (current !== version) throw new Error(`${manifest} is at ${current}, not ${version}; write it first (npm/write-version)`);
4282
- const args = [
4283
- "publish",
4284
- "--registry",
4285
- registry,
4286
- "--tag",
4287
- tag
4288
- ];
4289
- if (options.access) args.push("--access", options.access);
4290
- if (options.provenance) args.push("--provenance");
4291
- if (client === "pnpm") args.push("--no-git-checks");
4292
- await withToken(options.token ?? process.env.NPM_TOKEN ?? process.env.NODE_AUTH_TOKEN, registry, (env) => exec(client, args, {
4293
- cwd: dir,
4294
- env
4295
- }));
4296
- return {
4297
- output: {
4298
- ...output,
4299
- published: true
4300
- },
4301
- reason: `published ${target}`
4302
- };
4303
- }
4304
- });
4305
- async function detectClient(dir, json) {
4306
- if (typeof json.packageManager === "string") return json.packageManager.startsWith("pnpm@") ? "pnpm" : "npm";
4307
- for (let current = dir;; current = dirname(current)) {
4308
- if (await exists(join(current, "pnpm-lock.yaml"))) return "pnpm";
4309
- if (await exists(join(current, "package-lock.json"))) return "npm";
4310
- if (dirname(current) === current) return "npm";
4311
- }
4312
- }
4313
- function exists(path) {
4314
- return access(path).then(() => true, () => false);
4315
- }
4316
- /**
4317
- * Runs `run` with an environment that authenticates to the registry with the
4318
- * token, if there is one: a copy of the user's npmrc with the token's key
4319
- * added, which both npm and pnpm read. The token itself stays in the
4320
- * environment, never on disk.
4321
- */
4322
- async function withToken(token, registry, run) {
4323
- if (!token) return run(process.env);
4324
- const userconfig = process.env.NPM_CONFIG_USERCONFIG ?? process.env.npm_config_userconfig ?? join(homedir(), ".npmrc");
4325
- const existing = await readFile(userconfig, "utf8").catch(() => "");
4326
- const dir = await mkdtemp(join(tmpdir(), "rylease-npmrc-"));
4327
- try {
4328
- const npmrc = join(dir, ".npmrc");
4329
- await writeFile(npmrc, `${existing.trimEnd()}\n${authKey(registry)}=\${RYLEASE_NPM_TOKEN}\n`.trimStart());
4330
- const { npm_config_userconfig: _, ...env } = process.env;
4331
- return await run({
4332
- ...env,
4333
- NPM_CONFIG_USERCONFIG: npmrc,
4334
- RYLEASE_NPM_TOKEN: token
4335
- });
4336
- } finally {
4337
- await rm(dir, {
4338
- recursive: true,
4339
- force: true
4340
- });
4341
- }
4342
- }
4343
- /** The npmrc key of a registry's token, e.g. `//registry.npmjs.org/:_authToken`. */
4344
- function authKey(registry) {
4345
- const url = new URL(registry);
4346
- const path = url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`;
4347
- return `//${url.host}${path}:_authToken`;
4348
- }
4349
- //#endregion
4350
- //#region src/steps/index.ts
4351
- var steps_exports = /* @__PURE__ */ __exportAll({
4352
- changeFilesClean: () => changeFilesClean,
4353
- changeFilesRead: () => changeFilesRead,
4354
- componentsDependents: () => componentsDependents,
4355
- conventionalBuildNotes: () => conventionalBuildNotes,
4356
- conventionalDetermineKind: () => conventionalDetermineKind,
4357
- conventionalReadCommits: () => conventionalReadCommits,
4358
- execCommand: () => execCommand,
4359
- fsWriteChanges: () => fsWriteChanges,
4360
- fsWriteVersion: () => fsWriteVersion,
4361
- gitCommit: () => gitCommit,
4362
- gitCreateTag: () => gitCreateTag,
4363
- githubCreateRelease: () => githubCreateRelease,
4364
- githubOpenReleasePr: () => githubOpenReleasePr,
4365
- githubReadPullRequests: () => githubReadPullRequests,
4366
- githubUploadArtifact: () => githubUploadArtifact,
4367
- githubUploadReleaseAssets: () => githubUploadReleaseAssets,
4368
- gitlabCreateRelease: () => gitlabCreateRelease,
4369
- keepAChangelogReadUnreleased: () => keepAChangelogReadUnreleased,
4370
- keepAChangelogWriteRelease: () => keepAChangelogWriteRelease,
4371
- labelsBuildNotes: () => labelsBuildNotes,
4372
- labelsDetermineKind: () => labelsDetermineKind,
4373
- npmPublish: () => npmPublish
4374
- });
4375
- //#endregion
4376
- //#region src/presets/change-files.ts
4377
- /**
4378
- * Change files in `.changes/` → notes by kind → the changelog written and
4379
- * the change files deleted, committed and tagged → GitHub release. Each
4380
- * file names the components it changes and how much, so a monorepo's
4381
- * releases don't depend on commit messages. Add files with `rylease add`.
4382
- * Releases are found by their tags; set the release's `scheme`.
4383
- */
4384
- const changeFiles = definePreset({
4385
- name: "change-files",
4386
- description: "Change files in `.changes/` → changelog written and change files deleted, committed, git tag and GitHub release.",
4387
- steps: [
4388
- changeFilesRead,
4389
- labelsDetermineKind,
4390
- labelsBuildNotes,
4391
- changeFilesClean,
4392
- fsWriteChanges,
4393
- gitCommit,
4394
- gitCreateTag,
4395
- githubCreateRelease
4396
- ],
4397
- defaults: {
4398
- "labels/determine-kind": { kinds: {
4399
- breaking: "breaking",
4400
- feature: "feature",
4401
- fix: "fix",
4402
- none: "none"
4403
- } },
4404
- "labels/determine-notes": {
4405
- sections: [
4406
- {
4407
- title: "Breaking Changes",
4408
- labels: ["breaking"]
4409
- },
4410
- {
4411
- title: "Features",
4412
- labels: ["feature"]
4413
- },
4414
- {
4415
- title: "Fixes",
4416
- labels: ["fix"]
4417
- }
4418
- ],
4419
- other: false,
4420
- exclude: ["none"],
4421
- descriptions: true
4422
- },
4423
- "git/create-tag": { push: "origin" }
4424
- },
4425
- history: gitTags
4426
- });
4427
- //#endregion
4428
- //#region src/schemes/calver.ts
4429
- /** The date tokens of calver.org, by what they're made from. */
4430
- const DATES = {
4431
- YYYY: (date) => String(date.year),
4432
- YY: (date) => String(date.year - 2e3),
4433
- "0Y": (date) => String(date.year - 2e3).padStart(2, "0"),
4434
- MM: (date) => String(date.month),
4435
- "0M": (date) => String(date.month).padStart(2, "0"),
4436
- WW: (date) => String(date.week),
4437
- "0W": (date) => String(date.week).padStart(2, "0"),
4438
- DD: (date) => String(date.day),
4439
- "0D": (date) => String(date.day).padStart(2, "0")
4440
- };
4441
- /** Counters, from the kind each one is bumped for. */
4442
- const COUNTERS = {
4443
- MAJOR: "breaking",
4444
- MINOR: "feature",
4445
- MICRO: "fix"
4446
- };
4447
- const RANK = {
4448
- fix: 0,
4449
- feature: 1,
4450
- breaking: 2
4451
- };
4452
- const TOKENS = [...Object.keys(DATES), ...Object.keys(COUNTERS)];
4453
- /**
4454
- * Calendar versioning, as on calver.org: the format's date tokens come from
4455
- * the release date, and its counters (`MAJOR`, `MINOR`, `MICRO`) number the
4456
- * releases within the same period. A counter is bumped by the kind it stands
4457
- * for (`breaking`, `feature`, `fix`), or by a higher kind when the format has
4458
- * no counter for that; the counters after it start again from 0, as do all of
4459
- * them in a new period.
4460
- */
4461
- const calver = defineScheme({
4462
- name: "calver",
4463
- description: "Dates the version, as on calver.org, e.g. `YYYY.MM.MICRO` → `2026.10.0`; a counter numbers releases in the same period, bumped by the kind.",
4464
- options: {
4465
- format: z.string().default("YYYY.MM.MICRO").refine((format) => format.split(".").every((token) => TOKENS.includes(token)) && new Set(format.split(".")).size === format.split(".").length, `must be distinct tokens joined by '.', of ${TOKENS.join(", ")}`).refine((format) => format.split(".").some((token) => token in DATES), "must have a date token, e.g. YYYY").describe("Tokens joined by `.`: `YYYY`, `YY`, `0Y`, `MM`, `0M`, `WW`, `0W`, `DD`, `0D` (ISO 8601 weeks, with the ISO year), and the counters `MAJOR`, `MINOR`, `MICRO`. Zero-padded tokens (`0M` …) aren't valid semver, which npm needs."),
4466
- timezone: z.string().default("UTC").refine(isTimezone, "must be an IANA time zone, e.g. Europe/Berlin").describe("Where the release date is taken, e.g. `America/New_York`.")
4467
- },
4468
- create: (options) => {
4469
- const format = options.format.split(".");
4470
- const matches = (version) => {
4471
- const parts = version.trim().split(".");
4472
- return parts.length === format.length && parts.every((part, i) => SHAPES[format[i]].test(part));
4473
- };
4474
- const padded = format.some((token) => token.startsWith("0"));
4475
- return {
4476
- name: `calver ${options.format}`,
4477
- traits: padded ? [] : ["version.semver"],
4478
- matches,
4479
- compare: (a, b) => compareVersions(parseVersion(a), parseVersion(b)),
4480
- bump: ({ previous, kind, date: now }) => {
4481
- const date = dateParts(now, options.timezone, format);
4482
- const before = previous?.split(".").map(Number);
4483
- const samePeriod = before !== void 0 && format.every((token, i) => !(token in DATES) || Number(DATES[token](date)) === before[i]);
4484
- const counters = format.filter((token) => Object.hasOwn(COUNTERS, token));
4485
- const bumped = samePeriod ? bumpedCounter(counters, kind) : void 0;
4486
- if (samePeriod && bumped === void 0) throw new Error(`${previous} is already this period's release; add a counter (e.g. MICRO) to '${options.format}' to release more often`);
4487
- return {
4488
- version: format.map((token, i) => {
4489
- if (token in DATES) return DATES[token](date);
4490
- if (!samePeriod || bumped === void 0) return "0";
4491
- const index = format.indexOf(bumped);
4492
- if (i < index) return String(before?.[i]);
4493
- return i === index ? String((before?.[i] ?? 0) + 1) : "0";
4494
- }).join("."),
4495
- reason: previous === void 0 ? "first release" : samePeriod ? `bumps ${bumped}` : "new period"
4496
- };
4497
- },
4498
- fields: (version) => Object.fromEntries(format.map((token, i) => [token, version.split(".")[i] ?? ""]))
4499
- };
4500
- }
4501
- });
4502
- /** What each token looks like in a version. */
4503
- const SHAPES = {
4504
- YYYY: /^[1-9]\d{3}$/,
4505
- YY: /^(0|[1-9]\d*)$/,
4506
- "0Y": /^\d{2,}$/,
4507
- MM: /^([1-9]|1[0-2])$/,
4508
- "0M": /^(0[1-9]|1[0-2])$/,
4509
- WW: /^([1-9]|[1-4]\d|5[0-3])$/,
4510
- "0W": /^(0[1-9]|[1-4]\d|5[0-3])$/,
4511
- DD: /^([1-9]|[12]\d|3[01])$/,
4512
- "0D": /^(0[1-9]|[12]\d|3[01])$/,
4513
- MAJOR: /^(0|[1-9]\d*)$/,
4514
- MINOR: /^(0|[1-9]\d*)$/,
4515
- MICRO: /^(0|[1-9]\d*)$/
4516
- };
4517
- /**
4518
- * The counter a kind bumps: its own, or the nearest one below it, or else the
4519
- * lowest the format has (e.g. `MINOR` alone takes fixes too).
4520
- */
4521
- function bumpedCounter(counters, kind) {
4522
- const ranked = [...counters].sort((a, b) => RANK[COUNTERS[b]] - RANK[COUNTERS[a]]);
4523
- return ranked.find((counter) => RANK[COUNTERS[counter]] <= RANK[kind]) ?? ranked.at(-1);
4524
- }
4525
- function dateParts(now, timezone, format) {
4526
- const fields = Object.fromEntries(new Intl.DateTimeFormat("en-US", {
4527
- timeZone: timezone,
4528
- year: "numeric",
4529
- month: "numeric",
4530
- day: "numeric"
4531
- }).formatToParts(now).map(({ type, value }) => [type, Number(value)]));
4532
- const iso = isoWeek(fields.year, fields.month, fields.day);
4533
- const weekly = format.includes("WW") || format.includes("0W");
4534
- return {
4535
- ...fields,
4536
- year: weekly ? iso.year : fields.year,
4537
- week: iso.week
4538
- };
4539
- }
4540
- /** The ISO 8601 week of a date, and the year that week belongs to. */
4541
- function isoWeek(year, month, day) {
4542
- const date = new Date(Date.UTC(year, month - 1, day));
4543
- date.setUTCDate(date.getUTCDate() + 3 - weekday(date));
4544
- const weekYear = date.getUTCFullYear();
4545
- const first = new Date(Date.UTC(weekYear, 0, 4));
4546
- first.setUTCDate(first.getUTCDate() + 3 - weekday(first));
4547
- const weeks = (date.getTime() - first.getTime()) / 6048e5;
4548
- return {
4549
- year: weekYear,
4550
- week: 1 + Math.round(weeks)
4551
- };
4552
- }
4553
- /** Monday is 0. */
4554
- function weekday(date) {
4555
- return (date.getUTCDay() + 6) % 7;
4556
- }
4557
- function isTimezone(timezone) {
4558
- try {
4559
- new Intl.DateTimeFormat("en-US", { timeZone: timezone });
4560
- return true;
4561
- } catch {
4562
- return false;
4563
- }
4564
- }
4565
- //#endregion
4566
- //#region src/schemes/index.ts
4567
- var schemes_exports = /* @__PURE__ */ __exportAll({
4568
- bumpSemver: () => bumpSemver,
4569
- calver: () => calver,
4570
- formatSemver: () => formatSemver,
4571
- isSemver: () => isSemver,
4572
- parseSemver: () => parseSemver,
4573
- semver: () => semver
4574
- });
4575
- //#endregion
4576
- //#region src/presets/conventional.ts
4577
- /**
4578
- * Conventional commits since the previous release → git tag → GitHub
4579
- * release, versioned by semver and found by its tags. Nothing specific to a
4580
- * language or ecosystem; add steps like `fs/write-version`,
4581
- * `fs/write-changes` and `git/commit` for that. The tag is pushed, with the
4582
- * branch, to `origin`.
4583
- */
4584
- const conventional = definePreset({
4585
- name: "conventional",
4586
- description: "Conventional commits → notes → git tag and GitHub release.",
4587
- steps: [
4588
- conventionalReadCommits,
4589
- conventionalDetermineKind,
4590
- conventionalBuildNotes,
4591
- gitCreateTag,
4592
- githubCreateRelease
4593
- ],
4594
- defaults: { "git/create-tag": { push: "origin" } },
4595
- scheme: semver,
4596
- history: gitTags
4597
- });
4598
- //#endregion
4599
- //#region src/presets/conventional-pr.ts
4600
- /**
4601
- * Conventional commits → the changelog written and proposed in a release
4602
- * pull request, kept up to date with each push; merging it tags the release
4603
- * and creates its GitHub release. Versioned by semver; its history,
4604
- * `github/release-pr`, is how the run on the merge finds what the pull
4605
- * request proposed.
4606
- */
4607
- const conventionalPr = definePreset({
4608
- name: "conventional-pr",
4609
- description: "Conventional commits → changelog proposed in a release pull request; merging it tags the release and creates its GitHub release.",
4610
- steps: [
4611
- conventionalReadCommits,
4612
- conventionalDetermineKind,
4613
- conventionalBuildNotes,
4614
- fsWriteChanges,
4615
- githubOpenReleasePr,
4616
- gitCreateTag,
4617
- githubCreateRelease
4618
- ],
4619
- defaults: { "git/create-tag": { push: "origin" } },
4620
- scheme: semver,
4621
- history: githubReleasePr
4622
- });
4623
- /**
4624
- * `## [Unreleased]` in CHANGELOG.md → the changelog released, committed and
4625
- * tagged → GitHub release. Each entry's kind comes from its section:
4626
- * `Removed` is breaking, `Added`, `Changed` and `Deprecated` features, and
4627
- * the rest fixes. Releases are found by their tags; set the release's
4628
- * `scheme`, e.g. `semver()`, or `calver()` to date them.
4629
- */
4630
- const keepAChangelog = definePreset({
4631
- name: "keep-a-changelog",
4632
- description: "`## [Unreleased]` in CHANGELOG.md → changelog committed, git tag and GitHub release.",
4633
- steps: [
4634
- keepAChangelogReadUnreleased,
4635
- labelsDetermineKind,
4636
- labelsBuildNotes,
4637
- keepAChangelogWriteRelease,
4638
- gitCommit,
4639
- gitCreateTag,
4640
- githubCreateRelease
4641
- ],
4642
- defaults: {
4643
- "labels/determine-kind": { kinds: {
4644
- Removed: "breaking",
4645
- Added: "feature",
4646
- Changed: "feature",
4647
- Deprecated: "feature"
4648
- } },
4649
- "labels/determine-notes": {
4650
- sections: [
4651
- "Added",
4652
- "Changed",
4653
- "Deprecated",
4654
- "Removed",
4655
- "Fixed",
4656
- "Security"
4657
- ].map((title) => ({
4658
- title,
4659
- labels: [title]
4660
- })),
4661
- descriptions: true
4662
- },
4663
- "git/create-tag": { push: "origin" }
4664
- },
4665
- history: gitTags
4666
- });
4667
- //#endregion
4668
- //#region src/presets/index.ts
4669
- var presets_exports = /* @__PURE__ */ __exportAll({
4670
- changeFiles: () => changeFiles,
4671
- conventional: () => conventional,
4672
- conventionalPr: () => conventionalPr,
4673
- keepAChangelog: () => keepAChangelog
4674
- });
4675
- //#endregion
4676
- //#region src/config/workflow.ts
4677
- const builtins = new Map([
4678
- ...Object.values(steps_exports),
4679
- ...Object.values(presets_exports),
4680
- ...Object.values(schemes_exports),
4681
- ...Object.values(histories_exports),
4682
- ...Object.values(components_exports)
4683
- ].filter((item) => isFactory(item)).map((item) => [item.name, item]));
4684
- const CHANNELS = z.record(z.string(), z.strictObject({
4685
- branches: z.array(z.string()).optional(),
4686
- type: z.enum(["prerelease", "build"]).optional(),
4687
- format: z.string().optional()
4688
- }));
4689
- /**
4690
- * Turns a workflow's inputs into a release: its `steps`, and how versions are
4691
- * made (`scheme`), tagged (`tag`), found (`history`), and of what
4692
- * (`components`). Each of those but `tag` names what it `uses` and passes
4693
- * it options `with`, or is just a name:
4694
- *
4695
- * ```yaml
4696
- * scheme: { uses: calver, with: { format: YY.MINOR.MICRO } }
4697
- * components: npm/workspaces
4698
- * ```
4699
- */
4700
- async function resolveWorkflow(input, options) {
4701
- const scheme = input.scheme == null ? void 0 : await create(input.scheme, "scheme", "scheme", options.cwd);
4702
- const history = input.history == null ? void 0 : await Promise.all((Array.isArray(input.history) ? input.history : [input.history]).map((entry, index) => create(entry, "history", Array.isArray(input.history) ? `history[${index}]` : "history", options.cwd)));
4703
- if (input.tag != null && typeof input.tag !== "string") throw new OptionsError(["tag: expected a template, e.g. v{version}"]);
4704
- return {
4705
- steps: await resolveWorkflowSteps(input.steps, options),
4706
- ...scheme && { scheme },
4707
- ...typeof input.tag === "string" && input.tag !== "" && { tag: input.tag },
4708
- ...history && { history },
4709
- ...input.components != null && { components: await resolveComponentsInput(input.components, options.cwd) },
4710
- ...input.channels != null && { channels: parseChannels(input.channels) }
4711
- };
4712
- }
4713
- function parseChannels(value) {
4714
- const result = CHANNELS.safeParse(value);
4715
- if (result.success) return result.data;
4716
- throw new OptionsError(result.error.issues.map((issue) => `${joinPath("channels", issue.path)}: ${issue.message}`));
4717
- }
4718
- async function resolveComponentsInput(value, cwd) {
4719
- if (!Array.isArray(value)) return create(value, "components", "components", cwd);
4720
- const keys = [
4721
- "name",
4722
- "path",
4723
- "dependsOn",
4724
- "tag",
4725
- "scheme",
4726
- "history"
4727
- ];
4728
- const components = [];
4729
- for (const [index, entry] of value.entries()) {
4730
- const at = `components[${index}]`;
4731
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new OptionsError([`${at}: expected a map with 'name' and 'path'`]);
4732
- const fields = entry;
4733
- const unknown = Object.keys(fields).filter((key) => !keys.includes(key));
4734
- if (unknown.length > 0) throw new OptionsError(unknown.map((key) => `${at}.${key}: unknown key; use ${keys.join(", ")}`));
4735
- const text = (key) => {
4736
- const field = fields[key];
4737
- if (field != null && typeof field !== "string") throw new OptionsError([`${at}.${key}: expected text`]);
4738
- return field ?? void 0;
4739
- };
4740
- const dependsOn = fields.dependsOn;
4741
- if (dependsOn != null && !(Array.isArray(dependsOn) && dependsOn.every((item) => typeof item === "string"))) throw new OptionsError([`${at}.dependsOn: expected a list of component names`]);
4742
- const name = text("name");
4743
- const path = text("path");
4744
- const tag = text("tag");
4745
- components.push({
4746
- name: name ?? "",
4747
- ...path !== void 0 && { path },
4748
- ...tag !== void 0 && { tag },
4749
- ...dependsOn != null && { dependsOn },
4750
- ...fields.scheme != null && { scheme: await create(fields.scheme, "scheme", `${at}.scheme`, cwd) },
4751
- ...fields.history != null && { history: await create(fields.history, "history", `${at}.history`, cwd) }
4752
- });
4753
- }
4754
- return components;
4755
- }
4756
- /** Creates a scheme, history or component source from `name` or `{ uses, with }`. */
4757
- async function create(value, kind, at, cwd) {
4758
- const entry = typeof value === "string" ? {
4759
- uses: value,
4760
- with: void 0
4761
- } : value;
4762
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new OptionsError([`${at}: expected a name, or a map with 'uses' and 'with'`]);
4763
- const fields = entry;
4764
- const unknown = Object.keys(fields).filter((key) => key !== "uses" && key !== "with");
4765
- if (unknown.length > 0) throw new OptionsError(unknown.map((key) => `${at}.${key}: unknown key; use uses, with`));
4766
- if (typeof fields.uses !== "string" || fields.uses === "") throw new OptionsError([`${at}.uses: expected the name of a ${kind === "components" ? "component source" : kind}`]);
4767
- const target = await load(fields.uses, `${at}.uses`, cwd);
4768
- if (target.kind !== kind) throw new OptionsError([`${at}.uses: '${fields.uses}' is a ${target.kind}, not a ${kind === "components" ? "component source" : kind}`]);
4769
- try {
4770
- return target(fields.with ?? void 0);
4771
- } catch (error) {
4772
- if (!(error instanceof OptionsError)) throw error;
4773
- throw new OptionsError(error.issues.map((issue) => `${at}.with${issue.startsWith(target.name) ? issue.slice(target.name.length) : `: ${issue}`}`));
4774
- }
4775
- }
4776
- /**
4777
- * Turns workflow `steps` (already parsed from YAML) into steps. Each entry
4778
- * names what it `uses`, passes it options `with`, and may run only `if` an
4779
- * expression holds:
4780
- *
4781
- * ```yaml
4782
- * - uses: conventional # a bundled preset or step
4783
- * with:
4784
- * github/create-release: false
4785
- * - uses: ./release/announce.ts # a file's default export
4786
- * if: "!dryRun && kind != 'patch'"
4787
- * with:
4788
- * channel: "#releases"
4789
- * - uses: some-package#slackNotes # a package's named export
4790
- * ```
4791
- */
4792
- async function resolveWorkflowSteps(entries, options) {
4793
- if (!Array.isArray(entries)) throw new OptionsError(["steps: expected a list of steps"]);
4794
- const parsed = entries.map((entry, index) => parseEntry(entry, index));
4795
- const steps = [];
4796
- const fromPreset = /* @__PURE__ */ new Map();
4797
- const explicit = [];
4798
- for (const { uses, with: value, condition, at } of parsed) {
4799
- const target = await load(uses, `${at}.uses`, options.cwd);
4800
- if (target.kind !== "step" && target.kind !== "preset") throw new OptionsError([`${at}.uses: '${uses}' is a ${target.kind}, not a step; set it as \`${target.kind}\` instead`]);
4801
- let created;
4802
- try {
4803
- if (isPreset(target)) {
4804
- created = target(value);
4805
- for (const step of created) fromPreset.set(step.name, target.name);
4806
- } else {
4807
- created = [target(value)];
4808
- explicit.push({
4809
- name: target.name,
4810
- at
4811
- });
4812
- }
4813
- } catch (error) {
4814
- if (!(error instanceof OptionsError)) throw error;
4815
- throw new OptionsError(error.issues.map((issue) => `${at}.with${issue.startsWith(target.name) ? issue.slice(target.name.length) : `: ${issue}`}`));
4816
- }
4817
- steps.push(...created.map((step) => condition ? when(step, condition) : step));
4818
- }
4819
- const twice = explicit.flatMap(({ name, at }) => {
4820
- const preset = fromPreset.get(name);
4821
- return preset ? [`${at}.uses: '${name}' is already in '${preset}'; configure it there (with: { ${name}: ... }), or set it to false there to replace it`] : [];
4822
- });
4823
- if (twice.length > 0) throw new OptionsError(twice);
4824
- return steps;
4825
- }
4826
- const ENTRY_KEYS = [
4827
- "uses",
4828
- "with",
4829
- "if"
4830
- ];
4831
- function parseEntry(entry, index) {
4832
- const at = `steps[${index}]`;
4833
- if (typeof entry !== "object" || entry === null || Array.isArray(entry)) throw new OptionsError([`${at}: expected a map with 'uses', and optionally 'with' and 'if'`]);
4834
- const fields = entry;
4835
- const unknown = Object.keys(fields).filter((key) => !ENTRY_KEYS.includes(key));
4836
- if (unknown.length > 0) throw new OptionsError(unknown.map((key) => `${at}.${key}: unknown key; use ${ENTRY_KEYS.join(", ")}`));
4837
- if (typeof fields.uses !== "string" || fields.uses === "") throw new OptionsError([`${at}.uses: expected the name of a step or preset`]);
4838
- if (fields.with != null && (typeof fields.with !== "object" || Array.isArray(fields.with))) throw new OptionsError([`${at}.with: expected a map of options`]);
4839
- const condition = parseCondition(fields.if, `${at}.if`);
4840
- return {
4841
- uses: fields.uses,
4842
- with: fields.with ?? void 0,
4843
- ...condition && { condition },
4844
- at
4845
- };
4846
- }
4847
- /**
4848
- * `if:` is a rylease expression, or a boolean, e.g. what GitHub made of
4849
- * `${{ ... }}` before rylease ran.
4850
- */
4851
- function parseCondition(value, at) {
4852
- if (value === void 0 || value === null) return void 0;
4853
- if (typeof value === "boolean") return Object.assign(() => value, { source: String(value) });
4854
- if (typeof value !== "string") throw new OptionsError([`${at}: expected an expression or a boolean`]);
4855
- try {
4856
- const evaluate = compileExpression(value);
4857
- return Object.assign((input) => Boolean(evaluate(expressionContext(input))), { source: value });
4858
- } catch (error) {
4859
- if (!(error instanceof ExpressionError)) throw error;
4860
- throw new OptionsError([`${at}: ${error.message} in \`${value}\``]);
4861
- }
4862
- }
4863
- async function load(name, path, cwd) {
4864
- const builtin = builtins.get(name);
4865
- if (builtin) return builtin;
4866
- const [specifier = name, exportName = "default"] = name.split("#");
4867
- const isPath = /^\.{0,2}\//.test(specifier);
4868
- let url;
4869
- try {
4870
- url = isPath ? pathToFileURL(resolve(cwd, specifier)).href : pathToFileURL(createRequire(join(cwd, "package.json")).resolve(specifier)).href;
4871
- } catch {
4872
- throw new OptionsError([`${path}: unknown step ${name}; use one of ${[...builtins.keys()].join(", ")}, a path like ./steps/x.ts, or an installed package`]);
4873
- }
4874
- const target = (await import(url))[exportName];
4875
- if (isFactory(target)) return target;
4876
- throw new OptionsError([`${path}: export '${exportName}' of ${specifier} isn't made with defineStep, definePreset, defineScheme, defineHistory or defineComponents`]);
4877
- }
4878
- function isPreset(value) {
4879
- return value.kind === "preset";
4880
- }
4881
- //#endregion
4882
- //#region src/core/summary.ts
4883
- const ICONS = {
4884
- ok: "✅",
4885
- skipped: "⚪",
4886
- failed: "❌"
4887
- };
4888
- /**
4889
- * The report as Markdown, for GitHub's job summary: what happened, a row per
4890
- * step, and the release notes.
4891
- */
4892
- function markdownSummary(report) {
4893
- const dryRun = report.env.dryRun;
4894
- const several = report.components.length > 1;
4895
- const describe = (component) => {
4896
- const from = component.previousVersion ?? "nothing";
4897
- const to = component.version ?? "?";
4898
- if (component.status !== "released" && component.status !== "pending") return `**${from}** · nothing to release`;
4899
- return `${from === to ? `**${to}**` : `**${from}** → **${to}** (${component.kind})`} · ${component.status === "pending" ? dryRun ? "would be proposed" : "proposed" : dryRun ? "would release" : "released"}`;
4900
- };
4901
- const [only] = report.components;
4902
- const headline = report.status === "failed" ? "release failed" : !several && only ? describe(only) : report.status === "released" ? dryRun ? "would release" : "released" : report.status === "pending" ? dryRun ? "would be proposed" : "proposed" : "nothing to release";
4903
- const lines = [
4904
- `### ${report.status === "failed" ? "❌ " : ""}rylease${dryRun ? " dry run" : ""} · ${headline}`,
4905
- "",
4906
- `On \`${report.env.branch}\`${report.env.channel ? `, on the \`${report.env.channel.name}\` channel` : ""}${dryRun ? ", without side effects" : ""}.${report.deferred ? ` ${report.deferred[0]?.toUpperCase()}${report.deferred.slice(1)}.` : ""}`,
4907
- ...several && report.status !== "failed" ? ["", ...report.components.map((component) => `- \`${component.name}\`: ${describe(component)}`)] : [],
4908
- "",
4909
- several ? "| | Component | Type | Step | Time | |" : "| | Type | Step | Time | |",
4910
- several ? "|:-:|---|---|---|--:|---|" : "|:-:|---|---|--:|---|",
4911
- ...report.records.map((record) => `| ${ICONS[record.status]} |${several ? ` ${record.component ? `\`${record.component}\`` : "all"} |` : ""} \`${record.phase}\` | \`${record.name}\` | ${record.status === "skipped" ? "" : formatDuration(record.durationMs)} | ${cell(record.reason)} |`)
4912
- ];
4913
- for (const component of report.components) {
4914
- if (report.status === "failed" || report.status === "unchanged") continue;
4915
- if (component.status !== "released" && component.status !== "pending") continue;
4916
- if (!component.notes?.trim()) continue;
4917
- lines.push("", `<details open><summary>Release notes${several ? ` of ${component.name} ${component.version}` : ""}</summary>`, "", component.notes.trim(), "", "</details>");
4918
- }
4919
- return `${lines.join("\n")}\n\n`;
4920
- }
4921
- /** Text that stays inside one table cell or list item. */
4922
- function cell(text) {
4923
- return text.replaceAll("|", "\\|").replaceAll("<", "&lt;").replace(/\r?\n/g, "<br>");
4924
- }
4925
- //#endregion
4926
- export { KINDS, OptionsError, PHASES, ReleaseError, compileExpression, components_exports as components, defaultReporter, defineComponents, defineHistory, definePreset, defineRelease, defineScheme, defineStep, errorMessage, expressionContext, formatDuration, git, histories_exports as histories, joinPath, markdownSummary, parseOptions, presets_exports as presets, prettyReporter, resolveComponents, resolveWorkflow, resolveWorkflowSteps, runRelease, schemes_exports as schemes, sortSteps, steps_exports as steps, tryGit, when, z };
4927
-
4928
- //# sourceMappingURL=index.mjs.map
1
+ export { C as KINDS, P as OptionsError, w as PHASES, y as ReleaseError, p as compileExpression, j as components, T as defaultReporter, M as defineComponents, f as defineHistory, h as definePreset, g as defineRelease, c as defineScheme, _ as defineStep, b as errorMessage, m as expressionContext, E as formatDuration, k as git, d as histories, F as joinPath, n as markdownSummary, I as parseOptions, a as presets, D as prettyReporter, N as resolveComponents, r as resolveWorkflow, i as resolveWorkflowSteps, x as runRelease, o as schemes, S as sortSteps, s as steps, A as tryGit, v as when, L as z } from "./src-BkALSGzm.mjs";