@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.1
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 +159 -0
- package/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +39 -2
- package/tools/property-extractor/transformers.py +29 -13
- package/tools/rpk-docs/detect-platform-commands.js +566 -0
- package/tools/rpk-docs/generate-plugin-stubs.js +291 -0
- package/tools/rpk-docs/generate-rpk-docs.js +466 -30
- package/tools/rpk-docs/helpers/index.js +13 -3
- package/tools/rpk-docs/report-delta.js +314 -32
- package/tools/rpk-docs/rpk-docs-handler.js +1329 -208
- package/tools/rpk-docs/templates/command.hbs +17 -0
- package/tools/rpk-docs/templates/content-item.hbs +2 -2
- package/tools/rpk-docs/validate-overrides.js +16 -0
package/bin/doc-tools.js
CHANGED
|
@@ -1109,6 +1109,17 @@ automation
|
|
|
1109
1109
|
.option('-r, --ref <ref>', 'Git branch or tag to document (e.g., dev, v26.2.0). Clones from GitHub.')
|
|
1110
1110
|
.option('--from-source <path>', 'Path to local rpk source (src/go/rpk directory)')
|
|
1111
1111
|
.option('--from-json <path>', 'Regenerate docs from an existing versioned JSON file (skips building)')
|
|
1112
|
+
.option('--plugin <name>', 'Refresh a single rpk plugin\'s docs (ai, connect, k8s, check). Requires --from-json. Installs the plugin, splices its fresh subtree into the snapshot, and re-renders.')
|
|
1113
|
+
.option('--plugin-version <version>', 'Plugin version to install and record (for example, 4.102.0). Defaults to the latest published version.')
|
|
1114
|
+
.option('--plugin-pin <name=version>', 'Pin a plugin version for the installs during full generation (repeatable, for example --plugin-pin k8s=26.3.1-beta.1). Required for pre-GA plugins with no promoted latest version.', (value, pins) => {
|
|
1115
|
+
const eq = value.indexOf('=')
|
|
1116
|
+
if (eq < 1 || eq === value.length - 1) {
|
|
1117
|
+
throw new Error(`Invalid --plugin-pin '${value}': expected <name>=<version>`)
|
|
1118
|
+
}
|
|
1119
|
+
pins[value.slice(0, eq)] = value.slice(eq + 1)
|
|
1120
|
+
return pins
|
|
1121
|
+
}, {})
|
|
1122
|
+
.option('--rpk-bin <path>', 'Path to an existing rpk binary for the plugin refresh (skips download/build)')
|
|
1112
1123
|
.option('--overrides <path>', 'Path to overrides JSON file', 'docs-data/rpk-overrides.json')
|
|
1113
1124
|
.option('--diff <oldVersion>', 'Generate diff against previous version')
|
|
1114
1125
|
.option('--update-whats-new [path]', 'Update what\'s-new file with rpk changes from diff (default: modules/get-started/pages/release-notes/redpanda.adoc)')
|
|
@@ -1124,6 +1135,12 @@ automation
|
|
|
1124
1135
|
try {
|
|
1125
1136
|
const { handleRpkDocsGeneration } = require('../tools/rpk-docs/rpk-docs-handler.js')
|
|
1126
1137
|
|
|
1138
|
+
if (options.plugin && !options.fromJson) {
|
|
1139
|
+
console.error('Error: --plugin requires --from-json <snapshot>')
|
|
1140
|
+
console.error('A plugin refresh splices the fresh subtree into an existing committed snapshot.')
|
|
1141
|
+
process.exit(1)
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1127
1144
|
// Handle --update-whats-new with optional path
|
|
1128
1145
|
let whatsNewPath = null
|
|
1129
1146
|
if (options.updateWhatsNew !== undefined) {
|
|
@@ -1137,6 +1154,10 @@ automation
|
|
|
1137
1154
|
ref: options.ref,
|
|
1138
1155
|
fromSource: options.fromSource,
|
|
1139
1156
|
fromJson: options.fromJson,
|
|
1157
|
+
plugin: options.plugin,
|
|
1158
|
+
pluginVersion: options.pluginVersion,
|
|
1159
|
+
pluginPins: options.pluginPin,
|
|
1160
|
+
rpkBin: options.rpkBin,
|
|
1140
1161
|
overrides: options.overrides,
|
|
1141
1162
|
diff: options.diff,
|
|
1142
1163
|
updateWhatsNew: whatsNewPath,
|
|
@@ -1150,6 +1171,10 @@ automation
|
|
|
1150
1171
|
})
|
|
1151
1172
|
|
|
1152
1173
|
if (result.success) {
|
|
1174
|
+
if (result.skipped) {
|
|
1175
|
+
console.log(`\n✓ Skipped: ${result.reason}`)
|
|
1176
|
+
process.exit(0)
|
|
1177
|
+
}
|
|
1153
1178
|
console.log('\n✓ rpk documentation generated successfully')
|
|
1154
1179
|
|
|
1155
1180
|
// Write PR summary to file if requested (useful for GitHub Actions)
|
|
@@ -1175,6 +1200,140 @@ automation
|
|
|
1175
1200
|
}
|
|
1176
1201
|
})
|
|
1177
1202
|
|
|
1203
|
+
/**
|
|
1204
|
+
* generate rpk-plugin-stubs
|
|
1205
|
+
*
|
|
1206
|
+
* @description
|
|
1207
|
+
* Reconciles a consumer repo's single-source stub pages and nav section
|
|
1208
|
+
* against the rpk plugin partials generated in the docs repo. Run from the
|
|
1209
|
+
* consumer repo root (for example, adp-docs for rpk ai). Creates stubs for
|
|
1210
|
+
* new partials, deletes managed stubs whose partial is gone, rebuilds the
|
|
1211
|
+
* plugin's nav block, and proposes page aliases for likely renames.
|
|
1212
|
+
* Full reconcile, so it is idempotent and heals pre-existing drift.
|
|
1213
|
+
*/
|
|
1214
|
+
automation
|
|
1215
|
+
.command('rpk-plugin-stubs')
|
|
1216
|
+
.description('Reconcile single-source stub pages and nav against the docs repo\'s rpk plugin partials. Run from the consumer repo root.')
|
|
1217
|
+
.option('--plugin <name>', 'rpk plugin command name', 'ai')
|
|
1218
|
+
.option('--docs-repo <owner/repo>', 'Docs repo that owns the partials', 'redpanda-data/docs')
|
|
1219
|
+
.option('--docs-ref <ref>', 'Branch or tag to read partials from', 'main')
|
|
1220
|
+
.option('--partials-dir <path>', 'Local partials directory (skips cloning the docs repo)')
|
|
1221
|
+
.option('--source-path <path>', 'Path in the docs repo to read from (default: modules/reference/partials/rpk-<plugin>; use modules/reference/pages/rpk/rpk-connect for page-family content)')
|
|
1222
|
+
.option('--stub-dir <path>', 'Stub pages directory in the consumer repo (default: modules/reference/pages/rpk/rpk-<plugin>)')
|
|
1223
|
+
.option('--nav-file <path>', 'Nav file whose plugin block is rebuilt', 'modules/ROOT/nav.adoc')
|
|
1224
|
+
.option('--include-prefix <prefix>', 'Antora resource prefix for stub includes. Default: inferred from an existing stub.')
|
|
1225
|
+
.option('--attribute <line>', 'Page attribute line added to new stubs (repeatable)', (value, acc) => { acc.push(value); return acc }, [])
|
|
1226
|
+
.option('--summary-file <path>', 'Write a markdown summary (for PR bodies)')
|
|
1227
|
+
.option('--dry-run', 'Report what would change without writing')
|
|
1228
|
+
.action(async (options) => {
|
|
1229
|
+
try {
|
|
1230
|
+
const {
|
|
1231
|
+
readPartialTitles, fetchPartialsDir, inferIncludePrefix, reconcileStubs
|
|
1232
|
+
} = require('../tools/rpk-docs/generate-plugin-stubs.js')
|
|
1233
|
+
|
|
1234
|
+
const plugin = options.plugin
|
|
1235
|
+
const stubDir = options.stubDir || `modules/reference/pages/rpk/rpk-${plugin}`
|
|
1236
|
+
const partialsDir = options.partialsDir || fetchPartialsDir({
|
|
1237
|
+
docsRepo: options.docsRepo,
|
|
1238
|
+
docsRef: options.docsRef,
|
|
1239
|
+
plugin,
|
|
1240
|
+
sourcePath: options.sourcePath
|
|
1241
|
+
})
|
|
1242
|
+
|
|
1243
|
+
const includePrefix = options.includePrefix || inferIncludePrefix(stubDir, plugin)
|
|
1244
|
+
if (!includePrefix) {
|
|
1245
|
+
console.error('Error: could not infer the include prefix (no existing stubs). Pass --include-prefix, for example: streaming:reference:partial$rpk-ai/')
|
|
1246
|
+
process.exit(1)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
const partials = readPartialTitles(partialsDir)
|
|
1250
|
+
console.log(`Reconciling ${partials.length} partial(s) against ${stubDir}`)
|
|
1251
|
+
|
|
1252
|
+
const result = reconcileStubs({
|
|
1253
|
+
partials,
|
|
1254
|
+
stubDir,
|
|
1255
|
+
navFile: options.navFile,
|
|
1256
|
+
plugin,
|
|
1257
|
+
includePrefix,
|
|
1258
|
+
...(options.attribute.length > 0 ? { attributes: options.attribute } : {}),
|
|
1259
|
+
dryRun: options.dryRun
|
|
1260
|
+
})
|
|
1261
|
+
|
|
1262
|
+
console.log(` Created: ${result.created.length}, deleted: ${result.deleted.length}, nav updated: ${result.navUpdated}`)
|
|
1263
|
+
for (const f of result.created) console.log(` + ${f}`)
|
|
1264
|
+
for (const f of result.deleted) console.log(` - ${f}`)
|
|
1265
|
+
for (const f of result.keptNonStub) console.log(` ! kept (not a managed stub): ${f}`)
|
|
1266
|
+
|
|
1267
|
+
const lines = []
|
|
1268
|
+
lines.push(`## rpk ${plugin} stub reconciliation`)
|
|
1269
|
+
lines.push('')
|
|
1270
|
+
lines.push(`Reconciled against \`${options.partialsDir ? partialsDir : `${options.docsRepo}@${options.docsRef}`}\`.`)
|
|
1271
|
+
lines.push('')
|
|
1272
|
+
if (result.created.length + result.deleted.length === 0 && !result.navUpdated) {
|
|
1273
|
+
lines.push('No changes: stubs and nav already match the partials.')
|
|
1274
|
+
}
|
|
1275
|
+
if (result.created.length > 0) {
|
|
1276
|
+
lines.push(`### New stubs (${result.created.length})`)
|
|
1277
|
+
lines.push('')
|
|
1278
|
+
result.created.forEach(f => lines.push(`- \`${f}\``))
|
|
1279
|
+
lines.push('')
|
|
1280
|
+
}
|
|
1281
|
+
if (result.deleted.length > 0) {
|
|
1282
|
+
lines.push(`### Deleted stubs (${result.deleted.length})`)
|
|
1283
|
+
lines.push('')
|
|
1284
|
+
result.deleted.forEach(f => lines.push(`- \`${f}\``))
|
|
1285
|
+
lines.push('')
|
|
1286
|
+
}
|
|
1287
|
+
if ((result.skippedAliasTargets || []).length > 0) {
|
|
1288
|
+
lines.push('### Skipped: names claimed as page aliases')
|
|
1289
|
+
lines.push('')
|
|
1290
|
+
lines.push('These partials exist upstream, but a page here already claims the name as a `:page-aliases:` target — creating the stub would make the Antora build fatal. Usually this means a rename alias exists while the upstream partial for the old name has not been cleaned up yet:')
|
|
1291
|
+
lines.push('')
|
|
1292
|
+
for (const t of result.skippedAliasTargets) {
|
|
1293
|
+
lines.push(`- \`${t.file}\` (claimed by \`${t.claimedBy}\`)`)
|
|
1294
|
+
}
|
|
1295
|
+
lines.push('')
|
|
1296
|
+
}
|
|
1297
|
+
const straightDeletions = result.deleted.filter(d => !result.renameCandidates.some(rc => rc.deleted === d))
|
|
1298
|
+
if (straightDeletions.length > 0) {
|
|
1299
|
+
lines.push('### Deletions with no rename partner')
|
|
1300
|
+
lines.push('')
|
|
1301
|
+
lines.push('These pages were removed with no successor detected. Their published URLs will 404 — consider adding a redirect or an alias on a related page:')
|
|
1302
|
+
lines.push('')
|
|
1303
|
+
straightDeletions.forEach(f => lines.push(`- \`${f}\``))
|
|
1304
|
+
lines.push('')
|
|
1305
|
+
}
|
|
1306
|
+
if (result.renameCandidates.length > 0) {
|
|
1307
|
+
lines.push('### Possible renames — reviewer decision needed')
|
|
1308
|
+
lines.push('')
|
|
1309
|
+
lines.push('These deleted/created pairs look like renames. If so, add `:page-aliases:` for the old page name to the new stub so published URLs keep working:')
|
|
1310
|
+
lines.push('')
|
|
1311
|
+
for (const rc of result.renameCandidates) {
|
|
1312
|
+
lines.push(`- \`${rc.deleted}\` → \`${rc.created}\`: add \`:page-aliases: reference:rpk/rpk-${plugin}/${rc.deleted}\` to the new stub`)
|
|
1313
|
+
}
|
|
1314
|
+
lines.push('')
|
|
1315
|
+
}
|
|
1316
|
+
if (result.keptNonStub.length > 0) {
|
|
1317
|
+
lines.push('### Kept (not managed stubs)')
|
|
1318
|
+
lines.push('')
|
|
1319
|
+
lines.push('These pages do not match the managed stub shape, so they were not touched:')
|
|
1320
|
+
lines.push('')
|
|
1321
|
+
result.keptNonStub.forEach(f => lines.push(`- \`${f}\``))
|
|
1322
|
+
lines.push('')
|
|
1323
|
+
}
|
|
1324
|
+
const summary = lines.join('\n')
|
|
1325
|
+
if (options.summaryFile) {
|
|
1326
|
+
fs.writeFileSync(options.summaryFile, summary, 'utf8')
|
|
1327
|
+
console.log(`Summary written to: ${options.summaryFile}`)
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
process.exit(0)
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
console.error(`Error: ${err.message}`)
|
|
1333
|
+
process.exit(1)
|
|
1334
|
+
}
|
|
1335
|
+
})
|
|
1336
|
+
|
|
1178
1337
|
/**
|
|
1179
1338
|
* validate rpk-overrides
|
|
1180
1339
|
*
|
package/package.json
CHANGED
|
@@ -79,7 +79,7 @@ ENUM_PATTERN = re.compile(r'^[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)$') # Match full qu
|
|
|
79
79
|
CONSTRUCTOR_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\((.*)\)')
|
|
80
80
|
BRACED_CONSTRUCTOR_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\{(.*)\}')
|
|
81
81
|
DIGIT_SEPARATOR_PATTERN = re.compile(r"(?<=\d)'(?=\d)")
|
|
82
|
-
FUNCTION_CALL_PATTERN = re.compile(r'([a-zA-Z0-9_
|
|
82
|
+
FUNCTION_CALL_PATTERN = re.compile(r'([a-zA-Z0-9_:.]+)\(\)$')
|
|
83
83
|
CHRONO_PATTERN = re.compile(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}')
|
|
84
84
|
CHRONO_PAREN_PATTERN = re.compile(r'(?:std::)?chrono::([a-zA-Z]+)\s*\(\s*([^)]+)\s*\)')
|
|
85
85
|
TIME_UNIT_PATTERN = re.compile(r'(\d+)\s*(min|s|ms|h)')
|
|
@@ -159,6 +159,15 @@ class ConstexprCache:
|
|
|
159
159
|
re.compile(r'const\s+model::ns\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*"([^"]+)"\s*\)'),
|
|
160
160
|
]
|
|
161
161
|
|
|
162
|
+
# Member accessors on inline constants used in property defaults,
|
|
163
|
+
# for example model::schema_registry_internal_tp.topic() -> "_schemas".
|
|
164
|
+
# Each entry maps a variable declaration to the member name whose call
|
|
165
|
+
# returns the captured string, cached under "<variable>.<member>".
|
|
166
|
+
member_accessor_patterns = [
|
|
167
|
+
# Pattern: inline const model::topic_partition name{model::topic{"value"}, ...}
|
|
168
|
+
(re.compile(r'inline\s+const\s+(?:model::)?topic_partition\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\{\s*(?:model::)?topic\{\s*"([^"]+)"\s*\}'), 'topic'),
|
|
169
|
+
]
|
|
170
|
+
|
|
162
171
|
# Legacy specific patterns (kept for compatibility, but general patterns should cover these)
|
|
163
172
|
function_patterns = {}
|
|
164
173
|
|
|
@@ -206,6 +215,17 @@ class ConstexprCache:
|
|
|
206
215
|
qualified_name = f"{namespace}::{func_name}"
|
|
207
216
|
self.function_cache[qualified_name] = func_value
|
|
208
217
|
|
|
218
|
+
# Extract member accessors on inline constants
|
|
219
|
+
for pattern, member in member_accessor_patterns:
|
|
220
|
+
for match in pattern.finditer(content):
|
|
221
|
+
var_name = match.group(1)
|
|
222
|
+
value = match.group(2)
|
|
223
|
+
key = f"{var_name}.{member}"
|
|
224
|
+
namespace = self._extract_namespace_for_function(content, match.start())
|
|
225
|
+
self.function_cache[key] = value
|
|
226
|
+
if namespace:
|
|
227
|
+
self.function_cache[f"{namespace}::{key}"] = value
|
|
228
|
+
|
|
209
229
|
# Legacy: Extract function definitions from hardcoded patterns (if any)
|
|
210
230
|
for func_name, patterns in function_patterns.items():
|
|
211
231
|
for pattern in patterns:
|
|
@@ -753,7 +773,24 @@ def transform_files_with_properties(files_with_properties):
|
|
|
753
773
|
continue
|
|
754
774
|
|
|
755
775
|
if len(property_definition) > 0:
|
|
756
|
-
|
|
776
|
+
# Key the output by the property's registered name (the string
|
|
777
|
+
# literal passed to the constructor), which can differ from the
|
|
778
|
+
# C++ member identifier used as the parser key. For example,
|
|
779
|
+
# the member `default_topic_replication` registers the name
|
|
780
|
+
# "default_topic_replications".
|
|
781
|
+
output_key = property_definition.get("name") or name
|
|
782
|
+
existing = all_properties.get(output_key)
|
|
783
|
+
if existing is not None and existing.get("defined_in") == property_definition.get("defined_in"):
|
|
784
|
+
# Two members of the same config store register the same
|
|
785
|
+
# property name. config_store registers properties with
|
|
786
|
+
# emplace(), so the first registration wins at runtime.
|
|
787
|
+
# Match that behavior and keep the first definition.
|
|
788
|
+
logging.warning(
|
|
789
|
+
f"Duplicate property name '{output_key}' (member '{name}') in "
|
|
790
|
+
f"{property_definition.get('defined_in')}: keeping the first definition."
|
|
791
|
+
)
|
|
792
|
+
continue
|
|
793
|
+
all_properties[output_key] = property_definition
|
|
757
794
|
|
|
758
795
|
return all_properties
|
|
759
796
|
|
|
@@ -639,8 +639,9 @@ class BasicInfoTransformer:
|
|
|
639
639
|
|
|
640
640
|
NAME RESOLUTION PRIORITY:
|
|
641
641
|
1. Pre-existing property["name"] (from previous processing)
|
|
642
|
-
2.
|
|
643
|
-
|
|
642
|
+
2. First string-literal parameter if it looks like an identifier
|
|
643
|
+
(the name argument passed to the property constructor)
|
|
644
|
+
3. info["name_in_file"] (C++ member name from Tree-sitter) as fallback
|
|
644
645
|
|
|
645
646
|
DESCRIPTION EXTRACTION LOGIC:
|
|
646
647
|
1. If first parameter is a string and differs from name → use as description
|
|
@@ -673,29 +674,44 @@ class BasicInfoTransformer:
|
|
|
673
674
|
return property
|
|
674
675
|
|
|
675
676
|
# --- Step 1: find the "real" start of the property definition ---
|
|
676
|
-
# Skip lambdas, validators, and non-string literals at the start
|
|
677
|
-
|
|
677
|
+
# Skip lambdas, validators, and non-string literals at the start.
|
|
678
|
+
# The parser normalizes string_literal values to unquoted strings, so
|
|
679
|
+
# select by parameter type rather than by surrounding quotes.
|
|
680
|
+
start_idx = None
|
|
678
681
|
for i, p in enumerate(params):
|
|
679
|
-
val = str(p.get("value", ""))
|
|
680
|
-
typ = p.get("type", "")
|
|
681
682
|
if is_validator_param(p):
|
|
682
683
|
continue
|
|
683
|
-
if
|
|
684
|
-
continue
|
|
685
|
-
if not (val.startswith('"') and val.endswith('"')):
|
|
684
|
+
if p.get("type") != "string_literal":
|
|
686
685
|
continue
|
|
687
686
|
# First string literal we hit is the name
|
|
688
687
|
start_idx = i
|
|
689
688
|
break
|
|
690
689
|
|
|
691
690
|
# --- Step 2: extract name and description robustly ---
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
691
|
+
# The property's registered name is the first string-literal constructor
|
|
692
|
+
# argument, NOT the C++ member identifier. They usually match, but not
|
|
693
|
+
# always: for example, the member `default_topic_replication` registers
|
|
694
|
+
# the name "default_topic_replications". info["name_in_file"] (the
|
|
695
|
+
# member identifier) is only a fallback.
|
|
696
|
+
name = property.get("name")
|
|
697
|
+
if not name:
|
|
698
|
+
for p in params:
|
|
699
|
+
if p.get("type") != "string_literal":
|
|
700
|
+
continue
|
|
701
|
+
candidate = str(p.get("value", "")).strip().strip('"')
|
|
702
|
+
# The name literal is a single identifier with no whitespace.
|
|
703
|
+
# If the first string literal looks like a description instead
|
|
704
|
+
# (for example, the name was passed as a constant), fall back
|
|
705
|
+
# to the member identifier below.
|
|
706
|
+
if candidate and not re.search(r"\s", candidate):
|
|
707
|
+
name = candidate
|
|
708
|
+
break # only the first string literal can be the name
|
|
709
|
+
if not name:
|
|
710
|
+
name = info.get("name_in_file")
|
|
695
711
|
property["name"] = name
|
|
696
712
|
|
|
697
713
|
desc = None
|
|
698
|
-
if len(params) > start_idx + 1:
|
|
714
|
+
if start_idx is not None and len(params) > start_idx + 1:
|
|
699
715
|
v0 = params[start_idx].get("value")
|
|
700
716
|
v1 = params[start_idx + 1].get("value")
|
|
701
717
|
if isinstance(v1, str) and len(v1) > 10 and " " in v1:
|