aronrepo 3.0.0 → 3.0.2
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-ClWK0lQ9.js +1688 -0
- package/dist/commands-ClWK0lQ9.js.map +1 -0
- package/dist/{index-Co5yiJXA.d.ts → index-DLkjddC1.d.ts} +29 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +248 -88
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +68 -11
- package/dist/node.js +1 -1
- package/package.json +8 -5
- package/dist/commands-CO26JPbK.js +0 -1170
- package/dist/commands-CO26JPbK.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,34 @@ 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);
|
|
201
208
|
return {
|
|
202
209
|
mode,
|
|
203
|
-
...
|
|
210
|
+
...paths !== void 0 ? { paths } : {}
|
|
204
211
|
};
|
|
205
212
|
}
|
|
213
|
+
function validateReleasePaths(paths) {
|
|
214
|
+
const isGlobs = (value) => value === void 0 || Array.isArray(value) && value.every((glob) => typeof glob === "string");
|
|
215
|
+
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.");
|
|
216
|
+
const { include, exclude } = paths;
|
|
217
|
+
if (!isGlobs(include)) throw new Error("`release.paths.include` must be an array of globs.");
|
|
218
|
+
if (!isGlobs(exclude)) throw new Error("`release.paths.exclude` must be an array of globs.");
|
|
219
|
+
}
|
|
206
220
|
const defaultReleasePolicy = { mode: defaultReleaseMode };
|
|
207
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Returns a matcher for release paths, or `undefined` when every path may release.
|
|
223
|
+
* `include` defaults to every path; `!glob` entries in `include` exclude, as in
|
|
224
|
+
* `package.json` `files`, instead of matching everything else.
|
|
225
|
+
*/
|
|
208
226
|
function createReleasePathMatcher(paths) {
|
|
209
|
-
if (!paths
|
|
210
|
-
const
|
|
211
|
-
const
|
|
227
|
+
if (!paths) return void 0;
|
|
228
|
+
const globs = paths.include ?? ["**"];
|
|
229
|
+
const positive = globs.filter((glob) => !glob.startsWith("!"));
|
|
230
|
+
const negated = globs.filter((glob) => glob.startsWith("!")).map((glob) => glob.slice(1));
|
|
231
|
+
if (!positive.length) return () => false;
|
|
232
|
+
const include = picomatch(positive, { dot: true });
|
|
233
|
+
const exclude = picomatch([...paths.exclude ?? defaultReleasePathExcludes, ...negated], { dot: true });
|
|
212
234
|
return (file) => include(file) && !exclude(file);
|
|
213
235
|
}
|
|
214
236
|
/**
|
|
@@ -224,61 +246,92 @@ function capRelease(release, type, policy) {
|
|
|
224
246
|
if (policy.mode === "explicit" && type !== "Bump" && (release === "major" || release === "minor")) return "patch";
|
|
225
247
|
return release;
|
|
226
248
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const
|
|
232
|
-
const
|
|
249
|
+
/**
|
|
250
|
+
* The first character of a scope or summary: an uppercase letter, a letter
|
|
251
|
+
* without case (such as CJK), a digit, or a code, issue, or emphasis marker.
|
|
252
|
+
*/
|
|
253
|
+
const startCharacter = String.raw`[\p{Lu}\p{Lt}\p{Lo}\p{Nd}\x60_#~]`;
|
|
254
|
+
const startPattern = new RegExp(`^${startCharacter}`, "u");
|
|
255
|
+
const headerPattern = new RegExp(String.raw`^([A-Z]\w*)(?:\((${startCharacter}[^)]*)\))?: (${startCharacter}.*)$`, "u");
|
|
256
|
+
const breakingHeaderPattern = new RegExp(String.raw`^([A-Z]\w*)(?:\((${startCharacter}[^)]*)\))?!: (${startCharacter}.*)$`, "u");
|
|
257
|
+
const footerStartPattern = /^BREAKING[ -]CHANGE:[ \t]*(.*)$/;
|
|
258
|
+
const githubRevertPattern = /^(Revert|Reapply) "([\s\S]+)"(?: \(#\d+\))?$/;
|
|
259
|
+
const revertHashPattern = /This reverts commit ([0-9a-f]{7,40})/gi;
|
|
260
|
+
const fencePattern = /^\s*(```|~~~)/;
|
|
233
261
|
const trailerPattern = /^[\w-]+: /;
|
|
234
|
-
/**
|
|
235
|
-
|
|
262
|
+
/**
|
|
263
|
+
* `BREAKING CHANGE:` footers outside code fences. Each runs, line breaks kept,
|
|
264
|
+
* until another footer or a git trailer, as Conventional Commits footers do.
|
|
265
|
+
*/
|
|
266
|
+
function readBreakingFooters(lines) {
|
|
236
267
|
const notes = [];
|
|
268
|
+
const starts = [];
|
|
237
269
|
let current;
|
|
238
|
-
|
|
239
|
-
|
|
270
|
+
let fenced = false;
|
|
271
|
+
const finish = () => {
|
|
272
|
+
if (current) notes.push(current.join("\n").trim());
|
|
273
|
+
current = void 0;
|
|
274
|
+
};
|
|
275
|
+
lines.forEach((line, index) => {
|
|
276
|
+
if (fencePattern.test(line)) fenced = !fenced;
|
|
277
|
+
const start = fenced ? null : footerStartPattern.exec(line);
|
|
240
278
|
if (start) {
|
|
241
|
-
|
|
279
|
+
finish();
|
|
280
|
+
starts.push(index);
|
|
242
281
|
current = [start[1]];
|
|
243
|
-
} else if (current &&
|
|
244
|
-
else if (current)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
} else if (current && !fenced && trailerPattern.test(line)) finish();
|
|
283
|
+
else if (current) current.push(line.trimEnd());
|
|
284
|
+
});
|
|
285
|
+
finish();
|
|
286
|
+
return {
|
|
287
|
+
starts,
|
|
288
|
+
notes: notes.filter(Boolean)
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
function parseHeader(header) {
|
|
292
|
+
const regular = headerPattern.exec(header);
|
|
293
|
+
const match = regular ?? breakingHeaderPattern.exec(header);
|
|
294
|
+
return match ? {
|
|
295
|
+
type: match[1],
|
|
296
|
+
scope: match[2],
|
|
297
|
+
subject: match[3],
|
|
298
|
+
breaking: !regular
|
|
299
|
+
} : void 0;
|
|
251
300
|
}
|
|
252
301
|
function parseCommit(message, hash) {
|
|
253
|
-
const [
|
|
302
|
+
const [rawHeader = "", ...rest] = message.replace(/\r\n?/g, "\n").trim().split("\n");
|
|
303
|
+
const header = rawHeader.trimEnd();
|
|
254
304
|
const body = rest.join("\n").trim();
|
|
255
305
|
const parsed = {
|
|
256
306
|
header,
|
|
257
307
|
body,
|
|
258
308
|
...hash ? { hash } : {}
|
|
259
309
|
};
|
|
310
|
+
const footers = readBreakingFooters(rest);
|
|
260
311
|
const layout = [];
|
|
261
312
|
if (rest.length && rest[0].trim()) layout.push("body-leading-blank");
|
|
262
|
-
if (
|
|
313
|
+
if (footers.starts.some((index) => index > 0 && rest[index - 1].trim())) layout.push("footer-leading-blank");
|
|
263
314
|
if (layout.length) parsed.layout = layout;
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const regular = headerPattern.exec(header);
|
|
267
|
-
const match = regular ?? breakingHeaderPattern.exec(header);
|
|
315
|
+
if (footers.notes.length) parsed.breakingNotes = footers.notes;
|
|
316
|
+
const match = parseHeader(header);
|
|
268
317
|
if (match) {
|
|
269
|
-
parsed.type = match
|
|
270
|
-
if (match
|
|
271
|
-
parsed.subject = match
|
|
272
|
-
if (
|
|
318
|
+
parsed.type = match.type;
|
|
319
|
+
if (match.scope) parsed.scope = match.scope;
|
|
320
|
+
parsed.subject = match.subject;
|
|
321
|
+
if (match.breaking) parsed.breaking = "header";
|
|
273
322
|
}
|
|
274
|
-
if (!parsed.breaking &&
|
|
323
|
+
if (!parsed.breaking && footers.starts.length) parsed.breaking = "footer";
|
|
275
324
|
const githubRevert = githubRevertPattern.exec(header);
|
|
276
325
|
if (githubRevert || parsed.type === "Revert") {
|
|
277
|
-
const revertedHeader = githubRevert?.[
|
|
278
|
-
const
|
|
326
|
+
const revertedHeader = githubRevert?.[2] ?? (parsed.subject && parseHeader(parsed.subject) ? parsed.subject : void 0);
|
|
327
|
+
const hashes = [...new Set([...body.matchAll(revertHashPattern)].map((found) => found[1].toLowerCase()))];
|
|
279
328
|
parsed.revert = {
|
|
280
329
|
...revertedHeader ? { header: revertedHeader } : {},
|
|
281
|
-
...
|
|
330
|
+
...hashes.length ? {
|
|
331
|
+
hash: hashes[0],
|
|
332
|
+
hashes
|
|
333
|
+
} : {},
|
|
334
|
+
...githubRevert?.[1] === "Reapply" ? { reapply: true } : {}
|
|
282
335
|
};
|
|
283
336
|
}
|
|
284
337
|
return parsed;
|
|
@@ -295,7 +348,7 @@ const nonReleaseAlternatives = [
|
|
|
295
348
|
"Example"
|
|
296
349
|
];
|
|
297
350
|
function suggestNonReleaseHeader(commit) {
|
|
298
|
-
const subject = commit.subject ?? "";
|
|
351
|
+
const subject = (commit.subject ?? commit.header).replace(/\.+$/, "");
|
|
299
352
|
const scope = commit.scope ? `(${commit.scope})` : "";
|
|
300
353
|
switch (commit.type) {
|
|
301
354
|
case "Docs": return `Docs: ${subject}`;
|
|
@@ -303,17 +356,18 @@ function suggestNonReleaseHeader(commit) {
|
|
|
303
356
|
default: return `Chore${scope}: ${subject}`;
|
|
304
357
|
}
|
|
305
358
|
}
|
|
306
|
-
const looseHeaderPattern = /^(\w+)(?:\(([^)]*)\))?(!)?:
|
|
307
|
-
function diagnoseHeader(header) {
|
|
359
|
+
const looseHeaderPattern = /^(\w+)(?:\(([^)]*)\))?(!)?:(\s*)(.*)$/;
|
|
360
|
+
function diagnoseHeader(header, mode) {
|
|
308
361
|
const format = "Use `Type(Scope): Summary` or `Type: Summary` with an Aronrepo commit type.";
|
|
309
362
|
const match = looseHeaderPattern.exec(header);
|
|
310
363
|
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 &&
|
|
364
|
+
const [, type, scope, bang, space, subject] = match;
|
|
365
|
+
if (!/^[A-Z]/.test(type)) return `The type \`${type}\` must be PascalCase, such as \`${normalizeType(`${type[0].toUpperCase()}${type.slice(1)}`)}\`. ${format}`;
|
|
366
|
+
if (bang && mode === "explicit") return `Aronrepo headers do not use \`!\` in explicit mode. ${format}`;
|
|
367
|
+
if (scope !== void 0 && !startPattern.test(scope)) return `The scope \`${scope}\` must start with an uppercase letter or digit. ${format}`;
|
|
368
|
+
if (space !== " ") return `Put exactly one space after the colon. ${format}`;
|
|
315
369
|
if (!subject) return `The summary is empty. ${format}`;
|
|
316
|
-
if (
|
|
370
|
+
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
371
|
return format;
|
|
318
372
|
}
|
|
319
373
|
function formatRelease(release) {
|
|
@@ -328,6 +382,7 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
328
382
|
...commit.scope ? { scope: commit.scope } : {},
|
|
329
383
|
...commit.subject ? { subject: commit.subject } : {},
|
|
330
384
|
...commit.breakingNotes ? { breakingNotes: commit.breakingNotes } : {},
|
|
385
|
+
...commit.breaking ? { breaking: commit.breaking } : {},
|
|
331
386
|
release: false,
|
|
332
387
|
reasons: [],
|
|
333
388
|
violations: []
|
|
@@ -337,7 +392,7 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
337
392
|
decision.reasons.push("Header does not follow `Type(Scope): Summary`; no release.");
|
|
338
393
|
decision.violations.push({
|
|
339
394
|
rule: "header-format",
|
|
340
|
-
message: diagnoseHeader(commit.header)
|
|
395
|
+
message: diagnoseHeader(commit.header, policy.mode)
|
|
341
396
|
});
|
|
342
397
|
return decision;
|
|
343
398
|
} else if (!types.includes(commit.type) && !legacyTypes.includes(commit.type)) {
|
|
@@ -348,13 +403,21 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
348
403
|
});
|
|
349
404
|
return decision;
|
|
350
405
|
} else if (!findCommitRule(commit.type, commit.scope)) {
|
|
406
|
+
const scoped = commits.filter(({ type }) => type === commit.type).map(({ type, scope }) => `\`${type}(${scope})\``).join(", ");
|
|
351
407
|
decision.reasons.push(`\`${commit.type}${commit.scope ? `(${commit.scope})` : ""}\` is not an Aronrepo commit type; no release.`);
|
|
352
408
|
decision.violations.push({
|
|
353
409
|
rule: "unknown-type",
|
|
354
|
-
message: `\`${commit.type}
|
|
410
|
+
message: commit.scope ? `\`${commit.type}(${commit.scope})\` has an unknown scope. Use one of: ${scoped}.` : `\`${commit.type}\` needs a scope: ${scoped}.`
|
|
355
411
|
});
|
|
356
412
|
return decision;
|
|
357
413
|
}
|
|
414
|
+
const knownScope = commit.scope && commits.find(({ type, scope }) => type === normalizeType(commit.type) && scope && scope !== commit.scope && scope.toLowerCase() === commit.scope.toLowerCase())?.scope;
|
|
415
|
+
if (knownScope) decision.violations.push({
|
|
416
|
+
rule: "scope-case",
|
|
417
|
+
severity: "warning",
|
|
418
|
+
message: `\`${commit.type}(${commit.scope})\` is treated as \`${commit.type}\`. Write \`${commit.type}(${knownScope})\` for that rule.`,
|
|
419
|
+
suggestion: `${commit.type}(${knownScope}): ${commit.subject ?? ""}`
|
|
420
|
+
});
|
|
358
421
|
if (commit.subject?.endsWith(".")) decision.violations.push({
|
|
359
422
|
rule: "subject-full-stop",
|
|
360
423
|
message: "The summary must not end with a period.",
|
|
@@ -377,8 +440,15 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
377
440
|
}
|
|
378
441
|
let release;
|
|
379
442
|
if (decision.type === "Revert" && commit.revert?.header) {
|
|
380
|
-
|
|
381
|
-
|
|
443
|
+
const reverted = resolveCommitRelease(commit.revert.header, policy, options);
|
|
444
|
+
const verb = commit.revert.reapply ? "Reapplies" : "Reverts";
|
|
445
|
+
if (reverted.type === "Bump" && reverted.release) {
|
|
446
|
+
release = "patch";
|
|
447
|
+
decision.reasons.push(`${verb} \`${commit.revert.header}\`; reverting a \`Bump\` releases patch.`);
|
|
448
|
+
} else {
|
|
449
|
+
release = reverted.release;
|
|
450
|
+
decision.reasons.push(`${verb} \`${commit.revert.header}\`, which releases ${formatRelease(release)}.`);
|
|
451
|
+
}
|
|
382
452
|
} else {
|
|
383
453
|
const rule = findCommitRule(decision.type, decision.scope);
|
|
384
454
|
release = capRelease(rule.release, decision.type, policy);
|
|
@@ -402,10 +472,16 @@ function resolveCommitRelease(input, policy = defaultReleasePolicy, options = {}
|
|
|
402
472
|
const isBump = decision.type === "Bump";
|
|
403
473
|
if (release && !isBump && options.files && options.isReleasePath) {
|
|
404
474
|
if (!options.files.filter(options.isReleasePath).length) {
|
|
475
|
+
const label = `\`${decision.type}${decision.scope ? `(${decision.scope})` : ""}\``;
|
|
476
|
+
const onlyBreaking = commit.type !== void 0 && findCommitRule(commit.type, commit.scope)?.release === false;
|
|
405
477
|
decision.reasons.push("No changed file ships in a published package; no release.");
|
|
406
|
-
decision.violations.push({
|
|
478
|
+
decision.violations.push(onlyBreaking ? {
|
|
479
|
+
rule: "release-paths",
|
|
480
|
+
message: `${label} releases ${formatRelease(release)} only because of its breaking marker, but the commit changes no release paths. Remove the breaking marker.`,
|
|
481
|
+
...commit.breaking === "header" ? { suggestion: commit.header.replace("!:", ":") } : {}
|
|
482
|
+
} : {
|
|
407
483
|
rule: "release-paths",
|
|
408
|
-
message:
|
|
484
|
+
message: `${label} releases ${formatRelease(release)}, but the commit changes no release paths. Use a non-release type: ${nonReleaseAlternatives.join(", ")}.`,
|
|
409
485
|
suggestion: suggestNonReleaseHeader({
|
|
410
486
|
...commit,
|
|
411
487
|
type: decision.type
|
|
@@ -423,16 +499,27 @@ function resolveRelease(inputs, policy = defaultReleasePolicy, options = {}) {
|
|
|
423
499
|
...options,
|
|
424
500
|
...inputs[index].files ? { files: inputs[index].files } : {}
|
|
425
501
|
}));
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
502
|
+
const cancelled = /* @__PURE__ */ new Set();
|
|
503
|
+
const cancel = (target, other) => {
|
|
504
|
+
cancelled.add(target);
|
|
505
|
+
decisions[target].release = false;
|
|
506
|
+
decisions[target].reasons.push(`Cancelled by \`${parsed[other].header}\` in the same release.`);
|
|
507
|
+
};
|
|
508
|
+
for (let index = parsed.length - 1; index >= 0; index--) {
|
|
509
|
+
const hashes = parsed[index].revert?.hashes ?? [];
|
|
510
|
+
if (!hashes.length || cancelled.has(index)) continue;
|
|
511
|
+
const targets = hashes.map((revertedHash) => {
|
|
512
|
+
const matches = parsed.flatMap((candidate, candidateIndex) => candidateIndex < index && candidate.hash?.toLowerCase().startsWith(revertedHash) ? [candidateIndex] : []);
|
|
513
|
+
return matches.length === 1 && !cancelled.has(matches[0]) ? matches[0] : void 0;
|
|
514
|
+
});
|
|
515
|
+
for (const target of targets) if (target !== void 0) cancel(target, index);
|
|
516
|
+
if (targets.every((target) => target !== void 0)) {
|
|
517
|
+
const reverted = targets.map((target) => parsed[target].header).join("`, `");
|
|
518
|
+
cancelled.add(index);
|
|
519
|
+
decisions[index].release = false;
|
|
520
|
+
decisions[index].reasons.push(`Cancelled by \`${reverted}\` in the same release.`);
|
|
434
521
|
}
|
|
435
|
-
}
|
|
522
|
+
}
|
|
436
523
|
return {
|
|
437
524
|
release: maxRelease(...decisions.map(({ release }) => release)),
|
|
438
525
|
commits: decisions
|
|
@@ -570,7 +657,7 @@ function getReleaseTable(policy = defaultReleasePolicy) {
|
|
|
570
657
|
none: []
|
|
571
658
|
};
|
|
572
659
|
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");
|
|
660
|
+
if (policy.mode === "auto") rows.major.push("`BREAKING CHANGE:` footer", "`Type!:` header");
|
|
574
661
|
return rows;
|
|
575
662
|
}
|
|
576
663
|
const agentsBlockStart = "<!-- aronrepo:commit-policy:start -->";
|
|
@@ -578,7 +665,8 @@ const agentsBlockEnd = "<!-- aronrepo:commit-policy:end -->";
|
|
|
578
665
|
/** Renders the generated commit policy block for AGENTS.md, including its markers. */
|
|
579
666
|
function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
|
|
580
667
|
const agent = getAgentCommitPolicy(policy, options);
|
|
581
|
-
const paths = options.releasePaths;
|
|
668
|
+
const paths = options.releasePaths ?? (policy.paths || void 0);
|
|
669
|
+
const excludes = paths ? paths.exclude ?? defaultReleasePathExcludes : [];
|
|
582
670
|
const typeRows = commits.map(({ type, scope, description }) => `| ${formatRule(type, scope)} | ${getReleaseType(type, scope, policy) || "none"} | ${description} |`);
|
|
583
671
|
return [
|
|
584
672
|
agentsBlockStart,
|
|
@@ -586,7 +674,7 @@ function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
|
|
|
586
674
|
"",
|
|
587
675
|
"## Commit and Release Policy",
|
|
588
676
|
"",
|
|
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."}`,
|
|
677
|
+
`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
678
|
"",
|
|
591
679
|
"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
680
|
"",
|
|
@@ -613,13 +701,11 @@ function renderAgentsBlock(policy = defaultReleasePolicy, options = {}) {
|
|
|
613
701
|
"",
|
|
614
702
|
"### Release paths",
|
|
615
703
|
"",
|
|
616
|
-
...paths
|
|
704
|
+
...!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
705
|
"Only changes to these files can release a version:",
|
|
618
706
|
"",
|
|
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."],
|
|
707
|
+
...paths.include.map((glob) => `- \`${glob}\``)
|
|
708
|
+
] : ["Every path can release a version, except the excluded ones."], ...excludes.length ? ["", `Excluding: ${excludes.map((glob) => `\`${glob}\``).join(", ")}.`] : []],
|
|
623
709
|
"",
|
|
624
710
|
"### Before handing off commits",
|
|
625
711
|
"",
|
|
@@ -711,34 +797,100 @@ function getNextVersion(channel, release, tags) {
|
|
|
711
797
|
const target = stable ? semver.inc(stable, release) : initialVersion;
|
|
712
798
|
if (!channel.prerelease) return target;
|
|
713
799
|
const previous = highest(channelVersions(reachable, channel.prerelease).filter((version) => !stable || semver.gt(version, stable)));
|
|
714
|
-
|
|
715
|
-
const
|
|
800
|
+
let core = previous && semver.gte(coreVersion(previous), target) ? coreVersion(previous) : target;
|
|
801
|
+
const all = versionsOf(tags.all);
|
|
802
|
+
const released = highest(stableVersions(all));
|
|
803
|
+
if (released && semver.lte(core, released)) core = semver.inc(released, release);
|
|
804
|
+
const numbers = channelVersions(all, channel.prerelease).filter((version) => coreVersion(version) === core).map((version) => semver.prerelease(version)?.[1] ?? 0).filter((number) => Number.isInteger(number));
|
|
716
805
|
return `${core}-${channel.prerelease}.${Math.max(0, ...numbers) + 1}`;
|
|
717
806
|
}
|
|
718
807
|
// src/notes.ts
|
|
719
808
|
const breakingTitle = "⚠ Breaking Changes";
|
|
720
809
|
function groupOf(decision) {
|
|
721
|
-
if (decision.type === "Revert") return "Reverts";
|
|
722
810
|
return findCommitRule(decision.type, decision.scope)?.group;
|
|
723
811
|
}
|
|
812
|
+
/** Splits text into plain parts and CommonMark code spans: a backtick run closed by a run of the same length. */
|
|
813
|
+
function splitCodeSpans(text) {
|
|
814
|
+
const parts = [];
|
|
815
|
+
let plain = "";
|
|
816
|
+
let index = 0;
|
|
817
|
+
while (index < text.length) {
|
|
818
|
+
if (text[index] === "\\" && text[index + 1] === "`") {
|
|
819
|
+
plain += text.slice(index, index + 2);
|
|
820
|
+
index += 2;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
if (text[index] !== "`") {
|
|
824
|
+
plain += text[index++];
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
const run = /^`+/.exec(text.slice(index))[0];
|
|
828
|
+
const close = new RegExp(`(?<!\`)${run}(?!\`)`).exec(text.slice(index + run.length));
|
|
829
|
+
if (!close) {
|
|
830
|
+
plain += run;
|
|
831
|
+
index += run.length;
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (plain) parts.push({
|
|
835
|
+
code: false,
|
|
836
|
+
text: plain
|
|
837
|
+
});
|
|
838
|
+
plain = "";
|
|
839
|
+
const end = index + run.length + close.index + run.length;
|
|
840
|
+
parts.push({
|
|
841
|
+
code: true,
|
|
842
|
+
text: text.slice(index, end)
|
|
843
|
+
});
|
|
844
|
+
index = end;
|
|
845
|
+
}
|
|
846
|
+
if (plain) parts.push({
|
|
847
|
+
code: false,
|
|
848
|
+
text: plain
|
|
849
|
+
});
|
|
850
|
+
return parts;
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Keeps commit text from acting on GitHub outside code spans: `<` would start
|
|
854
|
+
* HTML, as in `Array<string>`, and `@name` or `@scope/pkg` would notify a user
|
|
855
|
+
* or team on every release.
|
|
856
|
+
*/
|
|
857
|
+
function escapeMarkdown(text) {
|
|
858
|
+
return splitCodeSpans(text).map(({ code, text: part }) => code ? part : part.replace(/</g, "<").replace(/(^|[^\w`])(@[A-Za-z0-9][\w-]*(?:\/[\w.-]+)?)/g, "$1`$2`")).join("");
|
|
859
|
+
}
|
|
860
|
+
/** A list item; continuation lines are indented so they stay in the item. */
|
|
861
|
+
function listItem(text) {
|
|
862
|
+
return `- ${text.split("\n").map((line, index) => index && line ? ` ${line}` : line).join("\n")}`;
|
|
863
|
+
}
|
|
724
864
|
function formatEntry(decision, repositoryUrl) {
|
|
725
|
-
const summary = decision.subject ?? decision.header;
|
|
726
|
-
const scope = decision.scope ? `**${decision.scope}:** ` : "";
|
|
865
|
+
const summary = escapeMarkdown(decision.subject ?? decision.header);
|
|
866
|
+
const scope = decision.scope ? `**${escapeMarkdown(decision.scope)}:** ` : "";
|
|
727
867
|
const hash = decision.hash ? decision.hash.slice(0, 7) : void 0;
|
|
728
868
|
return `- ${scope}${summary}${hash ? repositoryUrl ? ` ([${hash}](${repositoryUrl}/commit/${decision.hash}))` : ` (${hash})` : ""}`;
|
|
729
869
|
}
|
|
870
|
+
/** The page that lists every change of a release: the compare view, or the commits of a first release. */
|
|
871
|
+
function getChangelogUrl(repositoryUrl, version, previousTag) {
|
|
872
|
+
const base = repositoryUrl.replace(/\/+$/, "");
|
|
873
|
+
const tag = `v${version.replace(/^v/, "")}`;
|
|
874
|
+
return previousTag ? `${base}/compare/${previousTag}...${tag}` : `${base}/commits/${tag}`;
|
|
875
|
+
}
|
|
730
876
|
/**
|
|
731
877
|
* Markdown release notes. Only commits that release appear, grouped by type in
|
|
732
878
|
* the order of the commit table; `BREAKING CHANGE:` footers of major releases
|
|
733
879
|
* come first.
|
|
734
880
|
*/
|
|
735
881
|
function renderReleaseNotes(options) {
|
|
736
|
-
const {
|
|
737
|
-
const
|
|
738
|
-
const
|
|
882
|
+
const { previousTag } = options;
|
|
883
|
+
const version = options.version.replace(/^v/, "");
|
|
884
|
+
const repositoryUrl = options.repositoryUrl?.replace(/\/+$/, "");
|
|
885
|
+
const changelogUrl = repositoryUrl ? getChangelogUrl(repositoryUrl, version, previousTag) : void 0;
|
|
886
|
+
const sections = [];
|
|
739
887
|
const released = options.commits.filter(({ release }) => release);
|
|
740
|
-
const breaking = released.filter(({ release }) => release === "major").flatMap(({ breakingNotes }) => breakingNotes
|
|
741
|
-
if (breaking.length)
|
|
888
|
+
const breaking = released.filter(({ release }) => release === "major").flatMap(({ breakingNotes, breaking: marker, subject }) => breakingNotes?.length ? breakingNotes : marker === "header" && subject ? [subject] : []);
|
|
889
|
+
if (breaking.length) sections.push([
|
|
890
|
+
`### ${breakingTitle}`,
|
|
891
|
+
"",
|
|
892
|
+
...breaking.map((note) => listItem(escapeMarkdown(note)))
|
|
893
|
+
]);
|
|
742
894
|
const groups = /* @__PURE__ */ new Map();
|
|
743
895
|
for (const decision of released) {
|
|
744
896
|
const group = groupOf(decision);
|
|
@@ -749,11 +901,19 @@ function renderReleaseNotes(options) {
|
|
|
749
901
|
const entries = groups.get(group);
|
|
750
902
|
if (!entries) continue;
|
|
751
903
|
const sorted = [...entries].sort((a, b) => (a.scope ?? "").localeCompare(b.scope ?? "", "en") || (a.subject ?? a.header).localeCompare(b.subject ?? b.header, "en"));
|
|
752
|
-
|
|
904
|
+
sections.push([
|
|
905
|
+
`### ${group}`,
|
|
906
|
+
"",
|
|
907
|
+
...sorted.map((decision) => formatEntry(decision, repositoryUrl))
|
|
908
|
+
]);
|
|
753
909
|
}
|
|
754
|
-
if (!
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
|
|
910
|
+
if (!sections.length) sections.push([released.length ? "Version bump only; no other changes to published packages." : "No changes to published packages."]);
|
|
911
|
+
if (options.heading) {
|
|
912
|
+
const title = changelogUrl ? `[${version}](${changelogUrl})` : version;
|
|
913
|
+
sections.unshift([`## ${title}${options.date ? ` (${options.date})` : ""}`]);
|
|
914
|
+
} else if (changelogUrl) sections.push([`**Full Changelog**: ${changelogUrl}`]);
|
|
915
|
+
return `${sections.map((section) => section.join("\n")).join("\n\n")}\n`;
|
|
916
|
+
}
|
|
917
|
+
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
918
|
|
|
759
919
|
//# sourceMappingURL=index.js.map
|