@redpanda-data/docs-extensions-and-macros 4.10.6 → 4.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/doc-tools.js CHANGED
@@ -4,9 +4,7 @@ const { execSync, spawnSync } = require('child_process');
4
4
  const os = require('os');
5
5
  const { Command, Option } = require('commander');
6
6
  const path = require('path');
7
- const yaml = require('yaml');
8
7
  const fs = require('fs');
9
- const handlebars = require('handlebars');
10
8
  const { determineDocsBranch } = require('../cli-utils/self-managed-docs-branch.js');
11
9
  const fetchFromGithub = require('../tools/fetch-from-github.js');
12
10
  const { urlToXref } = require('../cli-utils/convert-doc-links.js');
@@ -1031,12 +1029,12 @@ automation
1031
1029
  });
1032
1030
 
1033
1031
  automation
1034
- .command('property-docs')
1032
+ .command('property-docs')
1035
1033
  .description(
1036
- 'Generate JSON and consolidated AsciiDoc partials for Redpanda configuration properties. ' +
1037
- 'By default, only extracts properties to JSON. Use --generate-partials to create consolidated ' +
1038
- 'AsciiDoc partials (including deprecated properties).'
1039
- )
1034
+ 'Generate JSON and consolidated AsciiDoc partials for Redpanda configuration properties. ' +
1035
+ 'By default, only extracts properties to JSON. Use --generate-partials to create consolidated ' +
1036
+ 'AsciiDoc partials (including deprecated properties).'
1037
+ )
1040
1038
  .option('--tag <tag>', 'Git tag or branch to extract from', 'dev')
1041
1039
  .option('--diff <oldTag>', 'Also diff autogenerated properties from <oldTag> to <tag>')
1042
1040
  .option('--overrides <path>', 'Optional JSON file with property description overrides')
@@ -1049,14 +1047,12 @@ automation
1049
1047
  .option('--template-deprecated-property <path>', 'Custom Handlebars template for individual deprecated property sections')
1050
1048
  .option('--generate-partials', 'Generate consolidated property partials (cluster-properties.adoc, topic-properties.adoc, etc.) in the partials directory')
1051
1049
  .option('--partials-dir <path>', 'Directory for property partials (relative to output-dir)', 'partials')
1052
- .action((options) => {
1053
- verifyPropertyDependencies();
1050
+ .action((options) => {
1051
+ verifyPropertyDependencies();
1054
1052
 
1055
1053
  // Validate cloud support dependencies if requested
1056
1054
  if (options.cloudSupport) {
1057
1055
  console.log('🔍 Validating cloud support dependencies...');
1058
-
1059
- // Check for GITHUB_TOKEN, GH_TOKEN, or REDPANDA_GITHUB_TOKEN
1060
1056
  const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.REDPANDA_GITHUB_TOKEN;
1061
1057
  if (!token) {
1062
1058
  console.error('❌ Cloud support requires GITHUB_TOKEN, GH_TOKEN, or REDPANDA_GITHUB_TOKEN environment variable');
@@ -1068,7 +1064,6 @@ automation
1068
1064
  console.error(' Or: export REDPANDA_GITHUB_TOKEN=your_token_here');
1069
1065
  process.exit(1);
1070
1066
  }
1071
-
1072
1067
  console.log('📦 Cloud support enabled - Python dependencies will be validated during execution');
1073
1068
  if (process.env.VIRTUAL_ENV) {
1074
1069
  console.log(` Using virtual environment: ${process.env.VIRTUAL_ENV}`);
@@ -1077,87 +1072,88 @@ automation
1077
1072
  console.log('✅ GitHub token validated');
1078
1073
  }
1079
1074
 
1080
- const newTag = options.tag;
1081
- const oldTag = options.diff;
1082
- const overridesPath = options.overrides;
1083
- const outputDir = options.outputDir;
1084
- const cwd = path.resolve(__dirname, '../tools/property-extractor');
1085
-
1086
- const make = (tag, overrides, templates = {}, outDir = 'modules/reference/', tempDir = null) => {
1087
- console.log(`⏳ Building property docs for ${tag}${tempDir ? ' (for diff)' : ''}…`);
1088
- const args = ['build', `TAG=${tag}`];
1089
-
1090
- // Pass all paths as environment variables for consistency
1091
- const env = { ...process.env };
1092
-
1093
- if (overrides) {
1094
- env.OVERRIDES = path.resolve(overrides);
1095
- }
1096
- if (options.cloudSupport) {
1097
- env.CLOUD_SUPPORT = '1';
1098
- }
1099
- if (templates.property) {
1100
- env.TEMPLATE_PROPERTY = path.resolve(templates.property);
1101
- }
1102
- if (templates.topicProperty) {
1103
- env.TEMPLATE_TOPIC_PROPERTY = path.resolve(templates.topicProperty);
1104
- }
1105
- if (templates.topicPropertyMappings) {
1106
- env.TEMPLATE_TOPIC_PROPERTY_MAPPINGS = path.resolve(templates.topicPropertyMappings);
1107
- }
1108
- if (templates.deprecated) {
1109
- env.TEMPLATE_DEPRECATED = path.resolve(templates.deprecated);
1110
- }
1111
- if (templates.deprecatedProperty) {
1112
- env.TEMPLATE_DEPRECATED_PROPERTY = path.resolve(templates.deprecatedProperty);
1113
- }
1114
-
1115
- if (tempDir) {
1116
- env.OUTPUT_JSON_DIR = path.resolve(tempDir, 'examples');
1117
- env.OUTPUT_AUTOGENERATED_DIR = path.resolve(tempDir);
1118
- } else {
1119
- env.OUTPUT_JSON_DIR = path.resolve(outDir, 'examples');
1120
- env.OUTPUT_AUTOGENERATED_DIR = path.resolve(outDir);
1121
- }
1122
-
1123
- // Partials generation options
1124
- if (options.generatePartials) {
1125
- env.GENERATE_PARTIALS = '1';
1126
- env.OUTPUT_PARTIALS_DIR = path.resolve(outDir, options.partialsDir || 'partials');
1127
- }
1128
-
1129
- const r = spawnSync('make', args, { cwd, stdio: 'inherit', env });
1130
- if (r.error) {
1131
- console.error(`❌ ${r.error.message}`);
1132
- process.exit(1);
1133
- }
1134
- if (r.status !== 0) process.exit(r.status);
1135
- };
1136
-
1137
- // Collect template options
1138
- const templates = {
1139
- property: options.templateProperty,
1140
- topicProperty: options.templateTopicProperty,
1141
- topicPropertyMappings: options.templateTopicPropertyMappings,
1142
- deprecated: options.templateDeprecated,
1143
- deprecatedProperty: options.templateDeprecatedProperty,
1144
- };
1145
-
1075
+ const newTag = options.tag;
1076
+ let oldTag = options.diff;
1077
+ const overridesPath = options.overrides;
1078
+ const outputDir = options.outputDir;
1079
+ const cwd = path.resolve(__dirname, '../tools/property-extractor');
1080
+
1081
+ // If --diff is not provided, try to get the latest-redpanda-tag from Antora attributes
1082
+ if (!oldTag) {
1083
+ oldTag = getAntoraValue('asciidoc.attributes.latest-redpanda-tag');
1146
1084
  if (oldTag) {
1147
- // Build old version first so its JSON exists for the diff step
1148
- make(oldTag, overridesPath, templates, outputDir, null);
1085
+ console.log(`Using latest-redpanda-tag from Antora attributes for --diff: ${oldTag}`);
1086
+ } else {
1087
+ console.log('No --diff provided and no latest-redpanda-tag found in Antora attributes. Skipping diff.');
1149
1088
  }
1089
+ }
1150
1090
 
1151
- // Build new version
1152
- make(newTag, overridesPath, templates, outputDir, null);
1153
-
1154
- if (oldTag) {
1155
- // Generate property comparison report using the JSON now in outputDir/examples
1156
- generatePropertyComparisonReport(oldTag, newTag, outputDir);
1091
+ const make = (tag, overrides, templates = {}, outDir = 'modules/reference/') => {
1092
+ console.log(`⏳ Building property docs for ${tag}…`);
1093
+ const args = ['build', `TAG=${tag}`];
1094
+ const env = { ...process.env };
1095
+ if (overrides) {
1096
+ env.OVERRIDES = path.resolve(overrides);
1097
+ }
1098
+ if (options.cloudSupport) {
1099
+ env.CLOUD_SUPPORT = '1';
1100
+ }
1101
+ if (templates.property) {
1102
+ env.TEMPLATE_PROPERTY = path.resolve(templates.property);
1103
+ }
1104
+ if (templates.topicProperty) {
1105
+ env.TEMPLATE_TOPIC_PROPERTY = path.resolve(templates.topicProperty);
1106
+ }
1107
+ if (templates.topicPropertyMappings) {
1108
+ env.TEMPLATE_TOPIC_PROPERTY_MAPPINGS = path.resolve(templates.topicPropertyMappings);
1109
+ }
1110
+ if (templates.deprecated) {
1111
+ env.TEMPLATE_DEPRECATED = path.resolve(templates.deprecated);
1112
+ }
1113
+ if (templates.deprecatedProperty) {
1114
+ env.TEMPLATE_DEPRECATED_PROPERTY = path.resolve(templates.deprecatedProperty);
1157
1115
  }
1116
+ env.OUTPUT_JSON_DIR = path.resolve(outDir, 'examples');
1117
+ env.OUTPUT_AUTOGENERATED_DIR = path.resolve(outDir);
1118
+ if (options.generatePartials) {
1119
+ env.GENERATE_PARTIALS = '1';
1120
+ env.OUTPUT_PARTIALS_DIR = path.resolve(outDir, options.partialsDir || 'partials');
1121
+ }
1122
+ const r = spawnSync('make', args, { cwd, stdio: 'inherit', env });
1123
+ if (r.error) {
1124
+ console.error(`❌ ${r.error.message}`);
1125
+ process.exit(1);
1126
+ }
1127
+ if (r.status !== 0) process.exit(r.status);
1128
+ };
1129
+
1130
+ const templates = {
1131
+ property: options.templateProperty,
1132
+ topicProperty: options.templateTopicProperty,
1133
+ topicPropertyMappings: options.templateTopicPropertyMappings,
1134
+ deprecated: options.templateDeprecated,
1135
+ deprecatedProperty: options.templateDeprecatedProperty,
1136
+ };
1137
+
1138
+ const tagsAreSame = oldTag && newTag && oldTag === newTag;
1139
+ if (oldTag && !tagsAreSame) {
1140
+ make(oldTag, overridesPath, templates, outputDir);
1141
+ }
1142
+ make(newTag, overridesPath, templates, outputDir);
1143
+ if (oldTag && !tagsAreSame) {
1144
+ generatePropertyComparisonReport(oldTag, newTag, outputDir);
1145
+ } else if (tagsAreSame) {
1146
+ console.log('--diff and --tag are the same. Skipping diff and Antora config update.');
1147
+ }
1158
1148
 
1159
- process.exit(0);
1160
- });
1149
+ // If we used Antora's latest-redpanda-tag for diff, update it to the new tag
1150
+ if (!options.diff && !tagsAreSame) {
1151
+ setAntoraValue('asciidoc.attributes.latest-redpanda-tag', newTag);
1152
+ console.log(`✅ Updated Antora latest-redpanda-tag to: ${newTag}`);
1153
+ }
1154
+
1155
+ process.exit(0);
1156
+ });
1161
1157
 
1162
1158
  automation
1163
1159
  .command('rpk-docs')
@@ -10,9 +10,17 @@ const yaml = require('js-yaml');
10
10
  * @returns {Object|undefined} The parsed YAML as a JavaScript object, or undefined if not found or on error.
11
11
  */
12
12
  function loadAntoraConfig() {
13
- const antoraPath = path.join(process.cwd(), 'antora.yml');
14
- if (!fs.existsSync(antoraPath)) {
15
- // No antora.yml in project root
13
+ // Support both antora.yml and antora.yaml
14
+ const cwd = process.cwd();
15
+ const ymlPath = path.join(cwd, 'antora.yml');
16
+ const yamlPath = path.join(cwd, 'antora.yaml');
17
+ let antoraPath;
18
+ if (fs.existsSync(ymlPath)) {
19
+ antoraPath = ymlPath;
20
+ } else if (fs.existsSync(yamlPath)) {
21
+ antoraPath = yamlPath;
22
+ } else {
23
+ // No antora.yml or antora.yaml in project root
16
24
  return undefined;
17
25
  }
18
26
 
@@ -20,12 +28,12 @@ function loadAntoraConfig() {
20
28
  const fileContents = fs.readFileSync(antoraPath, 'utf8');
21
29
  const config = yaml.load(fileContents);
22
30
  if (typeof config !== 'object' || config === null) {
23
- console.error('Warning: antora.yml parsed to a non‐object value.');
31
+ console.error(`Warning: ${path.basename(antoraPath)} parsed to a non‐object value.`);
24
32
  return undefined;
25
33
  }
26
34
  return config;
27
35
  } catch (err) {
28
- console.error(`Error reading/parsing antora.yml: ${err.message}`);
36
+ console.error(`Error reading/parsing ${path.basename(antoraPath)}: ${err.message}`);
29
37
  return undefined;
30
38
  }
31
39
  }
@@ -74,9 +82,17 @@ function getAntoraValue(keyPath) {
74
82
  * True if it succeeded, false otherwise.
75
83
  */
76
84
  function setAntoraValue(keyPath, newValue) {
77
- const antoraPath = path.join(process.cwd(), 'antora.yml');
78
- if (!fs.existsSync(antoraPath)) {
79
- console.error('Cannot update antora.yml: file not found in project root.');
85
+ // Support both antora.yml and antora.yaml
86
+ const cwd = process.cwd();
87
+ const ymlPath = path.join(cwd, 'antora.yml');
88
+ const yamlPath = path.join(cwd, 'antora.yaml');
89
+ let antoraPath;
90
+ if (fs.existsSync(ymlPath)) {
91
+ antoraPath = ymlPath;
92
+ } else if (fs.existsSync(yamlPath)) {
93
+ antoraPath = yamlPath;
94
+ } else {
95
+ console.error('Cannot update antora.yml or antora.yaml: file not found in project root.');
80
96
  return false;
81
97
  }
82
98
 
@@ -88,7 +104,7 @@ function setAntoraValue(keyPath, newValue) {
88
104
  config = {};
89
105
  }
90
106
  } catch (err) {
91
- console.error(`Error reading/parsing antora.yml: ${err.message}`);
107
+ console.error(`Error reading/parsing ${path.basename(antoraPath)}: ${err.message}`);
92
108
  return false;
93
109
  }
94
110
 
@@ -115,7 +131,37 @@ function setAntoraValue(keyPath, newValue) {
115
131
  fs.writeFileSync(antoraPath, newYaml, 'utf8');
116
132
  return true;
117
133
  } catch (err) {
118
- console.error(`Error writing antora.yml: ${err.message}`);
134
+ console.error(`Error writing ${path.basename(antoraPath)}: ${err.message}`);
135
+ return false;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Look for antora.yml in the current working directory
141
+ * (the project's root), load it if present, and return
142
+ * its `prerelease` value (boolean). If missing or on error,
143
+ * returns false.
144
+ */
145
+ function getPrereleaseFromAntora() {
146
+ // Support both antora.yml and antora.yaml
147
+ const cwd = process.cwd();
148
+ const ymlPath = path.join(cwd, 'antora.yml');
149
+ const yamlPath = path.join(cwd, 'antora.yaml');
150
+ let antoraPath;
151
+ if (fs.existsSync(ymlPath)) {
152
+ antoraPath = ymlPath;
153
+ } else if (fs.existsSync(yamlPath)) {
154
+ antoraPath = yamlPath;
155
+ } else {
156
+ return false;
157
+ }
158
+
159
+ try {
160
+ const fileContents = fs.readFileSync(antoraPath, 'utf8');
161
+ const antoraConfig = yaml.load(fileContents);
162
+ return antoraConfig.prerelease === true;
163
+ } catch (error) {
164
+ console.error(`Error reading ${path.basename(antoraPath)}:`, error.message);
119
165
  return false;
120
166
  }
121
167
  }
@@ -124,4 +170,5 @@ module.exports = {
124
170
  loadAntoraConfig,
125
171
  getAntoraValue,
126
172
  setAntoraValue,
173
+ getPrereleaseFromAntora
127
174
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.10.6",
3
+ "version": "4.10.8",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -4,7 +4,7 @@ const yaml = require('js-yaml');
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const GetLatestConsoleVersion = require('../extensions/version-fetcher/get-latest-console-version.js');
7
- const { getPrereleaseFromAntora } = require('../cli-utils/beta-from-antora.js');
7
+ const { getPrereleaseFromAntora } = require('../cli-utils/antora-utils.js');
8
8
 
9
9
  /**
10
10
  * Fetches and prints the latest Console version and Docker repo.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const GetLatestRedpandaVersion = require('../extensions/version-fetcher/get-latest-redpanda-version.js');
4
- const { getPrereleaseFromAntora } = require('../cli-utils/beta-from-antora.js');
4
+ const { getPrereleaseFromAntora } = require('../cli-utils/antora-utils.js');
5
5
 
6
6
  /**
7
7
  * Fetches and prints the latest Redpanda version and Docker repository.
@@ -22,7 +22,7 @@ const helpers = require('./helpers');
22
22
  * CLI Usage: node generate-handlebars-docs.js <input-file> <output-dir>
23
23
  */
24
24
 
25
- // Register all helpers
25
+ // Register helpers
26
26
  Object.entries(helpers).forEach(([name, fn]) => {
27
27
  if (typeof fn !== 'function') {
28
28
  console.error(`❌ Helper "${name}" is not a function`);
@@ -62,11 +62,6 @@ function getTemplatePath(defaultPath, envVar) {
62
62
 
63
63
  /**
64
64
  * Register Handlebars partials used to render property documentation.
65
- *
66
- * Registers:
67
- * - "property"
68
- * - "topic-property"
69
- * - "deprecated-property"
70
65
  */
71
66
  function registerPartials() {
72
67
  const templatesDir = path.join(__dirname, 'templates');
@@ -112,7 +107,7 @@ function registerPartials() {
112
107
  /**
113
108
  * Generate consolidated AsciiDoc partials for properties grouped by type.
114
109
  */
115
- function generatePropertyPartials(properties, partialsDir) {
110
+ function generatePropertyPartials(properties, partialsDir, onRender) {
116
111
  console.log(`📝 Generating consolidated property partials in ${partialsDir}…`);
117
112
 
118
113
  const propertyTemplate = handlebars.compile(
@@ -129,11 +124,24 @@ function generatePropertyPartials(properties, partialsDir) {
129
124
 
130
125
  Object.values(properties).forEach(prop => {
131
126
  if (!prop.name || !prop.config_scope) return;
132
- if (prop.config_scope === 'topic') propertyGroups.topic.push(prop);
133
- else if (prop.config_scope === 'broker') propertyGroups.broker.push(prop);
134
- else if (prop.config_scope === 'cluster') {
135
- if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
136
- else propertyGroups.cluster.push(prop);
127
+
128
+ switch (prop.config_scope) {
129
+ case 'topic':
130
+ propertyGroups.topic.push(prop);
131
+ break;
132
+ case 'broker':
133
+ propertyGroups.broker.push(prop);
134
+ break;
135
+ case 'cluster':
136
+ if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
137
+ else propertyGroups.cluster.push(prop);
138
+ break;
139
+ case 'object-storage':
140
+ propertyGroups['object-storage'].push(prop);
141
+ break;
142
+ default:
143
+ console.warn(`⚠️ Unknown config_scope: ${prop.config_scope} for ${prop.name}`);
144
+ break;
137
145
  }
138
146
  });
139
147
 
@@ -143,9 +151,16 @@ function generatePropertyPartials(properties, partialsDir) {
143
151
  if (props.length === 0) return;
144
152
  props.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
145
153
  const selectedTemplate = type === 'topic' ? topicTemplate : propertyTemplate;
146
- const content = props.map(p => selectedTemplate(p)).join('\n');
154
+ const pieces = [];
155
+ props.forEach(p => {
156
+ if (typeof onRender === 'function') {
157
+ try { onRender(p.name); } catch (err) { /* swallow callback errors */ }
158
+ }
159
+ pieces.push(selectedTemplate(p));
160
+ });
161
+ const content = pieces.join('\n');
147
162
  const filename = `${type}-properties.adoc`;
148
- fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
163
+ fs.writeFileSync(path.join(propertiesPartialsDir, filename), AUTOGEN_NOTICE + content, 'utf8');
149
164
  console.log(`✅ Generated ${filename} (${props.length} properties)`);
150
165
  totalCount += props.length;
151
166
  });
@@ -178,19 +193,18 @@ function generateDeprecatedDocs(properties, outputDir) {
178
193
  clusterProperties: clusterProperties.length ? clusterProperties : null
179
194
  };
180
195
 
181
- const output = template(data);
182
196
  const outputPath = process.env.OUTPUT_PARTIALS_DIR
183
197
  ? path.join(process.env.OUTPUT_PARTIALS_DIR, 'deprecated', 'deprecated-properties.adoc')
184
198
  : path.join(outputDir, 'partials', 'deprecated', 'deprecated-properties.adoc');
185
199
 
186
200
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
187
- fs.writeFileSync(outputPath, AUTOGEN_NOTICE + output, 'utf8');
201
+ fs.writeFileSync(outputPath, AUTOGEN_NOTICE + template(data), 'utf8');
188
202
  console.log(`✅ Generated ${outputPath}`);
189
203
  return deprecatedProperties.length;
190
204
  }
191
205
 
192
206
  /**
193
- * Generate topic-property-mappings.adoc using the mappings template and topic properties.
207
+ * Generate topic-property-mappings.adoc
194
208
  */
195
209
  function generateTopicPropertyMappings(properties, partialsDir) {
196
210
  const templatesDir = path.join(__dirname, 'templates');
@@ -218,31 +232,42 @@ function generateTopicPropertyMappings(properties, partialsDir) {
218
232
  }
219
233
 
220
234
  /**
221
- * Generate error reports for missing descriptions and deprecated properties.
235
+ * Generate error reports for missing descriptions, deprecated, and undocumented properties.
222
236
  */
223
- function generateErrorReports(properties) {
237
+ function generateErrorReports(properties, documentedProperties = []) {
224
238
  const emptyDescriptions = [];
225
239
  const deprecatedProperties = [];
240
+ const allKeys = Object.keys(properties);
226
241
 
227
- Object.values(properties).forEach(p => {
228
- if (!p.description || !p.description.trim()) emptyDescriptions.push(p.name);
229
- if (p.is_deprecated) deprecatedProperties.push(p.name);
242
+ // Use documentedProperties array (property names that were rendered into partials)
243
+ const documentedSet = new Set(documentedProperties);
244
+ const undocumented = [];
245
+
246
+ Object.entries(properties).forEach(([key, p]) => {
247
+ const name = p.name || key;
248
+ if (!p.description || !p.description.trim()) emptyDescriptions.push(name);
249
+ if (p.is_deprecated) deprecatedProperties.push(name);
250
+ if (!documentedSet.has(name)) undocumented.push(name);
230
251
  });
231
252
 
232
- const total = Object.keys(properties).length;
253
+ const total = allKeys.length;
233
254
  const pctEmpty = total ? ((emptyDescriptions.length / total) * 100).toFixed(2) : '0.00';
234
255
  const pctDeprecated = total ? ((deprecatedProperties.length / total) * 100).toFixed(2) : '0.00';
235
- console.log(`Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
236
- console.log(`Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
256
+ const pctUndocumented = total ? ((undocumented.length / total) * 100).toFixed(2) : '0.00';
257
+
258
+ console.log(`📉 Empty descriptions: ${emptyDescriptions.length} (${pctEmpty}%)`);
259
+ console.log(`🕸️ Deprecated: ${deprecatedProperties.length} (${pctDeprecated}%)`);
260
+ console.log(`🚫 Not documented: ${undocumented.length} (${pctUndocumented}%)`);
237
261
 
238
262
  return {
239
263
  empty_descriptions: emptyDescriptions.sort(),
240
- deprecated_properties: deprecatedProperties.sort()
264
+ deprecated_properties: deprecatedProperties.sort(),
265
+ undocumented_properties: undocumented.sort(),
241
266
  };
242
267
  }
243
268
 
244
269
  /**
245
- * Main generator — only supports partials and deprecated docs.
270
+ * Main generator
246
271
  */
247
272
  function generateAllDocs(inputFile, outputDir) {
248
273
  const data = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
@@ -252,13 +277,14 @@ function generateAllDocs(inputFile, outputDir) {
252
277
 
253
278
  let partialsCount = 0;
254
279
  let deprecatedCount = 0;
280
+ const documentedProps = []; // Track which property names were rendered
255
281
 
256
282
  if (process.env.GENERATE_PARTIALS === '1' && process.env.OUTPUT_PARTIALS_DIR) {
257
283
  console.log('📄 Generating property partials and deprecated docs...');
258
284
  deprecatedCount = generateDeprecatedDocs(properties, outputDir);
259
- partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR);
260
285
 
261
- // Generate topic-property-mappings.adoc
286
+ // Generate property partials using the shared helper and collect names via callback
287
+ partialsCount = generatePropertyPartials(properties, process.env.OUTPUT_PARTIALS_DIR, name => documentedProps.push(name));
262
288
  try {
263
289
  generateTopicPropertyMappings(properties, process.env.OUTPUT_PARTIALS_DIR);
264
290
  } catch (err) {
@@ -268,24 +294,34 @@ function generateAllDocs(inputFile, outputDir) {
268
294
  console.log('📄 Skipping partial generation (set GENERATE_PARTIALS=1 and OUTPUT_PARTIALS_DIR to enable)');
269
295
  }
270
296
 
271
- const errors = generateErrorReports(properties);
272
- const inputData = JSON.parse(fs.readFileSync(inputFile, 'utf8'));
273
- inputData.empty_descriptions = errors.empty_descriptions;
274
- inputData.deprecated_properties = errors.deprecated_properties;
275
- fs.writeFileSync(inputFile, JSON.stringify(inputData, null, 2), 'utf8');
297
+ const errors = generateErrorReports(properties, documentedProps);
276
298
 
277
- console.log('📊 Summary:');
278
- console.log(` Total properties: ${Object.keys(properties).length}`);
279
- console.log(` Total partials generated: ${partialsCount}`);
280
- console.log(` Deprecated properties: ${deprecatedCount}`);
299
+ const totalProperties = Object.keys(properties).length;
300
+ const notRendered = errors.undocumented_properties.length;
301
+ const pctRendered = totalProperties
302
+ ? ((partialsCount / totalProperties) * 100).toFixed(2)
303
+ : '0.00';
304
+
305
+ console.log('\n📊 Summary:');
306
+ console.log(` Total properties found: ${totalProperties}`);
307
+ console.log(` Property partials generated: ${partialsCount} (${pctRendered}% of total)`);
308
+ console.log(` Not documented: ${notRendered}`);
309
+ console.log(` Deprecated properties: ${deprecatedCount}`);
310
+
311
+ if (notRendered > 0) {
312
+ console.log('⚠️ Undocumented properties:\n ' + errors.undocumented_properties.join('\n '));
313
+ }
281
314
 
282
315
  return {
283
- totalProperties: Object.keys(properties).length,
284
- propertyPartials: partialsCount,
285
- deprecatedProperties: deprecatedCount
316
+ totalProperties,
317
+ generatedPartials: partialsCount,
318
+ undocumentedProperties: errors.undocumented_properties,
319
+ deprecatedProperties: deprecatedCount,
320
+ percentageRendered: pctRendered
286
321
  };
287
322
  }
288
323
 
324
+
289
325
  module.exports = {
290
326
  generateAllDocs,
291
327
  generateDeprecatedDocs,
@@ -0,0 +1,19 @@
1
+ // Returns 'cloud' if all topics are cloud-only, 'self-managed' if all are self-managed-only, else 'normal'
2
+ module.exports = function allTopicsConditional(related_topics) {
3
+ if (!Array.isArray(related_topics) || related_topics.length === 0) return null;
4
+ let allCloud = true;
5
+ let allSelfManaged = true;
6
+ for (const t of related_topics) {
7
+ if (typeof t !== 'string') {
8
+ allCloud = false;
9
+ allSelfManaged = false;
10
+ break;
11
+ }
12
+ const trimmed = t.trim();
13
+ if (!trimmed.startsWith('cloud-only:')) allCloud = false;
14
+ if (!trimmed.startsWith('self-managed-only:')) allSelfManaged = false;
15
+ }
16
+ if (allCloud) return 'cloud';
17
+ if (allSelfManaged) return 'self-managed';
18
+ return 'normal';
19
+ };
@@ -28,6 +28,10 @@ function processDefaults(inputString, suffix) {
28
28
  return inputString;
29
29
  }
30
30
 
31
+ // Remove C++ digit separators (apostrophes) from numbers/durations
32
+ // e.g. 30'000ms -> 30000ms, 1'500 -> 1500
33
+ inputString = inputString.replace(/(?<=\d)'(?=\d)/g, '');
34
+
31
35
  // Test for ip:port in vector: std::vector<net::unresolved_address>({{...}})
32
36
  const vectorMatch = inputString.match(/std::vector<net::unresolved_address>\(\{\{("([\d.]+)",\s*(\d+))\}\}\)/);
33
37
  if (vectorMatch) {
@@ -11,4 +11,6 @@ module.exports = {
11
11
  renderPropertyExample: require('./renderPropertyExample.js'),
12
12
  formatUnits: require('./formatUnits.js'),
13
13
  anchorName: require('./anchorName.js'),
14
+ parseRelatedTopic: require('./parseRelatedTopic.js'),
15
+ allTopicsConditional: require('./allTopicsConditional.js'),
14
16
  };
@@ -0,0 +1,13 @@
1
+ // Returns an object with type and value for a related topic
2
+ // type: 'cloud', 'self-managed', or 'normal'
3
+ module.exports = function parseRelatedTopic(topic) {
4
+ if (typeof topic !== 'string') return { type: 'normal', value: topic };
5
+ const trimmed = topic.trim();
6
+ if (trimmed.startsWith('cloud-only:')) {
7
+ return { type: 'cloud', value: trimmed.replace(/^cloud-only:/, '').trim() };
8
+ }
9
+ if (trimmed.startsWith('self-managed-only:')) {
10
+ return { type: 'self-managed', value: trimmed.replace(/^self-managed-only:/, '').trim() };
11
+ }
12
+ return { type: 'normal', value: trimmed };
13
+ };
@@ -1010,6 +1010,9 @@ def resolve_type_and_default(properties, definitions):
1010
1010
  processed (str): A string representing the JSON-ready value (for example: '"value"', 'null', '0', or the original input when no mapping applied).
1011
1011
  """
1012
1012
  arg_str = arg_str.strip()
1013
+ # Remove C++ digit separators (apostrophes) that may appear in numeric literals
1014
+ # Example: "30'000ms" -> "30000ms". Use conservative replace only between digits.
1015
+ arg_str = re.sub(r"(?<=\d)'(?=\d)", '', arg_str)
1013
1016
 
1014
1017
  # Handle std::nullopt -> null
1015
1018
  if arg_str == "std::nullopt":
@@ -1023,9 +1026,27 @@ def resolve_type_and_default(properties, definitions):
1023
1026
  resolved_value = resolve_cpp_function_call(function_name)
1024
1027
  if resolved_value is not None:
1025
1028
  return f'"{resolved_value}"'
1029
+
1030
+ # Handle std::chrono literals like std::chrono::minutes{5} -> "5min"
1031
+ chrono_match = re.match(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}', arg_str)
1032
+ if chrono_match:
1033
+ unit = chrono_match.group(1)
1034
+ value = chrono_match.group(2)
1035
+ unit_map = {
1036
+ 'hours': 'h',
1037
+ 'minutes': 'min',
1038
+ 'seconds': 's',
1039
+ 'milliseconds': 'ms',
1040
+ 'microseconds': 'us',
1041
+ 'nanoseconds': 'ns'
1042
+ }
1043
+ short = unit_map.get(unit.lower(), unit)
1044
+ return f'"{value} {short}"'
1026
1045
 
1027
- # Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled")
1028
- enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)', arg_str)
1046
+ # Handle enum-like patterns (such as fips_mode_flag::disabled -> "disabled").
1047
+ # Only treat bare 'X::Y' tokens as enums — do not match when the token
1048
+ # is followed by constructor braces/parentheses (e.g. std::chrono::minutes{5}).
1049
+ enum_match = re.match(r'[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)\s*$', arg_str)
1029
1050
  if enum_match:
1030
1051
  enum_value = enum_match.group(1)
1031
1052
  return f'"{enum_value}"'
@@ -23,6 +23,7 @@ endif::[]
23
23
  {{else}}
24
24
 
25
25
  No description available.
26
+
26
27
  {{/if}}
27
28
  {{#if is_enterprise}}
28
29
 
@@ -31,9 +32,11 @@ ifndef::env-cloud[]
31
32
  endif::[]
32
33
  {{/if}}
33
34
  {{#if cloud_byoc_only}}
35
+
34
36
  ifdef::env-cloud[]
35
37
  NOTE: This property is available only in Redpanda Cloud BYOC deployments.
36
38
  endif::[]
39
+
37
40
  {{/if}}
38
41
  {{#if units}}
39
42
 
@@ -50,6 +53,10 @@ endif::[]
50
53
  *Requires restart:* {{#if needs_restart}}Yes{{else}}No{{/if}}
51
54
  {{/if}}
52
55
  {{/if}}
56
+
57
+ ifndef::env-cloud[]
58
+ *Restored during xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]:* {{#if (ne gets_restored false)}}Yes{{else}}No{{/if}}
59
+ endif::[]
53
60
  {{#if visibility}}
54
61
 
55
62
  // tag::self-managed-only[]
@@ -89,13 +96,49 @@ endif::[]
89
96
  {{{renderPropertyExample this}}}
90
97
  {{/if}}
91
98
  {{#if related_topics}}
99
+ {{#with (allTopicsConditional related_topics) as |sectionType|}}
100
+
101
+ {{#if (eq sectionType "cloud")}}
102
+ ifdef::env-cloud[]
103
+ *Related topics:*
104
+
105
+ {{#each ../related_topics}}
106
+ {{#with (parseRelatedTopic this)}}
107
+ * {{{value}}}
108
+ {{/with}}
109
+ {{/each}}
110
+ endif::[]
111
+ {{else if (eq sectionType "self-managed")}}
112
+ ifndef::env-cloud[]
113
+ *Related topics:*
92
114
 
115
+ {{#each ../related_topics}}
116
+ {{#with (parseRelatedTopic this)}}
117
+ * {{{value}}}
118
+ {{/with}}
119
+ {{/each}}
120
+ endif::[]
121
+ {{else}}
93
122
  *Related topics:*
94
123
 
95
- {{#each related_topics}}
96
- * {{{this}}}
124
+ {{#each ../related_topics}}
125
+ {{#with (parseRelatedTopic this)}}
126
+ {{#if (eq type "cloud")}}
127
+ ifdef::env-cloud[]
128
+ * {{{value}}}
129
+ endif::[]
130
+ {{else if (eq type "self-managed")}}
131
+ ifndef::env-cloud[]
132
+ * {{{value}}}
133
+ endif::[]
134
+ {{else}}
135
+ * {{{value}}}
136
+ {{/if}}
137
+ {{/with}}
97
138
  {{/each}}
98
139
  {{/if}}
140
+ {{/with}}
141
+ {{/if}}
99
142
  {{#if aliases}}
100
143
 
101
144
  // tag::self-managed-only[]
@@ -74,17 +74,57 @@ endif::[]
74
74
  {{/if}}
75
75
 
76
76
  *Nullable:* {{#if nullable}}Yes{{else}}No{{/if}}
77
+
78
+ ifndef::env-cloud[]
79
+ *Restored during xref:manage:whole-cluster-restore.adoc[Whole Cluster Restore]:* {{#if (ne gets_restored false)}}Yes{{else}}No{{/if}}
80
+ endif::[]
77
81
  {{#if example}}
78
82
 
79
83
  {{{renderPropertyExample this}}}
80
84
  {{/if}}
81
85
  {{#if related_topics}}
86
+ {{#with (allTopicsConditional related_topics) as |sectionType|}}
82
87
 
88
+ {{#if (eq sectionType "cloud")}}
89
+ ifdef::env-cloud[]
83
90
  *Related topics:*
84
91
 
85
- {{#each related_topics}}
86
- * {{{this}}}
92
+ {{#each ../related_topics}}
93
+ {{#with (parseRelatedTopic this)}}
94
+ * {{{value}}}
95
+ {{/with}}
87
96
  {{/each}}
97
+ endif::[]
98
+ {{else if (eq sectionType "self-managed")}}
99
+ ifndef::env-cloud[]
100
+ *Related topics:*
101
+
102
+ {{#each ../related_topics}}
103
+ {{#with (parseRelatedTopic this)}}
104
+ * {{{value}}}
105
+ {{/with}}
106
+ {{/each}}
107
+ endif::[]
108
+ {{else}}
109
+ *Related topics:*
110
+
111
+ {{#each ../related_topics}}
112
+ {{#with (parseRelatedTopic this)}}
113
+ {{#if (eq type "cloud")}}
114
+ ifdef::env-cloud[]
115
+ * {{{value}}}
116
+ endif::[]
117
+ {{else if (eq type "self-managed")}}
118
+ ifndef::env-cloud[]
119
+ * {{{value}}}
120
+ endif::[]
121
+ {{else}}
122
+ * {{{value}}}
123
+ {{/if}}
124
+ {{/with}}
125
+ {{/each}}
126
+ {{/if}}
127
+ {{/with}}
88
128
  {{/if}}
89
129
  {{#if aliases}}
90
130
 
@@ -304,11 +304,29 @@ class TopicPropertyExtractor:
304
304
 
305
305
  def _determine_property_type(self, property_name: str) -> str:
306
306
  """Determine the type of a property based on its name and usage patterns"""
307
-
308
- # Type mapping based on property name patterns
307
+ # Explicit exceptions / overrides for properties whose name contains
308
+ # keywords that would otherwise map to boolean but which are actually
309
+ # string-valued (for example, a bucket name).
310
+ if property_name == "redpanda.remote.readreplica":
311
+ # This topic property contains the read-replica bucket identifier
312
+ # and should be treated as a string (not a boolean).
313
+ return "string"
314
+ # Explicit override: iceberg.delete is a boolean (whether to delete
315
+ # the corresponding Iceberg table when the topic is deleted).
316
+ if property_name == "redpanda.iceberg.delete":
317
+ return "boolean"
318
+
319
+ # Type mapping based on property name patterns (heuristic)
309
320
  if any(keyword in property_name for keyword in ["caching", "recovery", "read", "write", "delete"]):
310
- if property_name in ["write.caching", "redpanda.remote.recovery", "redpanda.remote.write",
311
- "redpanda.remote.read", "redpanda.remote.delete", "redpanda.remote.readreplica"]:
321
+ # Known boolean topic properties (keep list conservative)
322
+ boolean_props = [
323
+ "write.caching",
324
+ "redpanda.remote.recovery",
325
+ "redpanda.remote.write",
326
+ "redpanda.remote.read",
327
+ "redpanda.remote.delete",
328
+ ]
329
+ if property_name in boolean_props:
312
330
  return "boolean"
313
331
 
314
332
  elif any(suffix in property_name for suffix in [".bytes", ".ms", ".factor", ".lag.ms"]):
@@ -583,7 +601,8 @@ NOTE: All topic properties take effect immediately after being set.
583
601
  *Type:* {prop_type}
584
602
 
585
603
  """
586
- if acceptable_values:
604
+ # If the property type is boolean, never include an Accepted values section
605
+ if acceptable_values and str(prop_type).lower() not in ("boolean", "bool"):
587
606
  adoc_content += f"*Accepted values:* {acceptable_values}\n\n"
588
607
 
589
608
  adoc_content += "*Default:* null\n\n"
@@ -145,15 +145,19 @@ class IsArrayTransformer:
145
145
 
146
146
  class NeedsRestartTransformer:
147
147
  def accepts(self, info, file_pair):
148
- return True
149
-
148
+ # Only accept when the params blob exists and contains a needs_restart entry
149
+ return (
150
+ len(info.get("params", [])) > 2
151
+ and isinstance(info["params"][2].get("value"), dict)
152
+ and "needs_restart" in info["params"][2]["value"]
153
+ )
154
+
150
155
  def parse(self, property, info, file_pair):
151
- needs_restart = "yes"
152
- if len(info["params"]) > 2 and "needs_restart" in info["params"][2]["value"]:
153
- needs_restart = re.sub(
154
- r"^.*::", "", info["params"][2]["value"]["needs_restart"]
155
- )
156
- property["needs_restart"] = needs_restart != "no" # True by default, unless we find "no"
156
+ # We only get here if accepts(...) returned True, so the metadata blob is present
157
+ raw = info["params"][2]["value"]["needs_restart"]
158
+ flag = re.sub(r"^.*::", "", raw)
159
+ # Store as boolean; do not set any default when metadata is absent
160
+ property["needs_restart"] = (flag != "no")
157
161
 
158
162
  class GetsRestoredTransformer:
159
163
  def accepts(self, info, file_pair):
@@ -1,27 +0,0 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
- const yaml = require('js-yaml');
4
-
5
- /**
6
- * Look for antora.yml in the current working directory
7
- * (the project's root), load it if present, and return
8
- * its `prerelease` value (boolean). If missing or on error,
9
- * returns false.
10
- */
11
- function getPrereleaseFromAntora() {
12
- const antoraPath = path.join(process.cwd(), 'antora.yml');
13
- if (!fs.existsSync(antoraPath)) {
14
- return false;
15
- }
16
-
17
- try {
18
- const fileContents = fs.readFileSync(antoraPath, 'utf8');
19
- const antoraConfig = yaml.load(fileContents);
20
- return antoraConfig.prerelease === true;
21
- } catch (error) {
22
- console.error('Error reading antora.yml:', error.message);
23
- return false;
24
- }
25
- }
26
-
27
- module.exports = { getPrereleaseFromAntora };