@redpanda-data/docs-extensions-and-macros 5.53.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 (51) hide show
  1. package/bin/doc-tools-mcp.js +4 -5
  2. package/bin/doc-tools.js +166 -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 +64 -12
  11. package/extensions/add-git-dates.js +13 -1
  12. package/extensions/aggregate-terms.js +34 -20
  13. package/extensions/generate-rp-connect-info.js +79 -24
  14. package/extensions/modify-connect-tag-playbook.js +498 -75
  15. package/extensions/resolve-xrefs-in-attachments.js +1 -12
  16. package/extensions/url-to-xref.js +3 -0
  17. package/extensions/util/connect-catalog.js +10 -5
  18. package/extensions/util/connect-docs-asset.js +188 -0
  19. package/extensions/util/connect-github.js +16 -0
  20. package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
  21. package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
  22. package/extensions/version-fetcher/list-connect-releases.js +10 -0
  23. package/extensions/version-fetcher/release-lines.js +64 -0
  24. package/extensions/version-fetcher/set-latest-version.js +73 -3
  25. package/macros/README.adoc +1 -0
  26. package/macros/REFERENCE.adoc +99 -0
  27. package/macros/badge.js +5 -8
  28. package/macros/enterprise.js +2 -5
  29. package/macros/glossary.js +4 -14
  30. package/macros/iceberg-explorer.js +242 -0
  31. package/macros/prop.js +3 -13
  32. package/mcp/USER_GUIDE.adoc +25 -135
  33. package/package.json +4 -2
  34. package/tools/connect-docs/check-build-log.js +311 -0
  35. package/tools/connect-docs/check-rendered-html.js +469 -0
  36. package/tools/connect-docs/ci-playbook.yml +77 -0
  37. package/tools/connect-docs/diff-generated.js +382 -0
  38. package/tools/connect-docs/pr-comment.js +104 -0
  39. package/tools/doc-impact-recording/index.js +190 -0
  40. package/tools/overrides-audit/README.adoc +1 -2
  41. package/tools/overrides-audit/adapters/properties.js +1 -1
  42. package/tools/overrides-audit/index.js +3 -4
  43. package/tools/redpanda-connect/AUTOMATION.md +3 -36
  44. package/tools/redpanda-connect/README.adoc +5 -33
  45. package/tools/redpanda-connect/generate-rpcn-connector-docs.js +3 -3
  46. package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
  47. package/tools/redpanda-connect/report-delta.js +6 -3
  48. package/tools/redpanda-connect/rpcn-connector-docs-handler.js +38 -16
  49. package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
  50. package/tools/rpk-docs/templates/command.hbs +1 -1
  51. 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
  *
@@ -2930,6 +2967,133 @@ programCli
2930
2967
  runCli(options)
2931
2968
  })
2932
2969
 
2970
+ /**
2971
+ * check-build-log
2972
+ *
2973
+ * @description
2974
+ * Check an Antora JSON log (`antora --log-format json`): exit 1 when any
2975
+ * record is at level error or fatal, and, with --min-pages and --site-dir,
2976
+ * when fewer HTML pages were written than expected. Prints a Markdown
2977
+ * summary (counts by level, then the error and warning messages grouped and
2978
+ * deduplicated, with file and line) for $GITHUB_STEP_SUMMARY. Warnings never
2979
+ * fail the check. With --blocking-sources, only errors from those
2980
+ * repositories (or with no source) fail it, so a broken page in another
2981
+ * content source of the playbook is reported without blocking.
2982
+ *
2983
+ * @why
2984
+ * Antora's --log-failure-level fails the build but explains nothing in a job
2985
+ * summary, and cannot catch a build that exits 0 having written no files,
2986
+ * which is what Antora does on an unsupported Node version. The Connect docs
2987
+ * PR check runs Antora with --log-failure-level=fatal so this command, not
2988
+ * Antora, decides.
2989
+ *
2990
+ * @example
2991
+ * npx antora --log-format json --log-failure-level=fatal antora-playbook.yml > build.ndjson
2992
+ * npx doc-tools check-build-log build.ndjson --site-dir build/site/connect --min-pages 400 >> "$GITHUB_STEP_SUMMARY"
2993
+ *
2994
+ * # Block only on errors from these repositories; list the rest
2995
+ * npx doc-tools check-build-log build.ndjson --blocking-sources redpanda-data/rp-connect-docs,redpanda-data/connect
2996
+ */
2997
+ programCli
2998
+ .command('check-build-log')
2999
+ .description('Check an Antora JSON build log: fail on error or fatal records or too few pages, print a Markdown summary')
3000
+ .argument('<log>', 'Antora log written with --log-format json (one JSON record per line)')
3001
+ .option('--site-dir <dir>', 'Output directory to count HTML pages in (with --min-pages)')
3002
+ .option('--min-pages <n>', 'Fail when fewer than <n> HTML pages were written under --site-dir')
3003
+ .option('--blocking-sources <repos>', 'Comma-separated owner/name repositories whose errors block (default: all). Errors with no source always block; others are listed only')
3004
+ .option('--format <format>', 'Output format: markdown or json', 'markdown')
3005
+ .option('--output <path>', 'Also write the output to this file')
3006
+ .action((log, options) => {
3007
+ const { runCli } = require('../tools/connect-docs/check-build-log')
3008
+ runCli(log, options)
3009
+ })
3010
+
3011
+ /**
3012
+ * check-rendered-html
3013
+ *
3014
+ * @description
3015
+ * Scan the rendered HTML of one or more Antora component paths for AsciiDoc
3016
+ * that did not convert: literal backticks outside code, literal `xref:` or
3017
+ * `include::` text, leftover {page-*} or {env-*} attribute references, `|===`
3018
+ * table markup, Asciidoctor's "Unresolved include directive" text, links with
3019
+ * the `unresolved` class, and empty sections (a heading followed directly by a
3020
+ * heading of the same or higher level). Also checks links: a fragment with no
3021
+ * matching id on its target page (broken-anchor), and a relative or
3022
+ * root-relative link under a link root that is not in the build
3023
+ * (broken-link). External links are ignored; links outside every link root,
3024
+ * or into a root the build does not contain, are counted as unchecked. Only
3025
+ * the page body is scanned. Reports findings per page and exits 0 unless
3026
+ * --strict.
3027
+ *
3028
+ * With --changed-pages, findings are split into those on the listed pages and
3029
+ * the rest, and --strict exits 1 only when a listed page has findings, so
3030
+ * defects already on the base never fail a PR.
3031
+ *
3032
+ * @why
3033
+ * These defects reach readers without a single line in the Antora log: two
3034
+ * backticks for an empty enum option, or a backtick glued to the next word,
3035
+ * render as literal backticks on the published page.
3036
+ *
3037
+ * @example
3038
+ * npx doc-tools check-rendered-html build/site --component connect >> "$GITHUB_STEP_SUMMARY"
3039
+ *
3040
+ * # Fail on any finding
3041
+ * npx doc-tools check-rendered-html build/site --strict
3042
+ *
3043
+ * # Self-managed and Cloud Connect pages; fail only on the pages a PR changed
3044
+ * npx doc-tools check-rendered-html build/site --component connect,cloud-data-platform/develop/connect \
3045
+ * --changed-pages changed-pages.txt --strict
3046
+ */
3047
+ programCli
3048
+ .command('check-rendered-html')
3049
+ .description('Scan rendered Antora HTML for unconverted AsciiDoc (literal backticks, xref:, include::, attributes, tables, empty sections) and broken anchors and links')
3050
+ .argument('<site-dir>', 'Antora output directory (the --to-dir of the build)')
3051
+ .option('--component <path>', 'Component output path to scan under <site-dir>; repeat it or give a comma list, for example connect,cloud-data-platform/develop/connect (default: connect)', (value, previous = []) => previous.concat(value))
3052
+ .option('--link-root <path>', 'Site path whose linked pages must exist; repeat it or give a comma list. Scanned components are always included. Links outside every root, or into a root not in the build, are counted as unchecked (default: connect,cloud-data-platform/develop/connect)', (value, previous = []) => previous.concat(value))
3053
+ .option('--pages <file>', 'Only scan the pages listed in <file>, one path relative to <site-dir> per line')
3054
+ .option('--changed-pages <file>', 'Pages a PR changed: one path relative to <site-dir> per line (the sitePath from connect-docs-diff) or connect-docs-diff --format json output. All pages are still scanned; findings are split into changed and other pages, and --strict fails only on changed pages')
3055
+ .option('--format <format>', 'Output format: markdown or json', 'markdown')
3056
+ .option('--output <path>', 'Also write the output to this file')
3057
+ .option('--strict', 'Exit 1 when any finding exists, or with --changed-pages when a changed page has findings (default: always exit 0)')
3058
+ .action((siteDir, options) => {
3059
+ const { runCli } = require('../tools/connect-docs/check-rendered-html')
3060
+ runCli(siteDir, options)
3061
+ })
3062
+
3063
+ /**
3064
+ * connect-docs-diff
3065
+ *
3066
+ * @description
3067
+ * Compare two trees written by connect's docs generator, for example a PR's
3068
+ * merge base and its head, and map each added, removed, or changed partial or
3069
+ * example to the docs.redpanda.com page that includes it. Prints a Markdown
3070
+ * table of changed pages with links, then a unified diff per file in
3071
+ * collapsible blocks, capped in size. A tree root can be connect's docs/
3072
+ * directory, its modules/ directory, or the components/ directory.
3073
+ *
3074
+ * @why
3075
+ * Writers review Go string literals today and never see the page. The
3076
+ * generated output is the published change: connect strings, benthos strings
3077
+ * from a go.mod bump, and generator changes all show up here.
3078
+ *
3079
+ * @example
3080
+ * npx doc-tools connect-docs-diff base/modules head/modules >> "$GITHUB_STEP_SUMMARY"
3081
+ * npx doc-tools connect-docs-diff base/modules head/modules --format json
3082
+ */
3083
+ programCli
3084
+ .command('connect-docs-diff')
3085
+ .description('Diff two generated Connect docs trees and map each changed partial or example to its published page')
3086
+ .argument('<base-dir>', 'Generated tree of the base (merge base) commit')
3087
+ .argument('<head-dir>', 'Generated tree of the head commit')
3088
+ .option('--format <format>', 'Output format: markdown or json', 'markdown')
3089
+ .option('--site-url <url>', 'Docs site the page links point at', 'https://docs.redpanda.com')
3090
+ .option('--max-bytes <n>', 'Cap on the Markdown output size; diffs past it are counted, not shown', '60000')
3091
+ .option('--output <path>', 'Also write the output to this file')
3092
+ .action((baseDir, headDir, options) => {
3093
+ const { runCli } = require('../tools/connect-docs/diff-generated')
3094
+ runCli(baseDir, headDir, options)
3095
+ })
3096
+
2933
3097
  /**
2934
3098
  * preview-string
2935
3099
  *
@@ -3009,7 +3173,7 @@ overridesGroup
3009
3173
  .description('Classify each override field as redundant, upstreamable, or keep against extracted source strings')
3010
3174
  .requiredOption('--overrides <path>', 'Path to the overrides JSON file (for example docs-data/property-overrides.json)')
3011
3175
  .option('--extracted <path>', 'Path to the extracted source JSON (property extractor raw output; required for the properties surface)')
3012
- .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'))
3013
3177
  .addOption(new Option('--format <format>', 'Output format').choices(['json', 'human']).default('json'))
3014
3178
  .option('--repo <path>', 'Redpanda checkout to extract raw source strings from (alternative to --extracted)')
3015
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