@redpanda-data/docs-extensions-and-macros 5.3.6 → 5.3.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.3.6",
3
+ "version": "5.3.7",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -5,6 +5,7 @@ const path = require('path');
5
5
  const handlebars = require('handlebars');
6
6
  const yaml = require('yaml');
7
7
  const helpers = require('./helpers');
8
+ const { lostMetadataSections } = require('./metadata-utils.js');
8
9
 
9
10
  // Register each helper under handlebars, verifying that it’s a function
10
11
  Object.entries(helpers).forEach(([name, fn]) => {
@@ -376,6 +377,7 @@ async function generateRpcnConnectorDocs(options) {
376
377
  }
377
378
 
378
379
  let partialsWritten = 0;
380
+ const lostSectionWarnings = [];
379
381
  let draftsWritten = 0;
380
382
  const partialFiles = [];
381
383
  const draftFiles = [];
@@ -434,6 +436,25 @@ async function generateRpcnConnectorDocs(options) {
434
436
  const mPath = path.join(metadataOutRoot, typeDir, `${name}.adoc`);
435
437
  if (metadataOut.trim()) {
436
438
  fs.mkdirSync(path.dirname(mPath), { recursive: true });
439
+ if (fs.existsSync(mPath)) {
440
+ // Regeneration is authoritative, but a section vanishing from a
441
+ // published partial is usually content loss, not cleanup: the
442
+ // section either sits outside the upstream description's
443
+ // `== Metadata` block or was hand-migrated from a page, and
444
+ // overwriting drops it without a trace. Warn so the auto-docs PR
445
+ // reviewer can move the content to the connector page or restore
446
+ // it upstream before merging.
447
+ const lost = lostMetadataSections(fs.readFileSync(mPath, 'utf8'), metadataOut);
448
+ if (lost.length) {
449
+ lostSectionWarnings.push({ partial: path.relative(process.cwd(), mPath), sections: lost });
450
+ console.warn(
451
+ `Warning: regenerated metadata partial ${path.relative(process.cwd(), mPath)} ` +
452
+ `drops previously published section(s): ${lost.map((h) => `"${h}"`).join(', ')}. ` +
453
+ 'If this content is still wanted, move it to the connector page or restore it ' +
454
+ "in the upstream description's Metadata section."
455
+ );
456
+ }
457
+ }
437
458
  fs.writeFileSync(mPath, metadataOut);
438
459
  if (!writeFullDrafts) {
439
460
  partialsWritten++;
@@ -657,7 +678,8 @@ async function generateRpcnConnectorDocs(options) {
657
678
  partialsWritten,
658
679
  draftsWritten,
659
680
  partialFiles,
660
- draftFiles
681
+ draftFiles,
682
+ lostSectionWarnings
661
683
  };
662
684
  }
663
685
 
@@ -111,10 +111,69 @@ function descriptionWithMetadataInclude (item) {
111
111
  return description.slice(0, found.start) + metadataIncludeLine(item) + description.slice(found.end);
112
112
  }
113
113
 
114
+ // Markdown-style fence delimiter (``` or ~~~, possibly with a language tag).
115
+ // Metadata blocks can carry fenced examples alongside AsciiDoc ---- blocks.
116
+ const FENCE_DELIMITER = /^(`{3,}|~{3,})/;
117
+
118
+ /**
119
+ * Collect the section heading titles in an AsciiDoc block, ignoring lines
120
+ * inside `----` literal blocks and ```/~~~ fenced blocks. Titles are returned
121
+ * without their `=` markers so callers can compare sections across heading
122
+ * levels (the same section may be `==` in a connector description but `===`
123
+ * in a partial migrated from a page).
124
+ * @param {string} text
125
+ * @returns {string[]}
126
+ */
127
+ function sectionHeadings (text) {
128
+ if (!text || typeof text !== 'string') return [];
129
+ const headings = [];
130
+ let inBlock = false;
131
+ let fence = null;
132
+ for (const line of text.split('\n')) {
133
+ // Layered state: while inside one delimiter kind, the only thing that
134
+ // matters is its own closer. A fence-like line inside a ---- literal
135
+ // block (or a ---- line inside a fence) is content, not a delimiter —
136
+ // treating it as one leaks the state and swallows every later heading.
137
+ if (inBlock) {
138
+ if (BLOCK_DELIMITER.test(line)) inBlock = false;
139
+ continue;
140
+ }
141
+ if (fence) {
142
+ const closer = line.match(FENCE_DELIMITER);
143
+ if (closer && closer[1][0] === fence) fence = null;
144
+ continue;
145
+ }
146
+ const fenceMatch = line.match(FENCE_DELIMITER);
147
+ if (fenceMatch) { fence = fenceMatch[1][0]; continue; }
148
+ if (BLOCK_DELIMITER.test(line)) { inBlock = true; continue; }
149
+ const m = line.match(/^=+\s+(\S.*)$/);
150
+ if (m) headings.push(m[1].trim());
151
+ }
152
+ return headings;
153
+ }
154
+
155
+ /**
156
+ * Report the section headings present in a previously generated metadata
157
+ * partial that are missing from its regenerated replacement. Regeneration is
158
+ * authoritative, but published content silently disappearing is how docs lose
159
+ * examples: a section that lives outside the upstream description's
160
+ * `== Metadata` block (or was hand-migrated from a page) is dropped without a
161
+ * trace on the next run. Callers use this to warn before overwriting.
162
+ * @param {string} oldContent existing partial on disk
163
+ * @param {string} newContent regenerated partial about to be written
164
+ * @returns {string[]} heading titles present in oldContent but not newContent
165
+ */
166
+ function lostMetadataSections (oldContent, newContent) {
167
+ const newHeadings = new Set(sectionHeadings(newContent));
168
+ return sectionHeadings(oldContent).filter((h) => !newHeadings.has(h));
169
+ }
170
+
114
171
  module.exports = {
115
172
  locateMetadata,
116
173
  extractMetadata,
117
174
  typeDirFor,
118
175
  metadataIncludeLine,
119
176
  descriptionWithMetadataInclude,
177
+ sectionHeadings,
178
+ lostMetadataSections,
120
179
  };
@@ -122,6 +122,25 @@ function renderBloblangChanges(data, includeVersion = false) {
122
122
  * @param {array} draftedConnectors - Array of newly drafted connectors
123
123
  * @returns {string} Formatted summary
124
124
  */
125
+ /**
126
+ * Render the content-loss warning block that leads the PR summary. These
127
+ * warnings exist to stop a merge, so they go at the top of the summary body
128
+ * rather than into the collapsed workflow log where nobody reads them.
129
+ * @param {Array<{partial: string, sections: string[]}>} warnings
130
+ * @returns {string[]} lines (empty when there is nothing to warn about)
131
+ */
132
+ function renderLostSectionWarnings(warnings) {
133
+ if (!Array.isArray(warnings) || warnings.length === 0) return [];
134
+ const lines = [];
135
+ lines.push('> [!WARNING]');
136
+ lines.push('> **This update deletes previously published metadata sections.** Review before merging: move the content to the connector page or restore it in the upstream description.');
137
+ for (const w of warnings) {
138
+ lines.push(`> - \`${w.partial}\` drops: ${w.sections.map((h) => `"${h}"`).join(', ')}`);
139
+ }
140
+ lines.push('');
141
+ return lines;
142
+ }
143
+
125
144
  function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, draftedConnectors = null) {
126
145
  const lines = [];
127
146
 
@@ -133,6 +152,7 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
133
152
 
134
153
  lines.push('<!-- PR_SUMMARY_START -->');
135
154
  lines.push('');
155
+ lines.push(...renderLostSectionWarnings(masterDiff?.lostSectionWarnings));
136
156
  lines.push('## Redpanda Connect Documentation Update');
137
157
  lines.push('');
138
158
  lines.push(`**Multi-Release Update:** ${startVersion} → ${endVersion}`);
@@ -538,6 +558,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
538
558
  // Header with delimiters for GitHub Action parsing
539
559
  lines.push('<!-- PR_SUMMARY_START -->');
540
560
  lines.push('');
561
+ lines.push(...renderLostSectionWarnings(diffData.lostSectionWarnings));
541
562
 
542
563
  // Detect if this is a master diff
543
564
  if (!isMultiVersion && diffData.releases && diffData.totalSummary) {
@@ -1130,6 +1130,7 @@ async function handleRpcnConnectorDocs (options) {
1130
1130
 
1131
1131
  console.log('Generating connector partials...')
1132
1132
  let partialsWritten, partialFiles
1133
+ const lostSectionWarnings = []
1133
1134
 
1134
1135
  try {
1135
1136
  const result = await generateRpcnConnectorDocs({
@@ -1147,6 +1148,7 @@ async function handleRpcnConnectorDocs (options) {
1147
1148
  })
1148
1149
  partialsWritten = result.partialsWritten
1149
1150
  partialFiles = result.partialFiles
1151
+ lostSectionWarnings.push(...(result.lostSectionWarnings || []))
1150
1152
  } catch (err) {
1151
1153
  console.error(`Error: Failed to generate partials: ${err.message}`)
1152
1154
  process.exit(1)
@@ -1940,6 +1942,7 @@ async function handleRpcnConnectorDocs (options) {
1940
1942
  fs.unlinkSync(tempDataPath)
1941
1943
  draftsWritten = draftResult.draftsWritten
1942
1944
  draftFiles = draftResult.draftFiles
1945
+ lostSectionWarnings.push(...(draftResult.lostSectionWarnings || []))
1943
1946
  }
1944
1947
  } catch (err) {
1945
1948
  console.error(`Error: Could not draft missing: ${err.message}`)
@@ -1987,8 +1990,12 @@ async function handleRpcnConnectorDocs (options) {
1987
1990
  // Generate PR summary
1988
1991
  try {
1989
1992
  const { printPRSummary } = require('./pr-summary-formatter.js')
1990
- // Use master diff if available, otherwise use single diff
1991
- printPRSummary(masterDiff || diffJson, binaryAnalysis, draftFiles, masterDiff ? true : false)
1993
+ // Use master diff if available, otherwise use single diff. Content-loss
1994
+ // warnings ride the diff object so they lead the PR summary body instead
1995
+ // of dying in the collapsed workflow log.
1996
+ const summaryDiff = masterDiff || diffJson
1997
+ if (lostSectionWarnings.length) summaryDiff.lostSectionWarnings = lostSectionWarnings
1998
+ printPRSummary(summaryDiff, binaryAnalysis, draftFiles, masterDiff ? true : false)
1992
1999
  } catch (err) {
1993
2000
  console.error(`Warning: Failed to generate PR summary: ${err.message}`)
1994
2001
  }
@@ -23,7 +23,7 @@ const { spawnSync } = require('child_process')
23
23
  * authoritative: dashified filenames cannot be reversed unambiguously
24
24
  * (rpk-ai-llm-provider could be `llm provider` or `llm-provider`).
25
25
  * @param {string} partialsDir - Directory of generated .adoc partials
26
- * @returns {Array<{file: string, title: string}>} Sorted by command path
26
+ * @returns {Array<{file: string, title: string, description: string|undefined}>} Sorted by command path
27
27
  */
28
28
  function readPartialTitles(partialsDir) {
29
29
  const partials = []
@@ -35,7 +35,11 @@ function readPartialTitles(partialsDir) {
35
35
  console.warn(`Warning: no title line in ${file}; skipping`)
36
36
  continue
37
37
  }
38
- partials.push({ file, title: match[1].trim() })
38
+ // The partial repeats :description: inside its single-source tag, but
39
+ // Antora resolves page metadata with a header-only parse that never sees
40
+ // the include — the stub must carry the description in its own header.
41
+ const description = (content.match(/^:description:[ \t]*(.+)$/m) || [])[1]
42
+ partials.push({ file, title: match[1].trim(), description: description && description.trim() })
39
43
  }
40
44
  // Hierarchical order: sort by command words so parents precede children
41
45
  partials.sort((a, b) => {
@@ -114,12 +118,14 @@ function inferIncludePrefix(stubDir, plugin) {
114
118
  * @param {Object} params
115
119
  * @param {string} params.title - Command path (e.g. "rpk ai auth login")
116
120
  * @param {string} params.file - Partial filename
121
+ * @param {string} [params.description] - Meta description from the partial header
117
122
  * @param {string} params.includePrefix - Antora resource prefix
118
123
  * @param {Array<string>} params.attributes - Page attribute lines
119
124
  * @returns {string}
120
125
  */
121
- function renderStub({ title, file, includePrefix, attributes }) {
126
+ function renderStub({ title, file, description, includePrefix, attributes }) {
122
127
  const lines = [`= ${title}`]
128
+ if (description) lines.push(`:description: ${description}`)
123
129
  for (const attr of attributes) lines.push(attr)
124
130
  lines.push('')
125
131
  lines.push(`include::${includePrefix}${file}[tag=single-source]`)