aronrepo 2.2.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -35
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +110 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands-CO26JPbK.js +1170 -0
- package/dist/commands-CO26JPbK.js.map +1 -0
- package/dist/index-Co5yiJXA.d.ts +240 -0
- package/dist/index.d.ts +2 -5
- package/dist/index.js +757 -10
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +226 -0
- package/dist/node.js +1 -0
- package/package.json +54 -45
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
import { agentsBlockEnd, agentsBlockStart, commits, createReleasePathMatcher, defaultReleasePathExcludes, getAgentCommitPolicy, getBaseTag, getNextVersion, getReleaseChannel, getReleasePolicy, getReleaseTable, getReleaseType, legacyCommits, parseCommit, renderAgentsBlock, renderReleaseNotes, resolveCommitRelease, resolveRelease, toVersionTag } from "./index.js";
|
|
2
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { globSync } from "tinyglobby";
|
|
7
|
+
import { load } from "js-yaml";
|
|
8
|
+
import os from "node:os";
|
|
9
|
+
import { gunzipSync } from "node:zlib";
|
|
10
|
+
// src/project.ts
|
|
11
|
+
const configFileNames = ["aronrepo.config.js", "aronrepo.config.mjs"];
|
|
12
|
+
function toPosixPath(value) {
|
|
13
|
+
return value.split(path.sep).join("/");
|
|
14
|
+
}
|
|
15
|
+
function readJson$1(file) {
|
|
16
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
17
|
+
}
|
|
18
|
+
function findConfigFile(cwd = process.cwd()) {
|
|
19
|
+
return configFileNames.map((name) => path.join(cwd, name)).find((file) => fs.existsSync(file));
|
|
20
|
+
}
|
|
21
|
+
/** Loads `aronrepo.config.js` from the repository root; returns `{}` when it is missing. */
|
|
22
|
+
async function loadConfig(cwd = process.cwd()) {
|
|
23
|
+
const file = findConfigFile(cwd);
|
|
24
|
+
if (!file) return {};
|
|
25
|
+
const stat = fs.statSync(file);
|
|
26
|
+
return (await import(`${pathToFileURL(file).href}?mtime=${stat.mtimeMs}&size=${stat.size}`)).default ?? {};
|
|
27
|
+
}
|
|
28
|
+
function readPNPMWorkspaces(cwd) {
|
|
29
|
+
const workspacePath = path.join(cwd, "pnpm-workspace.yaml");
|
|
30
|
+
if (!fs.existsSync(workspacePath)) return void 0;
|
|
31
|
+
return load(fs.readFileSync(workspacePath, "utf8"))?.packages;
|
|
32
|
+
}
|
|
33
|
+
function readNPMWorkspaces(cwd) {
|
|
34
|
+
const packagePath = path.join(cwd, "package.json");
|
|
35
|
+
if (!fs.existsSync(packagePath)) return void 0;
|
|
36
|
+
const pkg = readJson$1(packagePath);
|
|
37
|
+
return Array.isArray(pkg.workspaces) ? pkg.workspaces : pkg.workspaces?.packages;
|
|
38
|
+
}
|
|
39
|
+
function readWorkspacePatterns(cwd = process.cwd()) {
|
|
40
|
+
return readPNPMWorkspaces(cwd) ?? readNPMWorkspaces(cwd) ?? [];
|
|
41
|
+
}
|
|
42
|
+
function queryWorkspaceDirs(patterns, cwd = process.cwd()) {
|
|
43
|
+
if (!patterns.length) return [];
|
|
44
|
+
return globSync(patterns.map((workspace) => `${workspace}/package.json`), {
|
|
45
|
+
cwd,
|
|
46
|
+
onlyFiles: true,
|
|
47
|
+
expandDirectories: false,
|
|
48
|
+
ignore: ["**/node_modules/**"]
|
|
49
|
+
}).map((workspacePackage) => toPosixPath(workspacePackage).replace(/\/?package\.json$/, "") || ".").sort();
|
|
50
|
+
}
|
|
51
|
+
function readWorkspacePackage(cwd, workspace) {
|
|
52
|
+
return readJson$1(path.join(cwd, workspace, "package.json"));
|
|
53
|
+
}
|
|
54
|
+
/** Workspace directories whose package is published to npm. */
|
|
55
|
+
function queryPublicWorkspaceDirs(cwd = process.cwd()) {
|
|
56
|
+
return queryWorkspaceDirs(readWorkspacePatterns(cwd), cwd).filter((workspace) => readWorkspacePackage(cwd, workspace).publishConfig?.access === "public");
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Resolves the files whose changes can release a version.
|
|
60
|
+
*
|
|
61
|
+
* Configured paths win. Otherwise public workspace packages define them; a
|
|
62
|
+
* single-package repository with a non-private root package uses every path.
|
|
63
|
+
* Returns `undefined` when no published package is found, which turns the check off.
|
|
64
|
+
*/
|
|
65
|
+
function resolveReleasePaths(cwd = process.cwd(), policy) {
|
|
66
|
+
if (policy?.paths === false) return void 0;
|
|
67
|
+
if (policy?.paths?.include) return {
|
|
68
|
+
include: policy.paths.include,
|
|
69
|
+
exclude: policy.paths.exclude ?? defaultReleasePathExcludes
|
|
70
|
+
};
|
|
71
|
+
const exclude = policy?.paths?.exclude ?? defaultReleasePathExcludes;
|
|
72
|
+
const publicWorkspaces = queryPublicWorkspaceDirs(cwd);
|
|
73
|
+
if (publicWorkspaces.length) return {
|
|
74
|
+
include: publicWorkspaces.map((dir) => dir === "." ? "**" : `${dir}/**`),
|
|
75
|
+
exclude
|
|
76
|
+
};
|
|
77
|
+
const rootPackage = path.join(cwd, "package.json");
|
|
78
|
+
if (!readWorkspacePatterns(cwd).length && fs.existsSync(rootPackage) && !readJson$1(rootPackage).private) return {
|
|
79
|
+
include: ["**"],
|
|
80
|
+
exclude
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const lockfiles = [
|
|
84
|
+
["bun.lock", "bun"],
|
|
85
|
+
["bun.lockb", "bun"],
|
|
86
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
87
|
+
["pnpm-workspace.yaml", "pnpm"],
|
|
88
|
+
["yarn.lock", "yarn"],
|
|
89
|
+
["package-lock.json", "npm"]
|
|
90
|
+
];
|
|
91
|
+
/** The package manager from `packageManager` in package.json, then from the lockfile; `npm` by default. */
|
|
92
|
+
function detectPackageManager(cwd = process.cwd()) {
|
|
93
|
+
const rootPackage = path.join(cwd, "package.json");
|
|
94
|
+
const declared = fs.existsSync(rootPackage) ? String(readJson$1(rootPackage).packageManager ?? "").split("@")[0] : "";
|
|
95
|
+
if (declared === "bun" || declared === "pnpm" || declared === "yarn" || declared === "npm") return declared;
|
|
96
|
+
return lockfiles.find(([file]) => fs.existsSync(path.join(cwd, file)))?.[1] ?? "npm";
|
|
97
|
+
}
|
|
98
|
+
/** How to run the aronrepo CLI with a package manager. */
|
|
99
|
+
function getCliCommand(packageManager) {
|
|
100
|
+
return {
|
|
101
|
+
bun: "bun x aronrepo",
|
|
102
|
+
pnpm: "pnpm exec aronrepo",
|
|
103
|
+
yarn: "yarn aronrepo",
|
|
104
|
+
npm: "npx aronrepo"
|
|
105
|
+
}[packageManager];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* How to run the aronrepo CLI in a repository: its `aronrepo` script when it has
|
|
109
|
+
* one, for example a workspace that builds aronrepo itself, or the installed binary.
|
|
110
|
+
*/
|
|
111
|
+
function getProjectCliCommand(cwd, packageManager = detectPackageManager(cwd)) {
|
|
112
|
+
const rootPackage = path.join(cwd, "package.json");
|
|
113
|
+
if ((fs.existsSync(rootPackage) ? readJson$1(rootPackage).scripts : void 0)?.aronrepo) return packageManager === "npm" ? "npm run aronrepo --" : `${packageManager} run aronrepo`;
|
|
114
|
+
return getCliCommand(packageManager);
|
|
115
|
+
}
|
|
116
|
+
/** Loads the config, release policy, and release paths of a repository. */
|
|
117
|
+
async function loadProjectPolicy(cwd = process.cwd(), overrides = {}) {
|
|
118
|
+
const config = await loadConfig(cwd);
|
|
119
|
+
const policy = {
|
|
120
|
+
...getReleasePolicy(config),
|
|
121
|
+
...overrides
|
|
122
|
+
};
|
|
123
|
+
const releasePaths = resolveReleasePaths(cwd, policy);
|
|
124
|
+
const configFile = findConfigFile(cwd);
|
|
125
|
+
const packageManager = detectPackageManager(cwd);
|
|
126
|
+
return {
|
|
127
|
+
cwd,
|
|
128
|
+
packageManager,
|
|
129
|
+
cli: getProjectCliCommand(cwd, packageManager),
|
|
130
|
+
config,
|
|
131
|
+
...configFile ? { configFile } : {},
|
|
132
|
+
policy,
|
|
133
|
+
...releasePaths ? { releasePaths } : {},
|
|
134
|
+
...releasePaths ? { isReleasePath: createReleasePathMatcher(releasePaths) } : {}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function git$1(cwd, args) {
|
|
138
|
+
return execFileSync("git", args, {
|
|
139
|
+
cwd,
|
|
140
|
+
encoding: "utf8",
|
|
141
|
+
maxBuffer: 67108864
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
function splitLines(output) {
|
|
145
|
+
return output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
146
|
+
}
|
|
147
|
+
/** Files changed by one commit. Merge commits report no files. */
|
|
148
|
+
function getCommitFiles(cwd, hash) {
|
|
149
|
+
return splitLines(git$1(cwd, [
|
|
150
|
+
"diff-tree",
|
|
151
|
+
"--no-commit-id",
|
|
152
|
+
"--name-only",
|
|
153
|
+
"-r",
|
|
154
|
+
"--root",
|
|
155
|
+
hash
|
|
156
|
+
]));
|
|
157
|
+
}
|
|
158
|
+
function getStagedFiles(cwd = process.cwd()) {
|
|
159
|
+
return splitLines(git$1(cwd, [
|
|
160
|
+
"diff",
|
|
161
|
+
"--cached",
|
|
162
|
+
"--name-only"
|
|
163
|
+
]));
|
|
164
|
+
}
|
|
165
|
+
/** Files changed between the merge base of `base` and `head`, like a pull request diff. */
|
|
166
|
+
function getDiffFiles(cwd, base, head) {
|
|
167
|
+
return splitLines(git$1(cwd, [
|
|
168
|
+
"diff",
|
|
169
|
+
"--name-only",
|
|
170
|
+
`${base}...${head}`
|
|
171
|
+
]));
|
|
172
|
+
}
|
|
173
|
+
/** Commits in `from..to`, oldest first, with their changed files. */
|
|
174
|
+
function getRangeCommits(cwd, from, to = "HEAD") {
|
|
175
|
+
return git$1(cwd, [
|
|
176
|
+
"log",
|
|
177
|
+
"--reverse",
|
|
178
|
+
"--format=%H%x1f%B%x1e",
|
|
179
|
+
from ? `${from}..${to}` : to
|
|
180
|
+
]).split("").map((entry) => entry.replace(/^\n/, "")).filter((entry) => entry.includes("")).map((entry) => {
|
|
181
|
+
const [hash, message] = entry.split("");
|
|
182
|
+
return {
|
|
183
|
+
hash,
|
|
184
|
+
message: message.trim(),
|
|
185
|
+
files: getCommitFiles(cwd, hash)
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Latest tag reachable from `ref`, or `undefined` when there is none. */
|
|
190
|
+
function getLatestTag(cwd, ref = "HEAD") {
|
|
191
|
+
try {
|
|
192
|
+
return git$1(cwd, [
|
|
193
|
+
"describe",
|
|
194
|
+
"--tags",
|
|
195
|
+
"--abbrev=0",
|
|
196
|
+
ref
|
|
197
|
+
]).trim() || void 0;
|
|
198
|
+
} catch {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/** Resolves the release of a commit range with the repository's policy and release paths. */
|
|
203
|
+
async function explainRange(cwd, from, to = "HEAD", overrides = {}) {
|
|
204
|
+
const project = await loadProjectPolicy(cwd, overrides);
|
|
205
|
+
const commits = getRangeCommits(cwd, from, to);
|
|
206
|
+
return {
|
|
207
|
+
...project,
|
|
208
|
+
from,
|
|
209
|
+
to,
|
|
210
|
+
...resolveRelease(commits, project.policy, project.isReleasePath ? { isReleasePath: project.isReleasePath } : {})
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// src/release.ts
|
|
214
|
+
const officialRegistry = "https://registry.npmjs.org/";
|
|
215
|
+
/** npm dist-tag that holds a release until every package of it is published. */
|
|
216
|
+
const stagingDistTag = "aronrepo-staging";
|
|
217
|
+
/** GitHub rejects release bodies over 125,000 characters. */
|
|
218
|
+
const releaseBodyLimit = 12e4;
|
|
219
|
+
const runCommand = (command, args, { cwd, env }) => new Promise((resolve, reject) => {
|
|
220
|
+
const child = spawn(command, args, {
|
|
221
|
+
cwd,
|
|
222
|
+
env,
|
|
223
|
+
stdio: [
|
|
224
|
+
"ignore",
|
|
225
|
+
"pipe",
|
|
226
|
+
"pipe"
|
|
227
|
+
],
|
|
228
|
+
shell: process.platform === "win32"
|
|
229
|
+
});
|
|
230
|
+
let stdout = "";
|
|
231
|
+
let stderr = "";
|
|
232
|
+
child.stdout.on("data", (chunk) => {
|
|
233
|
+
stdout += chunk;
|
|
234
|
+
});
|
|
235
|
+
child.stderr.on("data", (chunk) => {
|
|
236
|
+
stderr += chunk;
|
|
237
|
+
});
|
|
238
|
+
child.on("error", reject);
|
|
239
|
+
child.on("close", (code) => {
|
|
240
|
+
if (code === 0) resolve({
|
|
241
|
+
stdout,
|
|
242
|
+
stderr
|
|
243
|
+
});
|
|
244
|
+
else reject(/* @__PURE__ */ new Error(`\`${command} ${args.join(" ")}\` exited with ${code}.\n${stderr.trim() || stdout.trim()}`));
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
function createEnvironment(options = {}) {
|
|
248
|
+
return {
|
|
249
|
+
cwd: options.cwd ?? process.cwd(),
|
|
250
|
+
env: options.env ?? process.env,
|
|
251
|
+
run: options.run ?? runCommand,
|
|
252
|
+
fetch: options.fetch ?? ((input, init) => fetch(input, init)),
|
|
253
|
+
log: options.log ?? ((message) => console.log(message)),
|
|
254
|
+
wait: options.wait ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
async function git(environment, args) {
|
|
258
|
+
return (await environment.run("git", args, {
|
|
259
|
+
cwd: environment.cwd,
|
|
260
|
+
env: environment.env
|
|
261
|
+
})).stdout.trim();
|
|
262
|
+
}
|
|
263
|
+
function lines(output) {
|
|
264
|
+
return output.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
265
|
+
}
|
|
266
|
+
function readJson(file) {
|
|
267
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
268
|
+
}
|
|
269
|
+
const dependencyFields = [
|
|
270
|
+
"dependencies",
|
|
271
|
+
"optionalDependencies",
|
|
272
|
+
"peerDependencies"
|
|
273
|
+
];
|
|
274
|
+
/** Published packages of the repository in dependency order. */
|
|
275
|
+
function getReleasePackages(cwd) {
|
|
276
|
+
let dirs = queryPublicWorkspaceDirs(cwd);
|
|
277
|
+
if (!dirs.length && !readWorkspacePatterns(cwd).length) {
|
|
278
|
+
const root = path.join(cwd, "package.json");
|
|
279
|
+
if (fs.existsSync(root) && !readJson(root).private) dirs = ["."];
|
|
280
|
+
}
|
|
281
|
+
const packages = dirs.map((dir) => {
|
|
282
|
+
const manifest = readJson(path.join(cwd, dir, "package.json"));
|
|
283
|
+
if (!manifest.name) throw new Error(`${dir}/package.json has no name.`);
|
|
284
|
+
return {
|
|
285
|
+
dir,
|
|
286
|
+
name: manifest.name,
|
|
287
|
+
manifest
|
|
288
|
+
};
|
|
289
|
+
});
|
|
290
|
+
const names = new Set(packages.map(({ name }) => name));
|
|
291
|
+
const dependenciesOf = (pkg) => dependencyFields.flatMap((field) => Object.keys(pkg.manifest[field] ?? {})).filter((name) => names.has(name) && name !== pkg.name);
|
|
292
|
+
const ordered = [];
|
|
293
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
294
|
+
const visit = (pkg) => {
|
|
295
|
+
if (ordered.includes(pkg)) return;
|
|
296
|
+
if (visiting.has(pkg.name)) throw new Error(`Published packages depend on each other in a cycle through ${pkg.name}.`);
|
|
297
|
+
visiting.add(pkg.name);
|
|
298
|
+
for (const name of dependenciesOf(pkg)) visit(packages.find((candidate) => candidate.name === name));
|
|
299
|
+
visiting.delete(pkg.name);
|
|
300
|
+
ordered.push(pkg);
|
|
301
|
+
};
|
|
302
|
+
for (const pkg of [...packages].sort((a, b) => a.dir.localeCompare(b.dir))) visit(pkg);
|
|
303
|
+
return ordered;
|
|
304
|
+
}
|
|
305
|
+
/** The repository URL on GitHub, from `GITHUB_REPOSITORY` or the root package.json. */
|
|
306
|
+
function getRepositoryUrl(cwd, env = process.env) {
|
|
307
|
+
if (env.GITHUB_REPOSITORY) return `${env.GITHUB_SERVER_URL ?? "https://github.com"}/${env.GITHUB_REPOSITORY}`;
|
|
308
|
+
const root = path.join(cwd, "package.json");
|
|
309
|
+
if (!fs.existsSync(root)) return void 0;
|
|
310
|
+
const repository = readJson(root).repository;
|
|
311
|
+
const url = typeof repository === "string" ? repository : repository?.url;
|
|
312
|
+
const match = url && /github\.com[/:]([^/]+\/[^/.]+?)(?:\.git)?\/?$/.exec(url);
|
|
313
|
+
return match ? `https://github.com/${match[1]}` : void 0;
|
|
314
|
+
}
|
|
315
|
+
async function getBranch(environment, branch) {
|
|
316
|
+
return branch ?? environment.env.GITHUB_REF_NAME ?? await git(environment, [
|
|
317
|
+
"rev-parse",
|
|
318
|
+
"--abbrev-ref",
|
|
319
|
+
"HEAD"
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
/** Plans the next release of the checked-out commit without changing anything. */
|
|
323
|
+
async function planRelease(options = {}, environmentOptions = {}) {
|
|
324
|
+
const environment = createEnvironment(environmentOptions);
|
|
325
|
+
const { cwd } = environment;
|
|
326
|
+
const project = await loadProjectPolicy(cwd, options.mode ? { mode: options.mode } : {});
|
|
327
|
+
const branch = await getBranch(environment, options.branch);
|
|
328
|
+
const channel = getReleaseChannel(branch) ?? (options.fallbackToMain ? getReleaseChannel("main") : void 0);
|
|
329
|
+
const head = await git(environment, ["rev-parse", "HEAD"]);
|
|
330
|
+
const ignored = new Set(options.ignoreTags ?? []);
|
|
331
|
+
const reachable = lines(await git(environment, [
|
|
332
|
+
"tag",
|
|
333
|
+
"--list",
|
|
334
|
+
"v*",
|
|
335
|
+
"--merged",
|
|
336
|
+
"HEAD"
|
|
337
|
+
])).filter((tag) => !ignored.has(tag));
|
|
338
|
+
const all = lines(await git(environment, [
|
|
339
|
+
"tag",
|
|
340
|
+
"--list",
|
|
341
|
+
"v*"
|
|
342
|
+
])).filter((tag) => !ignored.has(tag));
|
|
343
|
+
const packages = getReleasePackages(cwd);
|
|
344
|
+
const base = {
|
|
345
|
+
branch,
|
|
346
|
+
head,
|
|
347
|
+
mode: project.policy.mode,
|
|
348
|
+
release: false,
|
|
349
|
+
commits: [],
|
|
350
|
+
packages
|
|
351
|
+
};
|
|
352
|
+
if (!channel) return base;
|
|
353
|
+
const from = getBaseTag(channel, reachable);
|
|
354
|
+
const decision = resolveRelease(getRangeCommits(cwd, from, "HEAD"), project.policy, project.isReleasePath ? { isReleasePath: project.isReleasePath } : {});
|
|
355
|
+
const plan = {
|
|
356
|
+
...base,
|
|
357
|
+
channel,
|
|
358
|
+
...from ? { from } : {},
|
|
359
|
+
release: decision.release,
|
|
360
|
+
commits: decision.commits
|
|
361
|
+
};
|
|
362
|
+
if (!decision.release) return plan;
|
|
363
|
+
const version = getNextVersion(channel, decision.release, {
|
|
364
|
+
reachable,
|
|
365
|
+
all
|
|
366
|
+
});
|
|
367
|
+
const repositoryUrl = getRepositoryUrl(cwd, environment.env);
|
|
368
|
+
return {
|
|
369
|
+
...plan,
|
|
370
|
+
version,
|
|
371
|
+
tag: toVersionTag(version),
|
|
372
|
+
notes: renderReleaseNotes({
|
|
373
|
+
version,
|
|
374
|
+
...from ? { previousTag: from } : {},
|
|
375
|
+
date: options.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
376
|
+
...repositoryUrl ? { repositoryUrl } : {},
|
|
377
|
+
commits: decision.commits
|
|
378
|
+
})
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
/** Registry of a package: its scoped registry, `publishConfig.registry`, `NPM_CONFIG_REGISTRY`, then npm. */
|
|
382
|
+
function getRegistry(manifest, env = process.env) {
|
|
383
|
+
const scope = manifest.name?.startsWith("@") ? manifest.name.split("/")[0] : void 0;
|
|
384
|
+
const configured = (scope ? manifest.publishConfig?.[`${scope}:registry`] : void 0) ?? manifest.publishConfig?.registry ?? env.NPM_CONFIG_REGISTRY;
|
|
385
|
+
const registry = typeof configured === "string" && configured ? configured : officialRegistry;
|
|
386
|
+
return registry.endsWith("/") ? registry : `${registry}/`;
|
|
387
|
+
}
|
|
388
|
+
function nerfDart(registry) {
|
|
389
|
+
const url = new URL(registry);
|
|
390
|
+
return `//${url.host}${url.pathname.endsWith("/") ? url.pathname : `${url.pathname}/`}`;
|
|
391
|
+
}
|
|
392
|
+
async function readPackument(environment, registry, name) {
|
|
393
|
+
const response = await environment.fetch(`${registry}${name.replace("/", "%2f")}`, { headers: {
|
|
394
|
+
accept: "application/json",
|
|
395
|
+
"cache-control": "no-cache"
|
|
396
|
+
} });
|
|
397
|
+
if (response.status === 404) return void 0;
|
|
398
|
+
if (!response.ok) throw new Error(`Could not read ${name} from ${registry}: ${response.status} ${response.statusText}`);
|
|
399
|
+
return await response.json();
|
|
400
|
+
}
|
|
401
|
+
async function getGitHubIdToken(environment, audience) {
|
|
402
|
+
const { ACTIONS_ID_TOKEN_REQUEST_URL: url, ACTIONS_ID_TOKEN_REQUEST_TOKEN: token } = environment.env;
|
|
403
|
+
if (!url || !token) return void 0;
|
|
404
|
+
const response = await environment.fetch(`${url}${url.includes("?") ? "&" : "?"}audience=${encodeURIComponent(audience)}`, { headers: {
|
|
405
|
+
authorization: `Bearer ${token}`,
|
|
406
|
+
accept: "application/json"
|
|
407
|
+
} });
|
|
408
|
+
if (!response.ok) {
|
|
409
|
+
environment.log(`! Could not get a GitHub Actions OIDC token: ${response.status} ${response.statusText}`);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
return (await response.json()).value;
|
|
413
|
+
}
|
|
414
|
+
/** Exchanges a GitHub Actions OIDC token for a short-lived npm publish token (npm trusted publishing). */
|
|
415
|
+
async function exchangeOidcToken(environment, name, registry) {
|
|
416
|
+
if (registry !== "https://registry.npmjs.org/") return void 0;
|
|
417
|
+
const identity = environment.env.NPM_ID_TOKEN ?? await getGitHubIdToken(environment, "npm:registry.npmjs.org");
|
|
418
|
+
if (!identity) return void 0;
|
|
419
|
+
const response = await environment.fetch(`${officialRegistry}-/npm/v1/oidc/token/exchange/package/${encodeURIComponent(name)}`, {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: { authorization: `Bearer ${identity}` }
|
|
422
|
+
});
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
environment.log(`! Could not exchange the OIDC token for ${name}: ${response.status} ${response.statusText}`);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
return (await response.json()).token;
|
|
428
|
+
}
|
|
429
|
+
function readNpmrc(file) {
|
|
430
|
+
return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
431
|
+
}
|
|
432
|
+
function hasRegistryAuth(content, registry) {
|
|
433
|
+
const nerfed = nerfDart(registry);
|
|
434
|
+
return content.split(/\r?\n/).some((line) => {
|
|
435
|
+
const key = line.split("=")[0].trim();
|
|
436
|
+
return key === "_authToken" || key === "_auth" || key.startsWith(`${nerfed}:_auth`);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* npm environment for one package: existing `.npmrc` auth, then npm trusted
|
|
441
|
+
* publishing through GitHub Actions OIDC, then `NPM_TOKEN`.
|
|
442
|
+
*/
|
|
443
|
+
async function createNpmEnv(environment, name, registry, directory) {
|
|
444
|
+
const userconfig = environment.env.NPM_CONFIG_USERCONFIG ?? path.join(os.homedir(), ".npmrc");
|
|
445
|
+
if (hasRegistryAuth([readNpmrc(path.join(environment.cwd, ".npmrc")), readNpmrc(userconfig)].join("\n"), registry)) return environment.env;
|
|
446
|
+
const token = await exchangeOidcToken(environment, name, registry) ?? environment.env.NPM_TOKEN;
|
|
447
|
+
if (!token) throw new Error(`No npm auth for ${name}. Configure npm trusted publishing with \`id-token: write\`, set NPM_TOKEN, or add auth to .npmrc for ${registry}.`);
|
|
448
|
+
const file = path.join(directory, `${name.replace(/[@/]/g, "_")}.npmrc`);
|
|
449
|
+
fs.writeFileSync(file, `${readNpmrc(userconfig).trim()}\n${nerfDart(registry)}:_authToken=${token}\n`, { mode: 384 });
|
|
450
|
+
return {
|
|
451
|
+
...environment.env,
|
|
452
|
+
NPM_CONFIG_USERCONFIG: file
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function detectIndent(content) {
|
|
456
|
+
return /^(\s+)"/m.exec(content)?.[1] ?? " ";
|
|
457
|
+
}
|
|
458
|
+
/** Writes the release version and git head into a package manifest. */
|
|
459
|
+
function writeReleaseManifest(cwd, dir, version, gitHead) {
|
|
460
|
+
const file = path.join(cwd, dir, "package.json");
|
|
461
|
+
const content = fs.readFileSync(file, "utf8");
|
|
462
|
+
const manifest = JSON.parse(content);
|
|
463
|
+
manifest.version = version;
|
|
464
|
+
manifest.gitHead = gitHead;
|
|
465
|
+
fs.writeFileSync(file, `${JSON.stringify(manifest, null, detectIndent(content))}\n`);
|
|
466
|
+
return manifest;
|
|
467
|
+
}
|
|
468
|
+
function packCommand(packageManager, destination) {
|
|
469
|
+
switch (packageManager) {
|
|
470
|
+
case "bun": return ["bun", [
|
|
471
|
+
"pm",
|
|
472
|
+
"pack",
|
|
473
|
+
"--destination",
|
|
474
|
+
destination,
|
|
475
|
+
"--quiet"
|
|
476
|
+
]];
|
|
477
|
+
case "pnpm": return ["pnpm", [
|
|
478
|
+
"pack",
|
|
479
|
+
"--pack-destination",
|
|
480
|
+
destination
|
|
481
|
+
]];
|
|
482
|
+
case "yarn": return ["yarn", [
|
|
483
|
+
"pack",
|
|
484
|
+
"--out",
|
|
485
|
+
path.join(destination, "package.tgz")
|
|
486
|
+
]];
|
|
487
|
+
default: return ["npm", [
|
|
488
|
+
"pack",
|
|
489
|
+
"--pack-destination",
|
|
490
|
+
destination
|
|
491
|
+
]];
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** Reads `package/package.json` from a packed tarball. */
|
|
495
|
+
function readPackedManifest(tarball) {
|
|
496
|
+
const archive = gunzipSync(fs.readFileSync(tarball));
|
|
497
|
+
let offset = 0;
|
|
498
|
+
let longName;
|
|
499
|
+
while (offset + 512 <= archive.length) {
|
|
500
|
+
const header = archive.subarray(offset, offset + 512);
|
|
501
|
+
if (header.every((byte) => byte === 0)) break;
|
|
502
|
+
const field = (start, length) => header.subarray(start, start + length).toString("utf8").replace(/\0.*$/s, "");
|
|
503
|
+
const size = parseInt(field(124, 12).trim() || "0", 8);
|
|
504
|
+
const type = field(156, 1);
|
|
505
|
+
const prefix = field(345, 155);
|
|
506
|
+
const name = longName ?? (prefix ? `${prefix}/${field(0, 100)}` : field(0, 100));
|
|
507
|
+
const body = archive.subarray(offset + 512, offset + 512 + size);
|
|
508
|
+
longName = void 0;
|
|
509
|
+
if (type === "L") longName = body.toString("utf8").replace(/\0.*$/s, "");
|
|
510
|
+
else if (type === "x") {
|
|
511
|
+
const pathRecord = /\d+ path=([^\n]+)\n/.exec(body.toString("utf8"));
|
|
512
|
+
if (pathRecord) longName = pathRecord[1];
|
|
513
|
+
} else if (name.replace(/^\.\//, "").split("/").slice(1).join("/") === "package.json") return JSON.parse(body.toString("utf8"));
|
|
514
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
515
|
+
}
|
|
516
|
+
throw new Error(`${tarball} has no package.json.`);
|
|
517
|
+
}
|
|
518
|
+
function validatePackedManifest(manifest, pkg, version, gitHead) {
|
|
519
|
+
if (manifest.name !== pkg.name) throw new Error(`The tarball of ${pkg.dir} is ${manifest.name}, not ${pkg.name}.`);
|
|
520
|
+
if (manifest.version !== version) throw new Error(`The tarball of ${pkg.name} has version ${manifest.version}, not ${version}.`);
|
|
521
|
+
if (manifest.gitHead !== gitHead) throw new Error(`The tarball of ${pkg.name} has gitHead ${String(manifest.gitHead)}, not ${gitHead}.`);
|
|
522
|
+
for (const field of dependencyFields) for (const [name, range] of Object.entries(manifest[field] ?? {})) if (range.startsWith("workspace:")) throw new Error(`The tarball of ${pkg.name} keeps \`${name}: ${range}\` in ${field}.`);
|
|
523
|
+
}
|
|
524
|
+
async function waitForVersion(environment, registry, name, version) {
|
|
525
|
+
for (let attempt = 1; attempt <= 30; attempt++) {
|
|
526
|
+
const published = (await readPackument(environment, registry, name))?.versions?.[version];
|
|
527
|
+
if (published) return published;
|
|
528
|
+
await environment.wait(1e4);
|
|
529
|
+
}
|
|
530
|
+
throw new Error(`${name}@${version} did not appear on ${registry}.`);
|
|
531
|
+
}
|
|
532
|
+
function truncateNotes(notes, url) {
|
|
533
|
+
if (notes.length <= 12e4) return notes;
|
|
534
|
+
const cut = notes.slice(0, releaseBodyLimit);
|
|
535
|
+
return `${cut.slice(0, cut.lastIndexOf("\n"))}\n\n…the notes are too long for a GitHub release${url ? `; see ${url}` : ""}.\n`;
|
|
536
|
+
}
|
|
537
|
+
/** Creates the GitHub release of a tag unless it exists. Needs `GITHUB_TOKEN` and the repository. */
|
|
538
|
+
async function publishGitHubRelease(environment, release) {
|
|
539
|
+
const token = environment.env.GITHUB_TOKEN ?? environment.env.GH_TOKEN;
|
|
540
|
+
const repositoryUrl = getRepositoryUrl(environment.cwd, environment.env);
|
|
541
|
+
const repository = repositoryUrl?.replace(/^https:\/\/[^/]+\//, "");
|
|
542
|
+
if (!token || !repository) {
|
|
543
|
+
environment.log("! Skip the GitHub release: set GITHUB_TOKEN and a GitHub repository URL.");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const api = `${environment.env.GITHUB_API_URL ?? "https://api.github.com"}/repos/${repository}/releases`;
|
|
547
|
+
const headers = {
|
|
548
|
+
authorization: `Bearer ${token}`,
|
|
549
|
+
accept: "application/vnd.github+json",
|
|
550
|
+
"content-type": "application/json",
|
|
551
|
+
"x-github-api-version": "2022-11-28"
|
|
552
|
+
};
|
|
553
|
+
const existing = await environment.fetch(`${api}/tags/${encodeURIComponent(release.tag)}`, { headers });
|
|
554
|
+
if (existing.ok) {
|
|
555
|
+
environment.log(`✓ GitHub release ${release.tag} exists.`);
|
|
556
|
+
return (await existing.json()).html_url;
|
|
557
|
+
}
|
|
558
|
+
const response = await environment.fetch(api, {
|
|
559
|
+
method: "POST",
|
|
560
|
+
headers,
|
|
561
|
+
body: JSON.stringify({
|
|
562
|
+
tag_name: release.tag,
|
|
563
|
+
name: release.tag,
|
|
564
|
+
body: truncateNotes(release.notes, repositoryUrl ? `${repositoryUrl}/compare` : void 0),
|
|
565
|
+
prerelease: release.prerelease,
|
|
566
|
+
make_latest: release.prerelease ? "false" : "true"
|
|
567
|
+
})
|
|
568
|
+
});
|
|
569
|
+
if (!response.ok) throw new Error(`Could not create the GitHub release ${release.tag}: ${response.status} ${await response.text()}`);
|
|
570
|
+
const url = (await response.json()).html_url;
|
|
571
|
+
environment.log(`✓ Created the GitHub release ${url ?? release.tag}.`);
|
|
572
|
+
return url;
|
|
573
|
+
}
|
|
574
|
+
function describePlan(plan) {
|
|
575
|
+
const range = plan.from ? `${plan.from}..${plan.head.slice(0, 9)}` : `all commits up to ${plan.head.slice(0, 9)}`;
|
|
576
|
+
return [
|
|
577
|
+
`Branch: ${plan.branch}${plan.channel ? ` (npm dist-tag ${plan.channel.distTag})` : ""}`,
|
|
578
|
+
`Mode: ${plan.mode}`,
|
|
579
|
+
`Range: ${range}`,
|
|
580
|
+
...plan.commits.map((decision) => ` ${(decision.release || "none").padEnd(5)} ${decision.hash?.slice(0, 9) ?? ""} ${decision.header}`),
|
|
581
|
+
`Release: ${plan.release || "none"}${plan.version ? ` → ${plan.version}` : ""}`,
|
|
582
|
+
`Packages: ${plan.packages.map(({ name }) => name).join(", ") || "none"}`
|
|
583
|
+
].join("\n");
|
|
584
|
+
}
|
|
585
|
+
/** Creates a missing GitHub release when HEAD carries a version tag from an earlier, interrupted run. */
|
|
586
|
+
async function resumeTaggedRelease(environment, options, plan) {
|
|
587
|
+
const tags = lines(await git(environment, [
|
|
588
|
+
"tag",
|
|
589
|
+
"--list",
|
|
590
|
+
"v*",
|
|
591
|
+
"--points-at",
|
|
592
|
+
"HEAD"
|
|
593
|
+
]));
|
|
594
|
+
const tag = plan.channel && tags.find((candidate) => {
|
|
595
|
+
const prerelease = candidate.includes("-");
|
|
596
|
+
return plan.channel.prerelease ? candidate.includes(`-${plan.channel.prerelease}.`) : !prerelease;
|
|
597
|
+
});
|
|
598
|
+
if (!tag || options.dryRun) return false;
|
|
599
|
+
const previous = await planRelease({
|
|
600
|
+
...options,
|
|
601
|
+
ignoreTags: [tag]
|
|
602
|
+
}, environment);
|
|
603
|
+
if (!previous.notes || previous.tag !== tag) return false;
|
|
604
|
+
environment.log(`${tag} points at HEAD; make sure its GitHub release exists.`);
|
|
605
|
+
await publishGitHubRelease(environment, {
|
|
606
|
+
tag,
|
|
607
|
+
notes: previous.notes,
|
|
608
|
+
prerelease: Boolean(plan.channel.prerelease)
|
|
609
|
+
});
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Releases the checked-out commit:
|
|
614
|
+
*
|
|
615
|
+
* 1. plan the version from the commits since the channel's last tag;
|
|
616
|
+
* 2. write the version and git head into every published package and pack it;
|
|
617
|
+
* 3. publish each package, in dependency order, on the `aronrepo-staging`
|
|
618
|
+
* dist-tag, skipping versions already published from the same commit;
|
|
619
|
+
* 4. wait until the registry serves every package, then move the channel's
|
|
620
|
+
* dist-tag to the new version;
|
|
621
|
+
* 5. push the version tag and create the GitHub release.
|
|
622
|
+
*
|
|
623
|
+
* Every step checks what an interrupted run already did, so rerunning the job
|
|
624
|
+
* finishes a release instead of skipping or duplicating it.
|
|
625
|
+
*/
|
|
626
|
+
async function runRelease(options = {}, environmentOptions = {}) {
|
|
627
|
+
const environment = createEnvironment(environmentOptions);
|
|
628
|
+
const { cwd, log } = environment;
|
|
629
|
+
const plan = await planRelease(options, environment);
|
|
630
|
+
log(describePlan(plan));
|
|
631
|
+
if (!plan.channel) {
|
|
632
|
+
log(`Branch ${plan.branch} does not release. Release branches: main, alpha, beta, rc, canary.`);
|
|
633
|
+
return {
|
|
634
|
+
status: "skipped",
|
|
635
|
+
plan
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
if (!plan.version || !plan.tag || !plan.notes) {
|
|
639
|
+
if (await resumeTaggedRelease(environment, options, plan)) return {
|
|
640
|
+
status: "resumed",
|
|
641
|
+
plan
|
|
642
|
+
};
|
|
643
|
+
log("No release.");
|
|
644
|
+
return {
|
|
645
|
+
status: "none",
|
|
646
|
+
plan
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
if (!plan.packages.length) throw new Error("No published package found. Add `\"publishConfig\": { \"access\": \"public\" }` to the packages that ship.");
|
|
650
|
+
if (options.dryRun) {
|
|
651
|
+
log(`\nDry run: would publish ${plan.tag} on ${plan.channel.distTag}.\n\n${plan.notes}`);
|
|
652
|
+
return {
|
|
653
|
+
status: "dry-run",
|
|
654
|
+
plan
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (await git(environment, ["status", "--porcelain"])) throw new Error("The working tree has changes. Release from a clean checkout.");
|
|
658
|
+
if (await git(environment, [
|
|
659
|
+
"tag",
|
|
660
|
+
"--list",
|
|
661
|
+
plan.tag
|
|
662
|
+
])) throw new Error(`${plan.tag} exists but is not reachable from ${plan.branch}.`);
|
|
663
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "aronrepo-release-"));
|
|
664
|
+
const manifests = new Map(plan.packages.map(({ dir }) => {
|
|
665
|
+
const file = path.join(cwd, dir, "package.json");
|
|
666
|
+
return [file, fs.readFileSync(file, "utf8")];
|
|
667
|
+
}));
|
|
668
|
+
try {
|
|
669
|
+
const packageManager = detectPackageManager(cwd);
|
|
670
|
+
for (const pkg of plan.packages) pkg.manifest = writeReleaseManifest(cwd, pkg.dir, plan.version, plan.head);
|
|
671
|
+
const tarballs = /* @__PURE__ */ new Map();
|
|
672
|
+
for (const [index, pkg] of plan.packages.entries()) {
|
|
673
|
+
const destination = path.join(directory, String(index));
|
|
674
|
+
fs.mkdirSync(destination);
|
|
675
|
+
const [command, args] = packCommand(packageManager, destination);
|
|
676
|
+
await environment.run(command, args, {
|
|
677
|
+
cwd: path.join(cwd, pkg.dir),
|
|
678
|
+
env: environment.env
|
|
679
|
+
});
|
|
680
|
+
const tarball = fs.readdirSync(destination).find((file) => file.endsWith(".tgz"));
|
|
681
|
+
if (!tarball) throw new Error(`Packing ${pkg.name} produced no tarball.`);
|
|
682
|
+
const file = path.join(destination, tarball);
|
|
683
|
+
validatePackedManifest(readPackedManifest(file), pkg, plan.version, plan.head);
|
|
684
|
+
tarballs.set(pkg.name, file);
|
|
685
|
+
log(`✓ Packed ${pkg.name}@${plan.version}`);
|
|
686
|
+
}
|
|
687
|
+
const npmEnvs = /* @__PURE__ */ new Map();
|
|
688
|
+
const npmEnv = async (pkg) => {
|
|
689
|
+
if (!npmEnvs.has(pkg.name)) npmEnvs.set(pkg.name, await createNpmEnv(environment, pkg.name, getRegistry(pkg.manifest, environment.env), directory));
|
|
690
|
+
return npmEnvs.get(pkg.name);
|
|
691
|
+
};
|
|
692
|
+
for (const pkg of plan.packages) {
|
|
693
|
+
const registry = getRegistry(pkg.manifest, environment.env);
|
|
694
|
+
const published = (await readPackument(environment, registry, pkg.name))?.versions?.[plan.version];
|
|
695
|
+
if (published) {
|
|
696
|
+
if (published.gitHead !== plan.head) throw new Error(`${pkg.name}@${plan.version} is already published from ${published.gitHead ?? "another commit"}, not ${plan.head}.`);
|
|
697
|
+
log(`✓ ${pkg.name}@${plan.version} is already published from this commit.`);
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
const args = [
|
|
701
|
+
"publish",
|
|
702
|
+
tarballs.get(pkg.name),
|
|
703
|
+
"--tag",
|
|
704
|
+
stagingDistTag,
|
|
705
|
+
"--registry",
|
|
706
|
+
registry
|
|
707
|
+
];
|
|
708
|
+
if (pkg.manifest.publishConfig?.access) args.push("--access", String(pkg.manifest.publishConfig.access));
|
|
709
|
+
if (pkg.manifest.publishConfig?.provenance) args.push("--provenance");
|
|
710
|
+
await environment.run("npm", args, {
|
|
711
|
+
cwd,
|
|
712
|
+
env: await npmEnv(pkg)
|
|
713
|
+
});
|
|
714
|
+
log(`✓ Published ${pkg.name}@${plan.version} on ${stagingDistTag}`);
|
|
715
|
+
}
|
|
716
|
+
for (const pkg of plan.packages) {
|
|
717
|
+
const registry = getRegistry(pkg.manifest, environment.env);
|
|
718
|
+
const published = await waitForVersion(environment, registry, pkg.name, plan.version);
|
|
719
|
+
if (pkg.manifest.publishConfig?.provenance && !published.dist?.attestations) log(`! ${pkg.name}@${plan.version} has no provenance attestation on ${registry}.`);
|
|
720
|
+
}
|
|
721
|
+
for (const pkg of plan.packages) {
|
|
722
|
+
const registry = getRegistry(pkg.manifest, environment.env);
|
|
723
|
+
const tags = (await readPackument(environment, registry, pkg.name))?.["dist-tags"] ?? {};
|
|
724
|
+
const env = await npmEnv(pkg);
|
|
725
|
+
if (tags[plan.channel.distTag] !== plan.version) await environment.run("npm", [
|
|
726
|
+
"dist-tag",
|
|
727
|
+
"add",
|
|
728
|
+
`${pkg.name}@${plan.version}`,
|
|
729
|
+
plan.channel.distTag,
|
|
730
|
+
"--registry",
|
|
731
|
+
registry
|
|
732
|
+
], {
|
|
733
|
+
cwd,
|
|
734
|
+
env
|
|
735
|
+
});
|
|
736
|
+
if (tags["aronrepo-staging"]) await environment.run("npm", [
|
|
737
|
+
"dist-tag",
|
|
738
|
+
"rm",
|
|
739
|
+
pkg.name,
|
|
740
|
+
stagingDistTag,
|
|
741
|
+
"--registry",
|
|
742
|
+
registry
|
|
743
|
+
], {
|
|
744
|
+
cwd,
|
|
745
|
+
env
|
|
746
|
+
}).catch((error) => log(`! Could not remove the ${stagingDistTag} dist-tag of ${pkg.name}: ${error.message}`));
|
|
747
|
+
log(`✓ ${pkg.name}@${plan.version} is on ${plan.channel.distTag}`);
|
|
748
|
+
}
|
|
749
|
+
} finally {
|
|
750
|
+
fs.rmSync(directory, {
|
|
751
|
+
recursive: true,
|
|
752
|
+
force: true
|
|
753
|
+
});
|
|
754
|
+
for (const [file, content] of manifests) fs.writeFileSync(file, content);
|
|
755
|
+
}
|
|
756
|
+
await git(environment, [
|
|
757
|
+
"tag",
|
|
758
|
+
plan.tag,
|
|
759
|
+
plan.head
|
|
760
|
+
]);
|
|
761
|
+
await git(environment, [
|
|
762
|
+
"push",
|
|
763
|
+
"origin",
|
|
764
|
+
`refs/tags/${plan.tag}`
|
|
765
|
+
]);
|
|
766
|
+
log(`✓ Pushed ${plan.tag}`);
|
|
767
|
+
await publishGitHubRelease(environment, {
|
|
768
|
+
tag: plan.tag,
|
|
769
|
+
notes: plan.notes,
|
|
770
|
+
prerelease: Boolean(plan.channel.prerelease)
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
status: "released",
|
|
774
|
+
plan
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
// src/commands.ts
|
|
778
|
+
const ignoredMessagePatterns = [
|
|
779
|
+
/^Merge pull request /,
|
|
780
|
+
/^Merge (?:branch|remote-tracking branch|tag) /,
|
|
781
|
+
/^Merge .+ into .+/,
|
|
782
|
+
/^Merged .+ (?:in|into) .+/,
|
|
783
|
+
/^Automatic merge/,
|
|
784
|
+
/^Auto-merged .+ into .+/,
|
|
785
|
+
/^(?:fixup|squash|amend)! /,
|
|
786
|
+
/^Initial commit$/
|
|
787
|
+
];
|
|
788
|
+
function isIgnoredMessage(message) {
|
|
789
|
+
const header = message.trim().split("\n")[0] ?? "";
|
|
790
|
+
return ignoredMessagePatterns.some((pattern) => pattern.test(header));
|
|
791
|
+
}
|
|
792
|
+
function readEditMessage(file) {
|
|
793
|
+
return fs.readFileSync(file, "utf8").split("\n").filter((line) => !line.startsWith("#")).join("\n").split(/^-+ >8 -+$/m)[0].trim();
|
|
794
|
+
}
|
|
795
|
+
async function project(options) {
|
|
796
|
+
return loadProjectPolicy(options.cwd ?? process.cwd(), options.mode ? { mode: options.mode } : {});
|
|
797
|
+
}
|
|
798
|
+
function isError(violation) {
|
|
799
|
+
return (violation.severity ?? "error") === "error";
|
|
800
|
+
}
|
|
801
|
+
function formatViolation(violation) {
|
|
802
|
+
return ` ${isError(violation) ? "✗" : "!"} ${violation.message}${violation.suggestion ? `\n Suggested header: ${violation.suggestion}` : ""}`;
|
|
803
|
+
}
|
|
804
|
+
function formatDecision(decision) {
|
|
805
|
+
return `${decision.hash ? `${decision.hash.slice(0, 9)} ` : ""}${(decision.release || "none").padEnd(5)} ${decision.header}\n${decision.reasons.map((reason) => ` ${reason}`).join("\n")}`;
|
|
806
|
+
}
|
|
807
|
+
async function check(options) {
|
|
808
|
+
const cwd = options.cwd ?? process.cwd();
|
|
809
|
+
const { policy, isReleasePath } = await project(options);
|
|
810
|
+
const pathOptions = isReleasePath ? { isReleasePath } : {};
|
|
811
|
+
let entries;
|
|
812
|
+
if (options.title !== void 0) {
|
|
813
|
+
const files = options.files ?? (options.base && options.head ? getDiffFiles(cwd, options.base, options.head) : void 0);
|
|
814
|
+
entries = [{
|
|
815
|
+
message: options.title,
|
|
816
|
+
...files ? { files } : {},
|
|
817
|
+
label: "PR title"
|
|
818
|
+
}];
|
|
819
|
+
} else if (options.edit || options.message !== void 0) entries = [{
|
|
820
|
+
message: options.edit ? readEditMessage(path.resolve(cwd, options.edit)) : options.message,
|
|
821
|
+
files: options.files ?? getStagedFiles(cwd),
|
|
822
|
+
label: "Commit message"
|
|
823
|
+
}];
|
|
824
|
+
else if (options.from) entries = getRangeCommits(cwd, options.from, options.to ?? "HEAD").map((commit) => ({
|
|
825
|
+
...commit,
|
|
826
|
+
label: commit.hash.slice(0, 9)
|
|
827
|
+
}));
|
|
828
|
+
else return {
|
|
829
|
+
code: 2,
|
|
830
|
+
output: "Pass --edit <file>, --message <text>, --title <text>, or --from <ref>."
|
|
831
|
+
};
|
|
832
|
+
const results = entries.filter(({ message }) => !isIgnoredMessage(message)).map((entry) => ({
|
|
833
|
+
entry,
|
|
834
|
+
decision: resolveCommitRelease(parseCommit(entry.message, entry.hash), policy, {
|
|
835
|
+
...pathOptions,
|
|
836
|
+
...entry.files ? { files: entry.files } : {}
|
|
837
|
+
})
|
|
838
|
+
}));
|
|
839
|
+
const failures = results.filter(({ decision }) => decision.violations.some(isError));
|
|
840
|
+
if (options.json) return {
|
|
841
|
+
code: failures.length ? 1 : 0,
|
|
842
|
+
output: JSON.stringify({
|
|
843
|
+
mode: policy.mode,
|
|
844
|
+
results: results.map(({ entry, decision }) => ({
|
|
845
|
+
label: entry.label,
|
|
846
|
+
...decision
|
|
847
|
+
}))
|
|
848
|
+
}, null, 2)
|
|
849
|
+
};
|
|
850
|
+
if (!failures.length) return {
|
|
851
|
+
code: 0,
|
|
852
|
+
output: [...results.map(({ entry, decision }) => [`✓ ${entry.label}: ${decision.release || "none"} ← ${decision.header}`, ...decision.violations.map(formatViolation)].join("\n")), `${results.length} ${results.length === 1 ? "header follows" : "headers follow"} the Aronrepo ${policy.mode} release policy.`].join("\n")
|
|
853
|
+
};
|
|
854
|
+
return {
|
|
855
|
+
code: 1,
|
|
856
|
+
output: [
|
|
857
|
+
...failures.map(({ entry, decision }) => `${entry.label}: ${decision.header}\n${decision.violations.map(formatViolation).join("\n")}`),
|
|
858
|
+
"",
|
|
859
|
+
"Run `aronrepo policy` to see which types release and which paths ship."
|
|
860
|
+
].join("\n")
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
async function explain(options) {
|
|
864
|
+
const cwd = options.cwd ?? process.cwd();
|
|
865
|
+
const { policy, isReleasePath, releasePaths } = await project(options);
|
|
866
|
+
let plan;
|
|
867
|
+
let from;
|
|
868
|
+
let to = options.to ?? "HEAD";
|
|
869
|
+
if (options.from === void 0 && options.to === void 0) {
|
|
870
|
+
plan = await planRelease({
|
|
871
|
+
fallbackToMain: true,
|
|
872
|
+
...options.mode ? { mode: options.mode } : {}
|
|
873
|
+
}, { cwd });
|
|
874
|
+
from = plan.from;
|
|
875
|
+
to = "HEAD";
|
|
876
|
+
} else {
|
|
877
|
+
from = options.from ?? getLatestTag(cwd, to);
|
|
878
|
+
plan = resolveRelease(getRangeCommits(cwd, from, to), policy, isReleasePath ? { isReleasePath } : {});
|
|
879
|
+
}
|
|
880
|
+
if (options.json) return {
|
|
881
|
+
code: 0,
|
|
882
|
+
output: JSON.stringify({
|
|
883
|
+
mode: policy.mode,
|
|
884
|
+
...plan.branch ? { branch: plan.branch } : {},
|
|
885
|
+
...plan.channel ? { channel: plan.channel } : {},
|
|
886
|
+
from: from ?? null,
|
|
887
|
+
to,
|
|
888
|
+
releasePaths: releasePaths ?? null,
|
|
889
|
+
release: plan.release,
|
|
890
|
+
version: plan.version ?? null,
|
|
891
|
+
tag: plan.tag ?? null,
|
|
892
|
+
commits: plan.commits
|
|
893
|
+
}, null, 2)
|
|
894
|
+
};
|
|
895
|
+
return {
|
|
896
|
+
code: 0,
|
|
897
|
+
output: [
|
|
898
|
+
`Range: ${from ? `${from}..${to}` : `all commits up to ${to}`}`,
|
|
899
|
+
`Mode: ${policy.mode}`,
|
|
900
|
+
...plan.channel ? [`Channel: ${plan.channel.branch} (npm dist-tag ${plan.channel.distTag})`] : [],
|
|
901
|
+
`Release paths: ${releasePaths?.include ? releasePaths.include.join(", ") : "every path"}`,
|
|
902
|
+
"",
|
|
903
|
+
...plan.commits.length ? plan.commits.map(formatDecision) : ["No commits."],
|
|
904
|
+
"",
|
|
905
|
+
`Next release: ${plan.release || "none"}${plan.version ? ` (${plan.version})` : ""}`
|
|
906
|
+
].join("\n")
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
async function release(options) {
|
|
910
|
+
const cwd = options.cwd ?? process.cwd();
|
|
911
|
+
const result = await runRelease({
|
|
912
|
+
...options.mode ? { mode: options.mode } : {},
|
|
913
|
+
...options.branch ? { branch: options.branch } : {},
|
|
914
|
+
...options.dryRun ? { dryRun: true } : {}
|
|
915
|
+
}, {
|
|
916
|
+
cwd,
|
|
917
|
+
log: options.json ? () => {} : options.log ?? ((message) => console.log(message))
|
|
918
|
+
});
|
|
919
|
+
if (!options.json) return {
|
|
920
|
+
code: 0,
|
|
921
|
+
output: ""
|
|
922
|
+
};
|
|
923
|
+
const { packages, ...plan } = result.plan;
|
|
924
|
+
return {
|
|
925
|
+
code: 0,
|
|
926
|
+
output: JSON.stringify({
|
|
927
|
+
status: result.status,
|
|
928
|
+
...plan,
|
|
929
|
+
packages: packages.map(({ dir, name }) => ({
|
|
930
|
+
dir,
|
|
931
|
+
name
|
|
932
|
+
}))
|
|
933
|
+
}, null, 2)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
async function policy(options) {
|
|
937
|
+
const resolved = await project(options);
|
|
938
|
+
if (options.markdown) return {
|
|
939
|
+
code: 0,
|
|
940
|
+
output: renderAgentsBlock(resolved.policy, {
|
|
941
|
+
cli: resolved.cli,
|
|
942
|
+
...resolved.releasePaths ? { releasePaths: resolved.releasePaths } : {}
|
|
943
|
+
})
|
|
944
|
+
};
|
|
945
|
+
const data = {
|
|
946
|
+
mode: resolved.policy.mode,
|
|
947
|
+
configFile: resolved.configFile ? path.relative(resolved.cwd, resolved.configFile) : null,
|
|
948
|
+
releasePaths: resolved.releasePaths ?? null,
|
|
949
|
+
releaseTable: getReleaseTable(resolved.policy),
|
|
950
|
+
types: commits.map(({ type, scope, description, group }) => ({
|
|
951
|
+
type,
|
|
952
|
+
...scope ? { scope } : {},
|
|
953
|
+
release: getReleaseType(type, scope, resolved.policy) ?? false,
|
|
954
|
+
description,
|
|
955
|
+
...group ? { group } : {}
|
|
956
|
+
})),
|
|
957
|
+
legacyTypes: legacyCommits,
|
|
958
|
+
agent: getAgentCommitPolicy(resolved.policy, { cli: resolved.cli })
|
|
959
|
+
};
|
|
960
|
+
if (options.json) return {
|
|
961
|
+
code: 0,
|
|
962
|
+
output: JSON.stringify(data, null, 2)
|
|
963
|
+
};
|
|
964
|
+
return {
|
|
965
|
+
code: 0,
|
|
966
|
+
output: [
|
|
967
|
+
`Mode: ${data.mode}${data.configFile ? ` (${data.configFile})` : " (default; no aronrepo.config.js)"}`,
|
|
968
|
+
`Release paths: ${data.releasePaths?.include ? data.releasePaths.include.join(", ") : "every path"}`,
|
|
969
|
+
...data.releasePaths?.include ? [`Excluded: ${(data.releasePaths.exclude ?? defaultReleasePathExcludes).join(", ")}`] : [],
|
|
970
|
+
"",
|
|
971
|
+
`Major: ${data.releaseTable.major.join(", ") || "-"}`,
|
|
972
|
+
`Minor: ${data.releaseTable.minor.join(", ") || "-"}`,
|
|
973
|
+
`Patch: ${data.releaseTable.patch.join(", ") || "-"}`,
|
|
974
|
+
`None: ${data.releaseTable.none.join(", ") || "-"}`,
|
|
975
|
+
"",
|
|
976
|
+
...data.agent.ownerDecisions.map((rule) => `- ${rule}`)
|
|
977
|
+
].join("\n")
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function replaceAgentsBlock(content, block) {
|
|
981
|
+
const start = content.indexOf(agentsBlockStart);
|
|
982
|
+
const end = content.indexOf(agentsBlockEnd);
|
|
983
|
+
if (start !== -1 && end > start) return content.slice(0, start) + block + content.slice(end + agentsBlockEnd.length);
|
|
984
|
+
return `${content.trimEnd()}${content.trim() ? "\n\n" : ""}${block}\n`;
|
|
985
|
+
}
|
|
986
|
+
function readAgentsBlock(content) {
|
|
987
|
+
const start = content.indexOf(agentsBlockStart);
|
|
988
|
+
const end = content.indexOf(agentsBlockEnd);
|
|
989
|
+
return start !== -1 && end > start ? content.slice(start, end + agentsBlockEnd.length) : void 0;
|
|
990
|
+
}
|
|
991
|
+
async function agents(options) {
|
|
992
|
+
const resolved = await project(options);
|
|
993
|
+
const file = path.resolve(resolved.cwd, options.file ?? "AGENTS.md");
|
|
994
|
+
const relative = path.relative(resolved.cwd, file);
|
|
995
|
+
const block = renderAgentsBlock(resolved.policy, {
|
|
996
|
+
cli: resolved.cli,
|
|
997
|
+
...resolved.releasePaths ? { releasePaths: resolved.releasePaths } : {}
|
|
998
|
+
});
|
|
999
|
+
const content = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
1000
|
+
if (options.write) {
|
|
1001
|
+
const next = replaceAgentsBlock(content || "# Agent Guide\n", block);
|
|
1002
|
+
if (next === content) return {
|
|
1003
|
+
code: 0,
|
|
1004
|
+
output: `✓ ${relative} is up to date.`
|
|
1005
|
+
};
|
|
1006
|
+
fs.writeFileSync(file, next);
|
|
1007
|
+
return {
|
|
1008
|
+
code: 0,
|
|
1009
|
+
output: `✓ Wrote the Aronrepo commit policy block to ${relative}.`
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
const current = readAgentsBlock(content);
|
|
1013
|
+
if (current === block) return {
|
|
1014
|
+
code: 0,
|
|
1015
|
+
output: `✓ ${relative} has the current Aronrepo commit policy block.`
|
|
1016
|
+
};
|
|
1017
|
+
return {
|
|
1018
|
+
code: 1,
|
|
1019
|
+
output: `✗ ${relative} ${current ? "has an outdated" : "is missing the"} Aronrepo commit policy block. Run \`aronrepo agents --write\`.`
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
function findFile(cwd, names) {
|
|
1023
|
+
return names.map((name) => path.join(cwd, name)).find((file) => fs.existsSync(file));
|
|
1024
|
+
}
|
|
1025
|
+
function readText(file) {
|
|
1026
|
+
return file ? fs.readFileSync(file, "utf8") : "";
|
|
1027
|
+
}
|
|
1028
|
+
const legacyDependencies = [
|
|
1029
|
+
"semantic-release",
|
|
1030
|
+
"@commitlint/cli",
|
|
1031
|
+
"@aronrepo/commitlint-config",
|
|
1032
|
+
"@aronrepo/conventional-changelog-config",
|
|
1033
|
+
".",
|
|
1034
|
+
"@aronrepo/semantic-release-config",
|
|
1035
|
+
"@aronrepo/semantic-release-pnpm"
|
|
1036
|
+
];
|
|
1037
|
+
const legacyConfigFiles = [
|
|
1038
|
+
"release.config.js",
|
|
1039
|
+
"release.config.mjs",
|
|
1040
|
+
"release.config.cjs",
|
|
1041
|
+
".releaserc",
|
|
1042
|
+
".releaserc.js",
|
|
1043
|
+
".releaserc.mjs",
|
|
1044
|
+
".releaserc.json",
|
|
1045
|
+
"commitlint.config.js",
|
|
1046
|
+
"commitlint.config.mjs",
|
|
1047
|
+
"commitlint.config.ts",
|
|
1048
|
+
".commitlintrc",
|
|
1049
|
+
".commitlintrc.js",
|
|
1050
|
+
".commitlintrc.mjs",
|
|
1051
|
+
".commitlintrc.json"
|
|
1052
|
+
];
|
|
1053
|
+
async function doctor(options) {
|
|
1054
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1055
|
+
const checks = [];
|
|
1056
|
+
let resolved;
|
|
1057
|
+
try {
|
|
1058
|
+
resolved = await project(options);
|
|
1059
|
+
checks.push(resolved.configFile ? {
|
|
1060
|
+
status: "pass",
|
|
1061
|
+
message: `Release mode ${resolved.policy.mode} from ${path.relative(cwd, resolved.configFile)}.`
|
|
1062
|
+
} : {
|
|
1063
|
+
status: "warn",
|
|
1064
|
+
message: `No aronrepo.config.js; using the default ${resolved.policy.mode} mode.`,
|
|
1065
|
+
fix: "Ask the repository owner whether releases are explicit (owner-controlled) or auto, then create aronrepo.config.js with `release.mode`."
|
|
1066
|
+
});
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
checks.push({
|
|
1069
|
+
status: "fail",
|
|
1070
|
+
message: `aronrepo.config.js failed to load: ${error.message}`
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
if (resolved) checks.push(resolved.releasePaths?.include ? {
|
|
1074
|
+
status: "pass",
|
|
1075
|
+
message: `Release paths: ${resolved.releasePaths.include.join(", ")}.`
|
|
1076
|
+
} : {
|
|
1077
|
+
status: "warn",
|
|
1078
|
+
message: "No published package found; every path can release a version.",
|
|
1079
|
+
fix: "Set `release.paths.include` in aronrepo.config.js, or add `publishConfig.access: \"public\"` to published packages."
|
|
1080
|
+
});
|
|
1081
|
+
const pkg = JSON.parse(readText(findFile(cwd, ["package.json"])) || "{}");
|
|
1082
|
+
const dependencies = {
|
|
1083
|
+
...pkg.dependencies,
|
|
1084
|
+
...pkg.devDependencies
|
|
1085
|
+
};
|
|
1086
|
+
checks.push(dependencies.aronrepo ? {
|
|
1087
|
+
status: "pass",
|
|
1088
|
+
message: "aronrepo is installed."
|
|
1089
|
+
} : {
|
|
1090
|
+
status: "fail",
|
|
1091
|
+
message: "aronrepo is not a dependency.",
|
|
1092
|
+
fix: "Install it as a dev dependency, for example `bun add -d aronrepo`."
|
|
1093
|
+
});
|
|
1094
|
+
const leftovers = [...legacyDependencies.filter((name) => dependencies[name]), ...legacyConfigFiles.filter((name) => fs.existsSync(path.join(cwd, name)))];
|
|
1095
|
+
checks.push(leftovers.length ? {
|
|
1096
|
+
status: "warn",
|
|
1097
|
+
message: `Leftovers from semantic-release or commitlint: ${leftovers.join(", ")}.`,
|
|
1098
|
+
fix: "Remove them; `aronrepo check` lints commits and `aronrepo release` publishes releases."
|
|
1099
|
+
} : {
|
|
1100
|
+
status: "pass",
|
|
1101
|
+
message: "No semantic-release or commitlint leftovers."
|
|
1102
|
+
});
|
|
1103
|
+
try {
|
|
1104
|
+
const releasePackages = getReleasePackages(cwd);
|
|
1105
|
+
const rootRepository = pkg.repository;
|
|
1106
|
+
const withoutRepository = releasePackages.filter(({ manifest }) => manifest.publishConfig?.provenance && !manifest.repository && !rootRepository);
|
|
1107
|
+
checks.push(!releasePackages.length ? {
|
|
1108
|
+
status: "warn",
|
|
1109
|
+
message: "`aronrepo release` finds no published package.",
|
|
1110
|
+
fix: "Add `\"publishConfig\": { \"access\": \"public\" }` to each package that ships."
|
|
1111
|
+
} : withoutRepository.length ? {
|
|
1112
|
+
status: "warn",
|
|
1113
|
+
message: `npm provenance needs a \`repository\` field: ${withoutRepository.map(({ name }) => name).join(", ")}.`,
|
|
1114
|
+
fix: "Add `repository` to those package.json files."
|
|
1115
|
+
} : {
|
|
1116
|
+
status: "pass",
|
|
1117
|
+
message: `\`aronrepo release\` publishes ${releasePackages.map(({ name }) => name).join(", ")}.`
|
|
1118
|
+
});
|
|
1119
|
+
} catch (error) {
|
|
1120
|
+
checks.push({
|
|
1121
|
+
status: "fail",
|
|
1122
|
+
message: error.message
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
const hook = readText(findFile(cwd, [".husky/commit-msg"]));
|
|
1126
|
+
checks.push(hook.includes("aronrepo check") ? {
|
|
1127
|
+
status: "pass",
|
|
1128
|
+
message: ".husky/commit-msg runs aronrepo check."
|
|
1129
|
+
} : {
|
|
1130
|
+
status: "warn",
|
|
1131
|
+
message: "No commit-msg hook runs `aronrepo check`.",
|
|
1132
|
+
fix: "Add .husky/commit-msg with `aronrepo check --edit \"$1\"`."
|
|
1133
|
+
});
|
|
1134
|
+
const agentsResult = await agents({
|
|
1135
|
+
...options,
|
|
1136
|
+
cwd
|
|
1137
|
+
});
|
|
1138
|
+
checks.push(agentsResult.code === 0 ? {
|
|
1139
|
+
status: "pass",
|
|
1140
|
+
message: "AGENTS.md has the current commit policy block."
|
|
1141
|
+
} : {
|
|
1142
|
+
status: "fail",
|
|
1143
|
+
message: agentsResult.output.replace(/^✗ /, ""),
|
|
1144
|
+
fix: "aronrepo agents --write"
|
|
1145
|
+
});
|
|
1146
|
+
const failed = checks.some(({ status }) => status === "fail");
|
|
1147
|
+
if (options.json) return {
|
|
1148
|
+
code: failed ? 1 : 0,
|
|
1149
|
+
output: JSON.stringify({
|
|
1150
|
+
ok: !failed,
|
|
1151
|
+
checks
|
|
1152
|
+
}, null, 2)
|
|
1153
|
+
};
|
|
1154
|
+
const icon = {
|
|
1155
|
+
pass: "✓",
|
|
1156
|
+
warn: "!",
|
|
1157
|
+
fail: "✗"
|
|
1158
|
+
};
|
|
1159
|
+
return {
|
|
1160
|
+
code: failed ? 1 : 0,
|
|
1161
|
+
output: [
|
|
1162
|
+
...checks.map(({ status, message, fix }) => `${icon[status]} ${message}${fix ? `\n Fix: ${fix}` : ""}`),
|
|
1163
|
+
"",
|
|
1164
|
+
failed ? "Aronrepo setup is incomplete." : "Aronrepo setup is complete."
|
|
1165
|
+
].join("\n")
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
export { getLatestTag as A, resolveReleasePaths as B, configFileNames as C, getCliCommand as D, findConfigFile as E, loadProjectPolicy as F, queryPublicWorkspaceDirs as I, queryWorkspaceDirs as L, getRangeCommits as M, getStagedFiles as N, getCommitFiles as O, loadConfig as P, readWorkspacePackage as R, writeReleaseManifest as S, explainRange as T, readPackedManifest as _, isIgnoredMessage as a, runRelease as b, release as c, getRegistry as d, getReleasePackages as f, publishGitHubRelease as g, planRelease as h, explain as i, getProjectCliCommand as j, getDiffFiles as k, replaceAgentsBlock as l, officialRegistry as m, check as n, policy as o, getRepositoryUrl as p, doctor as r, readAgentsBlock as s, agents as t, exchangeOidcToken as u, releaseBodyLimit as v, detectPackageManager as w, stagingDistTag as x, runCommand as y, readWorkspacePatterns as z };
|
|
1169
|
+
|
|
1170
|
+
//# sourceMappingURL=commands-CO26JPbK.js.map
|