@redpanda-data/docs-extensions-and-macros 5.1.0 → 5.1.2
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/extensions/algolia-indexer/generate-index.js +81 -26
- package/package.json +1 -1
- package/tools/redpanda-connect/pr-summary-formatter.js +5 -5
- package/tools/redpanda-connect/report-delta.js +4 -4
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +59 -17
- package/tools/rpk-docs/generate-rpk-docs.js +285 -34
- package/tools/rpk-docs/rpk-docs-handler.js +61 -7
- package/tools/rpk-docs/templates/command.hbs +1 -1
- package/tools/rpk-docs/validate-overrides.js +21 -6
|
@@ -292,40 +292,95 @@ function generateIndex (playbook, contentCatalog, { indexLatestOnly = false, exc
|
|
|
292
292
|
? page.asciidoc.attributes['page-commercial-names'].split(',').map(name => name.trim())
|
|
293
293
|
: []
|
|
294
294
|
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
295
|
+
// Algolia only indexes roughly the first ~290 words of any single record. On
|
|
296
|
+
// long reference pages (cluster/topic properties, metrics) every property or
|
|
297
|
+
// metric name lives in the `titles` array, and there can be hundreds of them, so
|
|
298
|
+
// every name past that window is stored but NOT searchable (DOC-1878). Split the
|
|
299
|
+
// titles of such pages across multiple records, each small enough to be fully
|
|
300
|
+
// indexed. Normal pages keep a single record. Chunks after the first get a unique
|
|
301
|
+
// objectID anchored to their first heading so search results deep-link into the
|
|
302
|
+
// page; the first chunk keeps the page's base objectID.
|
|
303
|
+
const baseObjectID = urlPath + page.pub.url
|
|
304
|
+
const titleChunks = isPropertiesPage ? chunkTitles(titles) : [titles]
|
|
305
|
+
|
|
306
|
+
titleChunks.forEach((chunk, chunkIndex) => {
|
|
307
|
+
const objectID = chunkIndex === 0
|
|
308
|
+
? baseObjectID
|
|
309
|
+
: `${baseObjectID}#${chunk[0].h}`
|
|
310
|
+
|
|
311
|
+
// keywords included in index item
|
|
312
|
+
const indexItem = {
|
|
313
|
+
title: documentTitle,
|
|
314
|
+
version: version,
|
|
315
|
+
text: text,
|
|
316
|
+
intro: intro,
|
|
317
|
+
objectID: objectID,
|
|
318
|
+
// Clean page path (no #fragment) shared by every chunk of a page. The search
|
|
319
|
+
// UI builds result links from `url` and appends its own matched-heading
|
|
320
|
+
// anchor, so chunk objectIDs (which carry a #anchor for uniqueness) must not
|
|
321
|
+
// be used for the href. `url` is also the attribute to dedupe chunks on.
|
|
322
|
+
url: baseObjectID,
|
|
323
|
+
titles: chunk,
|
|
324
|
+
keywords: keywords,
|
|
325
|
+
categories: categories,
|
|
326
|
+
commercialNames: commercialNames,
|
|
327
|
+
unixTimestamp: unixTimestamp
|
|
328
|
+
}
|
|
308
329
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
330
|
+
if (component.name !== 'labs') {
|
|
331
|
+
indexItem.product = component.title
|
|
332
|
+
indexItem.breadcrumbs = breadcrumbs
|
|
333
|
+
indexItem.type = 'Doc'
|
|
334
|
+
indexItem._tags = Array.isArray(tag) ? tag : [tag]
|
|
335
|
+
} else {
|
|
336
|
+
indexItem.deployment = deployment
|
|
337
|
+
indexItem.type = 'Lab'
|
|
338
|
+
indexItem.interactive = false
|
|
339
|
+
indexItem._tags = Array.isArray(tag) ? tag : [tag]
|
|
340
|
+
}
|
|
320
341
|
|
|
321
|
-
|
|
322
|
-
|
|
342
|
+
algolia[cname][version].push(indexItem)
|
|
343
|
+
algoliaCount++
|
|
344
|
+
})
|
|
323
345
|
}
|
|
324
346
|
|
|
325
347
|
logger.info(`Indexed ${algoliaCount} pages`)
|
|
326
348
|
return algolia
|
|
327
349
|
}
|
|
328
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Split a titles array into chunks whose combined token count stays within
|
|
353
|
+
* Algolia's per-record indexing window (~290 words; we leave a safety margin).
|
|
354
|
+
* Each returned chunk is small enough that every heading it contains is actually
|
|
355
|
+
* indexed for search, which keeps property/metric names on long reference pages
|
|
356
|
+
* searchable (DOC-1878).
|
|
357
|
+
*
|
|
358
|
+
* @param {Array<{t: string, h: string}>} titles - Section headings for a page.
|
|
359
|
+
* @param {number} [maxTokens=180] - Approximate token budget per chunk.
|
|
360
|
+
* @returns {Array<Array<{t: string, h: string}>>} One or more title chunks. An
|
|
361
|
+
* empty input yields a single empty chunk so the page still gets a record.
|
|
362
|
+
*/
|
|
363
|
+
function chunkTitles (titles, maxTokens = 180) {
|
|
364
|
+
if (!titles.length) return [[]]
|
|
365
|
+
const chunks = []
|
|
366
|
+
let current = []
|
|
367
|
+
let tokens = 0
|
|
368
|
+
for (const entry of titles) {
|
|
369
|
+
// Algolia tokenizes on separators (whitespace, underscores, dots, etc.), so a
|
|
370
|
+
// name like `partition_autobalancing_mode` counts as several tokens.
|
|
371
|
+
const entryTokens = String(entry.t || '').split(/[\s_./:-]+/).filter(Boolean).length || 1
|
|
372
|
+
if (current.length && tokens + entryTokens > maxTokens) {
|
|
373
|
+
chunks.push(current)
|
|
374
|
+
current = []
|
|
375
|
+
tokens = 0
|
|
376
|
+
}
|
|
377
|
+
current.push(entry)
|
|
378
|
+
tokens += entryTokens
|
|
379
|
+
}
|
|
380
|
+
if (current.length) chunks.push(current)
|
|
381
|
+
return chunks
|
|
382
|
+
}
|
|
383
|
+
|
|
329
384
|
/**
|
|
330
385
|
* Extract the path from a URL
|
|
331
386
|
* @param {string} url - The URL to extract path from
|
package/package.json
CHANGED
|
@@ -235,7 +235,7 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
|
|
|
235
235
|
else if (isSelfHostedOnly) platformIndicator = ' 🖥️';
|
|
236
236
|
if (isCgoOnly) platformIndicator += ' 🔧';
|
|
237
237
|
|
|
238
|
-
lines.push(`##### \`${comp.name}\` (${comp.type}, ${comp.status})${platformIndicator}`);
|
|
238
|
+
lines.push(`##### \`${comp.name}\` (${comp.status ? `${comp.type}, ${comp.status}` : comp.type})${platformIndicator}`);
|
|
239
239
|
|
|
240
240
|
// Add description (truncated to 2 sentences)
|
|
241
241
|
if (comp.description) {
|
|
@@ -982,7 +982,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
|
|
|
982
982
|
lines.push('**☁️ Cloud Supported:**');
|
|
983
983
|
lines.push('');
|
|
984
984
|
cloudSupportedNew.forEach(c => {
|
|
985
|
-
lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
|
|
985
|
+
lines.push(`- **${c.name}** (${c.status ? `${c.type}, ${c.status}` : c.type})`);
|
|
986
986
|
const desc = c.summary || c.description;
|
|
987
987
|
if (desc) {
|
|
988
988
|
const shortDesc = truncateToSentence(desc, 2);
|
|
@@ -996,7 +996,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
|
|
|
996
996
|
lines.push('**Self-Hosted Only:**');
|
|
997
997
|
lines.push('');
|
|
998
998
|
selfHostedOnlyNew.forEach(c => {
|
|
999
|
-
lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
|
|
999
|
+
lines.push(`- **${c.name}** (${c.status ? `${c.type}, ${c.status}` : c.type})`);
|
|
1000
1000
|
const desc = c.summary || c.description;
|
|
1001
1001
|
if (desc) {
|
|
1002
1002
|
const shortDesc = truncateToSentence(desc, 2);
|
|
@@ -1008,7 +1008,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
|
|
|
1008
1008
|
} else {
|
|
1009
1009
|
// No cloud support info, just list all
|
|
1010
1010
|
diffData.details.newComponents.forEach(c => {
|
|
1011
|
-
lines.push(`- **${c.name}** (${c.type}, ${c.status})`);
|
|
1011
|
+
lines.push(`- **${c.name}** (${c.status ? `${c.type}, ${c.status}` : c.type})`);
|
|
1012
1012
|
const desc = c.summary || c.description;
|
|
1013
1013
|
if (desc) {
|
|
1014
1014
|
const shortDesc = truncateToSentence(desc, 2);
|
|
@@ -1247,7 +1247,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
|
|
|
1247
1247
|
Object.entries(gapsByType).forEach(([type, connectors]) => {
|
|
1248
1248
|
lines.push(`**${type}:**`);
|
|
1249
1249
|
connectors.forEach(c => {
|
|
1250
|
-
lines.push(`- ${c.name} (${c.status})`);
|
|
1250
|
+
lines.push(`- ${c.name}${c.status ? ` (${c.status})` : ''}`);
|
|
1251
1251
|
});
|
|
1252
1252
|
lines.push('');
|
|
1253
1253
|
});
|
|
@@ -21,7 +21,7 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
21
21
|
return {
|
|
22
22
|
name,
|
|
23
23
|
type,
|
|
24
|
-
status: raw.status ||
|
|
24
|
+
status: raw.status || '',
|
|
25
25
|
version: raw.version || raw.introducedInVersion || '',
|
|
26
26
|
description: raw.description || '',
|
|
27
27
|
requiresCgo: metadata.requiresCgo || false,
|
|
@@ -39,7 +39,7 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
39
39
|
return {
|
|
40
40
|
name,
|
|
41
41
|
type,
|
|
42
|
-
status: raw.status ||
|
|
42
|
+
status: raw.status || '',
|
|
43
43
|
version: raw.version || raw.introducedInVersion || '',
|
|
44
44
|
description: raw.description || '',
|
|
45
45
|
requiresCgo: metadata.requiresCgo || false,
|
|
@@ -101,7 +101,7 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
101
101
|
deprecatedComponents.push({
|
|
102
102
|
name,
|
|
103
103
|
type,
|
|
104
|
-
status: raw.status ||
|
|
104
|
+
status: raw.status || '',
|
|
105
105
|
version: raw.version || raw.introducedInVersion || '',
|
|
106
106
|
description: raw.description || ''
|
|
107
107
|
});
|
|
@@ -561,7 +561,7 @@ function printDeltaReport(oldIndex, newIndex) {
|
|
|
561
561
|
newComponentKeys.forEach(key => {
|
|
562
562
|
const [type, name] = key.split(':');
|
|
563
563
|
const raw = newMap[key].raw;
|
|
564
|
-
const status = raw.status ||
|
|
564
|
+
const status = raw.status || '';
|
|
565
565
|
const version = raw.version || raw.introducedInVersion || '';
|
|
566
566
|
console.log(
|
|
567
567
|
` • ${type}/${name}${
|
|
@@ -30,6 +30,8 @@ function capToTwoSentences (description) {
|
|
|
30
30
|
|
|
31
31
|
const abbreviations = [
|
|
32
32
|
/https?:\/\/[^\s]+?(?=[.!?](?:\s|$)|\s|$)/gi, // Protect URLs from being split by sentence detection (non-greedy, preserve trailing punctuation)
|
|
33
|
+
/\bxref:[^\s[\]]+\[[^\]]*\]/g, // Protect Antora xref macros (e.g. xref:components:processors/try.adoc[`try`]) whose targets contain periods
|
|
34
|
+
/\b[\w/-]+\.(?:adoc|md|ya?ml|json|html)\b/gi, // Protect bare doc/config filenames (try.adoc, config.yaml) from splitting
|
|
33
35
|
/\bv\d+\.\d+(?:\.\d+)?/gi,
|
|
34
36
|
/\d+\.\d+/g,
|
|
35
37
|
/\be\.g\./gi,
|
|
@@ -64,6 +66,16 @@ function capToTwoSentences (description) {
|
|
|
64
66
|
const sentenceRegex = /[^.!?]+[.!?]+(?:\s|$)/g
|
|
65
67
|
const sentences = normalized.match(sentenceRegex)
|
|
66
68
|
|
|
69
|
+
// If an unprotected mid-token period prevented the leading text from
|
|
70
|
+
// forming a complete match, merge that dropped prefix into the first
|
|
71
|
+
// sentence rather than silently discarding it.
|
|
72
|
+
if (sentences && sentences.length > 0) {
|
|
73
|
+
const firstIndex = normalized.indexOf(sentences[0])
|
|
74
|
+
if (firstIndex > 0) {
|
|
75
|
+
sentences[0] = normalized.slice(0, firstIndex) + sentences[0]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
if (!sentences || sentences.length === 0) {
|
|
68
80
|
let result = normalized
|
|
69
81
|
placeholders.forEach(({ placeholder, original }) => {
|
|
@@ -135,7 +147,7 @@ function augmentConnectorData (connectorData, binaryAnalysis) {
|
|
|
135
147
|
let addedCloudOnlyCount = 0
|
|
136
148
|
|
|
137
149
|
const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
138
|
-
'buffers', 'metrics', 'scanners', 'tracers']
|
|
150
|
+
'buffers', 'metrics', 'scanners', 'tracers', 'config']
|
|
139
151
|
|
|
140
152
|
for (const type of connectorTypes) {
|
|
141
153
|
if (!Array.isArray(augmentedData[type])) {
|
|
@@ -195,6 +207,47 @@ function augmentConnectorData (connectorData, binaryAnalysis) {
|
|
|
195
207
|
return { augmentedData, augmentedCount, addedCgoCount, addedCloudOnlyCount }
|
|
196
208
|
}
|
|
197
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Build a pure-OSS snapshot from a (possibly augmented) connector index, for use
|
|
212
|
+
* as the baseline in binary analysis. Removes augmentation-only entries (the
|
|
213
|
+
* cloud-only and cgo-only connectors that augmentConnectorData() adds and persists
|
|
214
|
+
* back into the data file) and strips platform metadata so the snapshot reflects
|
|
215
|
+
* only what the standard OSS rpk build reports.
|
|
216
|
+
*
|
|
217
|
+
* @param {Object} connectorIndex - Connector index object with arrays per type
|
|
218
|
+
* @returns {Object} Deep-cloned clean copy safe to diff as pure OSS data
|
|
219
|
+
*/
|
|
220
|
+
function buildCleanOssData (connectorIndex) {
|
|
221
|
+
const cleanData = JSON.parse(JSON.stringify(connectorIndex))
|
|
222
|
+
const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
223
|
+
'buffers', 'metrics', 'scanners', 'tracers']
|
|
224
|
+
|
|
225
|
+
for (const type of connectorTypes) {
|
|
226
|
+
if (Array.isArray(cleanData[type])) {
|
|
227
|
+
// Keep only connectors from OSS rpk (have config/fields)
|
|
228
|
+
// Remove augmentation-only connectors (added by previous binary analysis)
|
|
229
|
+
cleanData[type] = cleanData[type].filter(c => c.config || c.fields)
|
|
230
|
+
|
|
231
|
+
// Remove platform metadata from remaining connectors
|
|
232
|
+
stripPlatformMetadata(cleanData[type])
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Config components have no config/fields wrappers, so the filter above
|
|
237
|
+
// can't distinguish OSS entries from augmentation-only ones. augmentConnectorData()
|
|
238
|
+
// adds cloud-only and cgo-only config entries and persists them back to the data
|
|
239
|
+
// file, so on subsequent runs they would otherwise be copied into the clean OSS
|
|
240
|
+
// snapshot and pollute binary analysis. Drop those augmentation-only entries by
|
|
241
|
+
// their platform markers (genuine OSS config entries never carry cloudOnly, and
|
|
242
|
+
// a standard-build component is never cgo-only) before stripping metadata.
|
|
243
|
+
if (Array.isArray(cleanData.config)) {
|
|
244
|
+
cleanData.config = cleanData.config.filter(c => c.cloudOnly !== true && c.requiresCgo !== true)
|
|
245
|
+
stripPlatformMetadata(cleanData.config)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return cleanData
|
|
249
|
+
}
|
|
250
|
+
|
|
198
251
|
/**
|
|
199
252
|
* Update whats-new.adoc with new release information
|
|
200
253
|
* @param {Object} params - Parameters
|
|
@@ -1097,20 +1150,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1097
1150
|
const cleanOssDataPath = path.join(dataDir, `._connect-${newVersion}-clean.json`)
|
|
1098
1151
|
|
|
1099
1152
|
// Create clean version by removing augmented connectors
|
|
1100
|
-
const cleanData =
|
|
1101
|
-
const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
1102
|
-
'buffers', 'metrics', 'scanners', 'tracers']
|
|
1103
|
-
|
|
1104
|
-
for (const type of connectorTypes) {
|
|
1105
|
-
if (Array.isArray(cleanData[type])) {
|
|
1106
|
-
// Keep only connectors from OSS rpk (have config/fields)
|
|
1107
|
-
// Remove augmentation-only connectors (added by previous binary analysis)
|
|
1108
|
-
cleanData[type] = cleanData[type].filter(c => c.config || c.fields)
|
|
1109
|
-
|
|
1110
|
-
// Remove platform metadata from remaining connectors
|
|
1111
|
-
stripPlatformMetadata(cleanData[type])
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1153
|
+
const cleanData = buildCleanOssData(newIndex)
|
|
1114
1154
|
|
|
1115
1155
|
fs.writeFileSync(cleanOssDataPath, JSON.stringify(cleanData, null, 2), 'utf8')
|
|
1116
1156
|
|
|
@@ -1357,7 +1397,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1357
1397
|
// Strip CGO/cloud-only connectors and metadata from old data
|
|
1358
1398
|
oldIndexForDiff = JSON.parse(JSON.stringify(oldIndex))
|
|
1359
1399
|
const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
1360
|
-
'buffers', 'metrics', 'scanners', 'tracers']
|
|
1400
|
+
'buffers', 'metrics', 'scanners', 'tracers', 'config']
|
|
1361
1401
|
|
|
1362
1402
|
let totalStripped = 0
|
|
1363
1403
|
for (const type of connectorTypes) {
|
|
@@ -1970,5 +2010,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1970
2010
|
module.exports = {
|
|
1971
2011
|
handleRpcnConnectorDocs,
|
|
1972
2012
|
updateWhatsNew,
|
|
1973
|
-
capToTwoSentences
|
|
2013
|
+
capToTwoSentences,
|
|
2014
|
+
augmentConnectorData,
|
|
2015
|
+
buildCleanOssData
|
|
1974
2016
|
}
|
|
@@ -274,6 +274,23 @@ function shouldExcludeCommand(overrides, commandPath) {
|
|
|
274
274
|
return commandOverride?.exclude === true
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Check if a command should be written to the partials directory (cloudSecretDir).
|
|
279
|
+
* Walks up the command path so setting asPartial: true on "rpk ai" applies to all children.
|
|
280
|
+
* @param {Object} overrides - Resolved overrides object
|
|
281
|
+
* @param {string} commandPath - Full command path (e.g., "rpk ai agent list")
|
|
282
|
+
* @returns {boolean}
|
|
283
|
+
*/
|
|
284
|
+
function shouldUsePartialDir(overrides, commandPath) {
|
|
285
|
+
if (!overrides?.commands) return false
|
|
286
|
+
const parts = commandPath.split(' ')
|
|
287
|
+
for (let i = parts.length; i >= 1; i--) {
|
|
288
|
+
const ancestor = parts.slice(0, i).join(' ')
|
|
289
|
+
if (overrides.commands[ancestor]?.asPartial === true) return true
|
|
290
|
+
}
|
|
291
|
+
return false
|
|
292
|
+
}
|
|
293
|
+
|
|
277
294
|
/**
|
|
278
295
|
* Get command metadata from overrides
|
|
279
296
|
* @param {Object} overrides - Overrides object (resolved)
|
|
@@ -390,13 +407,21 @@ function processContentArray(contentArray, context = '') {
|
|
|
390
407
|
}
|
|
391
408
|
|
|
392
409
|
switch (type) {
|
|
393
|
-
case 'section':
|
|
410
|
+
case 'section': {
|
|
411
|
+
// Escape {word} (no hyphens) in table cell lines — CLI format specifiers like {hex}
|
|
412
|
+
// are not AsciiDoc attribute refs and must be escaped to prevent substitution.
|
|
413
|
+
const escapedContent = typeof content === 'string'
|
|
414
|
+
? content.replace(/^\|(.*)$/gm, (m, cell) =>
|
|
415
|
+
/\{[a-z_][a-z0-9_]*\}/.test(cell)
|
|
416
|
+
? '|' + cell.replace(/\{([a-z_][a-z0-9_]*)\}/g, '\\{$1}')
|
|
417
|
+
: m)
|
|
418
|
+
: content
|
|
394
419
|
// Pass through all section properties (subsections, parent, headingLevel, exclude)
|
|
395
420
|
result.sections[position].push({
|
|
396
421
|
type: 'section',
|
|
397
422
|
id,
|
|
398
423
|
title,
|
|
399
|
-
content,
|
|
424
|
+
content: escapedContent,
|
|
400
425
|
subsections: item.subsections,
|
|
401
426
|
parent: item.parent,
|
|
402
427
|
headingLevel: item.headingLevel,
|
|
@@ -408,6 +433,7 @@ function processContentArray(contentArray, context = '') {
|
|
|
408
433
|
console.warn(` Use seeAlso key instead`)
|
|
409
434
|
}
|
|
410
435
|
break
|
|
436
|
+
}
|
|
411
437
|
|
|
412
438
|
case 'example':
|
|
413
439
|
// Structured single example
|
|
@@ -1184,7 +1210,10 @@ function convertIndentedTablesToAsciiDoc(text, options = {}) {
|
|
|
1184
1210
|
// Remove trailing colon from value (e.g., "Organization:" -> "Organization")
|
|
1185
1211
|
const cleanValue = value.replace(/:$/, '')
|
|
1186
1212
|
const desc = descParts.join(' ')
|
|
1187
|
-
|
|
1213
|
+
// Escape {word} (no hyphens) in table cells — CLI format specifiers like {hex}, {json}
|
|
1214
|
+
// are not AsciiDoc attribute refs and must be escaped to prevent substitution.
|
|
1215
|
+
const escapeAttrs = s => s.replace(/\{([a-z_][a-z0-9_]*)\}/g, '\\{$1}')
|
|
1216
|
+
result.push(`|${escapeAttrs(cleanValue)} |${escapeAttrs(desc)}`)
|
|
1188
1217
|
}
|
|
1189
1218
|
result.push('|===')
|
|
1190
1219
|
result.push('')
|
|
@@ -1203,6 +1232,28 @@ function convertIndentedTablesToAsciiDoc(text, options = {}) {
|
|
|
1203
1232
|
return result.join('\n')
|
|
1204
1233
|
}
|
|
1205
1234
|
|
|
1235
|
+
/**
|
|
1236
|
+
* Apply only the text replacements from textTransformations to a raw string.
|
|
1237
|
+
* Used for examples content where we want string substitutions (e.g. rpai → rpk ai)
|
|
1238
|
+
* but NOT AsciiDoc structural formatting or inlineCode wrapping.
|
|
1239
|
+
* @param {string} text
|
|
1240
|
+
* @param {Object|null} customTransformations
|
|
1241
|
+
* @returns {string}
|
|
1242
|
+
*/
|
|
1243
|
+
function applyTextTransformations(text, customTransformations) {
|
|
1244
|
+
if (!text || !customTransformations?.replacements) return text
|
|
1245
|
+
let result = text
|
|
1246
|
+
for (const rule of customTransformations.replacements) {
|
|
1247
|
+
try {
|
|
1248
|
+
const flags = rule.flags || 'g'
|
|
1249
|
+
result = result.replace(new RegExp(rule.pattern, flags), rule.replacement)
|
|
1250
|
+
} catch (err) {
|
|
1251
|
+
console.warn(`⚠ Invalid replacement pattern: ${rule.pattern}`)
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
return result
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1206
1257
|
/**
|
|
1207
1258
|
* Format description by adding backticks around flags and code
|
|
1208
1259
|
* @param {string} desc - Description text
|
|
@@ -1289,10 +1340,18 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1289
1340
|
})
|
|
1290
1341
|
|
|
1291
1342
|
// Protect inline code (backticked content)
|
|
1343
|
+
// Escape {word} (no hyphens) inside backtick spans — these are CLI format specifiers or
|
|
1344
|
+
// path placeholders, not AsciiDoc attribute refs (which use hyphens like {full-version}).
|
|
1345
|
+
// AsciiDoc single-backtick spans still apply attribute substitution, so {hex}/{namespace}/etc.
|
|
1346
|
+
// would expand to empty string without this escaping.
|
|
1292
1347
|
const inlineCode = []
|
|
1293
1348
|
preProcessed = preProcessed.replace(/`[^`]+`/g, (match) => {
|
|
1294
1349
|
const placeholder = `__INLINE_CODE_${inlineCode.length}__`
|
|
1295
|
-
|
|
1350
|
+
// Skip `+...+` passthrough spans — they already prevent attribute substitution
|
|
1351
|
+
const escaped = (match.startsWith('`+') && match.endsWith('+`'))
|
|
1352
|
+
? match
|
|
1353
|
+
: match.replace(/\{([a-z_][a-z0-9_]*)\}/g, '\\{$1}')
|
|
1354
|
+
inlineCode.push(escaped)
|
|
1296
1355
|
return placeholder
|
|
1297
1356
|
})
|
|
1298
1357
|
|
|
@@ -1536,28 +1595,46 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1536
1595
|
// Restore early code blocks FIRST (from indented command/YAML detection)
|
|
1537
1596
|
// This must happen before inline code restoration so that placeholders
|
|
1538
1597
|
// inside the early code blocks (like __INLINE_CODE_X__) get resolved
|
|
1598
|
+
// Use function replacements to prevent $ special-pattern interpretation (e.g. `$` → before-match)
|
|
1539
1599
|
earlyCodeBlocks.forEach((block, i) => {
|
|
1540
|
-
result = result.replace(`__EARLY_CODE_BLOCK_${i}__`, block)
|
|
1600
|
+
result = result.replace(`__EARLY_CODE_BLOCK_${i}__`, () => block)
|
|
1541
1601
|
})
|
|
1542
1602
|
|
|
1543
1603
|
// Restore inline code
|
|
1544
1604
|
inlineCode.forEach((code, i) => {
|
|
1545
|
-
result = result.replace(`__INLINE_CODE_${i}__`, code)
|
|
1605
|
+
result = result.replace(`__INLINE_CODE_${i}__`, () => code)
|
|
1546
1606
|
})
|
|
1547
1607
|
|
|
1548
1608
|
// Restore xrefs
|
|
1549
1609
|
xrefs.forEach((xref, i) => {
|
|
1550
|
-
result = result.replace(`__XREF_${i}__`, xref)
|
|
1610
|
+
result = result.replace(`__XREF_${i}__`, () => xref)
|
|
1551
1611
|
})
|
|
1552
1612
|
|
|
1553
1613
|
// Restore code blocks
|
|
1554
1614
|
codeBlocks.forEach((block, i) => {
|
|
1555
|
-
result = result.replace(`__CODE_BLOCK_${i}__`, block)
|
|
1615
|
+
result = result.replace(`__CODE_BLOCK_${i}__`, () => block)
|
|
1556
1616
|
})
|
|
1557
1617
|
|
|
1558
1618
|
// Clean up double backticks (moved AFTER restoration to catch nested backticks)
|
|
1559
1619
|
result = result.replace(/``+/g, '`')
|
|
1560
1620
|
|
|
1621
|
+
// Final pass: escape {word} (no hyphens) inside any backtick spans that were created
|
|
1622
|
+
// dynamically during processing (after the initial inline-code protection step).
|
|
1623
|
+
// Skip `+...+` passthrough spans — they already prevent attribute substitution.
|
|
1624
|
+
// Use negative lookbehind (?<!\\) to avoid double-escaping already-escaped \{word}.
|
|
1625
|
+
result = result.replace(/`[^`]+`/g, match => {
|
|
1626
|
+
if (match.startsWith('`+') && match.endsWith('+`')) return match
|
|
1627
|
+
return match.replace(/(?<!\\)\{([a-z_][a-z0-9_]*)\}/g, '\\{$1}')
|
|
1628
|
+
})
|
|
1629
|
+
|
|
1630
|
+
// Escape {word} (no hyphens) in AsciiDoc table cell lines that come from pre-formatted
|
|
1631
|
+
// AsciiDoc in CLI descriptions (e.g. format specifier tables with {hex}, {json}).
|
|
1632
|
+
// These never pass through backtick-span protection, so they need a separate pass.
|
|
1633
|
+
result = result.replace(/^\|(.*)$/gm, (match, cellContent) => {
|
|
1634
|
+
if (!/(?<!\\)\{[a-z_][a-z0-9_]*\}/.test(cellContent)) return match
|
|
1635
|
+
return '|' + cellContent.replace(/(?<!\\)\{([a-z_][a-z0-9_]*)\}/g, '\\{$1}')
|
|
1636
|
+
})
|
|
1637
|
+
|
|
1561
1638
|
// Remove trailing period after backticked commands on their own line
|
|
1562
1639
|
// e.g., "`rpk cluster status`." → "`rpk cluster status`"
|
|
1563
1640
|
// Only consume trailing spaces, not newlines (to preserve paragraph breaks)
|
|
@@ -1581,40 +1658,48 @@ function formatUsage(usage) {
|
|
|
1581
1658
|
}
|
|
1582
1659
|
|
|
1583
1660
|
/**
|
|
1584
|
-
* Format EXAMPLES section - convert indented commands and inline code to AsciiDoc code blocks
|
|
1661
|
+
* Format EXAMPLES section - convert indented commands and inline code to AsciiDoc code blocks.
|
|
1662
|
+
*
|
|
1663
|
+
* Handles the standard Go CLI example format where each group is separated by a blank line,
|
|
1664
|
+
* comment lines (# ...) describe the example, and command lines follow:
|
|
1665
|
+
*
|
|
1666
|
+
* # Description of the example
|
|
1667
|
+
* rpk some command --flag
|
|
1668
|
+
*
|
|
1669
|
+
* # Another example
|
|
1670
|
+
* rpk other command
|
|
1671
|
+
* rpk other command --variant
|
|
1672
|
+
*
|
|
1673
|
+
* Comments become plain text descriptions; consecutive command lines share one code block.
|
|
1674
|
+
*
|
|
1585
1675
|
* @param {string} text - Examples text with indented commands or inline code
|
|
1586
1676
|
* @returns {string} Formatted examples with code blocks
|
|
1587
1677
|
*/
|
|
1588
1678
|
function formatExamples(text) {
|
|
1589
1679
|
if (!text) return text
|
|
1590
1680
|
|
|
1591
|
-
// Convert both indented commands and inline backticked commands to AsciiDoc code blocks
|
|
1592
|
-
// Pattern 1: description line, then indented command (starts with 2+ spaces)
|
|
1593
|
-
// Pattern 2: description line ending with colon/period, followed by `rpk command`
|
|
1594
1681
|
const lines = text.split('\n')
|
|
1595
1682
|
const result = []
|
|
1596
1683
|
let i = 0
|
|
1597
1684
|
|
|
1598
1685
|
while (i < lines.length) {
|
|
1599
1686
|
const line = lines[i]
|
|
1687
|
+
const isIndented = /^\s{2,}\S/.test(line)
|
|
1600
1688
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
result.push(lines[i + 1].trim())
|
|
1609
|
-
result.push('----')
|
|
1610
|
-
i += 2
|
|
1611
|
-
} else if (/^\s{2,}\S/.test(line)) {
|
|
1612
|
-
// Standalone indented command without preceding description
|
|
1689
|
+
if (isIndented && line.trim().startsWith('#')) {
|
|
1690
|
+
// Comment line — render as a description paragraph outside the code block
|
|
1691
|
+
result.push(line.trim().replace(/^#\s*/, ''))
|
|
1692
|
+
i++
|
|
1693
|
+
} else if (isIndented) {
|
|
1694
|
+
// Command line — collect all consecutive command lines into one code block
|
|
1695
|
+
// (stops at a blank line or a comment line)
|
|
1613
1696
|
result.push('[,bash]')
|
|
1614
1697
|
result.push('----')
|
|
1615
|
-
|
|
1698
|
+
while (i < lines.length && /^\s{2,}\S/.test(lines[i]) && !lines[i].trim().startsWith('#')) {
|
|
1699
|
+
result.push(lines[i].trim())
|
|
1700
|
+
i++
|
|
1701
|
+
}
|
|
1616
1702
|
result.push('----')
|
|
1617
|
-
i++
|
|
1618
1703
|
} else if (/^\s*`rpk .+`\s*$/.test(line)) {
|
|
1619
1704
|
// Inline backticked command on its own line - convert to code block
|
|
1620
1705
|
const command = line.trim().replace(/^`|`$/g, '')
|
|
@@ -1637,7 +1722,7 @@ function formatExamples(text) {
|
|
|
1637
1722
|
result.push('----')
|
|
1638
1723
|
i += 2
|
|
1639
1724
|
} else {
|
|
1640
|
-
// Regular line (
|
|
1725
|
+
// Regular line (blank line, non-indented description text, etc.)
|
|
1641
1726
|
result.push(line)
|
|
1642
1727
|
i++
|
|
1643
1728
|
}
|
|
@@ -1997,6 +2082,91 @@ function findTopLevelWithSubcommands(tree) {
|
|
|
1997
2082
|
return result
|
|
1998
2083
|
}
|
|
1999
2084
|
|
|
2085
|
+
/**
|
|
2086
|
+
* Static entries that always appear at the top of the rpk nav section,
|
|
2087
|
+
* before the auto-generated command entries. These are hand-written pages
|
|
2088
|
+
* that are not part of the CLI command tree. The root `rpk` command is not
|
|
2089
|
+
* listed here: it is represented by the section's hand-written index.adoc
|
|
2090
|
+
* landing page (the section header), so it is skipped during generation.
|
|
2091
|
+
*/
|
|
2092
|
+
const STATIC_RPK_NAV_ENTRIES = [
|
|
2093
|
+
'*** xref:reference:rpk/rpk-commands.adoc[]',
|
|
2094
|
+
'*** xref:reference:rpk/rpk-x-options.adoc[rpk -X]',
|
|
2095
|
+
]
|
|
2096
|
+
|
|
2097
|
+
/**
|
|
2098
|
+
* Update the rpk section of nav.adoc with the current command tree.
|
|
2099
|
+
* Replaces auto-generated entries while preserving the section header and
|
|
2100
|
+
* the static hand-written entries defined in STATIC_RPK_NAV_ENTRIES.
|
|
2101
|
+
* @param {string} navFile - Absolute path to nav.adoc
|
|
2102
|
+
* @param {Array} commands - Flat command list from flattenCommands()
|
|
2103
|
+
* @param {Object} resolvedOverrides - Resolved overrides
|
|
2104
|
+
* @param {Set} topLevelWithSubcommands - Set of top-level command names with subcommands
|
|
2105
|
+
* @returns {Object} { navUpdated, navEntriesGenerated }
|
|
2106
|
+
*/
|
|
2107
|
+
function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands) {
|
|
2108
|
+
if (!fs.existsSync(navFile)) {
|
|
2109
|
+
console.warn(`Warning: nav file not found, skipping nav update: ${navFile}`)
|
|
2110
|
+
return { navUpdated: false }
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
const content = fs.readFileSync(navFile, 'utf8')
|
|
2114
|
+
const lines = content.split('\n')
|
|
2115
|
+
|
|
2116
|
+
// Locate the rpk section header
|
|
2117
|
+
const sectionStart = lines.findIndex(l => /xref:reference:rpk\/index\.adoc/.test(l))
|
|
2118
|
+
if (sectionStart === -1) {
|
|
2119
|
+
console.warn('Warning: rpk section not found in nav file, skipping nav update')
|
|
2120
|
+
return { navUpdated: false }
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
// Find where the section ends — next line with the same or fewer asterisks as the header
|
|
2124
|
+
const headerStars = (lines[sectionStart].match(/^\*+/) || [''])[0].length
|
|
2125
|
+
let sectionEnd = lines.length
|
|
2126
|
+
for (let i = sectionStart + 1; i < lines.length; i++) {
|
|
2127
|
+
const m = lines[i].match(/^\*+/)
|
|
2128
|
+
if (m && m[0].length <= headerStars && lines[i].trim() !== '') {
|
|
2129
|
+
sectionEnd = i
|
|
2130
|
+
break
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// Generate nav entries from the command tree (DFS order from flattenCommands)
|
|
2135
|
+
const newEntries = []
|
|
2136
|
+
for (const { path: commandPath } of commands) {
|
|
2137
|
+
if (commandPath === 'rpk') continue
|
|
2138
|
+
if (shouldExcludeCommand(resolvedOverrides, commandPath)) continue
|
|
2139
|
+
if (shouldUsePartialDir(resolvedOverrides, commandPath)) continue
|
|
2140
|
+
if (commandPath.startsWith('rpk cloud')) continue
|
|
2141
|
+
if (commandPath.startsWith('rpk security secret')) continue
|
|
2142
|
+
|
|
2143
|
+
const parts = commandPath.split(' ')
|
|
2144
|
+
const stars = '*'.repeat(parts.length + 1)
|
|
2145
|
+
const { subdir, fileName } = getOutputPath(commandPath, topLevelWithSubcommands)
|
|
2146
|
+
const xrefPath = subdir
|
|
2147
|
+
? `reference:rpk/${subdir}/${fileName}`
|
|
2148
|
+
: `reference:rpk/${fileName}`
|
|
2149
|
+
|
|
2150
|
+
newEntries.push(`${stars} xref:${xrefPath}[]`)
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// Rebuild: header + static entries + generated entries
|
|
2154
|
+
const newSection = [
|
|
2155
|
+
lines[sectionStart],
|
|
2156
|
+
...STATIC_RPK_NAV_ENTRIES,
|
|
2157
|
+
...newEntries,
|
|
2158
|
+
]
|
|
2159
|
+
|
|
2160
|
+
const newLines = [
|
|
2161
|
+
...lines.slice(0, sectionStart),
|
|
2162
|
+
...newSection,
|
|
2163
|
+
...lines.slice(sectionEnd),
|
|
2164
|
+
]
|
|
2165
|
+
|
|
2166
|
+
fs.writeFileSync(navFile, newLines.join('\n'), 'utf8')
|
|
2167
|
+
return { navUpdated: true, navEntriesGenerated: newEntries.length }
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2000
2170
|
/**
|
|
2001
2171
|
* Generate AsciiDoc documentation for all rpk commands
|
|
2002
2172
|
* @param {Object} options - Generation options
|
|
@@ -2011,7 +2181,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2011
2181
|
rpkVersion,
|
|
2012
2182
|
pluginVersions = {},
|
|
2013
2183
|
draftMissing = false,
|
|
2014
|
-
flatOutput = false // If true, output all files flat (legacy behavior)
|
|
2184
|
+
flatOutput = false, // If true, output all files flat (legacy behavior)
|
|
2185
|
+
navFile // Optional: path to nav.adoc for automatic nav updates
|
|
2015
2186
|
} = options
|
|
2016
2187
|
|
|
2017
2188
|
// Register partials
|
|
@@ -2058,8 +2229,18 @@ async function generateRpkDocs(options = {}) {
|
|
|
2058
2229
|
let filesGenerated = 0
|
|
2059
2230
|
let filesSkipped = 0
|
|
2060
2231
|
let filesFailed = 0
|
|
2232
|
+
let filesDeleted = 0
|
|
2233
|
+
const writtenFiles = new Set()
|
|
2061
2234
|
|
|
2062
2235
|
for (const { path: commandPath, command } of commands) {
|
|
2236
|
+
// Skip the root `rpk` command: its reference is the section's hand-written
|
|
2237
|
+
// index.adoc landing page, which generation must not overwrite. The nav
|
|
2238
|
+
// builder skips it for the same reason (see updateNavFile).
|
|
2239
|
+
if (commandPath === 'rpk') {
|
|
2240
|
+
filesSkipped++
|
|
2241
|
+
continue
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2063
2244
|
// Check if command should be excluded
|
|
2064
2245
|
if (shouldExcludeCommand(resolvedOverrides, commandPath)) {
|
|
2065
2246
|
filesSkipped++
|
|
@@ -2087,7 +2268,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2087
2268
|
if (mergedCommand.excludeExamples && mergedCommand.excludeExamples.length > 0) {
|
|
2088
2269
|
examplesContent = filterExamples(sectionContent, mergedCommand.excludeExamples)
|
|
2089
2270
|
}
|
|
2090
|
-
//
|
|
2271
|
+
// Apply text transformations (e.g. rpai → rpk ai) then format
|
|
2272
|
+
examplesContent = applyTextTransformations(examplesContent, textTransformations)
|
|
2091
2273
|
sections[sectionName] = formatExamples(examplesContent)
|
|
2092
2274
|
} else {
|
|
2093
2275
|
sections[sectionName] = formatDescription(sectionContent, textTransformations)
|
|
@@ -2097,11 +2279,15 @@ async function generateRpkDocs(options = {}) {
|
|
|
2097
2279
|
// Add examples from rpk source if not already in sections
|
|
2098
2280
|
// rpk --print-tree provides examples in a separate field
|
|
2099
2281
|
if (command.examples && !sections.EXAMPLES) {
|
|
2100
|
-
|
|
2282
|
+
// Strip surrounding blank lines only — preserve per-line indentation so formatExamples
|
|
2283
|
+
// can detect indented command lines vs plain description text
|
|
2284
|
+
let examplesContent = command.examples.replace(/^\n+/, '').replace(/\n+$/, '')
|
|
2101
2285
|
// Apply excludeExamples filter BEFORE formatting
|
|
2102
2286
|
if (mergedCommand.excludeExamples && mergedCommand.excludeExamples.length > 0) {
|
|
2103
2287
|
examplesContent = filterExamples(examplesContent, mergedCommand.excludeExamples)
|
|
2104
2288
|
}
|
|
2289
|
+
// Apply text transformations (e.g. rpai → rpk ai) then format
|
|
2290
|
+
examplesContent = applyTextTransformations(examplesContent, textTransformations)
|
|
2105
2291
|
sections.EXAMPLES = formatExamples(examplesContent)
|
|
2106
2292
|
}
|
|
2107
2293
|
|
|
@@ -2172,11 +2358,12 @@ async function generateRpkDocs(options = {}) {
|
|
|
2172
2358
|
}
|
|
2173
2359
|
|
|
2174
2360
|
// Build subcommands with correct xref paths
|
|
2175
|
-
// Filter out excluded
|
|
2361
|
+
// Filter out excluded and asPartial subcommands — excluded have no file,
|
|
2362
|
+
// asPartial ones live in the partials directory with no linkable xref.
|
|
2176
2363
|
const subcommands = (command.commands || [])
|
|
2177
2364
|
.filter(sub => {
|
|
2178
2365
|
const subPath = `${commandPath} ${sub.name}`
|
|
2179
|
-
return !shouldExcludeCommand(resolvedOverrides, subPath)
|
|
2366
|
+
return !shouldExcludeCommand(resolvedOverrides, subPath) && !shouldUsePartialDir(resolvedOverrides, subPath)
|
|
2180
2367
|
})
|
|
2181
2368
|
.map(sub => {
|
|
2182
2369
|
const subPath = `${commandPath} ${sub.name}`
|
|
@@ -2357,7 +2544,9 @@ async function generateRpkDocs(options = {}) {
|
|
|
2357
2544
|
// Check if this command should go to cloudSecretDir
|
|
2358
2545
|
const isCloudCommand = commandPath.startsWith('rpk cloud')
|
|
2359
2546
|
const isSecuritySecretCommand = commandPath.startsWith('rpk security secret')
|
|
2360
|
-
const useCloudSecretDir = cloudSecretDir && (
|
|
2547
|
+
const useCloudSecretDir = cloudSecretDir && (
|
|
2548
|
+
isCloudCommand || isSecuritySecretCommand || shouldUsePartialDir(resolvedOverrides, commandPath)
|
|
2549
|
+
)
|
|
2361
2550
|
const effectiveOutputDir = useCloudSecretDir ? cloudSecretDir : outputDir
|
|
2362
2551
|
|
|
2363
2552
|
let filePath
|
|
@@ -2384,6 +2573,7 @@ async function generateRpkDocs(options = {}) {
|
|
|
2384
2573
|
|
|
2385
2574
|
try {
|
|
2386
2575
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
2576
|
+
writtenFiles.add(filePath)
|
|
2387
2577
|
filesGenerated++
|
|
2388
2578
|
|
|
2389
2579
|
if (filesGenerated % 50 === 0) {
|
|
@@ -2395,12 +2585,71 @@ async function generateRpkDocs(options = {}) {
|
|
|
2395
2585
|
}
|
|
2396
2586
|
}
|
|
2397
2587
|
|
|
2588
|
+
// Delete stale .adoc files from removed/excluded commands, including whole
|
|
2589
|
+
// subtrees that no longer receive any writes this run.
|
|
2590
|
+
//
|
|
2591
|
+
// We only clean the "rpk-*" subdirectories (e.g. rpk-topic/, rpk-cluster/),
|
|
2592
|
+
// which are created and owned exclusively by generation. We deliberately do
|
|
2593
|
+
// NOT touch the root output directory: it holds hand-written pages alongside
|
|
2594
|
+
// generated ones (e.g. index.adoc, rpk-commands.adoc, rpk-x-options.adoc), and
|
|
2595
|
+
// those hand-written pages share the "rpk-*.adoc" naming, so there is no safe
|
|
2596
|
+
// way to distinguish a stale generated root page from a hand-written one. A
|
|
2597
|
+
// rare stale root page is preferable to deleting hand-written content.
|
|
2598
|
+
//
|
|
2599
|
+
// Deletions are further restricted to the generated "rpk.adoc"/"rpk-*.adoc"
|
|
2600
|
+
// pattern so a stray non-rpk file inside a subdir is never removed. This also
|
|
2601
|
+
// lets us clean the rpk-* subdirs of the shared partials directory
|
|
2602
|
+
// (cloudSecretDir) without touching hand-written partials at its root.
|
|
2603
|
+
const generatedFilePattern = /^rpk(-[^/\\]*)?\.adoc$/
|
|
2604
|
+
|
|
2605
|
+
const rpkSubdirs = (baseDir) => {
|
|
2606
|
+
if (!baseDir || !fs.existsSync(baseDir)) return []
|
|
2607
|
+
return fs.readdirSync(baseDir, { withFileTypes: true })
|
|
2608
|
+
.filter(entry => entry.isDirectory() && entry.name.startsWith('rpk-'))
|
|
2609
|
+
.map(entry => path.join(baseDir, entry.name))
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
const scanDirs = new Set()
|
|
2613
|
+
for (const dir of rpkSubdirs(outputDir)) scanDirs.add(dir)
|
|
2614
|
+
for (const dir of rpkSubdirs(cloudSecretDir)) scanDirs.add(dir)
|
|
2615
|
+
|
|
2616
|
+
for (const dir of scanDirs) {
|
|
2617
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
2618
|
+
if (!entry.isFile() || !generatedFilePattern.test(entry.name)) continue
|
|
2619
|
+
const full = path.join(dir, entry.name)
|
|
2620
|
+
if (!writtenFiles.has(full)) {
|
|
2621
|
+
try {
|
|
2622
|
+
fs.unlinkSync(full)
|
|
2623
|
+
filesDeleted++
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
console.warn(`Warning: Failed to delete stale file "${full}": ${err.message}`)
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
// Remove the rpk-* subdirectory itself if it is now empty (removed subtree).
|
|
2630
|
+
if (fs.readdirSync(dir).length === 0) {
|
|
2631
|
+
try {
|
|
2632
|
+
fs.rmdirSync(dir)
|
|
2633
|
+
} catch (err) {
|
|
2634
|
+
console.warn(`Warning: Failed to remove empty directory "${dir}": ${err.message}`)
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
// Update nav.adoc if a path was provided
|
|
2640
|
+
let navResult = { navUpdated: false }
|
|
2641
|
+
if (navFile) {
|
|
2642
|
+
navResult = updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcommands)
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2398
2645
|
return {
|
|
2399
2646
|
commandCount: commands.length,
|
|
2400
2647
|
filesGenerated,
|
|
2401
2648
|
filesSkipped,
|
|
2402
2649
|
filesFailed,
|
|
2403
|
-
|
|
2650
|
+
filesDeleted,
|
|
2651
|
+
subdirectoriesCreated: createdSubdirs.size,
|
|
2652
|
+
...navResult
|
|
2404
2653
|
}
|
|
2405
2654
|
}
|
|
2406
2655
|
|
|
@@ -2425,6 +2674,8 @@ module.exports = {
|
|
|
2425
2674
|
loadTemplate,
|
|
2426
2675
|
registerPartial,
|
|
2427
2676
|
shouldExcludeCommand,
|
|
2677
|
+
shouldUsePartialDir,
|
|
2678
|
+
updateNavFile,
|
|
2428
2679
|
getCommandMetadata,
|
|
2429
2680
|
processContentArray,
|
|
2430
2681
|
// Exported for testing
|
|
@@ -387,18 +387,27 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
387
387
|
|
|
388
388
|
// Start a container with the Go image, mount source, build rpk, install plugins, then print-tree
|
|
389
389
|
// Use a long-running container so we can run multiple commands
|
|
390
|
+
// Pin the Go image to the exact version required by go.mod to prevent
|
|
391
|
+
// "go.mod requires go >= X.Y.Z (running X.Y.Z-1)" build failures when
|
|
392
|
+
// golang:1 resolves to a patch release behind the requirement.
|
|
393
|
+
const requiredGoVersion = getRequiredGoVersion(absoluteSourcePath)
|
|
394
|
+
const goImage = requiredGoVersion ? `golang:${requiredGoVersion}` : 'golang:1'
|
|
395
|
+
|
|
390
396
|
console.log('Starting build container...')
|
|
391
397
|
const createResult = spawnSync('docker', [
|
|
392
398
|
'run', '-d', '--rm',
|
|
393
399
|
'-v', `${absoluteSourcePath}:/rpk-source:ro`,
|
|
394
400
|
'-w', '/rpk-source',
|
|
395
|
-
|
|
401
|
+
goImage,
|
|
396
402
|
'sh', '-c', 'sleep 600' // Keep container alive for 10 minutes
|
|
397
403
|
], {
|
|
398
404
|
encoding: 'utf8',
|
|
399
405
|
timeout: 60000
|
|
400
406
|
})
|
|
401
407
|
|
|
408
|
+
let activeResult = createResult
|
|
409
|
+
let activeImage = goImage
|
|
410
|
+
|
|
402
411
|
if (createResult.status !== 0) {
|
|
403
412
|
const stderr = createResult.stderr || ''
|
|
404
413
|
if (stderr.includes('Cannot connect to the Docker daemon')) {
|
|
@@ -407,13 +416,40 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
407
416
|
'Start Docker Desktop or the Docker service and try again.'
|
|
408
417
|
)
|
|
409
418
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
419
|
+
|
|
420
|
+
// If the pinned tag couldn't be pulled (not yet on Docker Hub for a new
|
|
421
|
+
// patch release), retry with golang:1 before giving up.
|
|
422
|
+
const isPullFailure = requiredGoVersion && (
|
|
423
|
+
stderr.includes('manifest unknown') ||
|
|
424
|
+
stderr.includes('pull access denied') ||
|
|
425
|
+
stderr.includes('not found') ||
|
|
426
|
+
stderr.includes('repository does not exist')
|
|
413
427
|
)
|
|
428
|
+
if (isPullFailure) {
|
|
429
|
+
console.warn(`⚠ Could not pull ${goImage}: ${stderr.trim()}`)
|
|
430
|
+
console.log('Retrying with golang:1...')
|
|
431
|
+
activeImage = 'golang:1'
|
|
432
|
+
activeResult = spawnSync('docker', [
|
|
433
|
+
'run', '-d', '--rm',
|
|
434
|
+
'-v', `${absoluteSourcePath}:/rpk-source:ro`,
|
|
435
|
+
'-w', '/rpk-source',
|
|
436
|
+
activeImage,
|
|
437
|
+
'sh', '-c', 'sleep 600'
|
|
438
|
+
], {
|
|
439
|
+
encoding: 'utf8',
|
|
440
|
+
timeout: 60000
|
|
441
|
+
})
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (activeResult.status !== 0) {
|
|
445
|
+
throw new Error(
|
|
446
|
+
`Failed to create build container: ${activeResult.stderr || stderr}\n` +
|
|
447
|
+
'Make sure Docker is running and has sufficient resources.'
|
|
448
|
+
)
|
|
449
|
+
}
|
|
414
450
|
}
|
|
415
451
|
|
|
416
|
-
const containerId =
|
|
452
|
+
const containerId = activeResult.stdout.trim()
|
|
417
453
|
console.log(`Build container started: ${containerId.substring(0, 12)}`)
|
|
418
454
|
|
|
419
455
|
try {
|
|
@@ -1041,6 +1077,9 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1041
1077
|
}
|
|
1042
1078
|
const finalCloudSecretDir = cloudSecretDir || defaultCloudSecretDir
|
|
1043
1079
|
|
|
1080
|
+
// nav.adoc lives at modules/ROOT/nav.adoc relative to the repo root
|
|
1081
|
+
const navFile = path.join(repoRoot, 'modules', 'ROOT', 'nav.adoc')
|
|
1082
|
+
|
|
1044
1083
|
// Determine which version to document
|
|
1045
1084
|
const version = effectiveRef || 'dev'
|
|
1046
1085
|
console.log(`\nGenerating rpk documentation for version: ${version}`)
|
|
@@ -1088,12 +1127,19 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1088
1127
|
rpkVersion,
|
|
1089
1128
|
pluginVersions: {},
|
|
1090
1129
|
draftMissing,
|
|
1091
|
-
preservationsDir: preserveFrom
|
|
1130
|
+
preservationsDir: preserveFrom,
|
|
1131
|
+
navFile
|
|
1092
1132
|
})
|
|
1093
1133
|
|
|
1094
1134
|
console.log(`\nGeneration complete!`)
|
|
1095
1135
|
console.log(` - Commands documented: ${result.commandCount}`)
|
|
1096
1136
|
console.log(` - Files generated: ${result.filesGenerated}`)
|
|
1137
|
+
if (result.filesDeleted > 0) {
|
|
1138
|
+
console.log(` - Stale files deleted: ${result.filesDeleted}`)
|
|
1139
|
+
}
|
|
1140
|
+
if (result.navUpdated) {
|
|
1141
|
+
console.log(` - Nav entries updated: ${result.navEntriesGenerated}`)
|
|
1142
|
+
}
|
|
1097
1143
|
console.log(` - Output directory: ${finalOutputDir}`)
|
|
1098
1144
|
|
|
1099
1145
|
// Run validation on generated output
|
|
@@ -1387,12 +1433,19 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1387
1433
|
rpkVersion,
|
|
1388
1434
|
pluginVersions,
|
|
1389
1435
|
draftMissing,
|
|
1390
|
-
preservationsDir: preserveFrom
|
|
1436
|
+
preservationsDir: preserveFrom,
|
|
1437
|
+
navFile
|
|
1391
1438
|
})
|
|
1392
1439
|
|
|
1393
1440
|
console.log(`\nGeneration complete!`)
|
|
1394
1441
|
console.log(` - Commands documented: ${result.commandCount}`)
|
|
1395
1442
|
console.log(` - Files generated: ${result.filesGenerated}`)
|
|
1443
|
+
if (result.filesDeleted > 0) {
|
|
1444
|
+
console.log(` - Stale files deleted: ${result.filesDeleted}`)
|
|
1445
|
+
}
|
|
1446
|
+
if (result.navUpdated) {
|
|
1447
|
+
console.log(` - Nav entries updated: ${result.navEntriesGenerated}`)
|
|
1448
|
+
}
|
|
1396
1449
|
console.log(` - Output directory: ${finalOutputDir}`)
|
|
1397
1450
|
|
|
1398
1451
|
// Run validation on generated output
|
|
@@ -1626,6 +1679,7 @@ module.exports = {
|
|
|
1626
1679
|
handleRpkDocsGeneration,
|
|
1627
1680
|
fetchRpkTreeFromSource,
|
|
1628
1681
|
fetchRpkTreeFromLinuxSource,
|
|
1682
|
+
getRequiredGoVersion,
|
|
1629
1683
|
prepareSourceFromRef,
|
|
1630
1684
|
detectLinuxOnlyFromSource,
|
|
1631
1685
|
addPlatformMarkersFromSource,
|
|
@@ -18,7 +18,7 @@ ifndef::env-cloud[]
|
|
|
18
18
|
:page-deprecated: true
|
|
19
19
|
{{/if}}
|
|
20
20
|
|
|
21
|
-
// This content is autogenerated. Do not edit manually. To customize content, see the writer's guide: https://github.com/redpanda-data/docs
|
|
21
|
+
// This content is autogenerated. Do not edit manually. To customize content, see the writer's guide: https://github.com/redpanda-data/docs/blob/main/docs-data/RPK_OVERRIDES_GUIDE.adoc
|
|
22
22
|
{{#each pageAttributes}}
|
|
23
23
|
:{{@key}}: {{this}}
|
|
24
24
|
{{/each}}
|
|
@@ -107,9 +107,23 @@ function normalizeCommandPath(cmdPath) {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Load and compile the JSON Schema for validation
|
|
110
|
+
* @param {string} [overridesPath] - Path to the overrides file; the schema is
|
|
111
|
+
* looked up as a sibling (same directory) before falling back to the
|
|
112
|
+
* package-relative path.
|
|
110
113
|
* @returns {Function} Compiled schema validator
|
|
111
114
|
*/
|
|
112
|
-
function loadSchemaValidator() {
|
|
115
|
+
function loadSchemaValidator(overridesPath) {
|
|
116
|
+
// Primary: look for the schema next to the overrides file (docs repo's docs-data/).
|
|
117
|
+
if (overridesPath) {
|
|
118
|
+
const siblingPath = path.join(path.dirname(path.resolve(overridesPath)), 'rpk-overrides.schema.json')
|
|
119
|
+
if (fs.existsSync(siblingPath)) {
|
|
120
|
+
const schema = JSON.parse(fs.readFileSync(siblingPath, 'utf8'))
|
|
121
|
+
const ajv = new Ajv2020({ allErrors: true, verbose: true })
|
|
122
|
+
return ajv.compile(schema)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fallback: package-relative path (legacy / monorepo usage).
|
|
113
127
|
const schemaPath = path.resolve(__dirname, '../../docs-data/rpk-overrides.schema.json')
|
|
114
128
|
|
|
115
129
|
if (!fs.existsSync(schemaPath)) {
|
|
@@ -124,13 +138,14 @@ function loadSchemaValidator() {
|
|
|
124
138
|
/**
|
|
125
139
|
* Validate overrides against JSON Schema
|
|
126
140
|
* @param {Object} overrides - Overrides object to validate
|
|
141
|
+
* @param {string} [overridesPath] - Path to the overrides file (used to locate schema)
|
|
127
142
|
* @returns {ValidationResult}
|
|
128
143
|
*/
|
|
129
|
-
function validateSchema(overrides) {
|
|
144
|
+
function validateSchema(overrides, overridesPath) {
|
|
130
145
|
const result = new ValidationResult()
|
|
131
146
|
|
|
132
147
|
try {
|
|
133
|
-
const validate = loadSchemaValidator()
|
|
148
|
+
const validate = loadSchemaValidator(overridesPath)
|
|
134
149
|
const valid = validate(overrides)
|
|
135
150
|
|
|
136
151
|
if (!valid) {
|
|
@@ -531,7 +546,7 @@ function validateDescriptions(commandOverride, context) {
|
|
|
531
546
|
* @param {Object} [commandTree] - Optional command tree for path validation
|
|
532
547
|
* @returns {ValidationResult}
|
|
533
548
|
*/
|
|
534
|
-
function validateOverrides(overrides, commandTree = null) {
|
|
549
|
+
function validateOverrides(overrides, commandTree = null, overridesPath = null) {
|
|
535
550
|
const result = new ValidationResult()
|
|
536
551
|
|
|
537
552
|
if (!overrides || typeof overrides !== 'object') {
|
|
@@ -540,7 +555,7 @@ function validateOverrides(overrides, commandTree = null) {
|
|
|
540
555
|
}
|
|
541
556
|
|
|
542
557
|
// 1. Schema validation
|
|
543
|
-
result.merge(validateSchema(overrides))
|
|
558
|
+
result.merge(validateSchema(overrides, overridesPath))
|
|
544
559
|
|
|
545
560
|
// 2. Reference validation
|
|
546
561
|
result.merge(validateReferences(overrides))
|
|
@@ -596,7 +611,7 @@ function loadAndValidateOverrides(overridesPath, commandTree = null) {
|
|
|
596
611
|
}
|
|
597
612
|
|
|
598
613
|
// Run validations
|
|
599
|
-
validation.merge(validateOverrides(overrides, commandTree))
|
|
614
|
+
validation.merge(validateOverrides(overrides, commandTree, overridesPath))
|
|
600
615
|
|
|
601
616
|
return { overrides, validation }
|
|
602
617
|
}
|