@redpanda-data/docs-extensions-and-macros 5.3.2 → 5.3.4
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/generate-rp-connect-info.js +9 -3
- package/extensions/process-context-switcher.js +5 -0
- package/package.json +1 -1
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +52 -3
- package/tools/rpk-docs/generate-rpk-docs.js +79 -3
- package/tools/rpk-docs/rpk-docs-handler.js +115 -4
- package/tools/rpk-docs/templates/command.hbs +7 -16
|
@@ -428,13 +428,19 @@ module.exports.register = function ({ config }) {
|
|
|
428
428
|
function buildContextSwitcher (connectorRows, currentRow, variant) {
|
|
429
429
|
const capitalize = (value) => value.charAt(0).toUpperCase() + value.slice(1)
|
|
430
430
|
const orderedRows = [currentRow, ...connectorRows.filter((row) => row !== currentRow)]
|
|
431
|
-
|
|
431
|
+
// The process-context-switcher extension and the UI's context-switcher partial
|
|
432
|
+
// both expect an array of { name, to } items; `to` is a root-relative pub URL.
|
|
433
|
+
const items = []
|
|
432
434
|
for (const row of orderedRows) {
|
|
433
435
|
const link = (variant === 'cloud' && row.redpandaCloudUrl) || row.redpandaConnectUrl
|
|
434
436
|
if (!link) continue
|
|
435
437
|
const type = row.type.trim()
|
|
436
|
-
|
|
438
|
+
// First occurrence wins: orderedRows puts the current page's row first,
|
|
439
|
+
// so a connector with duplicate types keeps its own entry. (The old
|
|
440
|
+
// object-keyed build let later rows overwrite earlier ones.)
|
|
441
|
+
if (items.some((item) => item.name === capitalize(type))) continue
|
|
442
|
+
items.push({ name: capitalize(type), to: link })
|
|
437
443
|
}
|
|
438
|
-
return JSON.stringify(
|
|
444
|
+
return JSON.stringify(items)
|
|
439
445
|
}
|
|
440
446
|
}
|
|
@@ -74,6 +74,11 @@ module.exports.register = function ({ config }) {
|
|
|
74
74
|
item.to = currentResourceId;
|
|
75
75
|
hasChanges = true;
|
|
76
76
|
logger.debug(`Replaced 'current' with '${currentResourceId}' in context-switcher`);
|
|
77
|
+
} else if (typeof item.to === 'string' && item.to.startsWith('/')) {
|
|
78
|
+
// Root-relative URLs (e.g. pub.url values set by generate-rp-connect-info)
|
|
79
|
+
// need no resolution or target injection: the UI's resolve-resource helper
|
|
80
|
+
// passes them through, and the producer decorates every variant directly.
|
|
81
|
+
continue;
|
|
77
82
|
} else if (item.to !== currentResourceId) {
|
|
78
83
|
// For non-current items, find and update the target page
|
|
79
84
|
const targetPage = findPageByResourceId(item.to, contentCatalog, page);
|
package/package.json
CHANGED
|
@@ -501,6 +501,52 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
|
|
|
501
501
|
}
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Derive the fragment anchor for a connector field heading.
|
|
506
|
+
*
|
|
507
|
+
* Field docs render each field as a section heading (for example,
|
|
508
|
+
* `=== \`batching.byte_size\``), and the docs site builds section IDs with
|
|
509
|
+
* `idprefix: ''` and `idseparator: '-'` (set in the shared global-attributes
|
|
510
|
+
* component loaded by the Antora playbook). Under those settings Asciidoctor
|
|
511
|
+
* derives IDs by downcasing the heading text, dropping characters that are
|
|
512
|
+
* not word characters, hyphens, dots, or spaces (so `[]` array markers are
|
|
513
|
+
* discarded), converting runs of dots and spaces to a single `-`, and
|
|
514
|
+
* trimming any leading or trailing separator.
|
|
515
|
+
*
|
|
516
|
+
* Examples of rendered IDs this must match:
|
|
517
|
+
* - `checkpoint_limit` -> `checkpoint_limit`
|
|
518
|
+
* - `batching.byte_size` -> `batching-byte_size`
|
|
519
|
+
* - `batching.processors[]` -> `batching-processors`
|
|
520
|
+
* - `sasl[].aws.credentials.from_ec2_role` -> `sasl-aws-credentials-from_ec2_role`
|
|
521
|
+
*
|
|
522
|
+
* Collision caveat: Asciidoctor de-duplicates repeated IDs on a page by
|
|
523
|
+
* appending `-2`, `-3`, ... to later occurrences. If two fields on one page
|
|
524
|
+
* normalize to the same anchor, the generated link resolves to the first
|
|
525
|
+
* heading and the second gets the suffixed ID. This helper cannot detect
|
|
526
|
+
* that, so a wrong-but-plausible link on a page with near-duplicate field
|
|
527
|
+
* names should be checked against this behavior first.
|
|
528
|
+
*
|
|
529
|
+
* @param {string} fieldName - Field name as it appears in the connector data
|
|
530
|
+
* @returns {string} Fragment anchor matching the rendered heading ID
|
|
531
|
+
*/
|
|
532
|
+
function fieldAnchor (fieldName) {
|
|
533
|
+
const anchor = String(fieldName)
|
|
534
|
+
.toLowerCase()
|
|
535
|
+
// Characters that are invalid in section IDs (for example, `[` and `]`
|
|
536
|
+
// array markers) are removed outright, matching Asciidoctor's
|
|
537
|
+
// InvalidSectionIdCharsRx. The dots still supply the separators, so
|
|
538
|
+
// every `[]` case stays correct (`sasl[].aws` -> `sasl-aws`).
|
|
539
|
+
.replace(/[^ \w\-.]+/g, '')
|
|
540
|
+
// idseparator '-' replaces runs of spaces and dots with a single hyphen.
|
|
541
|
+
.replace(/[ .]+/g, '-')
|
|
542
|
+
// idprefix '' means no leading separator, and Asciidoctor chomps a
|
|
543
|
+
// trailing separator.
|
|
544
|
+
.replace(/^-+/, '')
|
|
545
|
+
.replace(/-+$/, '')
|
|
546
|
+
// A name that normalizes to nothing would emit a malformed `#[...]` xref.
|
|
547
|
+
return anchor || String(fieldName)
|
|
548
|
+
}
|
|
549
|
+
|
|
504
550
|
/**
|
|
505
551
|
* Build a fields table for whats-new.adoc
|
|
506
552
|
* @param {Array} fields - Field data
|
|
@@ -541,7 +587,7 @@ function buildFieldsTable (fields, capFn) {
|
|
|
541
587
|
|
|
542
588
|
componentList += `*${typeLabel}:*\n\n`
|
|
543
589
|
names.forEach(name => {
|
|
544
|
-
componentList += `* xref:components:${type}/${name}.adoc#${fieldName}[${name}]\n`
|
|
590
|
+
componentList += `* xref:components:${type}/${name}.adoc#${fieldAnchor(fieldName)}[${name}]\n`
|
|
545
591
|
})
|
|
546
592
|
}
|
|
547
593
|
|
|
@@ -611,7 +657,7 @@ function buildChangedDefaultsTable (changedDefaults, capFn) {
|
|
|
611
657
|
|
|
612
658
|
componentList += `*${typeLabel}:*\n\n`
|
|
613
659
|
names.forEach(name => {
|
|
614
|
-
componentList += `* xref:components:${type}/${name}.adoc#${info.field}[${name}]\n`
|
|
660
|
+
componentList += `* xref:components:${type}/${name}.adoc#${fieldAnchor(info.field)}[${name}]\n`
|
|
615
661
|
})
|
|
616
662
|
}
|
|
617
663
|
|
|
@@ -2014,5 +2060,8 @@ module.exports = {
|
|
|
2014
2060
|
updateWhatsNew,
|
|
2015
2061
|
capToTwoSentences,
|
|
2016
2062
|
augmentConnectorData,
|
|
2017
|
-
buildCleanOssData
|
|
2063
|
+
buildCleanOssData,
|
|
2064
|
+
fieldAnchor,
|
|
2065
|
+
buildFieldsTable,
|
|
2066
|
+
buildChangedDefaultsTable
|
|
2018
2067
|
}
|
|
@@ -15,6 +15,19 @@ Object.entries(helpers).forEach(([name, fn]) => {
|
|
|
15
15
|
// Template paths
|
|
16
16
|
const TEMPLATES_DIR = path.resolve(__dirname, './templates')
|
|
17
17
|
|
|
18
|
+
// Full command paths from the tree being generated (e.g. "rpk ai run codex").
|
|
19
|
+
// Registered by generateRpkDocs so formatDescription can wrap real multi-word
|
|
20
|
+
// command paths as a unit instead of heuristically wrapping `rpk` alone.
|
|
21
|
+
let knownCommandPaths = new Set()
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Register the set of real command paths for the current generation run.
|
|
25
|
+
* @param {string[]} paths - Full command paths (e.g. "rpk ai run codex")
|
|
26
|
+
*/
|
|
27
|
+
function registerKnownCommandPaths(paths) {
|
|
28
|
+
knownCommandPaths = new Set(paths)
|
|
29
|
+
}
|
|
30
|
+
|
|
18
31
|
/**
|
|
19
32
|
* Register a Handlebars partial from file
|
|
20
33
|
* @param {string} name - Partial name
|
|
@@ -1607,6 +1620,25 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1607
1620
|
// Fix product name: "Redpanda cloud" → "Redpanda Cloud" (product name)
|
|
1608
1621
|
.replace(/Redpanda\s+cloud\b/g, 'Redpanda Cloud')
|
|
1609
1622
|
|
|
1623
|
+
// === RPK COMMAND FORMATTING (known paths first, ground truth) ===
|
|
1624
|
+
// Wrap full multi-word command paths that exist in the generated tree
|
|
1625
|
+
// (e.g. "rpk ai run codex" -> `rpk ai run codex`). The heuristic formatter
|
|
1626
|
+
// below only ever matches "rpk <word>", so without this pass it wraps
|
|
1627
|
+
// `rpk` alone and splits the command in half. Matching against the real
|
|
1628
|
+
// tree means prose that merely resembles a command is never wrapped.
|
|
1629
|
+
if (knownCommandPaths.size > 0) {
|
|
1630
|
+
result = result.replace(/(?<![`\w])rpk((?: [a-z][-a-z0-9]*)+)/g, (match, rest) => {
|
|
1631
|
+
const tokens = rest.trim().split(' ')
|
|
1632
|
+
for (let n = tokens.length; n >= 2; n--) {
|
|
1633
|
+
const path = `rpk ${tokens.slice(0, n).join(' ')}`
|
|
1634
|
+
if (knownCommandPaths.has(path)) {
|
|
1635
|
+
return `\`${path}\`` + match.slice(path.length)
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
return match
|
|
1639
|
+
})
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1610
1642
|
// === RPK COMMAND FORMATTING (context-aware) ===
|
|
1611
1643
|
// Process "rpk X" patterns based on what follows
|
|
1612
1644
|
result = result.replace(/(?<![`\w])rpk(\s+)([a-z][-a-z0-9]*)(?:\s+|(?=[.,;:!?)'"}\]]|$))/gi, (match, space, word, offset, str) => {
|
|
@@ -1702,6 +1734,11 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1702
1734
|
// Add backticks around standalone file names (not inside paths)
|
|
1703
1735
|
result = result.replace(/(?<![`/])(redpanda\.yaml|rpk\.yaml)(?!`)/gi, '`$1`')
|
|
1704
1736
|
|
|
1737
|
+
// Add backticks around internal topic names (_redpanda.transform_logs,
|
|
1738
|
+
// _redpanda.audit_log, ...). The leading underscore is also an AsciiDoc
|
|
1739
|
+
// italics delimiter, so these must never render as bare prose.
|
|
1740
|
+
result = result.replace(/(?<![`\w/])(_redpanda(?:\.[a-z_]+)+)(?!`)/g, '`$1`')
|
|
1741
|
+
|
|
1705
1742
|
// Add backticks around standalone "rpk" (at end of phrase, before punctuation)
|
|
1706
1743
|
// Also exclude rpk inside paths (preceded by /)
|
|
1707
1744
|
result = result.replace(/(?<![`\w/])rpk(?=[\s]*[.,;:!?)'"}\]]|[\s]*$)/g, '`rpk`')
|
|
@@ -1730,9 +1767,13 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1730
1767
|
result = result.replace(`__EARLY_CODE_BLOCK_${i}__`, () => transformed)
|
|
1731
1768
|
})
|
|
1732
1769
|
|
|
1733
|
-
// Restore inline code
|
|
1770
|
+
// Restore inline code. Spans are verbatim, but rules flagged applyToCode
|
|
1771
|
+
// (like the rpai -> rpk ai binary-name rewrite) must reach them the same
|
|
1772
|
+
// way they reach code blocks, or the internal binary name survives in
|
|
1773
|
+
// published spans like `rpai auth token`.
|
|
1734
1774
|
inlineCode.forEach((code, i) => {
|
|
1735
|
-
|
|
1775
|
+
const transformed = applyTextTransformations(code, customTransformations, { code: true })
|
|
1776
|
+
result = result.replace(`__INLINE_CODE_${i}__`, () => transformed)
|
|
1736
1777
|
})
|
|
1737
1778
|
|
|
1738
1779
|
// Restore xrefs
|
|
@@ -2173,6 +2214,12 @@ function capToTwoSentences(desc) {
|
|
|
2173
2214
|
// The pattern matches: colon, optional whitespace/newlines, then indented content
|
|
2174
2215
|
cleaned = cleaned.replace(/:\s*\n+[ \t]+.+$/s, ':')
|
|
2175
2216
|
|
|
2217
|
+
// A paragraph break is a sentence boundary even when the paragraph has no
|
|
2218
|
+
// terminal punctuation (cobra short descriptions often lack one: "Generate
|
|
2219
|
+
// a trial license\n\nThis command..."). Without this, flattening newlines
|
|
2220
|
+
// glues the paragraphs into one run-on "sentence".
|
|
2221
|
+
cleaned = cleaned.replace(/([^.!?:\s])[ \t]*\n[ \t]*\n/g, '$1.\n\n')
|
|
2222
|
+
|
|
2176
2223
|
// Normalize newlines to spaces for inline use (like :description: attribute)
|
|
2177
2224
|
const singleLine = cleaned.replace(/\s*\n+\s*/g, ' ').trim()
|
|
2178
2225
|
|
|
@@ -2202,6 +2249,14 @@ function capToTwoSentences(desc) {
|
|
|
2202
2249
|
// dropped by the sentence matcher
|
|
2203
2250
|
normalized = normalized.replace(/(\d)\.(\d)/g, '$1__DECIMAL__$2')
|
|
2204
2251
|
|
|
2252
|
+
// Protect ALL mid-token periods (no whitespace after): dotted names like
|
|
2253
|
+
// _redpanda.transform_logs or URLs like redpanda.com/contact are not
|
|
2254
|
+
// sentence boundaries. Without this the sentence matcher below cannot
|
|
2255
|
+
// match the sentence containing the token, silently drops everything up
|
|
2256
|
+
// to the mid-token period, and emits the tail fragment as a "sentence"
|
|
2257
|
+
// ("View logs for a transform. transform_logs.").
|
|
2258
|
+
normalized = normalized.replace(/\.(?=\S)/g, '__MIDDOT__')
|
|
2259
|
+
|
|
2205
2260
|
// Match sentences
|
|
2206
2261
|
let sentences = normalized.match(/[^.!?]+[.!?]+(?:\s|$)/g)
|
|
2207
2262
|
|
|
@@ -2217,6 +2272,7 @@ function capToTwoSentences(desc) {
|
|
|
2217
2272
|
if (!sentences || sentences.length === 0) {
|
|
2218
2273
|
// Restore and return
|
|
2219
2274
|
let result = normalized.replace(/__DECIMAL__/g, '.')
|
|
2275
|
+
result = result.replace(/__MIDDOT__/g, '.')
|
|
2220
2276
|
placeholders.forEach(({ ph, original }) => {
|
|
2221
2277
|
result = result.replace(ph, original)
|
|
2222
2278
|
})
|
|
@@ -2230,6 +2286,7 @@ function capToTwoSentences(desc) {
|
|
|
2230
2286
|
|
|
2231
2287
|
// Restore decimal points
|
|
2232
2288
|
result = result.replace(/__DECIMAL__/g, '.')
|
|
2289
|
+
result = result.replace(/__MIDDOT__/g, '.')
|
|
2233
2290
|
|
|
2234
2291
|
// Restore abbreviations
|
|
2235
2292
|
placeholders.forEach(({ ph, original }) => {
|
|
@@ -2578,6 +2635,9 @@ async function generateRpkDocs(options = {}) {
|
|
|
2578
2635
|
// Flatten command tree
|
|
2579
2636
|
const commands = flattenCommands(tree)
|
|
2580
2637
|
|
|
2638
|
+
// Let formatDescription wrap real multi-word command paths as a unit
|
|
2639
|
+
registerKnownCommandPaths(commands.map(c => c.path))
|
|
2640
|
+
|
|
2581
2641
|
// Determine which managed plugins are protected this run: explicitly
|
|
2582
2642
|
// passed by the caller (failed installs) merged with auto-detection of
|
|
2583
2643
|
// absent or shim-only plugin subtrees. Protection covers the whole
|
|
@@ -3000,6 +3060,21 @@ async function generateRpkDocs(options = {}) {
|
|
|
3000
3060
|
}
|
|
3001
3061
|
|
|
3002
3062
|
try {
|
|
3063
|
+
// A page that HAD a Flags section and loses it in this render is the
|
|
3064
|
+
// signature of missing flag data (a snapshot without extraction, or a
|
|
3065
|
+
// curated table removed before the extracted one exists). Nothing else
|
|
3066
|
+
// catches this class: the run exits 0 with a clean log while the
|
|
3067
|
+
// published page silently drops its flag documentation (seen on
|
|
3068
|
+
// rpk connect run).
|
|
3069
|
+
if (fs.existsSync(filePath)) {
|
|
3070
|
+
const previous = fs.readFileSync(filePath, 'utf8')
|
|
3071
|
+
if (/^== Flags$/m.test(previous) && !/^== Flags$/m.test(content)) {
|
|
3072
|
+
console.warn(
|
|
3073
|
+
`⚠️ ${commandPath}: the previously rendered page had a Flags section and this render has none. ` +
|
|
3074
|
+
'The command likely lost its flag data (snapshot without extraction, or a curated Flags override removed too early).'
|
|
3075
|
+
)
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3003
3078
|
fs.writeFileSync(filePath, content, 'utf8')
|
|
3004
3079
|
writtenFiles.add(filePath)
|
|
3005
3080
|
filesGenerated++
|
|
@@ -3121,5 +3196,6 @@ module.exports = {
|
|
|
3121
3196
|
// Exported for testing
|
|
3122
3197
|
filterExamples,
|
|
3123
3198
|
formatExamples,
|
|
3124
|
-
applyTextTransformationsToExamples
|
|
3199
|
+
applyTextTransformationsToExamples,
|
|
3200
|
+
registerKnownCommandPaths
|
|
3125
3201
|
}
|
|
@@ -894,7 +894,7 @@ function loadVersionedJson(version, dataDir) {
|
|
|
894
894
|
* version (the page note renders "introduced in <plugin> version X"), not
|
|
895
895
|
* the rpk version, because plugins release on their own cadence.
|
|
896
896
|
*/
|
|
897
|
-
function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, pluginVersions = {}) {
|
|
897
|
+
function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, pluginVersions = {}, options = {}) {
|
|
898
898
|
const hasNewCommands = diffData.details.newCommands && diffData.details.newCommands.length > 0
|
|
899
899
|
const hasNewFlags = diffData.details.newFlags && diffData.details.newFlags.length > 0
|
|
900
900
|
|
|
@@ -908,6 +908,23 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version,
|
|
|
908
908
|
return pluginVersions[topLevel] || version
|
|
909
909
|
}
|
|
910
910
|
|
|
911
|
+
// Plugin-owned entries are only stamped when the caller vouches that the
|
|
912
|
+
// baseline is manifest-adjacent to this run's plugin version (see
|
|
913
|
+
// isPluginStampAttributable). "New relative to the snapshot" is not "new
|
|
914
|
+
// in this release" when the snapshot skipped releases. Callers that omit
|
|
915
|
+
// attributablePlugins keep legacy stamp-everything behavior.
|
|
916
|
+
const attributable = options.attributablePlugins
|
|
917
|
+
? new Set(options.attributablePlugins)
|
|
918
|
+
: null
|
|
919
|
+
const skippedByPlugin = new Map()
|
|
920
|
+
const stampable = (cmdPath) => {
|
|
921
|
+
const topLevel = cmdPath.split(' ')[1]
|
|
922
|
+
if (!KNOWN_PLUGINS.includes(topLevel) || !attributable) return true
|
|
923
|
+
if (attributable.has(topLevel)) return true
|
|
924
|
+
skippedByPlugin.set(topLevel, (skippedByPlugin.get(topLevel) || 0) + 1)
|
|
925
|
+
return false
|
|
926
|
+
}
|
|
927
|
+
|
|
911
928
|
let overrides = {}
|
|
912
929
|
if (fs.existsSync(overridesPath)) {
|
|
913
930
|
try {
|
|
@@ -929,6 +946,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version,
|
|
|
929
946
|
if (hasNewCommands) {
|
|
930
947
|
for (const newCmd of diffData.details.newCommands) {
|
|
931
948
|
const cmdPath = newCmd.path
|
|
949
|
+
if (!stampable(cmdPath)) continue
|
|
932
950
|
if (!overrides.commands[cmdPath]) {
|
|
933
951
|
overrides.commands[cmdPath] = {}
|
|
934
952
|
}
|
|
@@ -945,6 +963,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version,
|
|
|
945
963
|
for (const newFlag of diffData.details.newFlags) {
|
|
946
964
|
const cmdPath = newFlag.commandPath
|
|
947
965
|
const flagName = newFlag.flagName
|
|
966
|
+
if (!stampable(cmdPath)) continue
|
|
948
967
|
|
|
949
968
|
if (!overrides.commands[cmdPath]) {
|
|
950
969
|
overrides.commands[cmdPath] = {}
|
|
@@ -964,6 +983,15 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version,
|
|
|
964
983
|
}
|
|
965
984
|
}
|
|
966
985
|
|
|
986
|
+
for (const [plugin, count] of skippedByPlugin) {
|
|
987
|
+
console.warn(
|
|
988
|
+
`\u26a0 Skipped stamping introducedInVersion for ${count} new '${plugin}' entr${count === 1 ? 'y' : 'ies'}: ` +
|
|
989
|
+
`the baseline snapshot's ${plugin} version is not the release immediately before ` +
|
|
990
|
+
`${pluginVersions[plugin] || version} in the plugin manifest, so the introduction ` +
|
|
991
|
+
'version cannot be attributed automatically. Attribute manually against released binaries.'
|
|
992
|
+
)
|
|
993
|
+
}
|
|
994
|
+
|
|
967
995
|
if (commandsUpdated > 0 || flagsUpdated > 0) {
|
|
968
996
|
fs.writeFileSync(overridesPath, JSON.stringify(overrides, null, 2), 'utf8')
|
|
969
997
|
const updates = []
|
|
@@ -1444,6 +1472,75 @@ function fetchLatestPluginVersion(plugin) {
|
|
|
1444
1472
|
}
|
|
1445
1473
|
}
|
|
1446
1474
|
|
|
1475
|
+
/**
|
|
1476
|
+
* Fetch the ordered list of published versions for a plugin from its
|
|
1477
|
+
* manifest. Cached per plugin for the process lifetime.
|
|
1478
|
+
* @param {string} plugin - rpk command name (ai, connect, k8s, check)
|
|
1479
|
+
* @returns {string[]|null} Versions in manifest (release) order, or null
|
|
1480
|
+
*/
|
|
1481
|
+
const pluginManifestVersionsCache = new Map()
|
|
1482
|
+
function fetchPluginManifestVersions(plugin) {
|
|
1483
|
+
if (pluginManifestVersionsCache.has(plugin)) {
|
|
1484
|
+
return pluginManifestVersionsCache.get(plugin)
|
|
1485
|
+
}
|
|
1486
|
+
const slug = PLUGIN_MANIFEST_SLUGS[plugin] || plugin
|
|
1487
|
+
const result = spawnSync('curl', [
|
|
1488
|
+
'-fsSL', '--retry', '3', '--connect-timeout', '15', '--max-time', '30',
|
|
1489
|
+
`${PLUGIN_MANIFEST_HOST}/${slug}/manifest.json`
|
|
1490
|
+
], { encoding: 'utf8', timeout: 60000 })
|
|
1491
|
+
let versions = null
|
|
1492
|
+
if (result.status === 0) {
|
|
1493
|
+
try {
|
|
1494
|
+
versions = (JSON.parse(result.stdout).archives || []).map(a => a.version)
|
|
1495
|
+
} catch (err) {
|
|
1496
|
+
console.warn(`Could not parse plugin manifest for ${plugin}: ${err.message}`)
|
|
1497
|
+
}
|
|
1498
|
+
} else {
|
|
1499
|
+
console.warn(`Could not fetch plugin manifest for ${plugin} (slug: ${slug})`)
|
|
1500
|
+
}
|
|
1501
|
+
pluginManifestVersionsCache.set(plugin, versions)
|
|
1502
|
+
return versions
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Decide whether new commands under a plugin can truthfully be attributed
|
|
1507
|
+
* to newVersion: only when the baseline snapshot recorded the plugin
|
|
1508
|
+
* version AND that version is the release immediately before newVersion in
|
|
1509
|
+
* the plugin's manifest. Any gap means a "new" command may have shipped in
|
|
1510
|
+
* an intermediate release, so a stamp would fabricate history (30 rpk ai
|
|
1511
|
+
* commands were once labeled 0.2.32 when they shipped in 0.2.26/0.2.28).
|
|
1512
|
+
* @param {string} plugin - rpk command name
|
|
1513
|
+
* @param {string|undefined} oldVersion - Plugin version recorded in the
|
|
1514
|
+
* baseline snapshot's plugin_versions, if any
|
|
1515
|
+
* @param {string} newVersion - Plugin version in this run
|
|
1516
|
+
* @returns {boolean}
|
|
1517
|
+
*/
|
|
1518
|
+
function isPluginStampAttributable(plugin, oldVersion, newVersion) {
|
|
1519
|
+
if (!oldVersion || !newVersion) return false
|
|
1520
|
+
if (oldVersion === newVersion) return true
|
|
1521
|
+
const versions = fetchPluginManifestVersions(plugin)
|
|
1522
|
+
if (!versions) return false
|
|
1523
|
+
const oldIdx = versions.indexOf(oldVersion)
|
|
1524
|
+
const newIdx = versions.indexOf(newVersion)
|
|
1525
|
+
return oldIdx !== -1 && newIdx === oldIdx + 1
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* Compute the set of plugins whose new commands may be stamped this run.
|
|
1530
|
+
* @param {Object} oldPluginVersions - plugin_versions from the baseline snapshot
|
|
1531
|
+
* @param {Object} newPluginVersions - plugin_versions for this run
|
|
1532
|
+
* @returns {Set<string>}
|
|
1533
|
+
*/
|
|
1534
|
+
function attributablePluginSet(oldPluginVersions = {}, newPluginVersions = {}) {
|
|
1535
|
+
const attributable = new Set()
|
|
1536
|
+
for (const plugin of Object.keys(newPluginVersions)) {
|
|
1537
|
+
if (isPluginStampAttributable(plugin, oldPluginVersions[plugin], newPluginVersions[plugin])) {
|
|
1538
|
+
attributable.add(plugin)
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return attributable
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1447
1544
|
/**
|
|
1448
1545
|
* Check whether a plugin node contains real plugin commands, as opposed to
|
|
1449
1546
|
* only the install/uninstall/upgrade shim compiled into rpk itself.
|
|
@@ -1952,7 +2049,12 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1952
2049
|
pluginDiffData,
|
|
1953
2050
|
overridesPath || path.join(dataDir, 'rpk-overrides.json'),
|
|
1954
2051
|
resolvedVersion,
|
|
1955
|
-
{ [plugin]: resolvedVersion }
|
|
2052
|
+
{ [plugin]: resolvedVersion },
|
|
2053
|
+
{
|
|
2054
|
+
attributablePlugins: isPluginStampAttributable(
|
|
2055
|
+
plugin, (jsonData.plugin_versions || {})[plugin], resolvedVersion
|
|
2056
|
+
) ? [plugin] : []
|
|
2057
|
+
}
|
|
1956
2058
|
)
|
|
1957
2059
|
}
|
|
1958
2060
|
|
|
@@ -1996,7 +2098,9 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1996
2098
|
newDeprecatedCommands: jsonData.deprecated_commands || {}
|
|
1997
2099
|
})
|
|
1998
2100
|
if (stampDiff.details.newCommands.length > 0 || stampDiff.details.newFlags.length > 0) {
|
|
1999
|
-
updateOverridesWithIntroducedVersions(stampDiff, effectiveOverridesPath, rpkVersion, pluginVersions
|
|
2101
|
+
updateOverridesWithIntroducedVersions(stampDiff, effectiveOverridesPath, rpkVersion, pluginVersions, {
|
|
2102
|
+
attributablePlugins: attributablePluginSet(oldDataForStamp.plugin_versions, pluginVersions)
|
|
2103
|
+
})
|
|
2000
2104
|
}
|
|
2001
2105
|
}
|
|
2002
2106
|
}
|
|
@@ -2400,7 +2504,9 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2400
2504
|
// cadence, so the rpk version would be wrong and the page note would
|
|
2401
2505
|
// render "introduced in <plugin> version <rpk version>").
|
|
2402
2506
|
if ((diffData.details.newCommands.length > 0 || diffData.details.newFlags.length > 0) && effectiveOverridesPath) {
|
|
2403
|
-
updateOverridesWithIntroducedVersions(diffData, effectiveOverridesPath, rpkVersion, pluginVersions
|
|
2507
|
+
updateOverridesWithIntroducedVersions(diffData, effectiveOverridesPath, rpkVersion, pluginVersions, {
|
|
2508
|
+
attributablePlugins: attributablePluginSet(oldData.plugin_versions, pluginVersions)
|
|
2509
|
+
})
|
|
2404
2510
|
}
|
|
2405
2511
|
|
|
2406
2512
|
// Update what's-new file if requested
|
|
@@ -2819,6 +2925,11 @@ module.exports = {
|
|
|
2819
2925
|
getPlatformDescription,
|
|
2820
2926
|
getCurrentPlatform,
|
|
2821
2927
|
updateOverridesWithIntroducedVersions,
|
|
2928
|
+
isPluginStampAttributable,
|
|
2929
|
+
attributablePluginSet,
|
|
2930
|
+
fetchPluginManifestVersions,
|
|
2931
|
+
// Exported for tests to seed manifest data without network access
|
|
2932
|
+
pluginManifestVersionsCache,
|
|
2822
2933
|
filterDiffForWhatsNew,
|
|
2823
2934
|
computeDescriptionCoverage,
|
|
2824
2935
|
updateWhatsNewFile,
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
{{!--
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{{/if}}
|
|
1
|
+
{{!-- cloudOnly/selfHostedOnly deliberately do NOT wrap the page. The
|
|
2
|
+
gating that matters happens inside the single-source tag region
|
|
3
|
+
(subcommand rows, flags, content items): includes only extract that
|
|
4
|
+
region, and the docs component never builds with env-cloud set, so a
|
|
5
|
+
page-level conditional is dead code at best and an empty, untitled
|
|
6
|
+
page at worst. Availability pages are excluded from cloud by not
|
|
7
|
+
generating a stub for them in cloud-docs. --}}
|
|
9
8
|
= {{commandPath}}
|
|
10
9
|
:description: {{{wrapDescriptionPassthrough shortDesc}}}
|
|
11
10
|
{{#if pageAliases}}
|
|
@@ -583,11 +582,3 @@ include::{{{this}}}[]
|
|
|
583
582
|
{{> content-item this commandPath=../commandPath}}
|
|
584
583
|
{{/each}}
|
|
585
584
|
// end::single-source[]
|
|
586
|
-
{{!-- Self-hosted-only wrapper end --}}
|
|
587
|
-
{{#if selfHostedOnly}}
|
|
588
|
-
endif::[]
|
|
589
|
-
{{/if}}
|
|
590
|
-
{{!-- Cloud-only wrapper end --}}
|
|
591
|
-
{{#if cloudOnly}}
|
|
592
|
-
endif::[]
|
|
593
|
-
{{/if}}
|