@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.
@@ -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
- const data = {}
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
- data[type.toLowerCase()] = { name: capitalize(type), to: link }
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(data)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.3.2",
3
+ "version": "5.3.4",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -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
- result = result.replace(`__INLINE_CODE_${i}__`, () => code)
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
- {{!-- Cloud-only wrapper start --}}
2
- {{#if cloudOnly}}
3
- ifdef::env-cloud[]
4
- {{/if}}
5
- {{!-- Self-hosted-only wrapper start --}}
6
- {{#if selfHostedOnly}}
7
- ifndef::env-cloud[]
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}}