@oorabona/release-it-preset 1.3.0 → 1.4.1
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
CHANGED
|
@@ -7,6 +7,7 @@ Shareable [release-it](https://github.com/release-it/release-it) configuration a
|
|
|
7
7
|
[](https://opensource.org/licenses/MIT)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
[](https://docs.npmjs.com/trusted-publishers)
|
|
10
|
+
[](docs/VERIFY.md)
|
|
10
11
|
[](https://github.com/oorabona/release-it-preset/actions/workflows/ci.yml)
|
|
11
12
|
[](https://github.com/oorabona/release-it-preset/actions/workflows/audit.yml)
|
|
12
13
|
[](https://codecov.io/github/oorabona/release-it-preset)
|
|
@@ -37,6 +38,13 @@ pnpm release:minor # bump + commit + tag + push (CI publishes)
|
|
|
37
38
|
|
|
38
39
|
→ Full reference: [docs/USAGE.md](docs/USAGE.md) · [Migration v0→v1](docs/MIGRATION.md) · [Public API](docs/PUBLIC_API.md)
|
|
39
40
|
|
|
41
|
+
## Supply chain verification
|
|
42
|
+
|
|
43
|
+
Releases use npm OIDC provenance and, starting with v1.4.0, attach the npm
|
|
44
|
+
registry tarball, SLSA L3 provenance, and a cosign keyless bundle to the GitHub
|
|
45
|
+
release. See [docs/VERIFY.md](docs/VERIFY.md) for copy-paste verification
|
|
46
|
+
commands.
|
|
47
|
+
|
|
40
48
|
## Why this preset?
|
|
41
49
|
|
|
42
50
|
Most release workflows fall into one of three traps: too much manual work (plain release-it, you assemble everything), too much ceremony (changesets, great for 5+ maintainers, heavy for one), or too much automation with too little control (semantic-release, hands-off by design and format).
|
|
@@ -357,20 +357,55 @@ function normalizeBlockText(value) {
|
|
|
357
357
|
.replace(/[ \t]+/g, ' ')
|
|
358
358
|
.trim();
|
|
359
359
|
}
|
|
360
|
+
// Replaces fenced code regions (``` / ~~~) with spaces of identical length so
|
|
361
|
+
// marker scanning never sees them while every index still maps back to the
|
|
362
|
+
// original text. Grammar examples in PR bodies stay inert this way.
|
|
363
|
+
function maskFencedRegions(value) {
|
|
364
|
+
let openFence = null;
|
|
365
|
+
return value
|
|
366
|
+
.split('\n')
|
|
367
|
+
.map(line => {
|
|
368
|
+
// Any indentation is accepted on purpose: fences nested under list
|
|
369
|
+
// items are indented beyond CommonMark's top-level 3-space cap, and a
|
|
370
|
+
// safety mask must over-mask rather than import a fenced example.
|
|
371
|
+
const delimiter = line.match(/^\s*(`{3,}|~{3,})/);
|
|
372
|
+
if (openFence === null) {
|
|
373
|
+
if (delimiter) {
|
|
374
|
+
openFence = { char: delimiter[1][0], length: delimiter[1].length };
|
|
375
|
+
return ' '.repeat(line.length);
|
|
376
|
+
}
|
|
377
|
+
return line;
|
|
378
|
+
}
|
|
379
|
+
// CommonMark: the closing fence must use the same character and be at
|
|
380
|
+
// least as long as the opener — a ```` fence is NOT closed by an inner
|
|
381
|
+
// ``` example, which is precisely how docs show fenced code.
|
|
382
|
+
if (delimiter &&
|
|
383
|
+
delimiter[1][0] === openFence.char &&
|
|
384
|
+
delimiter[1].length >= openFence.length &&
|
|
385
|
+
/^\s*(`{3,}|~{3,})\s*$/.test(line)) {
|
|
386
|
+
openFence = null;
|
|
387
|
+
}
|
|
388
|
+
return ' '.repeat(line.length);
|
|
389
|
+
})
|
|
390
|
+
.join('\n');
|
|
391
|
+
}
|
|
360
392
|
export function extractStructuredChangelogNotes(body, options = {}) {
|
|
361
393
|
if (!body) {
|
|
362
394
|
return [];
|
|
363
395
|
}
|
|
364
396
|
const notes = [];
|
|
365
397
|
const normalizedBody = body.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
398
|
+
// Markers are matched against the masked text (fenced regions blanked, all
|
|
399
|
+
// indices preserved); the block TEXT is sliced from the real body.
|
|
400
|
+
const scanBody = maskFencedRegions(normalizedBody);
|
|
366
401
|
const openMarker = /<!--\s*changelog\s*:\s*([a-z]+)\s*-->/gi;
|
|
367
402
|
const closeMarker = /<!--\s*\/changelog\s*-->/i;
|
|
368
403
|
const prNumber = options.prNumber ?? 0;
|
|
369
|
-
for (const match of
|
|
404
|
+
for (const match of scanBody.matchAll(openMarker)) {
|
|
370
405
|
const type = match[1].toLowerCase();
|
|
371
406
|
const contentStart = match.index + match[0].length;
|
|
372
|
-
const
|
|
373
|
-
const closeMatch = closeMarker.exec(
|
|
407
|
+
const scanRest = scanBody.slice(contentStart);
|
|
408
|
+
const closeMatch = closeMarker.exec(scanRest);
|
|
374
409
|
if (!closeMatch) {
|
|
375
410
|
warnForPr(prNumber, options, 'unclosed marker');
|
|
376
411
|
continue;
|
|
@@ -380,12 +415,13 @@ export function extractStructuredChangelogNotes(body, options = {}) {
|
|
|
380
415
|
warnForPr(prNumber, options, `unknown changelog type "${type}"`);
|
|
381
416
|
continue;
|
|
382
417
|
}
|
|
383
|
-
const rawContent =
|
|
418
|
+
const rawContent = normalizedBody.slice(contentStart, contentStart + closeMatch.index);
|
|
384
419
|
// A second open marker before the close means overlapping blocks: the
|
|
385
420
|
// outer region is malformed and must never leak a raw marker (or
|
|
386
421
|
// duplicated text) into the changelog. The inner block, if well-formed,
|
|
387
|
-
// is still picked up by its own matchAll iteration.
|
|
388
|
-
|
|
422
|
+
// is still picked up by its own matchAll iteration. (Checked on the
|
|
423
|
+
// masked region too, so fenced examples inside a block stay inert.)
|
|
424
|
+
if (/<!--\s*changelog\s*:/i.test(scanRest.slice(0, closeMatch.index))) {
|
|
389
425
|
warnForPr(prNumber, options, `nested changelog marker inside ${type} block`);
|
|
390
426
|
continue;
|
|
391
427
|
}
|
|
@@ -497,7 +533,7 @@ export function renderAnnotatedBody(parsed, resolvedGroups, repoUrl, deps) {
|
|
|
497
533
|
for (const heading of orderedPending) {
|
|
498
534
|
emitSection(heading, [], additionsBySection.get(heading) ?? []);
|
|
499
535
|
}
|
|
500
|
-
return `\n${lines.join('\n').trim()}\n\n
|
|
536
|
+
return { body: `\n${lines.join('\n').trim()}\n\n`, appliedPrCount: annotatedGroups };
|
|
501
537
|
}
|
|
502
538
|
function readChangelog(changelogPath, deps) {
|
|
503
539
|
try {
|
|
@@ -524,13 +560,13 @@ export function annotateChangelog(deps) {
|
|
|
524
560
|
}
|
|
525
561
|
const { repoUrl, ownerRepo } = getRequiredGitHubContext(deps);
|
|
526
562
|
const resolved = resolvePullRequestGroups(groups, ownerRepo, deps);
|
|
527
|
-
const
|
|
528
|
-
if (
|
|
563
|
+
const rendered = renderAnnotatedBody(parsed, resolved.resolved, repoUrl, deps);
|
|
564
|
+
if (rendered === null) {
|
|
529
565
|
deps.log('No changelog blocks found in the resolved pull requests — nothing to annotate');
|
|
530
566
|
return;
|
|
531
567
|
}
|
|
532
|
-
deps.writeFileSync(changelogPath, `${block.prefix}${
|
|
533
|
-
deps.log(`Annotated ${
|
|
568
|
+
deps.writeFileSync(changelogPath, `${block.prefix}${rendered.body}${block.suffix}`);
|
|
569
|
+
deps.log(`Annotated ${rendered.appliedPrCount} pull request(s)`);
|
|
534
570
|
}
|
|
535
571
|
/* c8 ignore start */
|
|
536
572
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { execSync } from 'node:child_process';
|
|
14
14
|
import { appendFileSync, readFileSync } from 'node:fs';
|
|
15
|
+
import { extractStructuredChangelogNotes } from './annotate-changelog.js';
|
|
15
16
|
import { STRICT_CONVENTIONAL_COMMIT_REGEX } from './lib/commit-parser.js';
|
|
16
17
|
import { runScript } from './lib/run-script.js';
|
|
17
18
|
const SKIP_CHANGELOG_REGEX = /\[skip-changelog]/i;
|
|
18
|
-
const CHANGELOG_BLOCK_REGEX = /<!--\s*changelog\s*:\s*(added|changed|deprecated|removed|fixed|security)\s*-->[\s\S]*?<!--\s*\/changelog\s*-->/i;
|
|
19
19
|
export function safeExec(command, deps) {
|
|
20
20
|
try {
|
|
21
21
|
return deps.execSync(command, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
@@ -96,7 +96,10 @@ export function evaluateChangelogBlockStatus(deps) {
|
|
|
96
96
|
if (typeof body !== 'string') {
|
|
97
97
|
return 'unknown';
|
|
98
98
|
}
|
|
99
|
-
|
|
99
|
+
// Same parser as annotate (typed markers, fence masking): a fenced
|
|
100
|
+
// documentation example must not count as present here while annotate
|
|
101
|
+
// would ignore it.
|
|
102
|
+
return extractStructuredChangelogNotes(body).length > 0 ? 'present' : 'absent';
|
|
100
103
|
}
|
|
101
104
|
catch {
|
|
102
105
|
return 'unknown';
|
|
@@ -81,6 +81,9 @@ export declare function extractStructuredChangelogNotes(body: string | null | un
|
|
|
81
81
|
prNumber?: number;
|
|
82
82
|
warn?: (message: string) => void;
|
|
83
83
|
}): ChangelogNote[];
|
|
84
|
-
export declare function renderAnnotatedBody(parsed: ParsedUnreleased, resolvedGroups: ResolvedGroup[], repoUrl: string, deps: AnnotateChangelogDeps):
|
|
84
|
+
export declare function renderAnnotatedBody(parsed: ParsedUnreleased, resolvedGroups: ResolvedGroup[], repoUrl: string, deps: AnnotateChangelogDeps): {
|
|
85
|
+
body: string;
|
|
86
|
+
appliedPrCount: number;
|
|
87
|
+
} | null;
|
|
85
88
|
export declare function annotateChangelog(deps: AnnotateChangelogDeps): void;
|
|
86
89
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oorabona/release-it-preset",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
4
|
"description": "Release tooling for solo and small-team JS maintainers: human-curated changelogs, OIDC zero-config publishing, recovery presets, monorepo support, pre-release diagnostics.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|