@redpanda-data/docs-extensions-and-macros 4.2.5 → 4.4.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 (45) hide show
  1. package/README.adoc +184 -21
  2. package/bin/doc-tools.js +328 -0
  3. package/cli-utils/add-caret-external-links.py +68 -0
  4. package/cli-utils/beta-from-antora.js +27 -0
  5. package/cli-utils/generate-cluster-docs.sh +83 -0
  6. package/cli-utils/install-test-dependencies.sh +158 -0
  7. package/cli-utils/python-venv.sh +20 -0
  8. package/cli-utils/start-cluster.sh +53 -0
  9. package/docker-compose/bootstrap.yml +67 -0
  10. package/docker-compose/docker-compose.yml +414 -0
  11. package/docker-compose/generate-profiles.yaml +77 -0
  12. package/docker-compose/rpk-profile.yaml +24 -0
  13. package/docker-compose/transactions-schema.json +37 -0
  14. package/docker-compose/transactions.md +46 -0
  15. package/docker-compose/transform/README.adoc +73 -0
  16. package/docker-compose/transform/go.mod +5 -0
  17. package/docker-compose/transform/go.sum +2 -0
  18. package/docker-compose/transform/regex.wasm +0 -0
  19. package/docker-compose/transform/transform.go +122 -0
  20. package/docker-compose/transform/transform.yaml +33 -0
  21. package/extension-utils/compute-out.js +38 -0
  22. package/extension-utils/create-asciidoc-file.js +15 -0
  23. package/macros/data-template.js +591 -0
  24. package/package.json +21 -4
  25. package/tools/docusaurus-to-antora-conversion-scripts/convert-docs.sh +114 -0
  26. package/tools/docusaurus-to-antora-conversion-scripts/get-file-changes.sh +9 -0
  27. package/tools/docusaurus-to-antora-conversion-scripts/post-process-asciidoc.js +63 -0
  28. package/tools/docusaurus-to-antora-conversion-scripts/pre-process-markdown.js +108 -0
  29. package/tools/fetch-from-github.js +63 -0
  30. package/tools/gen-rpk-ascii.py +477 -0
  31. package/tools/get-console-version.js +53 -0
  32. package/tools/get-redpanda-version.js +53 -0
  33. package/tools/metrics/metrics.py +199 -0
  34. package/tools/metrics/requirements.txt +1 -0
  35. package/tools/property-extractor/Makefile +99 -0
  36. package/tools/property-extractor/README.adoc +206 -0
  37. package/tools/property-extractor/definitions.json +245 -0
  38. package/tools/property-extractor/file_pair.py +7 -0
  39. package/tools/property-extractor/json-to-asciidoc/generate_docs.py +460 -0
  40. package/tools/property-extractor/parser.py +224 -0
  41. package/tools/property-extractor/property_bag.py +4 -0
  42. package/tools/property-extractor/property_extractor.py +243 -0
  43. package/tools/property-extractor/requirements.txt +2 -0
  44. package/tools/property-extractor/tests/transformers_test.py +376 -0
  45. package/tools/property-extractor/transformers.py +397 -0
package/README.adoc CHANGED
@@ -123,7 +123,7 @@ antora:
123
123
  - home:ROOT:attachment$custom-file.txt
124
124
  ----
125
125
 
126
- ==== Registration example
126
+ ==== Registration
127
127
 
128
128
  [source,yaml]
129
129
  ----
@@ -155,7 +155,7 @@ Any elements, classes, or IDs that you want to exclude from the index.
155
155
  index-latest-only (optional)::
156
156
  Whether to index all versions or just the latest version of a component.
157
157
 
158
- ==== Registration example
158
+ ==== Registration
159
159
 
160
160
  ```yaml
161
161
  antora:
@@ -246,7 +246,7 @@ This extension does not require any environment variables.
246
246
 
247
247
  There are no configurable options for this extension.
248
248
 
249
- ==== Registration example
249
+ ==== Registration
250
250
 
251
251
  ```yaml
252
252
  antora:
@@ -297,7 +297,7 @@ antora:
297
297
  upgrade_doc: ROOT:upgrade:index.adoc
298
298
  ----
299
299
 
300
- ==== Registration example
300
+ ==== Registration
301
301
 
302
302
  You can register the extension with a customized configuration for different components in your playbook:
303
303
 
@@ -456,7 +456,7 @@ NOTE: If you don't set the environment variable, the latest version of Redpanda
456
456
 
457
457
  There are no configurable options for this extension.
458
458
 
459
- ==== Registration Example
459
+ ==== Registration
460
460
 
461
461
  ```yaml
462
462
  antora:
@@ -488,7 +488,7 @@ The following attributes are available to the latest version of the `ROOT` compo
488
488
 
489
489
  NOTE: If you don't set the environment variable, the latest versions may not be fetched. When the environment variable is not set, the extension sends unauthenticated requests to GitHub. Unauthenticated requests may result in hitting the API rate limit and cause GitHub to reject the request.
490
490
 
491
- ==== Registration example
491
+ ==== Registration
492
492
 
493
493
  ```yaml
494
494
  antora:
@@ -508,7 +508,7 @@ This extension does not require any environment variables.
508
508
 
509
509
  There are no configurable options for this extension. It operates based on site attributes defined in `add-global-attributes.js` to determine valid categories and subcategories.
510
510
 
511
- ==== Registration example
511
+ ==== Registration
512
512
 
513
513
  Register the `validate-attributes` extension in the Antora playbook under the `antora.extensions` key like so:
514
514
 
@@ -531,7 +531,7 @@ This extension operates without requiring any specific environment variables.
531
531
 
532
532
  This extension does not offer configurable options. It uses the inherent attributes of pages to determine relationships based on `page-categories` and deployment types (`env-kubernetes`, `env-linux`, `env-docker`, `page-cloud`).
533
533
 
534
- ==== Registration example
534
+ ==== Registration
535
535
 
536
536
  To integrate the `related-docs-extension` into your Antora playbook, add it under the `antora.extensions` key as demonstrated below:
537
537
 
@@ -554,7 +554,7 @@ This extension does not require any environment variables.
554
554
 
555
555
  The extension operates without explicit configuration options. It automatically processes documentation pages to identify and link related labs based on shared `page-categories` attributes and deployment types (`env-kubernetes`, `env-linux`, `env-docker`, `page-cloud`).
556
556
 
557
- ==== Registration example
557
+ ==== Registration
558
558
 
559
559
  Include the `related-labs-extension` in the Antora playbook under the `antora.extensions` key as follows:
560
560
 
@@ -579,7 +579,7 @@ The extension accepts the following configuration options:
579
579
 
580
580
  attributespath (optional):: Specifies the path to a local YAML file that contains global attributes. If this is provided, the extension will load attributes from this file first. If this path is not provided or no valid attributes are found in the file, the extension will fall back to loading attributes from the `shared` component.
581
581
 
582
- ==== Registration example
582
+ ==== Registration
583
583
 
584
584
  ```yml
585
585
  antora:
@@ -602,7 +602,7 @@ This extension does not require any environment variables.
602
602
 
603
603
  There are no configurable options for this extension.
604
604
 
605
- ==== Registration example
605
+ ==== Registration
606
606
 
607
607
  ```yaml
608
608
  antora:
@@ -640,7 +640,7 @@ data.replacements (required):: An array of replacement configurations. Each conf
640
640
 
641
641
  NOTE: Ensure that `file_patterns` accurately reflect the paths of the attachments you want to process. Overly broad patterns may include unintended files, while overly restrictive patterns might exclude necessary resources.
642
642
 
643
- ==== Registration Example
643
+ ==== Registration
644
644
 
645
645
  This is an example of how to register and configure the `replace-attributes-in-attachments` extension in your Antora playbook. This example demonstrates defining multiple replacement configurations, each targeting different components and specifying their own file patterns and custom replacements.
646
646
 
@@ -698,7 +698,7 @@ Term files should follow the following structure:
698
698
  This is the detailed description of the term.
699
699
  ```
700
700
 
701
- ==== Registration example
701
+ ==== Registration
702
702
 
703
703
  ```yml
704
704
  antora:
@@ -729,7 +729,7 @@ Whether to add unlisted pages to the navigation. The default is `false` (unliste
729
729
  unlistedPagesHeading (optional)::
730
730
  The heading under which to list the unlisted pages in the navigation. The default is 'Unlisted Pages'.
731
731
 
732
- ==== Registration example
732
+ ==== Registration
733
733
 
734
734
  ```yaml
735
735
  antora:
@@ -749,7 +749,7 @@ IMPORTANT: Be sure to register each extension under the `asciidoc.extensions` ke
749
749
 
750
750
  This extension adds the necessary classes to make line numbers and line highlighting work with Prism.js.
751
751
 
752
- ==== Registration example
752
+ ==== Registration
753
753
 
754
754
  ```yaml
755
755
  antora:
@@ -763,6 +763,108 @@ This section documents the Asciidoc macros that are provided by this library and
763
763
 
764
764
  IMPORTANT: Be sure to register each extension under the `asciidoc.extensions` key in the playbook, not the `antora.extensions` key.
765
765
 
766
+ === data_template
767
+
768
+ The `data_template` block processor lets you render dynamic AsciiDoc content from external or local data sources (JSON, YAML, or plain text) using Handlebars templates.
769
+
770
+ This is useful for generating documentation from structured data like config fields, component metadata, or examples.
771
+
772
+ === Usage
773
+
774
+ You can use the `data_template` block macro to dynamically generate AsciiDoc content from structured data files such as JSON or YAML. The macro uses a Handlebars template to render the data.
775
+
776
+ [source,asciidoc]
777
+ ----
778
+ [data_template, ROOT:example$connect.json]
779
+ --
780
+ == Component: {{{name}}}
781
+
782
+ Summary: {{{summary}}}
783
+
784
+ {{#each fields}}
785
+ === {{name}}
786
+
787
+ *Type*: `{{type}}`
788
+
789
+ {{{description}}}
790
+ {{/each}}
791
+ --
792
+ ----
793
+
794
+ The block content is a Handlebars template. Fields from the data file are injected into this template during site build.
795
+
796
+ The macro accepts one or two positional attributes:
797
+
798
+ 1. The first attribute (`dataPath`) is required and should be the resource ID of the data file.
799
+ 2. The second attribute (`overrides`) is optional and allows you to override or merge values from a secondary file.
800
+
801
+ You can apply overrides by specifying a second file:
802
+
803
+ [source,asciidoc]
804
+ ----
805
+ [data_template, ROOT:example$connect.json, ROOT:example$overrides.json]
806
+ --
807
+ ...template content...
808
+ --
809
+ ----
810
+
811
+ In this case:
812
+
813
+ - The macro first loads and parses `connect.json`.
814
+ - Then it loads `overrides.json` and **merges** its values into the base file.
815
+ - Arrays of objects (such as fields or processors) are merged by matching objects with the same `name` key.
816
+ - The result is passed into the Handlebars template.
817
+
818
+ This is useful for tweaking content (like updating a `description`) without modifying the original source file.
819
+
820
+ ==== Triple mustaches
821
+
822
+ When your data contains AsciiDoc markup (like lists, admonitions, or headings), use triple curly braces:
823
+
824
+ [source,handlebars]
825
+ ----
826
+ {{{description}}}
827
+ ----
828
+
829
+ This tells Handlebars to not escape the content, so Asciidoctor can render it correctly.
830
+
831
+ ==== Handlebars helpers
832
+
833
+ The following helpers are available inside templates:
834
+
835
+ * `eq`, `ne` — Equality helpers.
836
+ * `uppercase` — Converts text to uppercase.
837
+ * `renderConnectFields` — Renders config fields for Redpanda Connect.
838
+ * `renderConnectExamples` — Renders usage examples.
839
+ * `selectByJsonPath` — Selects items from the data using a JSONPath expression.
840
+
841
+ ==== Registration
842
+
843
+ To use this macro, register it in your Antora playbook:
844
+
845
+ [source,yaml]
846
+ ----
847
+ asciidoc:
848
+ extensions:
849
+ - '@redpanda-data/docs-extensions-and-macros/macros/data-template'
850
+ ----
851
+
852
+ ==== Example
853
+
854
+ You can use the `selectByJsonPath` helper to filter data. For example, if you want to render only the `redis` processor's fields from a JSON file, you can do it like this:
855
+
856
+ [source,asciidoc]
857
+ ----
858
+ [data_template, redpanda-connect:ROOT:example$connect.json]
859
+ --
860
+ {{#selectByJsonPath this "$.processors[?(@.name=='redis')]" }}
861
+ {{renderConnectFields this.config.children}}
862
+ {{/selectByJsonPath}}
863
+ --
864
+ ----
865
+
866
+ This will render only the fields for the `redis` processor.
867
+
766
868
  === config_ref
767
869
 
768
870
  This inline macro is used to generate a reference to a configuration value in the Redpanda documentation. The macro's parameters allow for control over the generated reference's format and the type of output produced.
@@ -798,7 +900,7 @@ For example:
798
900
  config_ref:example_config,true,tunable-properties[]
799
901
  ----
800
902
 
801
- ==== Registration example
903
+ ==== Registration
802
904
 
803
905
  [,yaml]
804
906
  ----
@@ -807,6 +909,67 @@ asciidoc:
807
909
  - '@redpanda-data/docs-extensions-and-macros/macros/config-ref'
808
910
  ----
809
911
 
912
+ === data_template
913
+
914
+ The `data_template` macro provides a way to dynamically generate AsciiDoc content by combining external or local data sources with Handlebars templates. When you use the `data_template` block macro, the extension performs the following steps:
915
+
916
+ * Resolves the `dataPath` attribute to locate a data file (in JSON, YAML, or raw text format).
917
+ * Fetches and caches external resources or reads local files from the Antora content catalog.
918
+ * Parses the data file.
919
+ * Compiles the block's content as a Handlebars template, injecting the parsed data.
920
+ * Processes the resulting text as AsciiDoc using Asciidoctor to generate the final HTML output.
921
+
922
+ By default, Handlebars escapes HTML to prevent potential security issues. However, if your data includes AsciiDoc markup (such as headings, lists, or formatting directives), escaping it will prevent Asciidoctor from converting the markup correctly.
923
+
924
+ To ensure that your AsciiDoc syntax is preserved during template rendering, **use the triple curly braces syntax** in your Handlebars templates. For example, if your JSON file contains a `description` field with AsciiDoc content, reference it like this:
925
+
926
+ [source,handlebars]
927
+ ----
928
+ {{{description}}}
929
+ ----
930
+
931
+ This tells Handlebars to output the content unescaped, allowing Asciidoctor to process the raw AsciiDoc markup correctly.
932
+
933
+ ==== Usage
934
+
935
+ In an AsciiDoc document, you can invoke the data template macro as follows:
936
+
937
+ [,asciidoc]
938
+ ----
939
+ [data_template, ROOT:example$connect.json]
940
+ --
941
+ Version: {{{version}}}
942
+
943
+ {{#each buffers}}
944
+
945
+ === {{{this.name}}}
946
+
947
+ Status: {{{this.status}}}
948
+
949
+ {{#if (eq this.name 'memory')}}
950
+ This is a custom description for the memory buffer.
951
+ {{else}}
952
+ {{{this.summary}}}
953
+ {{/if}}
954
+
955
+ {{/each}}
956
+
957
+ --
958
+ ----
959
+
960
+ ==== Registration
961
+
962
+ Register the macro in your Antora playbook under the `asciidoc.extensions` key:
963
+
964
+ [source,yaml]
965
+ ----
966
+ asciidoc:
967
+ extensions:
968
+ - require: '@redpanda-data/docs-extensions-and-macros/macros/data-template'
969
+ ----
970
+
971
+ This configuration ensures that during the build process, the data template macro is executed to fetch, parse, and render data as part of your docs.
972
+
810
973
  === glossterm
811
974
 
812
975
  The `glossterm` inline macro provides a way to define and reference glossary terms in your AsciiDoc documents.
@@ -855,7 +1018,7 @@ Whether to enable tooltips for the defined terms. Valid values are:
855
1018
 
856
1019
  The last two options are intended to support js/css tooltip solutions such as tippy.js.
857
1020
 
858
- ==== Registration example
1021
+ ==== Registration
859
1022
 
860
1023
  [,yaml]
861
1024
  ----
@@ -897,7 +1060,7 @@ For default values and documentation for configuration options, see the https://
897
1060
 
898
1061
  If you do not specify a Helm reference value, the macro generates a link without specifying a path.
899
1062
 
900
- ==== Registration example
1063
+ ==== Registration
901
1064
 
902
1065
  [,yaml]
903
1066
  ----
@@ -918,7 +1081,7 @@ The categories are fetched from the `connectCategoriesData` that's generated in
918
1081
  components_by_category::[<type>]
919
1082
  ```
920
1083
 
921
- ==== Registration example
1084
+ ==== Registration
922
1085
 
923
1086
  ```yaml
924
1087
  asciidoc:
@@ -938,7 +1101,7 @@ The types are fetched from the `flatComponentsData` that's generated in the <<Co
938
1101
  component_table::[]
939
1102
  ```
940
1103
 
941
- ==== Registration example
1104
+ ==== Registration
942
1105
 
943
1106
  ```yaml
944
1107
  asciidoc:
@@ -958,7 +1121,7 @@ The types are fetched from the `flatComponentsData` that's generated in the <<Co
958
1121
  component_type_dropdown::[]
959
1122
  ```
960
1123
 
961
- ==== Registration example
1124
+ ==== Registration
962
1125
 
963
1126
  ```yaml
964
1127
  asciidoc:
@@ -0,0 +1,328 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawnSync } = require('child_process');
4
+ const { Command } = require('commander');
5
+ const path = require('path');
6
+ const fs = require('fs');
7
+
8
+ // --------------------------------------------------------------------
9
+ // Dependency check functions
10
+ // --------------------------------------------------------------------
11
+ function checkDependency(command, versionArg, name, helpURL) {
12
+ try {
13
+ execSync(`${command} ${versionArg}`, { stdio: 'ignore' });
14
+ } catch (error) {
15
+ console.error(`Error: ${name} is required but not found or not working properly.
16
+ Please install ${name} and try again.
17
+ For more info, see: ${helpURL}`);
18
+ process.exit(1);
19
+ }
20
+ }
21
+
22
+ function checkCommandExists(command) {
23
+ try {
24
+ execSync(`which ${command}`, { stdio: 'ignore' });
25
+ return true;
26
+ } catch (error) {
27
+ console.error(`Error: \`${command}\` is required but not found. Please install \`${command}\` and try again.`);
28
+ return false;
29
+ }
30
+ }
31
+
32
+ function checkMake() {
33
+ if (!checkCommandExists('make')) {
34
+ console.error('Error: `make` is required but not found. Please install `make` to use the automation Makefile. For help, see: https://www.google.com/search?q=how+to+install+make');
35
+ process.exit(1);
36
+ }
37
+ }
38
+
39
+ function checkPython() {
40
+ const candidates = ['python3', 'python'];
41
+ let found = false;
42
+
43
+ for (const cmd of candidates) {
44
+ try {
45
+ const versionOutput = execSync(`${cmd} --version`, {
46
+ encoding: 'utf8',
47
+ stdio: ['pipe', 'pipe', 'ignore']
48
+ }).trim();
49
+ // versionOutput looks like "Python 3.x.y"
50
+ const versionString = versionOutput.split(' ')[1];
51
+ const [major, minor] = versionString.split('.').map(Number);
52
+ if (major > 3 || (major === 3 && minor >= 10)) {
53
+ found = true;
54
+ break;
55
+ } else {
56
+ console.error(`Error: Python 3.10 or higher is required. Detected version: ${versionString}`);
57
+ process.exit(1);
58
+ }
59
+ } catch {
60
+ // this candidate didn’t exist or errored—try the next one
61
+ }
62
+ }
63
+ if (!found) {
64
+ console.error('Error: Python 3.10 or higher is required but not found.\nPlease install Python and ensure `python3 --version` or `python --version` returns at least 3.10: https://www.geeksforgeeks.org/how-to-install-python-on-mac/');
65
+ process.exit(1);
66
+ }
67
+ }
68
+
69
+ function checkCompiler() {
70
+ const gccInstalled = checkCommandExists('gcc');
71
+ const clangInstalled = checkCommandExists('clang');
72
+ if (!gccInstalled && !clangInstalled) {
73
+ console.error('Error: A C++ compiler (such as gcc or clang) is required but not found. Please install one: https://osxdaily.com/2023/05/02/how-install-gcc-mac/');
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ function checkDocker() {
79
+ checkDependency('docker', '--version', 'Docker', 'https://docs.docker.com/get-docker/');
80
+ try {
81
+ execSync('docker info', { stdio: 'ignore' });
82
+ } catch (error) {
83
+ console.error('Error: Docker daemon appears to be not running. Please start Docker.');
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ function verifyPropertyDependencies() {
89
+ checkMake();
90
+ checkPython();
91
+ checkCompiler();
92
+ }
93
+
94
+ function verifyMetricsDependencies() {
95
+ checkPython();
96
+ if (!checkCommandExists('curl') || !checkCommandExists('tar')) {
97
+ // `checkCommandExists` already prints a helpful message.
98
+ process.exit(1);
99
+ }
100
+ checkDocker();
101
+ }
102
+ // --------------------------------------------------------------------
103
+ // Main CLI Definition
104
+ // --------------------------------------------------------------------
105
+ const programCli = new Command();
106
+
107
+ programCli
108
+ .name('doc-tools')
109
+ .description('Redpanda Document Automation CLI')
110
+ .version('1.0.0');
111
+
112
+ // Top-level commands.
113
+ programCli
114
+ .command('install-test-dependencies')
115
+ .description('Install packages for doc test workflows')
116
+ .action(() => {
117
+ const scriptPath = path.join(__dirname, '../cli-utils/install-test-dependencies.sh');
118
+ const result = spawnSync(scriptPath, { stdio: 'inherit', shell: true });
119
+ process.exit(result.status);
120
+ });
121
+
122
+ programCli
123
+ .command('get-redpanda-version')
124
+ .description('Print the latest Redpanda version')
125
+ .option('--beta', 'Return the latest RC (beta) version if available')
126
+ .option('--from-antora', 'Read prerelease flag from local antora.yml')
127
+ .action(async (options) => {
128
+ try {
129
+ await require('../tools/get-redpanda-version.js')(options);
130
+ } catch (err) {
131
+ console.error(err);
132
+ process.exit(1);
133
+ }
134
+ });
135
+
136
+ programCli
137
+ .command('get-console-version')
138
+ .description('Print the latest Console version')
139
+ .option('--beta', 'Return the latest beta version if available')
140
+ .option('--from-antora', 'Read prerelease flag from local antora.yml')
141
+ .action(async (options) => {
142
+ try {
143
+ await require('../tools/get-console-version.js')(options);
144
+ } catch (err) {
145
+ console.error(err);
146
+ process.exit(1);
147
+ }
148
+ });
149
+
150
+ // Create an "automation" subcommand group.
151
+ const automation = new Command('generate')
152
+ .description('Run docs automations (properties, metrics, and rpk docs generation)');
153
+
154
+ // --------------------------------------------------------------------
155
+ // Automation Subcommands: Delegate to a unified Bash script internally.
156
+ // --------------------------------------------------------------------
157
+
158
+ // Common options for both automation tasks.
159
+ const commonOptions = {
160
+ tag: 'latest',
161
+ dockerRepo: 'redpanda',
162
+ consoleTag: 'latest',
163
+ consoleDockerRepo: 'console'
164
+ };
165
+
166
+ function runClusterDocs(mode, tag, options) {
167
+ const script = path.join(__dirname, '../cli-utils/generate-cluster-docs.sh');
168
+ const args = [ mode, tag, options.dockerRepo, options.consoleTag, options.consoleDockerRepo ];
169
+ console.log(`Running ${script} with arguments: ${args.join(' ')}`);
170
+ const r = spawnSync('bash', [ script, ...args ], { stdio: 'inherit', shell: true });
171
+ if (r.status !== 0) process.exit(r.status);
172
+ }
173
+
174
+ // helper to diff two autogenerated directories
175
+ function diffDirs(kind, oldTag, newTag) {
176
+ const oldDir = path.join('autogenerated', oldTag, kind);
177
+ const newDir = path.join('autogenerated', newTag, kind);
178
+ const diffDir = path.join('autogenerated', 'diffs', kind, `${oldTag}_to_${newTag}`);
179
+ const patch = path.join(diffDir, 'changes.patch');
180
+
181
+ if (!fs.existsSync(oldDir)) {
182
+ console.error(`❌ Cannot diff: missing ${oldDir}`);
183
+ process.exit(1);
184
+ }
185
+ if (!fs.existsSync(newDir)) {
186
+ console.error(`❌ Cannot diff: missing ${newDir}`);
187
+ process.exit(1);
188
+ }
189
+
190
+ fs.mkdirSync(diffDir, { recursive: true });
191
+
192
+ const cmd = `diff -ru "${oldDir}" "${newDir}" > "${patch}" || true`;
193
+ const res = spawnSync(cmd, { stdio: 'inherit', shell: true });
194
+
195
+ if (res.error) {
196
+ console.error(`❌ diff failed: ${res.error.message}`);
197
+ process.exit(1);
198
+ }
199
+ console.log(`✅ Wrote patch: ${patch}`);
200
+ }
201
+
202
+ automation
203
+ .command('metrics-docs')
204
+ .description('Extract Redpanda metrics and generate JSON/AsciiDoc docs')
205
+ .option('--tag <tag>', 'Redpanda tag (default: latest)', commonOptions.tag)
206
+ .option('--docker-repo <repo>', '...', commonOptions.dockerRepo)
207
+ .option('--console-tag <tag>', '...', commonOptions.consoleTag)
208
+ .option('--console-docker-repo <repo>', '...', commonOptions.consoleDockerRepo)
209
+ .option('--diff <oldTag>', 'Also diff autogenerated metrics from <oldTag> → <tag>')
210
+ .action((options) => {
211
+ verifyMetricsDependencies();
212
+
213
+ const newTag = options.tag;
214
+ const oldTag = options.diff;
215
+
216
+ if (oldTag) {
217
+ const oldDir = path.join('autogenerated', oldTag, 'metrics');
218
+ if (!fs.existsSync(oldDir)) {
219
+ console.log(`⏳ Generating metrics docs for old tag ${oldTag}…`);
220
+ runClusterDocs('metrics', oldTag, options);
221
+ }
222
+ }
223
+
224
+ console.log(`⏳ Generating metrics docs for new tag ${newTag}…`);
225
+ runClusterDocs('metrics', newTag, options);
226
+
227
+ if (oldTag) {
228
+ diffDirs('metrics', oldTag, newTag);
229
+ }
230
+
231
+ process.exit(0);
232
+ });
233
+
234
+ automation
235
+ .command('property-docs')
236
+ .description('Extract properties from Redpanda source')
237
+ .option('--tag <tag>', 'Git tag or branch to extract from (default: dev)', 'dev')
238
+ .option('--diff <oldTag>', 'Also diff autogenerated properties from <oldTag> → <tag>')
239
+ .action((options) => {
240
+ verifyPropertyDependencies();
241
+
242
+ const newTag = options.tag;
243
+ const oldTag = options.diff;
244
+ const cwd = path.resolve(__dirname, '../tools/property-extractor');
245
+ const make = (tag) => {
246
+ console.log(`⏳ Building property docs for ${tag}…`);
247
+ const r = spawnSync('make', ['build', `TAG=${tag}`], { cwd, stdio: 'inherit' });
248
+ if (r.error ) { console.error(r.error); process.exit(1); }
249
+ if (r.status !== 0) process.exit(r.status);
250
+ };
251
+
252
+ if (oldTag) {
253
+ const oldDir = path.join('autogenerated', oldTag, 'properties');
254
+ if (!fs.existsSync(oldDir)) make(oldTag);
255
+ }
256
+
257
+ make(newTag);
258
+
259
+ if (oldTag) {
260
+ diffDirs('properties', oldTag, newTag);
261
+ }
262
+
263
+ process.exit(0);
264
+ });
265
+
266
+ automation
267
+ .command('rpk-docs')
268
+ .description('Generate documentation for rpk commands')
269
+ .option('--tag <tag>', 'Redpanda tag (default: latest)', commonOptions.tag)
270
+ .option('--docker-repo <repo>', '...', commonOptions.dockerRepo)
271
+ .option('--console-tag <tag>', '...', commonOptions.consoleTag)
272
+ .option('--console-docker-repo <repo>', '...', commonOptions.consoleDockerRepo)
273
+ .option('--diff <oldTag>', 'Also diff autogenerated rpk docs from <oldTag> → <tag>')
274
+ .action((options) => {
275
+ verifyMetricsDependencies();
276
+
277
+ const newTag = options.tag;
278
+ const oldTag = options.diff;
279
+
280
+ if (oldTag) {
281
+ const oldDir = path.join('autogenerated', oldTag, 'rpk');
282
+ if (!fs.existsSync(oldDir)) {
283
+ console.log(`⏳ Generating rpk docs for old tag ${oldTag}…`);
284
+ runClusterDocs('rpk', oldTag, options);
285
+ }
286
+ }
287
+
288
+ console.log(`⏳ Generating rpk docs for new tag ${newTag}…`);
289
+ runClusterDocs('rpk', newTag, options);
290
+
291
+ if (oldTag) {
292
+ diffDirs('rpk', oldTag, newTag);
293
+ }
294
+
295
+ process.exit(0);
296
+ });
297
+
298
+ programCli
299
+ .command('fetch')
300
+ .description('Fetch a file or directory from GitHub and save locally')
301
+ .requiredOption('-o, --owner <owner>', 'GitHub repo owner or org')
302
+ .requiredOption('-r, --repo <repo>', 'GitHub repo name')
303
+ .requiredOption('-p, --remote-path <path>', 'Path in the repo to fetch')
304
+ .requiredOption('-d, --save-dir <dir>', 'Local directory to save into')
305
+ .option('-f, --filename <name>', 'Custom filename to save as')
306
+ .action(async (options) => {
307
+ try {
308
+ const fetchFromGithub = await require('../tools/fetch-from-github.js');
309
+ // options.owner, options.repo, options.remotePath, options.saveDir, options.filename
310
+ await fetchFromGithub(
311
+ options.owner,
312
+ options.repo,
313
+ options.remotePath,
314
+ options.saveDir,
315
+ options.filename
316
+ );
317
+ } catch (err) {
318
+ console.error('❌', err.message);
319
+ process.exit(1);
320
+ }
321
+ });
322
+
323
+
324
+ // Attach the automation group to the main program.
325
+ programCli.addCommand(automation);
326
+
327
+ programCli.parse(process.argv);
328
+