@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.55.0

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.
Files changed (42) hide show
  1. package/bin/doc-tools-mcp.js +4 -5
  2. package/bin/doc-tools.js +39 -2
  3. package/bin/mcp-tools/generated-docs-review.js +26 -9
  4. package/bin/mcp-tools/overrides-audit.js +1 -1
  5. package/bin/mcp-tools/rpcn-docs.js +4 -14
  6. package/cli-utils/format-helm-spec.js +4 -3
  7. package/cli-utils/helm-commented-values.js +88 -18
  8. package/cli-utils/self-managed-docs-branch.js +36 -30
  9. package/extension-utils/html-utils.js +27 -0
  10. package/extensions/REFERENCE.adoc +20 -4
  11. package/extensions/add-git-dates.js +13 -1
  12. package/extensions/aggregate-terms.js +34 -20
  13. package/extensions/modify-connect-tag-playbook.js +181 -3
  14. package/extensions/resolve-xrefs-in-attachments.js +1 -12
  15. package/extensions/util/connect-docs-asset.js +6 -5
  16. package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
  17. package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
  18. package/extensions/version-fetcher/list-connect-releases.js +3 -3
  19. package/extensions/version-fetcher/release-lines.js +64 -0
  20. package/extensions/version-fetcher/set-latest-version.js +73 -3
  21. package/macros/README.adoc +1 -0
  22. package/macros/REFERENCE.adoc +99 -0
  23. package/macros/badge.js +5 -8
  24. package/macros/enterprise.js +2 -5
  25. package/macros/glossary.js +4 -14
  26. package/macros/iceberg-explorer.js +242 -0
  27. package/macros/prop.js +3 -13
  28. package/mcp/USER_GUIDE.adoc +25 -135
  29. package/package.json +4 -2
  30. package/tools/doc-impact-recording/index.js +190 -0
  31. package/tools/overrides-audit/README.adoc +1 -2
  32. package/tools/overrides-audit/adapters/properties.js +1 -1
  33. package/tools/overrides-audit/index.js +3 -4
  34. package/tools/redpanda-connect/AUTOMATION.md +3 -36
  35. package/tools/redpanda-connect/README.adoc +5 -33
  36. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +3 -3
  37. package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
  38. package/tools/redpanda-connect/report-delta.js +6 -3
  39. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +38 -16
  40. package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
  41. package/tools/rpk-docs/templates/command.hbs +1 -1
  42. package/tools/overrides-audit/adapters/connect.js +0 -144
@@ -10,7 +10,6 @@
10
10
  * - Context-aware: Works from any repository based on cwd
11
11
  * - Antora intelligence: Understands component/module structure
12
12
  * - Automation: Run doc-tools generate commands
13
- * - Auto-discovery: Prompts loaded automatically from mcp/prompts/
14
13
  * - Validation: Startup checks ensure all resources are accessible
15
14
  * - Telemetry: Usage tracking for adoption metrics
16
15
  */
@@ -201,7 +200,7 @@ const tools = [
201
200
  },
202
201
  {
203
202
  name: 'generate_rpcn_connector_docs',
204
- description: 'Generate Redpanda Connect connector documentation. Creates component documentation for all connectors. Writers use this when updating connector reference docs. Automatically uses docs-data/overrides.json if present.',
203
+ description: 'Generate Redpanda Connect connector documentation. Creates component documentation for all connectors. Writers use this when updating connector reference docs. Field, component, and Bloblang descriptions come from the component specs in redpanda-data/connect and redpanda-data/benthos, so fix wrong text with a PR there.',
205
204
  inputSchema: {
206
205
  type: 'object',
207
206
  properties: {
@@ -239,7 +238,7 @@ const tools = [
239
238
  },
240
239
  overrides: {
241
240
  type: 'string',
242
- description: 'Path to optional JSON file with overrides (defaults to docs-data/overrides.json if present)'
241
+ description: 'Path to an optional JSON file of description overrides (optional). Nothing is merged unless you pass it.'
243
242
  },
244
243
  cloud_version: {
245
244
  type: 'string',
@@ -516,7 +515,7 @@ const tools = [
516
515
  surface: {
517
516
  type: 'string',
518
517
  description: 'Override surface to audit (optional, defaults to "properties")',
519
- enum: ['properties', 'rpk', 'connect']
518
+ enum: ['properties', 'rpk']
520
519
  },
521
520
  output: {
522
521
  type: 'string',
@@ -594,7 +593,7 @@ const resourceMap = {
594
593
 
595
594
  /**
596
595
  * Load resource content from team-standards directory
597
- * @param {string} uri - Resource URI (such as 'redpanda://style-guide')
596
+ * @param {string} uri - Resource URI (such as 'redpanda://personas')
598
597
  * @returns {Object} Resource content
599
598
  */
600
599
  function getResourceContent(uri) {
package/bin/doc-tools.js CHANGED
@@ -896,7 +896,7 @@ automation
896
896
  .option('--template-metadata <path>', 'Metadata section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/metadata-partials.hbs'))
897
897
  .option('--template-description <path>', 'Description section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/descriptions-partials.hbs'))
898
898
  .option('--template-bloblang <path>', 'Custom Handlebars template for bloblang function/method partials')
899
- .option('--overrides <path>', 'Optional JSON file with overrides', 'docs-data/overrides.json')
899
+ .option('--overrides <path>', 'Optional JSON file of description overrides to merge into the generated output. Nothing is merged unless you pass it. Connector descriptions come from the connect and benthos component specs, so fix wrong text there')
900
900
  .option('--include-bloblang', 'Include Bloblang functions and methods in generation')
901
901
  .option('--no-partials', 'Skip the generated partials, config snippets, and Bloblang reference because the connect repo publishes them. What\'s new, diffs, drafts, nav, and the version bump still run')
902
902
  .option('--prune-orphaned-descriptions', 'Allow the description-partial orphan sweep to blank more than 10% of the tree. Only use this when the connector dataset is confirmed complete: an incomplete dataset looks identical to a mass upstream deletion and the sweep blanks published content')
@@ -2881,6 +2881,43 @@ programCli
2881
2881
  runCli(options)
2882
2882
  })
2883
2883
 
2884
+ /**
2885
+ * doc-impact-recording
2886
+ *
2887
+ * @description
2888
+ * Extracts every redpanda-docs MCP call and its result from a Claude Code
2889
+ * transcript (the claude-code-action execution file, or `claude -p
2890
+ * --output-format stream-json` output) and writes them as a doc-impact eval
2891
+ * recording. Only the tool name, arguments and result content are kept.
2892
+ * Writes nothing when the session made no docs calls.
2893
+ *
2894
+ * @why
2895
+ * The doc-impact eval replays docs answers per PR. Answers recorded later
2896
+ * come from docs that may already contain the writer's fix, so the eval
2897
+ * would score a correct "already documented" as a miss. The doc-strings
2898
+ * review saves what the docs server told it at review time, and the eval
2899
+ * miner turns that into the item's recording.
2900
+ *
2901
+ * @example
2902
+ * # What the doc-strings review runs after its Claude step
2903
+ * npx doc-tools doc-impact-recording --execution-file "$RUNNER_TEMP/claude-execution-output.json" \
2904
+ * --output recording.json --item redpanda-operator-1615 --server-url https://docs.redpanda.com/mcp
2905
+ */
2906
+ programCli
2907
+ .command('doc-impact-recording')
2908
+ .description('Extract the docs MCP calls from a Claude Code transcript into a doc-impact eval recording')
2909
+ .requiredOption('--execution-file <path>', 'Transcript: a JSON array of SDK messages or stream-json lines')
2910
+ .requiredOption('--output <path>', 'Recording file to write (not written when there are no docs calls)')
2911
+ .option('--item <id>', 'Item id to store in the recording')
2912
+ .option('--server <name>', 'MCP server name in the transcript tool names', 'redpanda-docs')
2913
+ .option('--server-url <url>', 'URL of that MCP server, stored in the recording')
2914
+ .option('--model <name>', 'Model to store (default: the model in the transcript init message)')
2915
+ .option('--recorded-at <iso>', 'Timestamp to store (default: now)')
2916
+ .action((options) => {
2917
+ const { runCli } = require('../tools/doc-impact-recording')
2918
+ process.exit(runCli(options))
2919
+ })
2920
+
2884
2921
  /**
2885
2922
  * lint-screenshots
2886
2923
  *
@@ -3136,7 +3173,7 @@ overridesGroup
3136
3173
  .description('Classify each override field as redundant, upstreamable, or keep against extracted source strings')
3137
3174
  .requiredOption('--overrides <path>', 'Path to the overrides JSON file (for example docs-data/property-overrides.json)')
3138
3175
  .option('--extracted <path>', 'Path to the extracted source JSON (property extractor raw output; required for the properties surface)')
3139
- .addOption(new Option('--surface <surface>', 'Override surface to audit').choices(['properties', 'rpk', 'connect']).default('properties'))
3176
+ .addOption(new Option('--surface <surface>', 'Override surface to audit').choices(['properties', 'rpk']).default('properties'))
3140
3177
  .addOption(new Option('--format <format>', 'Output format').choices(['json', 'human']).default('json'))
3141
3178
  .option('--repo <path>', 'Redpanda checkout to extract raw source strings from (alternative to --extracted)')
3142
3179
  .option('--output <path>', 'Also write the JSON result to this file')
@@ -189,7 +189,7 @@ function generateReviewReport(results, outputPath) {
189
189
  report += `. Regenerate docs\n\n`;
190
190
  } else if (issue.issue.includes('Invalid $ref')) {
191
191
  report += `*Action:*\n\n`;
192
- report += `. Open \`docs-data/overrides.json\`\n`;
192
+ report += `. Open \`${issue.file}\`\n`;
193
193
  report += `. Find the reference at \`${issue.path}\`\n`;
194
194
  report += `. Either add the missing definition or fix the reference\n`;
195
195
  report += `. Regenerate docs\n\n`;
@@ -273,6 +273,17 @@ function generateReviewReport(results, outputPath) {
273
273
  return report;
274
274
  }
275
275
 
276
+ /**
277
+ * Where to fix Redpanda Connect reference text. The connect repo generates
278
+ * the field, component, and Bloblang partials from its specs and the benthos
279
+ * specs, and each Connect release publishes them for the docs build.
280
+ */
281
+ const RPCN_SOURCE_GUIDANCE =
282
+ 'Connector field, component, and Bloblang descriptions are generated from the component specs in ' +
283
+ 'redpanda-data/connect and redpanda-data/benthos. To fix wrong or missing text, open a PR against ' +
284
+ 'the spec in that repository; the fix ships in the next Connect release. Edit hand-written page ' +
285
+ 'prose in rp-connect-docs.';
286
+
276
287
  /**
277
288
  * Review generated documentation for quality issues
278
289
  * @param {Object} args - Arguments
@@ -282,7 +293,7 @@ function generateReviewReport(results, outputPath) {
282
293
  * @param {string} [args.properties_dir] - Custom path to properties directory (default: modules/reference)
283
294
  * @param {string} [args.metrics_file] - Custom path to metrics file (default: modules/reference/pages/public-metrics-reference.adoc)
284
295
  * @param {string} [args.rpk_dir] - Custom path to RPK docs directory (default: autogenerated/{version}/rpk)
285
- * @param {string} [args.overrides_file] - Custom path to overrides.json for RPCN connectors (default: docs-data/overrides.json)
296
+ * @param {string} [args.overrides_file] - Optional connector overrides file to validate for RPCN connectors. Without it, the review points at the connect and benthos specs, where connector descriptions live
286
297
  * @returns {Object} Review results with issues and suggestions
287
298
  */
288
299
  function reviewGeneratedDocs(args) {
@@ -524,16 +535,21 @@ function reviewGeneratedDocs(args) {
524
535
  }
525
536
 
526
537
  case 'rpcn_connectors': {
527
- // Read overrides.json (use custom path or default)
528
- const overridesPath = args.overrides_file ?
529
- path.join(repoRoot.root, args.overrides_file) :
530
- path.join(repoRoot.root, 'docs-data', 'overrides.json');
538
+ // Connector field, component, and Bloblang descriptions are generated
539
+ // from the connect and benthos specs, so there is no overrides file to
540
+ // check by default. Only an overrides file passed explicitly is validated.
541
+ if (!args.overrides_file) {
542
+ suggestions.push(RPCN_SOURCE_GUIDANCE);
543
+ break;
544
+ }
545
+
546
+ const overridesPath = path.join(repoRoot.root, args.overrides_file);
531
547
 
532
548
  if (!fs.existsSync(overridesPath)) {
533
549
  return {
534
550
  success: false,
535
- error: `overrides.json not found at ${overridesPath}`,
536
- suggestion: 'Generate RPCN connector docs first using generate_rpcn_connector_docs tool, or specify custom path with overrides_file parameter'
551
+ error: `Overrides file not found at ${overridesPath}`,
552
+ suggestion: `Check the overrides_file path, or omit it. ${RPCN_SOURCE_GUIDANCE}`
537
553
  };
538
554
  }
539
555
 
@@ -736,5 +752,6 @@ function reviewGeneratedDocs(args) {
736
752
 
737
753
  module.exports = {
738
754
  generateReviewReport,
739
- reviewGeneratedDocs
755
+ reviewGeneratedDocs,
756
+ RPCN_SOURCE_GUIDANCE
740
757
  };
@@ -11,7 +11,7 @@ const { findRepoRoot, getDocToolsCommand, MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_
11
11
  * @param {string} args.overrides - Path to the overrides JSON file (for example "docs-data/property-overrides.json")
12
12
  * @param {string} [args.extracted] - Path to the extracted source JSON (property extractor raw output; required for the properties surface)
13
13
  * @param {string} [args.repo] - Redpanda checkout to extract raw source strings from (alternative to args.extracted, properties surface only)
14
- * @param {string} [args.surface] - Override surface: 'properties', 'rpk', or 'connect'
14
+ * @param {string} [args.surface] - Override surface: 'properties' or 'rpk'
15
15
  * @param {string} [args.output] - Also write the JSON result to this file (relative to repo root)
16
16
  * @returns {Object} Audit results with manifest and per-class summary
17
17
  */
@@ -12,8 +12,6 @@ const { execSync, spawnSync } = require('child_process');
12
12
  const { findRepoRoot, MAX_EXEC_BUFFER_SIZE, DEFAULT_COMMAND_TIMEOUT } = require('./utils');
13
13
  const { getAntoraStructure } = require('./antora');
14
14
  const cache = require('./cache');
15
- const fs = require('fs');
16
- const path = require('path');
17
15
 
18
16
  /**
19
17
  * Generate Redpanda Connect connector documentation
@@ -59,19 +57,11 @@ function generateRpConnectDocs(args = {}) {
59
57
  cmdArgs.push(args.csv);
60
58
  }
61
59
 
62
- // Default to using overrides.json from docs-data if it exists
63
- let overridesPath = args.overrides;
64
- if (!overridesPath) {
65
- // Try to find docs-data/overrides.json in current directory
66
- const defaultOverrides = path.join(process.cwd(), 'docs-data', 'overrides.json');
67
- if (fs.existsSync(defaultOverrides)) {
68
- overridesPath = defaultOverrides;
69
- }
70
- }
71
-
72
- if (overridesPath) {
60
+ // Overrides are opt-in. Connector descriptions come from the connect and
61
+ // benthos specs, so no file is picked up implicitly.
62
+ if (args.overrides) {
73
63
  cmdArgs.push('--overrides');
74
- cmdArgs.push(overridesPath);
64
+ cmdArgs.push(args.overrides);
75
65
  }
76
66
  if (args.cloud_version) {
77
67
  cmdArgs.push('--cloud-version');
@@ -9,9 +9,10 @@
9
9
  */
10
10
  function formatHelmSpec (doc) {
11
11
  const formatted = doc
12
- // pandoc escapes footnote-style references inconsistently
13
- .replace(/(\[\d+\])\]\./g, '$1\\].')
14
- .replace(/(\[\d+\])\]\]/g, '$1\\]\\]')
12
+ // an array index inside a heading's link text (ingress.hosts[0].host)
13
+ // closes the link text early unless its "]" is escaped
14
+ .replace(/(\[\d+)\]\./g, '$1\\].')
15
+ .replace(/(\[\d+)\]\]/g, '$1\\]]')
15
16
  // a section title that is a bare URL has to be a link macro to render
16
17
  .replace(/^=== +(https?:\/\/[^[]*)\[([^\]]*)\]/gm, '=== link:++$1++[$2]')
17
18
  .replace(/^== # (.*)$/gm, '= $1')
@@ -24,6 +24,19 @@
24
24
  *
25
25
  * yields `external.domain`.
26
26
  *
27
+ * The description attaches to the first commented-out key after it. When
28
+ * more commented-out siblings follow in the same comment run, each one
29
+ * with its own plain comment above it is documented by that comment:
30
+ *
31
+ * external:
32
+ * # -- Optional domain advertised to external clients.
33
+ * # domain: local
34
+ * # Optional list of advertised addresses.
35
+ * # addresses:
36
+ *
37
+ * yields `external.domain` and `external.addresses`. A sibling with no
38
+ * comment of its own is not documented.
39
+ *
27
40
  * 2. Explicit, fully-qualified syntax that works anywhere in the file:
28
41
  *
29
42
  * # @doc external.addresses -- Optional list of advertised addresses.
@@ -89,10 +102,26 @@ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
89
102
  const records = []
90
103
  const stack = [] // enclosing real keys: { name, indent }
91
104
 
92
- // Accumulating comment block: { descLines, default, candidate }. The
93
- // candidate is the key the block documents; emission is deferred to the
94
- // end of the block so that a later key line at the same depth supersedes a
95
- // prose line that merely looks like a key (`# example:`, `# default: 30s`).
105
+ // Accumulating comment block: { marked, descLines, default, candidate }.
106
+ // A marked block opens at a `# --` line. Its description is every comment
107
+ // line up to the first commented-out key, and that key (the candidate) is
108
+ // the one it documents, the same way helm-docs reads a `# --` block as the
109
+ // description of the key directly below it.
110
+ //
111
+ // Optional values often ship as a run of commented-out siblings under one
112
+ // marker, each with its own plain comment above it:
113
+ //
114
+ // # -- Optional domain.
115
+ // # domain: local
116
+ // # Optional list of addresses.
117
+ // # addresses:
118
+ //
119
+ // Once a block has its key, a plain comment line at that key's depth (or
120
+ // shallower) ends it and opens an unmarked block for the next sibling, so
121
+ // `addresses` is documented by its own comment instead of handing its key
122
+ // line to the domain description. An unmarked block is only ever emitted
123
+ // when it has description text and a key; it never becomes a dead marker.
124
+ // The run ends at the first non-comment line, as a marked block does.
96
125
  let block = null
97
126
  let atDoc = null // accumulating @doc entry: { path, descLines, default }
98
127
  let skipScalarIndent = -1 // inside a block scalar when >= 0
@@ -135,6 +164,7 @@ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
135
164
  // The generator drops these; the linter reports them, because their
136
165
  // description silently never ships.
137
166
  const emitDead = (b) => {
167
+ if (!b.marked) return
138
168
  record({
139
169
  kind: 'dead-marker',
140
170
  descLines: b.descLines,
@@ -145,12 +175,26 @@ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
145
175
  })
146
176
  }
147
177
 
178
+ const openUnmarkedBlock = (line, firstDescLine) => ({
179
+ marked: false,
180
+ markerLine: line,
181
+ lastLine: line,
182
+ descLines: firstDescLine === null ? [] : [firstDescLine],
183
+ default: '',
184
+ annotations: {},
185
+ candidate: null,
186
+ })
187
+
148
188
  // Emit the active block's candidate, if any, and close the block. The
149
189
  // emitted key suppresses its own commented subtree until real content
150
190
  // appears, so nested example structures do not emit bogus paths.
151
191
  const flushBlock = () => {
152
192
  if (!block) return
153
- if (block.candidate) {
193
+ if (block.candidate && !block.marked && !block.descLines.some((l) => l.trim() !== '')) {
194
+ // An unmarked sibling with no comment of its own has nothing to say;
195
+ // still suppress its commented subtree.
196
+ suppressInnerIndent = block.candidate.effIndent
197
+ } else if (block.candidate) {
154
198
  const { name, effIndent } = block.candidate
155
199
  const parents = stack.filter((k) => k.indent < effIndent).map((k) => k.name)
156
200
  record({
@@ -233,13 +277,26 @@ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
233
277
  // "key" is a URL scheme and the "value" is the scheme-relative part.
234
278
  const simpleValue = (value === '' || !/\s/.test(value)) && !value.startsWith('/')
235
279
  if (block && simpleValue) {
236
- if (!block.candidate || effIndent <= block.candidate.effIndent) {
237
- // The last key-shaped line at the block's shallowest depth wins:
238
- // an earlier line at the same depth was prose shaped like a key,
239
- // and deeper lines are nested example structures.
240
- block.candidate = { name: commentedKey[3], effIndent }
280
+ const key = { name: commentedKey[3], effIndent }
281
+ if (!block.candidate || effIndent < block.candidate.effIndent) {
282
+ // The first key line after the description is the documented key.
283
+ // A shallower key replaces it, because the deeper line was a nested
284
+ // example structure rather than the key itself.
285
+ block.candidate = key
286
+ block.lastLine = i
287
+ } else if (effIndent > block.candidate.effIndent) {
288
+ // A nested line of the documented key's commented-out example.
289
+ block.lastLine = i
290
+ } else {
291
+ // A sibling key with no comment of its own. The documented key is
292
+ // complete; the sibling opens an unmarked block, which flushBlock
293
+ // drops because it has no description, while still suppressing
294
+ // the sibling's own commented subtree.
295
+ flushBlock()
296
+ suppressInnerIndent = -1
297
+ block = openUnmarkedBlock(i, null)
298
+ block.candidate = key
241
299
  }
242
- block.lastLine = i
243
300
  continue
244
301
  }
245
302
  }
@@ -248,18 +305,30 @@ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
248
305
  if (descMarker) {
249
306
  flushAtDoc()
250
307
  flushBlock()
251
- block = { markerLine: i, lastLine: i, descLines: [descMarker[1]], default: '', annotations: {}, candidate: null }
308
+ block = { marked: true, markerLine: i, lastLine: i, descLines: [descMarker[1]], default: '', annotations: {}, candidate: null }
252
309
  continue
253
310
  }
254
311
 
255
312
  const comment = line.match(COMMENT_RE)
256
313
  if (comment && line.trim().startsWith('#')) {
257
314
  if (atDoc) { atDoc.descLines.push(comment[1]); atDoc.lastLine = i }
258
- // Description lines precede the documented key; once a candidate
259
- // exists, trailing comment lines belong to its example structure and
260
- // only extend the span.
315
+ // Description lines precede the documented key.
261
316
  else if (block && !block.candidate) { block.descLines.push(comment[1]); block.lastLine = i }
262
- else if (block) block.lastLine = i
317
+ else if (block) {
318
+ // After the key: deeper lines, list items, and bare `#` lines belong
319
+ // to its commented-out example. Prose at the key's own depth or
320
+ // shallower is the description of the next commented-out sibling.
321
+ const [, before, after] = line.match(/^(\s*)#(\s*)/)
322
+ const proseIndent = before.length + Math.max(0, after.length - 1)
323
+ const text = comment[1].trim()
324
+ if (text === '' || text.startsWith('- ') || text === '-' || proseIndent > block.candidate.effIndent) {
325
+ block.lastLine = i
326
+ } else {
327
+ flushBlock()
328
+ suppressInnerIndent = -1
329
+ block = openUnmarkedBlock(i, comment[1])
330
+ }
331
+ }
263
332
  continue
264
333
  }
265
334
 
@@ -270,8 +339,9 @@ function parseValuesFile (yamlText, { attachRealKeys = false } = {}) {
270
339
  flushAtDoc()
271
340
 
272
341
  const realKey = line.match(REAL_KEY_RE)
273
- if (attachRealKeys && realKey && block && !block.candidate && block.lastLine === i - 1) {
274
- // helm-docs' own attachment: the block sits DIRECTLY above a real key.
342
+ if (attachRealKeys && realKey && block && block.marked && !block.candidate && block.lastLine === i - 1) {
343
+ // helm-docs' own attachment: the marked block sits DIRECTLY above a
344
+ // real key.
275
345
  const indent = realKey[1].length
276
346
  while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop()
277
347
  const name = realKey[2].replace(/^"|"$/g, '')
@@ -43,51 +43,57 @@ async function fetchRemoteAntoraVersion(deps = {}) {
43
43
  }
44
44
 
45
45
  /**
46
- * Determines the appropriate documentation branch for a given operator tag based on the remote Antora version.
46
+ * Reports whether a branch exists in the docs repository.
47
47
  *
48
- * Normalizes the input tag, extracts the major.minor version, and applies version-specific logic to select the correct branch. Verifies that the chosen branch exists in the remote repository.
48
+ * @param {string} branch - Branch name, such as "main" or "v/25.1".
49
+ * @returns {boolean} True when `git ls-remote` finds the branch.
50
+ */
51
+ function docsBranchExists(branch) {
52
+ const repo = 'https://github.com/redpanda-data/docs.git'
53
+ return spawnSync('git', ['ls-remote', '--exit-code', '--heads', repo, `refs/heads/${branch}`], {
54
+ stdio: 'ignore'
55
+ }).status === 0
56
+ }
57
+
58
+ /**
59
+ * Determines the appropriate documentation branch for a given operator tag or branch based on the remote Antora version.
60
+ *
61
+ * Normalizes the input, extracts the major.minor version, and applies version-specific logic to select the correct branch. Verifies that the chosen branch exists in the remote repository.
62
+ * A ref with no version, such as `main`, `dev`, or a feature branch, holds content for the next release, which the docs repository keeps on `main`.
49
63
  *
50
- * @param {string} operatorTag - The operator tag to evaluate (for example, "operator/v25.1.2" or "v25.1.2").
64
+ * @param {string} operatorRef - The operator tag or branch to evaluate (for example, "operator/v25.1.2", "v25.1.2", "release/v25.1.x", or "main").
65
+ * @param {object} [deps] - Injectable for tests: { antoraVersion, branchExists, fetchImpl, token }.
51
66
  * @returns {Promise<string>} The name of the documentation branch to use.
52
67
  *
53
- * @throws {Error} If the tag cannot be parsed or if the determined branch does not exist in the remote repository.
68
+ * @throws {Error} If the Antora version cannot be fetched or if the determined branch does not exist in the remote repository.
54
69
  */
55
- async function determineDocsBranch(operatorTag) {
56
- // Strip any "operator/" prefix
57
- const TAG = operatorTag.replace(/^(?:operator|release)\//, '')
58
- // Pull in the remote Antora version
59
- const ANTORA = await fetchRemoteAntoraVersion()
70
+ async function determineDocsBranch(operatorRef, deps = {}) {
71
+ const branchExists = deps.branchExists || docsBranchExists
72
+ // Strip any "operator/" or "release/" prefix
73
+ const TAG = operatorRef.replace(/^(?:operator|release)\//, '')
60
74
  // Extract v<major>.<minor>
61
75
  const mm = TAG.match(/^v(\d+\.\d+)/)
62
76
  const filtered = mm ? `v${mm[1]}` : null
63
77
 
64
- if (!filtered) {
65
- throw new Error(`Could not parse major.minor from ${TAG}`)
66
- }
67
- // We started versioning the operator in line with Redpanda core versions. But when v2.4.x was the latest version, the docs were still on 25.1 and v25.1.x of the operator was still in beta. So we need to handle this special case.
68
78
  let branch
69
- if (filtered === 'v2.4') {
70
- if (ANTORA === '25.1') {
79
+ if (!filtered) {
80
+ branch = 'main'
81
+ } else {
82
+ // Pull in the remote Antora version
83
+ const ANTORA = deps.antoraVersion || await fetchRemoteAntoraVersion(deps)
84
+ // We started versioning the operator in line with Redpanda core versions. But when v2.4.x was the latest version, the docs were still on 25.1 and v25.1.x of the operator was still in beta. So we need to handle this special case.
85
+ if (filtered === 'v2.4') {
86
+ branch = ANTORA === '25.1' ? 'main' : 'v/24.3'
87
+ // For all other versions use the v<major>.<minor> branch unless it is the current version, in which case we use 'main'.
88
+ } else if (filtered === `v${ANTORA}`) {
71
89
  branch = 'main'
72
90
  } else {
73
- branch = 'v/24.3'
91
+ branch = `v/${filtered.slice(1)}`
74
92
  }
75
- // For all other versions use the v<major>.<minor> branch unless it is the current version, in which case we use 'main'.
76
- } else if (filtered === `v${ANTORA}`) {
77
- branch = 'main'
78
- } else {
79
- branch = `v/${filtered.slice(1)}`
80
93
  }
81
94
 
82
- // Verify branch exists
83
- const repo = 'https://github.com/redpanda-data/docs.git'
84
- const ref = `refs/heads/${branch}`
85
- const ok = spawnSync('git', ['ls-remote', '--exit-code', '--heads', repo, ref], {
86
- stdio: 'ignore'
87
- }).status === 0
88
-
89
- if (!ok) {
90
- throw new Error(`Docs branch ${branch} not found in ${repo}`)
95
+ if (!branchExists(branch)) {
96
+ throw new Error(`Docs branch ${branch} not found in https://github.com/redpanda-data/docs.git`)
91
97
  }
92
98
 
93
99
  return branch
@@ -0,0 +1,27 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Shared HTML utility functions for extensions and macros.
5
+ */
6
+
7
+ /**
8
+ * Escape a value for safe inclusion in HTML text or inside a double-quoted
9
+ * attribute. Escaping `&` first is what keeps the result idempotent-safe for a
10
+ * single pass; escaping `"` is what stops an author-supplied string from ending
11
+ * the attribute it sits in.
12
+ *
13
+ * @param {*} value - Value to escape. `null` and `undefined` become ''.
14
+ * @returns {string} The escaped string.
15
+ */
16
+ function escapeHtml (value) {
17
+ if (value === null || value === undefined) return ''
18
+ return String(value)
19
+ .replace(/&/g, '&amp;')
20
+ .replace(/</g, '&lt;')
21
+ .replace(/>/g, '&gt;')
22
+ .replace(/"/g, '&quot;')
23
+ }
24
+
25
+ module.exports = {
26
+ escapeHtml,
27
+ }
@@ -412,7 +412,8 @@ At build time, when the playbook has the `connect` component, the extension:
412
412
 
413
413
  . Picks the release: the latest stable `vX.Y.Z` release by default, so the site never shows fields from unreleased code on connect's `main` branch. It asks the GitHub API for the latest release and falls back to the highest stable `vX.Y.Z` tag from `git ls-remote`. If neither works, the build stops with an error. The `tag` option picks a specific release instead.
414
414
  . Downloads that release's `redpanda-connect-docs.tar.gz` asset. With a GitHub token it downloads through the releases API, which keeps working if the connect repository becomes private. Without one, or if GitHub rejects the token, it uses the public download URL.
415
- . Adds `modules/components/partials/**` as partials and `modules/components/examples/**` as examples of the `components` module, in every version of the `connect` component. A file that another source, such as rp-connect-docs, already provides is skipped and counted in the log, so rp-connect-docs can keep committing its own copies until it stops.
415
+ . Adds `modules/components/partials/**` as partials, `modules/components/examples/**` as examples, and `modules/components/attachments/**` (the release's `connect-<version>.json`, read by the Bloblang playground) as attachments of the `components` module, in every version of the `connect` component. A file that another source, such as rp-connect-docs, already provides is skipped and counted in the log, so rp-connect-docs can keep committing its own copies until it stops.
416
+ . When the playbook also registers `add-git-dates`, dates each page by the Connect release that last changed its reference. For every generated partial or example a page includes, directly or through another generated partial such as `bloblang/methods.adoc`, the extension compares the release with older releases that have the asset, newest first, and takes the publish date of the newest release that changed the file. A page that single-sources such a connect page, such as a Cloud stub, gets the same date. `add-git-dates` uses the date as `page-git-modified-date` when it is later than the page's last commit. The walk stops at v4.113.0, the first release with the asset, and after at most 10 older releases. A file that is unchanged for the whole walk keeps the page's git date. If the releases listing or an older release's download fails, the extension logs a warning and pages keep their git dates, so the build never stops for a date. Without `add-git-dates` in the playbook, the extension skips this step and downloads only the release it uses.
416
417
 
417
418
  A release from before the asset existed has none, and the download returns 404. A new release can also be missing it for a while: Connect publishes the release first and attaches the asset 15 to 20 minutes later in a separate job, which can fail.
418
419
 
@@ -490,8 +491,8 @@ The following attributes are available to all versions of all Antora components:
490
491
  `console-beta-version`:: The latest beta version of Redpanda Console.
491
492
  `operator-beta-version`:: The latest beta version of the Redpanda Operator.
492
493
  `helm-beta-version`:: The latest beta version of the Redpanda Helm chart.
493
- `latest-operator-version`:: The latest release version of the Redpanda Operator. This value keeps its `v` prefix (for example, `v25.1.3`) because pages pass it straight to `helm --version`.
494
- `latest-redpanda-helm-chart-version`:: The latest release version of the Redpanda Helm chart.
494
+ `latest-operator-version`:: The latest release version of the Redpanda Operator for the component version's release line (see <<operator-and-chart-per-version>>). This value keeps its `v` prefix (for example, `v25.1.3`) because pages pass it straight to `helm --version`.
495
+ `latest-redpanda-helm-chart-version`:: The latest release version of the Redpanda Helm chart for the component version's release line (see <<operator-and-chart-per-version>>). This value has no `v` prefix (for example, `25.3.11`).
495
496
 
496
497
  The following attributes are available to the latest version of each Antora component:
497
498
 
@@ -511,6 +512,19 @@ ever moves forwards.
511
512
 
512
513
  A short attribute is only set when a `major.minor` pair can be derived. Release channels that are not versions, such as `nightly` or `latest`, get a `-version` and a `-tag` but no `-version-short`, so a page never renders a fabricated release line.
513
514
 
515
+ [[operator-and-chart-per-version]]
516
+ ==== Operator and Helm chart versions per component version
517
+
518
+ An operator or Helm chart release supports its own Redpanda version and one minor version above and below it, so each component version gets the release of its own line:
519
+
520
+ * A component version named `<major>.<minor>` that is older than the component's latest version gets the newest stable release in that line. For example, version `25.3` gets the newest `v25.3.x` operator and the newest `25.3.x` chart, even when a `26.2.x` release exists.
521
+ * The component's latest version, any prerelease version ahead of it, and unversioned components get the newest stable release overall.
522
+ * If an older line has no stable operator release (for example, lines that shipped only legacy `v2.x.y-<redpanda-version>` operator tags), the newest release does not support it, so both attributes keep the values the component version's `antora.yml` sets. If the line has an operator release but no chart release, only the chart attribute keeps its `antora.yml` value. An attribute that `antora.yml` leaves unset or empty falls back to the newest release overall. Either way, the build logs a warning that names the line.
523
+
524
+ The release lines come from the fetched operator tags, so no version map needs updating when a line ships. The extension reads up to three pages (300 tags) of the operator's Docker Hub tags, newest pushes first.
525
+
526
+ The chart version of a line is the newest stable `charts/redpanda/v<major>.<minor>.*` tag in the redpanda-operator repository, which marks a published chart (what `helm search repo` reports). A release branch's `Chart.yaml` can be ahead of the published chart, so the extension reads it only when the line has no chart tag, trying each path the chart has used in order: `charts/redpanda/chart/Chart.yaml`, then `charts/redpanda/Chart.yaml` (the layout of `release/v25.1.x`).
527
+
514
528
  ==== Seed a fallback in antora.yml
515
529
 
516
530
  Every attribute here depends on a successful GitHub lookup at build time. If a lookup fails, for example because of a rate limit or a missing token, the attribute is never set and any page that references it renders the attribute name in braces.
@@ -878,6 +892,8 @@ The extension accepts the following configuration options:
878
892
 
879
893
  termspath (optional):: Specifies the path to a local directory containing term files (in `.adoc` format). If this path is provided, the extension will attempt to load terms from this directory first. If this path is not provided or no valid terms are found in the specified directory, the extension will fall back to loading terms from the `shared` component.
880
894
 
895
+ glossarypage (optional):: The resource ID of one page that holds the glossary for the whole site, such as `home:ROOT:glossary.adoc`. The extension appends every term to that page and sets every component's `glossary-page` attribute to it, so `glossterm` links from any component point at the one glossary. Any `reference:glossary.adoc` pages that components still have are filled too, so they stay complete until they are removed. If the page does not exist, the extension logs a warning and links each component to its own `reference:glossary.adoc`, as without this option. When the page exists, it takes precedence over a `glossary-page` attribute set in a component's `antora.yml`, so a per-component value only applies if this page is missing.
896
+
881
897
  Term files should follow the following structure:
882
898
 
883
899
  ```asciidoc
@@ -1113,7 +1129,7 @@ This extension adds accurate git-based created and modified dates to documentati
1113
1129
  The extension adds the following attributes to each page:
1114
1130
 
1115
1131
  - `page-git-created-date`: The date when the file was first committed (YYYY-MM-DD format)
1116
- - `page-git-modified-date`: The date when the file was last modified (YYYY-MM-DD format)
1132
+ - `page-git-modified-date`: The date when the file was last modified (YYYY-MM-DD format). For a page with Connect reference content, this is the date of the most recent Connect release that changed that content, when it is later than the page's last commit (see <<Redpanda Connect tag modifier>>).
1117
1133
 
1118
1134
  These attributes are available for use in UI templates (for metadata, display banners, etc.) and are exported to markdown files for AI consumption.
1119
1135
 
@@ -180,6 +180,16 @@ async function getTreeFiles (git, gitdir, oid, prefix, cache, treeCache) {
180
180
  return files
181
181
  }
182
182
 
183
+ // The later of two YYYY-MM-DD dates. modify-connect-tag-playbook sets
184
+ // page.connectReferenceModified to the date of the Connect release that last
185
+ // changed a page's generated reference, which lives in a release asset and so
186
+ // in no git commit of the page's repo.
187
+ function laterDate (a, b) {
188
+ if (!b) return a
189
+ if (!a) return b
190
+ return b > a ? b : a
191
+ }
192
+
183
193
  module.exports.register = function () {
184
194
  raiseListenerLimit(this)
185
195
  const logger = this.getLogger('add-git-dates-extension')
@@ -271,7 +281,7 @@ module.exports.register = function () {
271
281
  const dates = fileDateMap.get(relativeFilePath)
272
282
  if (dates) {
273
283
  page.asciidoc.attributes['page-git-created-date'] = dates.created
274
- page.asciidoc.attributes['page-git-modified-date'] = dates.modified
284
+ page.asciidoc.attributes['page-git-modified-date'] = laterDate(dates.modified, page.connectReferenceModified)
275
285
  processedCount++
276
286
  }
277
287
  }
@@ -288,3 +298,5 @@ module.exports.register = function () {
288
298
  logger.info(`Git dates added: processed=${processedCount}, skipped=${skippedCount}, duration=${duration}ms (${perPage}ms/page)`)
289
299
  })
290
300
  }
301
+
302
+ module.exports._internal = Object.assign(module.exports._internal || {}, { laterDate })