@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
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Node](https://img.shields.io/node/v/@oorabona/release-it-preset.svg)](https://nodejs.org/)
9
9
  [![OIDC trusted publishing](https://img.shields.io/badge/npm-OIDC%20trusted%20publishing-green.svg)](https://docs.npmjs.com/trusted-publishers)
10
+ [![SLSA Level 3](https://slsa.dev/images/gh-badge-level3.svg)](docs/VERIFY.md)
10
11
  [![CI](https://github.com/oorabona/release-it-preset/actions/workflows/ci.yml/badge.svg)](https://github.com/oorabona/release-it-preset/actions/workflows/ci.yml)
11
12
  [![Audit](https://github.com/oorabona/release-it-preset/actions/workflows/audit.yml/badge.svg)](https://github.com/oorabona/release-it-preset/actions/workflows/audit.yml)
12
13
  [![codecov](https://codecov.io/github/oorabona/release-it-preset/graph/badge.svg?token=6RMN34Z7TX)](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 normalizedBody.matchAll(openMarker)) {
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 rest = normalizedBody.slice(contentStart);
373
- const closeMatch = closeMarker.exec(rest);
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 = rest.slice(0, closeMatch.index);
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
- if (/<!--\s*changelog\s*:/i.test(rawContent)) {
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 nextBody = renderAnnotatedBody(parsed, resolved.resolved, repoUrl, deps);
528
- if (nextBody === null) {
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}${nextBody}${block.suffix}`);
533
- deps.log(`Annotated ${resolved.resolved.length} pull request(s)`);
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
- return CHANGELOG_BLOCK_REGEX.test(body) ? 'present' : 'absent';
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): string | null;
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.0",
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": [