aronrepo 3.0.1 → 3.0.3
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/LICENSE +21 -0
- package/README.md +3 -1
- package/dist/cli.js +145 -47
- package/dist/cli.js.map +1 -1
- package/dist/commands-D2xBkCvw.js +1745 -0
- package/dist/commands-D2xBkCvw.js.map +1 -0
- package/dist/{index-Co5yiJXA.d.ts → index-CmnjSEga.d.ts} +41 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +254 -88
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +73 -11
- package/dist/node.js +1 -1
- package/package.json +8 -5
- package/dist/commands-BRnoLH3z.js +0 -1167
- package/dist/commands-BRnoLH3z.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ function maxRelease(...releases) {
|
|
|
20
20
|
*
|
|
21
21
|
* Release notes list the groups in this order.
|
|
22
22
|
*/
|
|
23
|
-
const commits = [
|
|
23
|
+
const commits = Object.freeze([
|
|
24
24
|
{
|
|
25
25
|
type: "Bump",
|
|
26
26
|
scope: "Major",
|
|
@@ -139,12 +139,12 @@ const commits = [
|
|
|
139
139
|
release: false,
|
|
140
140
|
description: "Maintenance: development dependencies, lockfiles, metadata, agent guidance, and unpublished workspaces."
|
|
141
141
|
}
|
|
142
|
-
];
|
|
142
|
+
].map((commit) => Object.freeze(commit)));
|
|
143
143
|
/**
|
|
144
144
|
* Types from earlier Aronrepo versions. They still resolve in history so old
|
|
145
145
|
* commits release as before, but `aronrepo check` rejects them in new commits.
|
|
146
146
|
*/
|
|
147
|
-
const legacyCommits = [
|
|
147
|
+
const legacyCommits = Object.freeze([
|
|
148
148
|
{
|
|
149
149
|
type: "Feat",
|
|
150
150
|
replacement: "Add"
|
|
@@ -161,7 +161,7 @@ const legacyCommits = [
|
|
|
161
161
|
type: "Misc",
|
|
162
162
|
replacement: "Chore"
|
|
163
163
|
}
|
|
164
|
-
];
|
|
164
|
+
].map((commit) => Object.freeze(commit)));
|
|
165
165
|
const types = [...new Set(commits.map(({ type }) => type))];
|
|
166
166
|
const legacyTypes = legacyCommits.map(({ type }) => type);
|
|
167
167
|
const releaseCommits = commits.filter(({ release }) => release !== false);
|
|
@@ -186,6 +186,11 @@ const defaultReleasePathExcludes = [
|
|
|
186
186
|
"**/test/**",
|
|
187
187
|
"**/__tests__/**",
|
|
188
188
|
"**/*.{test,spec}.*",
|
|
189
|
+
"**/*.test-d.*",
|
|
190
|
+
"**/__snapshots__/**",
|
|
191
|
+
"**/__fixtures__/**",
|
|
192
|
+
"**/__mocks__/**",
|
|
193
|
+
"**/fixtures/**",
|
|
189
194
|
"**/benchmarks/**",
|
|
190
195
|
"**/examples/**",
|
|
191
196
|
"**/CHANGELOG.md",
|
|
@@ -198,17 +203,40 @@ function defineConfig(config) {
|
|
|
198
203
|
function getReleasePolicy(config = {}) {
|
|
199
204
|
const mode = config.release?.mode ?? "explicit";
|
|
200
205
|
if (!releaseModes.includes(mode)) throw new Error(`Unknown Aronrepo release mode "${String(mode)}". Use one of: ${releaseModes.join(", ")}.`);
|
|
206
|
+
const paths = config.release?.paths;
|
|
207
|
+
if (paths !== void 0 && paths !== false) validateReleasePaths(paths);
|
|
208
|
+
const assets = config.release?.assets;
|
|
209
|
+
if (assets !== void 0) validateReleaseAssets(assets);
|
|
201
210
|
return {
|
|
202
211
|
mode,
|
|
203
|
-
...
|
|
212
|
+
...paths !== void 0 ? { paths } : {},
|
|
213
|
+
...assets !== void 0 ? { assets } : {}
|
|
204
214
|
};
|
|
205
215
|
}
|
|
216
|
+
function validateReleaseAssets(assets) {
|
|
217
|
+
if (!(Array.isArray(assets) && assets.every((asset) => typeof asset === "object" && asset !== null && typeof asset.path === "string" && ["name", "label"].every((key) => asset[key] === void 0 || typeof asset[key] === "string")))) throw new Error("`release.assets` must be an array of `{ path, name?, label? }` objects.");
|
|
218
|
+
}
|
|
219
|
+
function validateReleasePaths(paths) {
|
|
220
|
+
const isGlobs = (value) => value === void 0 || Array.isArray(value) && value.every((glob) => typeof glob === "string");
|
|
221
|
+
if (typeof paths !== "object" || paths === null || Array.isArray(paths)) throw new Error("`release.paths` must be `false` or an object with `include` and `exclude` glob arrays.");
|
|
222
|
+
const { include, exclude } = paths;
|
|
223
|
+
if (!isGlobs(include)) throw new Error("`release.paths.include` must be an array of globs.");
|
|
224
|
+
if (!isGlobs(exclude)) throw new Error("`release.paths.exclude` must be an array of globs.");
|
|
225
|
+
}
|
|
206
226
|
const defaultReleasePolicy = { mode: defaultReleaseMode };
|
|
207
|
-
/**
|
|
227
|
+
/**
|
|
228
|
+
* Returns a matcher for release paths, or `undefined` when every path may release.
|
|
229
|
+
* `include` defaults to every path; `!glob` entries in `include` exclude, as in
|
|
230
|
+
* `package.json` `files`, instead of matching everything else.
|
|
231
|
+
*/
|
|
208
232
|
function createReleasePathMatcher(paths) {
|
|
209
|
-
if (!paths
|
|
210
|
-
const
|
|
211
|
-
const
|
|
233
|
+
if (!paths) return void 0;
|
|
234
|
+
const globs = paths.include ?? ["**"];
|
|
235
|
+
const positive = globs.filter((glob) => !glob.startsWith("!"));
|
|
236
|
+
const negated = globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1));
|
|
237
|
+
if (!positive.length) return () => false;
|
|
238
|
+
const include = picomatch(positive, { dot: true });
|
|
239
|
+
const exclude = picomatch([...paths.exclude ?? defaultReleasePathExcludes, ...negated], { dot: true });
|
|
212
240
|
return (file) => include(file) && !exclude(file);
|
|
213
241
|
}
|
|
214
242
|
/**
|
|
@@ -224,61 +252,92 @@ function capRelease(release, type, policy) {
|
|
|
224
252
|
if (policy.mode === "explicit" && type !== "Bump" && (release === "major" || release === "minor")) return "patch";
|
|
225
253
|
return release;
|
|
226
254
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const
|
|
255
|
+
/**
|
|
256
|
+
* The first character of a scope or summary: an uppercase letter, a letter
|
|
257
|
+
* without case (such as CJK), a digit, or a code, issue, or emphasis marker.
|
|
258
|
+
*/
|
|
259
|
+
const startCharacter = String.raw`[\p{Lu}\p{Lt}\p{Lo}\p{Nd}\x60_#~]`;
|
|
260
|
+
const startPattern = new RegExp(`^${startCharacter}`, "u");
|
|
261
|
+
const headerPattern = new RegExp(String.raw`^([A-Z]\w*)(?:\((${startCharacter}[^)]*)\))?: (${startCharacter}.*)$`, "u");
|
|
262
|
+
const breakingHeaderPattern = new RegExp(String.raw`^([A-Z]\w*)(?:\((${startCharacter}[^)]*)\))?!: (${startCharacter}.*)$`, "u");
|
|
263
|
+
const footerStartPattern = /^BREAKING[ -]CHANGE:[ \t]*(.*)$/;
|
|
264
|
+
const githubRevertPattern = /^(Revert|Reapply) "([\s\S]+)"(?: \(#\d+\))?$/;
|
|
265
|
+
const revertHashPattern = /This reverts commit ([0-9a-f]{7,40})/gi;
|
|
266
|
+
const fencePattern = /^\s*(```|~~~)/;
|
|
233
267
|
const trailerPattern = /^[\w-]+: /;
|
|
234
|
-
/**
|
|
235
|
-
|
|
268
|
+
/**
|
|
269
|
+
* `BREAKING CHANGE:` footers outside code fences. Each runs, line breaks kept,
|
|
270
|
+
* until another footer or a git trailer, as Conventional Commits footers do.
|
|
271
|
+
*/
|
|
272
|
+
function readBreakingFooters(lines) {
|
|
236
273
|
const notes = [];
|
|
274
|
+
const starts = [];
|
|
237
275
|
let current;
|
|
238
|
-
|
|
239
|
-
|
|
276
|
+
let fenced = false;
|
|
277
|
+
const finish = () => {
|
|
278
|
+
if (current) notes.push(current.join("\n").trim());
|
|
279
|
+
current = void 0;
|
|
280
|
+
};
|
|
281
|
+
lines.forEach((line, index) => {
|
|
282
|
+
if (fencePattern.test(line)) fenced = !fenced;
|
|
283
|
+
const start = fenced ? null : footerStartPattern.exec(line);
|
|
240
284
|
if (start) {
|
|
241
|
-
|
|
285
|
+
finish();
|
|
286
|
+
starts.push(index);
|
|
242
287
|
current = [start[1]];
|
|
243
|
-
} else if (current &&
|
|
244
|
-
else if (current)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
288
|
+
} else if (current && !fenced && trailerPattern.test(line)) finish();
|
|
289
|
+
else if (current) current.push(line.trimEnd());
|
|
290
|
+
});
|
|
291
|
+
finish();
|
|
292
|
+
return {
|
|
293
|
+
starts,
|
|
294
|
+
notes: notes.filter(Boolean)
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function parseHeader(header) {
|
|
298
|
+
const regular = headerPattern.exec(header);
|
|
299
|
+
const match = regular ?? breakingHeaderPattern.exec(header);
|
|
300
|
+
return match ? {
|
|
301
|
+
type: match[1],
|
|
302
|
+
scope: match[2],
|
|
303
|
+
subject: match[3],
|
|
304
|
+
breaking: !regular
|
|
305
|
+
} : void 0;
|
|
251
306
|
}
|
|
252
307
|
function parseCommit(message, hash) {
|
|
253
|
-
const [
|
|
308
|
+
const [rawHeader = "", ...rest] = message.replace(/\r\n?/g, "\n").trim().split("\n");
|
|
309
|
+
const header = rawHeader.trimEnd();
|
|
254
310
|
const body = rest.join("\n").trim();
|
|
255
311
|
const parsed = {
|
|
256
312
|
header,
|
|
257
313
|
body,
|
|
258
314
|
...hash ? { hash } : {}
|
|
259
315
|
};
|
|
316
|
+
const footers = readBreakingFooters(rest);
|
|
260
317
|
const layout = [];
|
|
261
318
|
if (rest.length && rest[0].trim()) layout.push("body-leading-blank");
|
|
262
|
-
if (
|
|
319
|
+
if (footers.starts.some((index) => index > 0 && rest[index - 1].trim())) layout.push("footer-leading-blank");
|
|
263
320
|
if (layout.length) parsed.layout = layout;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const regular = headerPattern.exec(header);
|
|
267
|
-
const match = regular ?? breakingHeaderPattern.exec(header);
|
|
321
|
+
if (footers.notes.length) parsed.breakingNotes = footers.notes;
|
|
322
|
+
const match = parseHeader(header);
|
|
268
323
|
if (match) {
|
|
269
|
-
parsed.type = match
|
|
270
|
-
if (match
|
|
271
|
-
parsed.subject = match
|
|
272
|
-
if (
|
|
324
|
+
parsed.type = match.type;
|
|
325
|
+
if (match.scope) parsed.scope = match.scope;
|
|
326
|
+
parsed.subject = match.subject;
|
|
327
|
+
if (match.breaking) parsed.breaking = "header";
|
|
273
328
|
}
|
|
274
|
-
if (!parsed.breaking &&
|
|
329
|
+
if (!parsed.breaking && footers.starts.length) parsed.breaking = "footer";
|
|
275
330
|
const githubRevert = githubRevertPattern.exec(header);
|
|
276
331
|
if (githubRevert || parsed.type === "Revert") {
|
|
277
|
-
const revertedHeader = githubRevert?.[
|
|
278
|
-
const
|
|
332
|
+
const revertedHeader = githubRevert?.[2] ?? (parsed.subject && parseHeader(parsed.subject) ? parsed.subject : void 0);
|
|
333
|
+
const hashes = [...new Set([...body.matchAll(revertHashPattern)].map((found) => found[1].toLowerCase()))];
|
|
279
334
|
parsed.revert = {
|
|
280
335
|
...revertedHeader ? { header: revertedHeader } : {},
|
|
281
|
-
...
|
|
336
|
+
...hashes.length ? {
|
|
337
|
+
hash: hashes[0],
|
|
338
|
+
hashes
|
|
339
|
+
} : {},
|
|
340
|
+
...githubRevert?.[1] === "Reapply" ? { reapply: true } : {}
|
|
282
341
|
};
|
|
283
342
|
}
|
|
284
343
|
return parsed;
|
|
@@ -295,7 +354,7 @@ const nonReleaseAlternatives = [
|
|
|
295
354
|
"Example"
|
|
296
355
|
];
|
|
297
356
|
function suggestNonReleaseHeader(commit) {
|
|
298
|
-
const subject = commit.subject ?? "";
|
|
357
|
+
const subject = (commit.subject ?? commit.header).replace(/\.+$/, "");
|
|
299
358
|
const scope = commit.scope ? `(${commit.scope})` : "";
|
|
300
359
|
switch (commit.type) {
|
|
301
360
|
case "Docs": return `Docs: ${subject}`;
|
|
@@ -303,17 +362,18 @@ function suggestNonReleaseHeader(commit) {
|
|
|
303
362
|
default: return `Chore${scope}: ${subject}`;
|
|
304
363
|
}
|
|
305
364
|
}
|
|
306
|
-
const looseHeaderPattern = /^(\w+)(?:\(([^)]*)\))?(!)?:
|
|
307
|
-
function diagnoseHeader(header) {
|
|
365
|
+
const looseHeaderPattern = /^(\w+)(?:\(([^)]*)\))?(!)?:(\s*)(.*)$/;
|
|
366
|
+
function diagnoseHeader(header, mode) {
|
|
308
367
|
const format = "Use `Type(Scope): Summary` or `Type: Summary` with an Aronrepo commit type.";
|
|
309
368
|
const match = looseHeaderPattern.exec(header);
|
|
310
369
|
if (!match) return format;
|
|
311
|
-
const [, type, scope, bang, subject] = match;
|
|
312
|
-
if (!/^[A-Z]/.test(type)) return `The type \`${type}\` must be PascalCase, such as \`${type[0].toUpperCase()}${type.slice(1)}\`. ${format}`;
|
|
313
|
-
if (bang) return `Aronrepo headers do not use
|
|
314
|
-
if (scope !== void 0 &&
|
|
370
|
+
const [, type, scope, bang, space, subject] = match;
|
|
371
|
+
if (!/^[A-Z]/.test(type)) return `The type \`${type}\` must be PascalCase, such as \`${normalizeType(`${type[0].toUpperCase()}${type.slice(1)}`)}\`. ${format}`;
|
|
372
|
+
if (bang && mode === "explicit") return `Aronrepo headers do not use \`!\` in explicit mode. ${format}`;
|
|
373
|
+
if (scope !== void 0 && !startPattern.test(scope)) return `The scope \`${scope}\` must start with an uppercase letter or digit. ${format}`;
|
|
374
|
+
if (space !== " ") return `Put exactly one space after the colon. ${format}`;
|
|
315
375
|
if (!subject) return `The summary is empty. ${format}`;
|
|
316
|
-
if (
|
|
376
|
+
if (!startPattern.test(subject)) return `The summary must start with an uppercase letter, a digit, or a letter without case such as CJK. ${format}`;
|
|
317
377
|
return format;
|
|
318
378
|
}
|
|
319
379
|
function formatRelease(release) {
|
|
@@ -328,6 +388,7 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
328
388
|
...commit.scope ? { scope: commit.scope } : {},
|
|
329
389
|
...commit.subject ? { subject: commit.subject } : {},
|
|
330
390
|
...commit.breakingNotes ? { breakingNotes: commit.breakingNotes } : {},
|
|
391
|
+
...commit.breaking ? { breaking: commit.breaking } : {},
|
|
331
392
|
release: false,
|
|
332
393
|
reasons: [],
|
|
333
394
|
violations: []
|
|
@@ -337,7 +398,7 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
337
398
|
decision.reasons.push("Header does not follow `Type(Scope): Summary`; no release.");
|
|
338
399
|
decision.violations.push({
|
|
339
400
|
rule: "header-format",
|
|
340
|
-
message: diagnoseHeader(commit.header)
|
|
401
|
+
message: diagnoseHeader(commit.header, policy.mode)
|
|
341
402
|
});
|
|
342
403
|
return decision;
|
|
343
404
|
} else if (!types.includes(commit.type) && !legacyTypes.includes(commit.type)) {
|
|
@@ -348,13 +409,21 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
348
409
|
});
|
|
349
410
|
return decision;
|
|
350
411
|
} else if (!findCommitRule(commit.type, commit.scope)) {
|
|
412
|
+
const scoped = commits.filter(({ type }) => type === commit.type).map(({ type, scope }) => `\`${type}(${scope})\``).join(", ");
|
|
351
413
|
decision.reasons.push(`\`${commit.type}${commit.scope ? `(${commit.scope})` : ""}\` is not an Aronrepo commit type; no release.`);
|
|
352
414
|
decision.violations.push({
|
|
353
415
|
rule: "unknown-type",
|
|
354
|
-
message: `\`${commit.type}
|
|
416
|
+
message: commit.scope ? `\`${commit.type}(${commit.scope})\` has an unknown scope. Use one of: ${scoped}.` : `\`${commit.type}\` needs a scope: ${scoped}.`
|
|
355
417
|
});
|
|
356
418
|
return decision;
|
|
357
419
|
}
|
|
420
|
+
const knownScope = commit.scope && commits.find(({ type, scope }) => type === normalizeType(commit.type) && scope && scope !== commit.scope && scope.toLowerCase() === commit.scope.toLowerCase())?.scope;
|
|
421
|
+
if (knownScope) decision.violations.push({
|
|
422
|
+
rule: "scope-case",
|
|
423
|
+
severity: "warning",
|
|
424
|
+
message: `\`${commit.type}(${commit.scope})\` is treated as \`${commit.type}\`. Write \`${commit.type}(${knownScope})\` for that rule.`,
|
|
425
|
+
suggestion: `${commit.type}(${knownScope}): ${commit.subject ?? ""}`
|
|
426
|
+
});
|
|
358
427
|
if (commit.subject?.endsWith(".")) decision.violations.push({
|
|
359
428
|
rule: "subject-full-stop",
|
|
360
429
|
message: "The summary must not end with a period.",
|
|
@@ -377,8 +446,15 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
377
446
|
}
|
|
378
447
|
let release;
|
|
379
448
|
if (decision.type === "Revert" && commit.revert?.header) {
|
|
380
|
-
|
|
381
|
-
|
|
449
|
+
const reverted = resolveCommitRelease(commit.revert.header, policy, options);
|
|
450
|
+
const verb = commit.revert.reapply ? "Reapplies" : "Reverts";
|
|
451
|
+
if (reverted.type === "Bump" && reverted.release) {
|
|
452
|
+
release = "patch";
|
|
453
|
+
decision.reasons.push(`${verb} \`${commit.revert.header}\`; reverting a \`Bump\` releases patch.`);
|
|
454
|
+
} else {
|
|
455
|
+
release = reverted.release;
|
|
456
|
+
decision.reasons.push(`${verb} \`${commit.revert.header}\`, which releases ${formatRelease(release)}.`);
|
|
457
|
+
}
|
|
382
458
|
} else {
|
|
383
459
|
const rule = findCommitRule(decision.type, decision.scope);
|
|
384
460
|
release = capRelease(rule.release, decision.type, policy);
|
|
@@ -402,10 +478,16 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
402
478
|
const isBump = decision.type === "Bump";
|
|
403
479
|
if (release && !isBump && options.files && options.isReleasePath) {
|
|
404
480
|
if (!options.files.filter(options.isReleasePath).length) {
|
|
481
|
+
const label = `\`${decision.type}${decision.scope ? `(${decision.scope})` : ""}\``;
|
|
482
|
+
const onlyBreaking = commit.type !== void 0 && findCommitRule(commit.type, commit.scope)?.release === false;
|
|
405
483
|
decision.reasons.push("No changed file ships in a published package; no release.");
|
|
406
|
-
decision.violations.push({
|
|
484
|
+
decision.violations.push(onlyBreaking ? {
|
|
485
|
+
rule: "release-paths",
|
|
486
|
+
message: `${label} releases ${formatRelease(release)} only because of its breaking marker, but the commit changes no release paths. Remove the breaking marker.`,
|
|
487
|
+
...commit.breaking === "header" ? { suggestion: commit.header.replace("!:", ":") } : {}
|
|
488
|
+
} : {
|
|
407
489
|
rule: "release-paths",
|
|
408
|
-
message:
|
|
490
|
+
message: `${label} releases ${formatRelease(release)}, but the commit changes no release paths. Use a non-release type: ${nonReleaseAlternatives.join(", ")}.`,
|
|
409
491
|
suggestion: suggestNonReleaseHeader({
|
|
410
492
|
...commit,
|
|
411
493
|
type: decision.type
|
|
@@ -423,16 +505,27 @@ function resolveRelease(inputs, policy = defaultReleasePolicy, options = {}) {
|
|
|
423
505
|
...options,
|
|
424
506
|
...inputs[index].files ? { files: inputs[index].files } : {}
|
|
425
507
|
}));
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
508
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
509
|
+
const cancel = (target, other) => {
|
|
510
|
+
cancelled.add(target);
|
|
511
|
+
decisions[target].release = false;
|
|
512
|
+
decisions[target].reasons.push(`Cancelled by \`${parsed[other].header}\` in the same release.`);
|
|
513
|
+
};
|
|
514
|
+
for (let index = parsed.length - 1; index >= 0; index--) {
|
|
515
|
+
const hashes = parsed[index].revert?.hashes ?? [];
|
|
516
|
+
if (!hashes.length || cancelled.has(index)) continue;
|
|
517
|
+
const targets = hashes.map((revertedHash) => {
|
|
518
|
+
const matches = parsed.flatMap((candidate, candidateIndex) => candidateIndex < index && candidate.hash?.toLowerCase().startsWith(revertedHash) ? [candidateIndex] : []);
|
|
519
|
+
return matches.length === 1 && !cancelled.has(matches[0]) ? matches[0] : void 0;
|
|
520
|
+
});
|
|
521
|
+
for (const target of targets) if (target !== void 0) cancel(target, index);
|
|
522
|
+
if (targets.every((target) => target !== void 0)) {
|
|
523
|
+
const reverted = targets.map((target) => parsed[target].header).join("`, `");
|
|
524
|
+
cancelled.add(index);
|
|
525
|
+
decisions[index].release = false;
|
|
526
|
+
decisions[index].reasons.push(`Cancelled by \`${reverted}\` in the same release.`);
|
|
434
527
|
}
|
|
435
|
-
}
|
|
528
|
+
}
|
|
436
529
|
return {
|
|
437
530
|
release: maxRelease(...decisions.map(({ release }) => release)),
|
|
438
531
|
commits: decisions
|
|
@@ -570,7 +663,7 @@ function getReleaseTable(policy = defaultReleasePolicy) {
|
|
|
570
663
|
none: []
|
|
571
664
|
};
|
|
572
665
|
for (const { type, scope } of commits) rows[getReleaseType(type, scope, policy) || "none"].push(formatRule(type, scope));
|
|
573
|
-
if (policy.mode === "auto") rows.major.push("`BREAKING CHANGE:` footer");
|
|
666
|
+
if (policy.mode === "auto") rows.major.push("`BREAKING CHANGE:` footer", "`Type!:` header");
|
|
574
667
|
return rows;
|
|
575
668
|
}
|
|
576
669
|
const agentsBlockStart = "<!-- aronrepo:commit-policy:start -->";
|
|
@@ -578,7 +671,8 @@ const agentsBlockEnd = "<!-- aronrepo:commit-policy:end -->";
|
|
|
578
671
|
/** Renders the generated commit policy block for AGENTS.md, including its markers. */
|
|
579
672
|
function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
|
|
580
673
|
const agent = getAgentCommitPolicy(policy, options);
|
|
581
|
-
const paths = options.releasePaths;
|
|
674
|
+
const paths = options.releasePaths ?? (policy.paths || void 0);
|
|
675
|
+
const excludes = paths ? paths.exclude ?? defaultReleasePathExcludes : [];
|
|
582
676
|
const typeRows = commits.map(({ type, scope, description }) => `| ${formatRule(type, scope)} | ${getReleaseType(type, scope, policy) || "none"} | ${description} |`);
|
|
583
677
|
return [
|
|
584
678
|
agentsBlockStart,
|
|
@@ -586,7 +680,7 @@ function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
|
|
|
586
680
|
"",
|
|
587
681
|
"## Commit and Release Policy",
|
|
588
682
|
"",
|
|
589
|
-
`Release mode: **${policy.mode}**. ${policy.mode === "explicit" ? "Only the repository owner decides minor and major versions, with `Bump(Minor)` and `Bump(Major)` commits." : "`Add` releases minor versions, and `BREAKING CHANGE:` footers release major versions."}`,
|
|
683
|
+
`Release mode: **${policy.mode}**. ${policy.mode === "explicit" ? "Only the repository owner decides minor and major versions, with `Bump(Minor)` and `Bump(Major)` commits." : "`Add` releases minor versions, and `BREAKING CHANGE:` footers and `Type!:` headers release major versions."}`,
|
|
590
684
|
"",
|
|
591
685
|
"Commit headers and PR titles use `Type(Scope): Summary` or `Type: Summary`: a PascalCase type, an optional scope for the package or area, and a sentence-case summary without a trailing period.",
|
|
592
686
|
"",
|
|
@@ -613,13 +707,11 @@ function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
|
|
|
613
707
|
"",
|
|
614
708
|
"### Release paths",
|
|
615
709
|
"",
|
|
616
|
-
...paths
|
|
710
|
+
...!paths ? ["Every path can release a version in this repository."] : paths.include?.length === 0 ? ["No path can release a version in this repository."] : [...paths.include ? [
|
|
617
711
|
"Only changes to these files can release a version:",
|
|
618
712
|
"",
|
|
619
|
-
...paths.include.map((glob) => `- \`${glob}\``)
|
|
620
|
-
|
|
621
|
-
`Excluding: ${(paths.exclude ?? defaultReleasePathExcludes).map((glob) => `\`${glob}\``).join(", ")}.`
|
|
622
|
-
] : ["Every path can release a version in this repository."],
|
|
713
|
+
...paths.include.map((glob) => `- \`${glob}\``)
|
|
714
|
+
] : ["Every path can release a version, except the excluded ones."], ...excludes.length ? ["", `Excluding: ${excludes.map((glob) => `\`${glob}\``).join(", ")}.`] : []],
|
|
623
715
|
"",
|
|
624
716
|
"### Before handing off commits",
|
|
625
717
|
"",
|
|
@@ -711,34 +803,100 @@ function getNextVersion(channel, release, tags) {
|
|
|
711
803
|
const target = stable ? semver.inc(stable, release) : initialVersion;
|
|
712
804
|
if (!channel.prerelease) return target;
|
|
713
805
|
const previous = highest(channelVersions(reachable, channel.prerelease).filter((version) => !stable || semver.gt(version, stable)));
|
|
714
|
-
|
|
715
|
-
const
|
|
806
|
+
let core = previous && semver.gte(coreVersion(previous), target) ? coreVersion(previous) : target;
|
|
807
|
+
const all = versionsOf(tags.all);
|
|
808
|
+
const released = highest(stableVersions(all));
|
|
809
|
+
if (released && semver.lte(core, released)) core = semver.inc(released, release);
|
|
810
|
+
const numbers = channelVersions(all, channel.prerelease).filter((version) => coreVersion(version) === core).map((version) => semver.prerelease(version)?.[1] ?? 0).filter((number) => Number.isInteger(number));
|
|
716
811
|
return `${core}-${channel.prerelease}.${Math.max(0, ...numbers) + 1}`;
|
|
717
812
|
}
|
|
718
813
|
// src/notes.ts
|
|
719
814
|
const breakingTitle = "⚠ Breaking Changes";
|
|
720
815
|
function groupOf(decision) {
|
|
721
|
-
if (decision.type === "Revert") return "Reverts";
|
|
722
816
|
return findCommitRule(decision.type, decision.scope)?.group;
|
|
723
817
|
}
|
|
818
|
+
/** Splits text into plain parts and CommonMark code spans: a backtick run closed by a run of the same length. */
|
|
819
|
+
function splitCodeSpans(text) {
|
|
820
|
+
const parts = [];
|
|
821
|
+
let plain = "";
|
|
822
|
+
let index = 0;
|
|
823
|
+
while (index < text.length) {
|
|
824
|
+
if (text[index] === "\\" && text[index + 1] === "`") {
|
|
825
|
+
plain += text.slice(index, index + 2);
|
|
826
|
+
index += 2;
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (text[index] !== "`") {
|
|
830
|
+
plain += text[index++];
|
|
831
|
+
continue;
|
|
832
|
+
}
|
|
833
|
+
const run = /^`+/.exec(text.slice(index))[0];
|
|
834
|
+
const close = new RegExp(`(?<!\`)${run}(?!\`)`).exec(text.slice(index + run.length));
|
|
835
|
+
if (!close) {
|
|
836
|
+
plain += run;
|
|
837
|
+
index += run.length;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (plain) parts.push({
|
|
841
|
+
code: false,
|
|
842
|
+
text: plain
|
|
843
|
+
});
|
|
844
|
+
plain = "";
|
|
845
|
+
const end = index + run.length + close.index + run.length;
|
|
846
|
+
parts.push({
|
|
847
|
+
code: true,
|
|
848
|
+
text: text.slice(index, end)
|
|
849
|
+
});
|
|
850
|
+
index = end;
|
|
851
|
+
}
|
|
852
|
+
if (plain) parts.push({
|
|
853
|
+
code: false,
|
|
854
|
+
text: plain
|
|
855
|
+
});
|
|
856
|
+
return parts;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Keeps commit text from acting on GitHub outside code spans: `<` would start
|
|
860
|
+
* HTML, as in `Array<string>`, and `@name` or `@scope/pkg` would notify a user
|
|
861
|
+
* or team on every release.
|
|
862
|
+
*/
|
|
863
|
+
function escapeMarkdown(text) {
|
|
864
|
+
return splitCodeSpans(text).map(({ code, text: part }) => code ? part : part.replace(/</g, "<").replace(/(^|[^\w`])(@[A-Za-z0-9][\w-]*(?:\/[\w.-]+)?)/g, "$1`$2`")).join("");
|
|
865
|
+
}
|
|
866
|
+
/** A list item; continuation lines are indented so they stay in the item. */
|
|
867
|
+
function listItem(text) {
|
|
868
|
+
return `- ${text.split("\n").map((line, index) => index && line ? ` ${line}` : line).join("\n")}`;
|
|
869
|
+
}
|
|
724
870
|
function formatEntry(decision, repositoryUrl) {
|
|
725
|
-
const summary = decision.subject ?? decision.header;
|
|
726
|
-
const scope = decision.scope ? `**${decision.scope}:** ` : "";
|
|
871
|
+
const summary = escapeMarkdown(decision.subject ?? decision.header);
|
|
872
|
+
const scope = decision.scope ? `**${escapeMarkdown(decision.scope)}:** ` : "";
|
|
727
873
|
const hash = decision.hash ? decision.hash.slice(0, 7) : void 0;
|
|
728
874
|
return `- ${scope}${summary}${hash ? repositoryUrl ? ` ([${hash}](${repositoryUrl}/commit/${decision.hash}))` : ` (${hash})` : ""}`;
|
|
729
875
|
}
|
|
876
|
+
/** The page that lists every change of a release: the compare view, or the commits of a first release. */
|
|
877
|
+
function getChangelogUrl(repositoryUrl, version, previousTag) {
|
|
878
|
+
const base = repositoryUrl.replace(/\/+$/, "");
|
|
879
|
+
const tag = `v${version.replace(/^v/, "")}`;
|
|
880
|
+
return previousTag ? `${base}/compare/${previousTag}...${tag}` : `${base}/commits/${tag}`;
|
|
881
|
+
}
|
|
730
882
|
/**
|
|
731
883
|
* Markdown release notes. Only commits that release appear, grouped by type in
|
|
732
884
|
* the order of the commit table; `BREAKING CHANGE:` footers of major releases
|
|
733
885
|
* come first.
|
|
734
886
|
*/
|
|
735
887
|
function renderReleaseNotes(options) {
|
|
736
|
-
const {
|
|
737
|
-
const
|
|
738
|
-
const
|
|
888
|
+
const { previousTag } = options;
|
|
889
|
+
const version = options.version.replace(/^v/, "");
|
|
890
|
+
const repositoryUrl = options.repositoryUrl?.replace(/\/+$/, "");
|
|
891
|
+
const changelogUrl = repositoryUrl ? getChangelogUrl(repositoryUrl, version, previousTag) : void 0;
|
|
892
|
+
const sections = [];
|
|
739
893
|
const released = options.commits.filter(({ release }) => release);
|
|
740
|
-
const breaking = released.filter(({ release }) => release === "major").flatMap(({ breakingNotes }) => breakingNotes
|
|
741
|
-
if (breaking.length)
|
|
894
|
+
const breaking = released.filter(({ release }) => release === "major").flatMap(({ breakingNotes, breaking: marker, subject }) => breakingNotes?.length ? breakingNotes : marker === "header" && subject ? [subject] : []);
|
|
895
|
+
if (breaking.length) sections.push([
|
|
896
|
+
`### ${breakingTitle}`,
|
|
897
|
+
"",
|
|
898
|
+
...breaking.map((note) => listItem(escapeMarkdown(note)))
|
|
899
|
+
]);
|
|
742
900
|
const groups = /* @__PURE__ */ new Map();
|
|
743
901
|
for (const decision of released) {
|
|
744
902
|
const group = groupOf(decision);
|
|
@@ -749,11 +907,19 @@ function renderReleaseNotes(options) {
|
|
|
749
907
|
const entries = groups.get(group);
|
|
750
908
|
if (!entries) continue;
|
|
751
909
|
const sorted = [...entries].sort((a, b) => (a.scope ?? "").localeCompare(b.scope ?? "", "en") || (a.subject ?? a.header).localeCompare(b.subject ?? b.header, "en"));
|
|
752
|
-
|
|
910
|
+
sections.push([
|
|
911
|
+
`### ${group}`,
|
|
912
|
+
"",
|
|
913
|
+
...sorted.map((decision) => formatEntry(decision, repositoryUrl))
|
|
914
|
+
]);
|
|
753
915
|
}
|
|
754
|
-
if (!
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
916
|
+
if (!sections.length) sections.push([released.length ? "Version bump only; no other changes to published packages." : "No changes to published packages."]);
|
|
917
|
+
if (options.heading) {
|
|
918
|
+
const title = changelogUrl ? `[${version}](${changelogUrl})` : version;
|
|
919
|
+
sections.unshift([`## ${title}${options.date ? ` (${options.date})` : ""}`]);
|
|
920
|
+
} else if (changelogUrl) sections.push([`**Full Changelog**: ${changelogUrl}`]);
|
|
921
|
+
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n`;
|
|
922
|
+
}
|
|
923
|
+
export { agentCommitPolicy, agentsBlockEnd, agentsBlockStart, commits, compareRelease, createReleasePathMatcher, defaultReleaseMode, defaultReleasePathExcludes, defaultReleasePolicy, defineConfig, findCommitRule, getAgentCommitPolicy, getBaseTag, getChangelogUrl, getLatestStableVersion, getNextVersion, getReleaseChannel, getReleasePolicy, getReleaseTable, getReleaseType, headerPattern, initialVersion, legacyCommits, legacyTypes, maxRelease, nonReleaseAlternatives, nonReleaseCommits, nonReleaseTypes, normalizeType, parseCommit, parseVersionTag, prereleaseChannels, releaseBranches, releaseCommits, releaseGroups, releaseLevels, releaseModes, releaseTypes, renderAgentsBlock, renderReleaseNotes, resolveCommitRelease, resolveRelease, toVersionTag, types };
|
|
758
924
|
|
|
759
925
|
//# sourceMappingURL=index.js.map
|