@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +159 -0
- package/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +18 -1
- package/tools/property-extractor/transformers.py +29 -13
- package/tools/rpk-docs/generate-plugin-stubs.js +291 -0
- package/tools/rpk-docs/generate-rpk-docs.js +440 -25
- package/tools/rpk-docs/helpers/index.js +13 -3
- package/tools/rpk-docs/report-delta.js +314 -32
- package/tools/rpk-docs/rpk-docs-handler.js +1230 -102
- package/tools/rpk-docs/templates/command.hbs +5 -0
- package/tools/rpk-docs/templates/content-item.hbs +2 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { spawnSync } = require('child_process')
|
|
4
|
+
const crypto = require('crypto')
|
|
4
5
|
const path = require('path')
|
|
5
6
|
const fs = require('fs')
|
|
6
7
|
const os = require('os')
|
|
7
8
|
const semver = require('semver')
|
|
8
9
|
const { findRepoRoot } = require('../../cli-utils/doc-tools-utils')
|
|
9
|
-
const { generateRpkDocs, applyOverridesToTree, resolveReferences } = require('./generate-rpk-docs')
|
|
10
|
-
const { generateRpkDiff, printDiffReport, generateWhatsNewSection } = require('./report-delta')
|
|
10
|
+
const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir } = require('./generate-rpk-docs')
|
|
11
|
+
const { generateRpkDiff, printDiffReport, generateWhatsNewSection, flattenToMap } = require('./report-delta')
|
|
11
12
|
const { loadAndValidateOverrides, ValidationResult } = require('./validate-overrides')
|
|
12
13
|
const { validateDirectory, formatResults } = require('./validate-output')
|
|
13
14
|
|
|
@@ -16,6 +17,37 @@ const { validateDirectory, formatResults } = require('./validate-output')
|
|
|
16
17
|
*/
|
|
17
18
|
const KNOWN_PLUGINS = ['ai', 'check', 'connect', 'k8s', 'oxla']
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Plugins whose docs can be refreshed individually with --plugin.
|
|
22
|
+
* oxla is excluded: it is a "Coming Soon" stub with no installable binary.
|
|
23
|
+
*/
|
|
24
|
+
const REFRESHABLE_PLUGINS = ['ai', 'check', 'connect', 'k8s']
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Per-plugin install flags that pin a version (rpk <plugin> install <flag> <version>).
|
|
28
|
+
* k8s uses the generic flag name; the others embed the plugin name.
|
|
29
|
+
*/
|
|
30
|
+
const PLUGIN_INSTALL_VERSION_FLAGS = {
|
|
31
|
+
ai: '--ai-version',
|
|
32
|
+
check: '--check-version',
|
|
33
|
+
connect: '--connect-version',
|
|
34
|
+
k8s: '--plugin-version'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Manifest slugs at https://rpk-plugins.redpanda.com/<slug>/manifest.json
|
|
39
|
+
* where the slug differs from the rpk command name.
|
|
40
|
+
*/
|
|
41
|
+
const PLUGIN_MANIFEST_SLUGS = { ai: 'rpai' }
|
|
42
|
+
|
|
43
|
+
const PLUGIN_MANIFEST_HOST = 'https://rpk-plugins.redpanda.com'
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Subcommands compiled into rpk itself for managed plugins. A plugin node
|
|
47
|
+
* whose children are only these never actually installed.
|
|
48
|
+
*/
|
|
49
|
+
const PLUGIN_SHIM_SUBCOMMANDS = new Set(['install', 'uninstall', 'upgrade'])
|
|
50
|
+
|
|
19
51
|
/**
|
|
20
52
|
* Parse Go version from 'go version' output
|
|
21
53
|
* @param {string} versionOutput - Output from 'go version' command
|
|
@@ -347,9 +379,12 @@ function fetchRpkTreeFromSource(sourcePath) {
|
|
|
347
379
|
* Builds rpk binary, installs plugins, then runs --print-tree for complete command coverage.
|
|
348
380
|
* Falls back to native Go build if Docker is unavailable.
|
|
349
381
|
* @param {string} sourcePath - Path to rpk Go source directory (e.g., ~/redpanda/src/go/rpk)
|
|
350
|
-
* @returns {Object}
|
|
382
|
+
* @returns {Object} { tree, failedPlugins } — the parsed JSON tree plus the
|
|
383
|
+
* names of managed plugins whose install failed this run. Callers pass
|
|
384
|
+
* failedPlugins to generateRpkDocs as protectedPlugins so those plugins'
|
|
385
|
+
* existing pages and nav entries are preserved instead of treated as stale.
|
|
351
386
|
*/
|
|
352
|
-
function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
387
|
+
function fetchRpkTreeFromLinuxSource(sourcePath, pluginPins = {}) {
|
|
353
388
|
// Resolve to absolute path
|
|
354
389
|
const absoluteSourcePath = path.resolve(sourcePath)
|
|
355
390
|
|
|
@@ -455,18 +490,46 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
455
490
|
try {
|
|
456
491
|
// Step 1: Build rpk binary
|
|
457
492
|
console.log('Building rpk binary...')
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
493
|
+
// Module downloads from proxy.golang.org fail transiently (stream
|
|
494
|
+
// INTERNAL_ERROR), especially on a cold module cache. Retry: the second
|
|
495
|
+
// attempt reuses whatever the first already downloaded.
|
|
496
|
+
let buildResult
|
|
497
|
+
let binaryExists = false
|
|
498
|
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
499
|
+
buildResult = spawnSync('docker', [
|
|
500
|
+
'exec', containerId,
|
|
501
|
+
'go', 'build', '-o', '/tmp/rpk', './cmd/rpk'
|
|
502
|
+
], {
|
|
503
|
+
encoding: 'utf8',
|
|
504
|
+
timeout: 300000 // 5 minutes per attempt
|
|
505
|
+
})
|
|
465
506
|
|
|
466
|
-
|
|
507
|
+
if (buildResult.status === 0) {
|
|
508
|
+
// Trust but verify: docker exec has been observed returning zero for
|
|
509
|
+
// a build that produced nothing, and everything downstream execs
|
|
510
|
+
// /tmp/rpk. Treat a phantom success as a failed attempt.
|
|
511
|
+
const binCheck = spawnSync('docker', [
|
|
512
|
+
'exec', containerId, 'test', '-x', '/tmp/rpk'
|
|
513
|
+
], { encoding: 'utf8', timeout: 15000 })
|
|
514
|
+
if (binCheck.status === 0) {
|
|
515
|
+
binaryExists = true
|
|
516
|
+
break
|
|
517
|
+
}
|
|
518
|
+
if (attempt < 4) {
|
|
519
|
+
console.warn(` Build attempt ${attempt} reported success but produced no binary; retrying...`)
|
|
520
|
+
continue
|
|
521
|
+
}
|
|
522
|
+
} else if (attempt < 4) {
|
|
523
|
+
const firstError = (buildResult.stderr || buildResult.signal || 'unknown error')
|
|
524
|
+
.toString().trim().split('\n').slice(-1)[0]
|
|
525
|
+
console.warn(` Build attempt ${attempt} failed (${firstError}); retrying...`)
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!binaryExists) {
|
|
467
530
|
const stderr = buildResult.stderr || ''
|
|
468
531
|
throw new Error(
|
|
469
|
-
`Failed to build rpk in Linux container: ${stderr}\n` +
|
|
532
|
+
`Failed to build rpk in Linux container: ${stderr || 'build produced no binary'}\n` +
|
|
470
533
|
'Common causes:\n' +
|
|
471
534
|
' 1. Source code is out of date - run: git pull origin dev\n' +
|
|
472
535
|
' 2. Go module issues - the container will download dependencies automatically\n' +
|
|
@@ -477,15 +540,31 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
477
540
|
|
|
478
541
|
// Step 2: Install plugins
|
|
479
542
|
console.log('Installing plugins for complete command coverage...')
|
|
543
|
+
const failedPlugins = []
|
|
480
544
|
for (const plugin of KNOWN_PLUGINS) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
]
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
545
|
+
// A pin installs a specific version instead of the manifest's latest.
|
|
546
|
+
// This is how pre-GA plugins (no version promoted to latest) get into a
|
|
547
|
+
// full regeneration at all — without a pin their install resolves
|
|
548
|
+
// nothing and their commands are absent from the tree.
|
|
549
|
+
const pin = pluginPins[plugin]
|
|
550
|
+
const versionFlag = PLUGIN_INSTALL_VERSION_FLAGS[plugin]
|
|
551
|
+
const runInstall = (pinned) => {
|
|
552
|
+
const args = ['exec', containerId, '/tmp/rpk', plugin, 'install']
|
|
553
|
+
if (pinned && pin && versionFlag) {
|
|
554
|
+
args.push(versionFlag, pin)
|
|
555
|
+
}
|
|
556
|
+
return spawnSync('docker', args, { encoding: 'utf8', timeout: 120000 })
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
console.log(` Installing plugin: ${plugin}${pin ? ` (pinned to ${pin})` : ''}...`)
|
|
560
|
+
let installResult = runInstall(true)
|
|
561
|
+
if (installResult.status !== 0 && pin) {
|
|
562
|
+
const output = `${installResult.stderr || ''}${installResult.stdout || ''}`
|
|
563
|
+
if (output.includes('unknown flag') || output.includes('is not valid')) {
|
|
564
|
+
console.warn(` rpk rejected the version pin for ${plugin}; retrying without the pin (installs latest)`)
|
|
565
|
+
installResult = runInstall(false)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
489
568
|
|
|
490
569
|
if (installResult.status === 0) {
|
|
491
570
|
console.log(` ✓ ${plugin} installed`)
|
|
@@ -498,13 +577,14 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
498
577
|
console.log(` - ${plugin} is not an installable plugin`)
|
|
499
578
|
} else {
|
|
500
579
|
// A failed install is non-fatal: generation continues, but this
|
|
501
|
-
// plugin's commands will be absent from the tree. Expected for
|
|
502
|
-
//
|
|
503
|
-
// release because the plugin publisher's
|
|
504
|
-
// pure X.Y.Z versions, so
|
|
505
|
-
//
|
|
580
|
+
// plugin's commands will be absent from the tree. Expected for
|
|
581
|
+
// beta-only plugins with no version pin — `rpk <plugin> install`
|
|
582
|
+
// finds no `latest` release because the plugin publisher's
|
|
583
|
+
// stableVersionRe only promotes pure X.Y.Z versions, so their
|
|
584
|
+
// commands only appear at GA unless the run pins a version.
|
|
506
585
|
// See redpanda-data/docs#1801.
|
|
507
586
|
console.warn(` ✗ Failed to install ${plugin}: ${stderr || stdout}`)
|
|
587
|
+
failedPlugins.push(plugin)
|
|
508
588
|
}
|
|
509
589
|
}
|
|
510
590
|
}
|
|
@@ -536,8 +616,9 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
536
616
|
)
|
|
537
617
|
}
|
|
538
618
|
|
|
619
|
+
let tree
|
|
539
620
|
try {
|
|
540
|
-
|
|
621
|
+
tree = JSON.parse(result.stdout)
|
|
541
622
|
} catch (err) {
|
|
542
623
|
throw new Error(
|
|
543
624
|
`Failed to parse rpk tree JSON from Linux source build: ${err.message}\n` +
|
|
@@ -545,6 +626,26 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
545
626
|
'This may indicate a version mismatch or corrupted source.'
|
|
546
627
|
)
|
|
547
628
|
}
|
|
629
|
+
|
|
630
|
+
// Step 4: Fill in flags for plugin subtrees. Their commands come from
|
|
631
|
+
// --help-autocomplete, which carries no flag data, so without this every
|
|
632
|
+
// plugin command page renders an empty flags section.
|
|
633
|
+
console.log('Extracting flags from plugin command help output...')
|
|
634
|
+
for (const plugin of KNOWN_PLUGINS) {
|
|
635
|
+
const node = (tree.commands || []).find(c => c.name === plugin)
|
|
636
|
+
if (!node || !pluginNodeHasRealCommands(node)) continue
|
|
637
|
+
const enriched = enrichPluginTreeWithFlags(node, (argPath) => {
|
|
638
|
+
const helpResult = spawnSync('docker', [
|
|
639
|
+
'exec', containerId, '/tmp/rpk', ...argPath, '--help'
|
|
640
|
+
], { encoding: 'utf8', timeout: 30000 })
|
|
641
|
+
return helpResult.status === 0 ? helpResult.stdout : null
|
|
642
|
+
})
|
|
643
|
+
if (enriched > 0) {
|
|
644
|
+
console.log(` ${plugin}: extracted flags for ${enriched} command(s)`)
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return { tree, failedPlugins }
|
|
548
649
|
} finally {
|
|
549
650
|
// Clean up container
|
|
550
651
|
console.log('Cleaning up build container...')
|
|
@@ -773,7 +874,58 @@ function loadOverrides(overridesPath, commandTree = null, options = {}) {
|
|
|
773
874
|
)
|
|
774
875
|
}
|
|
775
876
|
|
|
776
|
-
return overrides
|
|
877
|
+
return { overrides, validation }
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Merge deprecation metadata for commands still present in the tree into the
|
|
882
|
+
* overrides file, so their pages render deprecation banners without manual
|
|
883
|
+
* curation. Hidden deprecated commands are excluded: they are absent from
|
|
884
|
+
* --print-tree, have no pages, and would only produce unknown-path warnings.
|
|
885
|
+
* Commands whose overrides already carry a `deprecated` value are left alone.
|
|
886
|
+
* @param {Object} deprecatedCommands - Map from scan-deprecated-commands.js
|
|
887
|
+
* @param {Object} tree - Current command tree
|
|
888
|
+
* @param {string} overridesPath - Path to overrides JSON file
|
|
889
|
+
*/
|
|
890
|
+
function mergeVisibleDeprecationsIntoOverrides(deprecatedCommands, tree, overridesPath) {
|
|
891
|
+
const entries = Object.entries(deprecatedCommands || {})
|
|
892
|
+
if (entries.length === 0 || !overridesPath || !fs.existsSync(overridesPath)) {
|
|
893
|
+
return
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
const treePaths = new Set(flattenToMap(tree).keys())
|
|
897
|
+
|
|
898
|
+
let overrides
|
|
899
|
+
try {
|
|
900
|
+
overrides = JSON.parse(fs.readFileSync(overridesPath, 'utf8'))
|
|
901
|
+
} catch (err) {
|
|
902
|
+
console.warn(`Warning: Could not parse overrides file for deprecation merge: ${err.message}`)
|
|
903
|
+
return
|
|
904
|
+
}
|
|
905
|
+
if (!overrides.commands) {
|
|
906
|
+
overrides.commands = {}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
let annotated = 0
|
|
910
|
+
for (const [cmdPath, info] of entries) {
|
|
911
|
+
if (!treePaths.has(cmdPath)) continue
|
|
912
|
+
const existing = overrides.commands[cmdPath] || {}
|
|
913
|
+
if (existing.deprecated !== undefined) continue
|
|
914
|
+
overrides.commands[cmdPath] = {
|
|
915
|
+
...existing,
|
|
916
|
+
deprecated: true,
|
|
917
|
+
...(info.deprecatedMessage && !existing.deprecatedMessage
|
|
918
|
+
? { deprecatedMessage: info.deprecatedMessage } : {}),
|
|
919
|
+
...(info.replacement && !existing.replacement
|
|
920
|
+
? { replacement: info.replacement } : {})
|
|
921
|
+
}
|
|
922
|
+
annotated++
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
if (annotated > 0) {
|
|
926
|
+
fs.writeFileSync(overridesPath, JSON.stringify(overrides, null, 2), 'utf8')
|
|
927
|
+
console.log(`Annotated ${annotated} visible deprecated command(s) in ${overridesPath}`)
|
|
928
|
+
}
|
|
777
929
|
}
|
|
778
930
|
|
|
779
931
|
/**
|
|
@@ -824,8 +976,12 @@ function loadVersionedJson(version, dataDir) {
|
|
|
824
976
|
* @param {Object} diffData - Diff data with new commands and flags
|
|
825
977
|
* @param {string} overridesPath - Path to overrides JSON file
|
|
826
978
|
* @param {string} version - Version to set as introducedInVersion
|
|
979
|
+
* @param {Object} [pluginVersions] - Plugin versions keyed by rpk command
|
|
980
|
+
* name. Commands under a plugin subtree are stamped with the plugin's own
|
|
981
|
+
* version (the page note renders "introduced in <plugin> version X"), not
|
|
982
|
+
* the rpk version, because plugins release on their own cadence.
|
|
827
983
|
*/
|
|
828
|
-
function updateOverridesWithIntroducedVersions(diffData, overridesPath, version) {
|
|
984
|
+
function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, pluginVersions = {}) {
|
|
829
985
|
const hasNewCommands = diffData.details.newCommands && diffData.details.newCommands.length > 0
|
|
830
986
|
const hasNewFlags = diffData.details.newFlags && diffData.details.newFlags.length > 0
|
|
831
987
|
|
|
@@ -833,6 +989,12 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version)
|
|
|
833
989
|
return
|
|
834
990
|
}
|
|
835
991
|
|
|
992
|
+
// "rpk connect lint" -> pluginVersions.connect, else the rpk version
|
|
993
|
+
const versionFor = (cmdPath) => {
|
|
994
|
+
const topLevel = cmdPath.split(' ')[1]
|
|
995
|
+
return pluginVersions[topLevel] || version
|
|
996
|
+
}
|
|
997
|
+
|
|
836
998
|
let overrides = {}
|
|
837
999
|
if (fs.existsSync(overridesPath)) {
|
|
838
1000
|
try {
|
|
@@ -859,7 +1021,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version)
|
|
|
859
1021
|
}
|
|
860
1022
|
// Only set if not already set (preserve manual overrides)
|
|
861
1023
|
if (!overrides.commands[cmdPath].introducedInVersion) {
|
|
862
|
-
overrides.commands[cmdPath].introducedInVersion =
|
|
1024
|
+
overrides.commands[cmdPath].introducedInVersion = versionFor(cmdPath)
|
|
863
1025
|
commandsUpdated++
|
|
864
1026
|
}
|
|
865
1027
|
}
|
|
@@ -883,7 +1045,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version)
|
|
|
883
1045
|
|
|
884
1046
|
// Only set if not already set (preserve manual overrides)
|
|
885
1047
|
if (!overrides.commands[cmdPath].flags[flagName].introducedInVersion) {
|
|
886
|
-
overrides.commands[cmdPath].flags[flagName].introducedInVersion =
|
|
1048
|
+
overrides.commands[cmdPath].flags[flagName].introducedInVersion = versionFor(cmdPath)
|
|
887
1049
|
flagsUpdated++
|
|
888
1050
|
}
|
|
889
1051
|
}
|
|
@@ -970,8 +1132,49 @@ function countCommands(node) {
|
|
|
970
1132
|
* @param {string} whatsNewPath - Path to what's-new.adoc file
|
|
971
1133
|
* @param {string} version - Version string for display
|
|
972
1134
|
*/
|
|
973
|
-
|
|
974
|
-
|
|
1135
|
+
/**
|
|
1136
|
+
* Build a predicate that reports whether a command path renders as a
|
|
1137
|
+
* linkable page (not excluded and not routed to partials by the overrides).
|
|
1138
|
+
* @param {Object|null} overridesData - Loaded overrides
|
|
1139
|
+
* @returns {Function} (commandPath) => boolean
|
|
1140
|
+
*/
|
|
1141
|
+
/**
|
|
1142
|
+
* Build a predicate that reports whether a command path has subcommands,
|
|
1143
|
+
* which determines its page location (groups render into their own dir).
|
|
1144
|
+
* @param {Object} tree - Full command tree
|
|
1145
|
+
* @returns {Function} (commandPath) => boolean
|
|
1146
|
+
*/
|
|
1147
|
+
function makeSubcommandPredicate(tree) {
|
|
1148
|
+
const commandMap = flattenToMap(tree)
|
|
1149
|
+
return (commandPath) => {
|
|
1150
|
+
const node = commandMap.get(commandPath)
|
|
1151
|
+
return Boolean(node && (node.commands || []).length > 0)
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function makeLinkablePredicate(overridesData) {
|
|
1156
|
+
const resolved = overridesData ? resolveReferences(overridesData, overridesData) : null
|
|
1157
|
+
return (commandPath) => {
|
|
1158
|
+
// rpk cloud and rpk security secret render to partials (single-sourced
|
|
1159
|
+
// into cloud docs), so this repo has no linkable pages for them
|
|
1160
|
+
if (commandPath.startsWith('rpk cloud') || commandPath.startsWith('rpk security secret')) {
|
|
1161
|
+
return false
|
|
1162
|
+
}
|
|
1163
|
+
if (!resolved) return true
|
|
1164
|
+
return !shouldExcludeCommand(resolved, commandPath) &&
|
|
1165
|
+
!shouldUsePartialDir(resolved, commandPath)
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
function updateWhatsNewFile(diffData, whatsNewPath, version, options = {}) {
|
|
1170
|
+
// Each block opens with a "=== <version>" heading so accumulated blocks
|
|
1171
|
+
// (successive RCs, multiple plugin releases) never collide on section ids
|
|
1172
|
+
const sectionHeading = options.sectionHeading || '== Redpanda CLI'
|
|
1173
|
+
const whatsNewContent = generateWhatsNewSection(diffData, {
|
|
1174
|
+
version,
|
|
1175
|
+
blockLabel: version,
|
|
1176
|
+
...options
|
|
1177
|
+
})
|
|
975
1178
|
|
|
976
1179
|
if (!whatsNewContent) {
|
|
977
1180
|
console.log('No Redpanda CLI changes to add to what\'s new')
|
|
@@ -987,9 +1190,39 @@ function updateWhatsNewFile(diffData, whatsNewPath, version) {
|
|
|
987
1190
|
|
|
988
1191
|
const existingContent = fs.readFileSync(whatsNewPath, 'utf8')
|
|
989
1192
|
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
1193
|
+
// Version-scoped marker block: re-runs for the same version replace their
|
|
1194
|
+
// own block, and later versions (for example, successive RCs in a beta
|
|
1195
|
+
// cycle, or plugin releases) append their own blocks inside the existing
|
|
1196
|
+
// Redpanda CLI section instead of being dropped. Writers can edit or
|
|
1197
|
+
// remove blocks freely; the automation only ever touches content between
|
|
1198
|
+
// its own markers for the same version label.
|
|
1199
|
+
const startMarker = `// AUTOGEN-RPK-CHANGES ${version} START`
|
|
1200
|
+
const endMarker = `// AUTOGEN-RPK-CHANGES ${version} END`
|
|
1201
|
+
const sectionBody = whatsNewContent.replace(/^== [^\n]*\n+/, '')
|
|
1202
|
+
const block = `${startMarker}\n${sectionBody.trimEnd()}\n${endMarker}`
|
|
1203
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1204
|
+
|
|
1205
|
+
if (existingContent.includes(startMarker)) {
|
|
1206
|
+
// Replace this version's existing block (idempotent re-runs)
|
|
1207
|
+
const blockRe = new RegExp(`${escapeRe(startMarker)}[\\s\\S]*?${escapeRe(endMarker)}`)
|
|
1208
|
+
const updatedContent = existingContent.replace(blockRe, block)
|
|
1209
|
+
fs.writeFileSync(whatsNewPath, updatedContent, 'utf8')
|
|
1210
|
+
console.log(`Refreshed existing ${version} block in what's-new file: ${whatsNewPath}`)
|
|
1211
|
+
return
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const headingMatch = existingContent.match(new RegExp(`^${escapeRe(sectionHeading)}[^\\n]*$`, 'm'))
|
|
1215
|
+
if (headingMatch) {
|
|
1216
|
+
// Append this version's block at the end of the existing section, just
|
|
1217
|
+
// before the next level-2 heading (or end of file)
|
|
1218
|
+
const sectionStart = headingMatch.index + headingMatch[0].length
|
|
1219
|
+
const rest = existingContent.slice(sectionStart)
|
|
1220
|
+
const nextHeading = rest.search(/\n== /)
|
|
1221
|
+
const insertAt = nextHeading === -1 ? existingContent.length : sectionStart + nextHeading
|
|
1222
|
+
const before = existingContent.slice(0, insertAt).replace(/\s*$/, '\n\n')
|
|
1223
|
+
const after = existingContent.slice(insertAt).replace(/^\n*/, '\n')
|
|
1224
|
+
fs.writeFileSync(whatsNewPath, `${before}${block}${after}`, 'utf8')
|
|
1225
|
+
console.log(`Appended ${version} block to the "${sectionHeading}" section in: ${whatsNewPath}`)
|
|
993
1226
|
return
|
|
994
1227
|
}
|
|
995
1228
|
|
|
@@ -1011,19 +1244,563 @@ function updateWhatsNewFile(diffData, whatsNewPath, version) {
|
|
|
1011
1244
|
}
|
|
1012
1245
|
}
|
|
1013
1246
|
|
|
1247
|
+
const fullSection = `${sectionHeading}\n\n${block}\n`
|
|
1248
|
+
|
|
1014
1249
|
let updatedContent
|
|
1015
1250
|
if (insertIndex > 0) {
|
|
1016
1251
|
// Insert before the matched section
|
|
1017
1252
|
updatedContent = existingContent.slice(0, insertIndex) +
|
|
1018
|
-
|
|
1253
|
+
fullSection + '\n' +
|
|
1019
1254
|
existingContent.slice(insertIndex)
|
|
1020
1255
|
} else {
|
|
1021
1256
|
// Append at the end
|
|
1022
|
-
updatedContent = existingContent
|
|
1257
|
+
updatedContent = existingContent.replace(/\s*$/, '\n\n') + fullSection
|
|
1023
1258
|
}
|
|
1024
1259
|
|
|
1025
1260
|
fs.writeFileSync(whatsNewPath, updatedContent, 'utf8')
|
|
1026
|
-
console.log(`
|
|
1261
|
+
console.log(`Created "${sectionHeading}" section in what's-new file: ${whatsNewPath}`)
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
/**
|
|
1265
|
+
* Build an rpk binary natively from Go source.
|
|
1266
|
+
* @param {string} sourcePath - Path to rpk Go source directory (src/go/rpk)
|
|
1267
|
+
* @param {string} outPath - Where to write the binary
|
|
1268
|
+
* @returns {string} Path to the built binary
|
|
1269
|
+
*/
|
|
1270
|
+
function buildRpkBinary(sourcePath, outPath) {
|
|
1271
|
+
const goCheck = spawnSync('go', ['version'], { encoding: 'utf8', timeout: 5000 })
|
|
1272
|
+
if (goCheck.status !== 0) {
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
'Go is required to build rpk from source but was not found.\n' +
|
|
1275
|
+
'Install Go from https://go.dev/ and ensure it\'s in your PATH.'
|
|
1276
|
+
)
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const installedGoVersion = parseGoVersion(goCheck.stdout)
|
|
1280
|
+
const requiredGoVersion = getRequiredGoVersion(sourcePath)
|
|
1281
|
+
if (installedGoVersion && requiredGoVersion &&
|
|
1282
|
+
!checkGoVersionSufficient(installedGoVersion, requiredGoVersion)) {
|
|
1283
|
+
throw new Error(
|
|
1284
|
+
`Go version mismatch: installed ${installedGoVersion}, required >= ${requiredGoVersion}\n` +
|
|
1285
|
+
`The rpk source (go.mod) requires Go ${requiredGoVersion} or newer.`
|
|
1286
|
+
)
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
console.log(`Building rpk from source at ${sourcePath}...`)
|
|
1290
|
+
const buildResult = spawnSync('go', ['build', '-o', outPath, './cmd/rpk'], {
|
|
1291
|
+
cwd: sourcePath,
|
|
1292
|
+
encoding: 'utf8',
|
|
1293
|
+
timeout: 300000
|
|
1294
|
+
})
|
|
1295
|
+
|
|
1296
|
+
if (buildResult.status !== 0) {
|
|
1297
|
+
throw new Error(`Failed to build rpk from source: ${buildResult.stderr}`)
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
return outPath
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Download an official rpk release binary for the current platform.
|
|
1305
|
+
* @param {string} tag - Release tag (e.g., v26.1.12)
|
|
1306
|
+
* @param {string} destDir - Directory to download and extract into
|
|
1307
|
+
* @returns {string|null} Path to the extracted binary, or null if the
|
|
1308
|
+
* release asset is unavailable (caller falls back to a source build)
|
|
1309
|
+
*/
|
|
1310
|
+
function downloadRpkRelease(tag, destDir) {
|
|
1311
|
+
const osName = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[process.platform]
|
|
1312
|
+
const archName = { arm64: 'arm64', x64: 'amd64' }[process.arch]
|
|
1313
|
+
if (!osName || !archName) {
|
|
1314
|
+
console.warn(`No rpk release asset for platform ${process.platform}/${process.arch}`)
|
|
1315
|
+
return null
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
const assetName = `rpk-${osName}-${archName}.zip`
|
|
1319
|
+
const baseUrl = `https://github.com/redpanda-data/redpanda/releases/download/${tag}`
|
|
1320
|
+
const zipPath = path.join(destDir, assetName)
|
|
1321
|
+
|
|
1322
|
+
console.log(`Downloading ${assetName} for ${tag}...`)
|
|
1323
|
+
const curlResult = spawnSync('curl', [
|
|
1324
|
+
'-fL', '--retry', '5', '--retry-all-errors',
|
|
1325
|
+
'--connect-timeout', '30', '--max-time', '300',
|
|
1326
|
+
'-o', zipPath, `${baseUrl}/${assetName}`
|
|
1327
|
+
], { encoding: 'utf8', timeout: 360000 })
|
|
1328
|
+
|
|
1329
|
+
if (curlResult.status !== 0) {
|
|
1330
|
+
console.warn(`Could not download rpk release for ${tag} (draft or missing release asset)`)
|
|
1331
|
+
return null
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// Verify against the release checksum file when it exists
|
|
1335
|
+
const checksumAsset = `rpk_${tag.replace(/^v/, '')}_checksums.txt`
|
|
1336
|
+
const checksumPath = path.join(destDir, checksumAsset)
|
|
1337
|
+
const checksumResult = spawnSync('curl', [
|
|
1338
|
+
'-fsSL', '--retry', '3', '--connect-timeout', '30', '--max-time', '60',
|
|
1339
|
+
'-o', checksumPath, `${baseUrl}/${checksumAsset}`
|
|
1340
|
+
], { encoding: 'utf8', timeout: 90000 })
|
|
1341
|
+
|
|
1342
|
+
if (checksumResult.status === 0) {
|
|
1343
|
+
const expectedLine = fs.readFileSync(checksumPath, 'utf8')
|
|
1344
|
+
.split('\n')
|
|
1345
|
+
.find(line => line.trim().endsWith(assetName))
|
|
1346
|
+
if (expectedLine) {
|
|
1347
|
+
const expected = expectedLine.trim().split(/\s+/)[0]
|
|
1348
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(zipPath)).digest('hex')
|
|
1349
|
+
if (expected !== actual) {
|
|
1350
|
+
throw new Error(
|
|
1351
|
+
`Checksum mismatch for ${assetName} (${tag}):\n` +
|
|
1352
|
+
` expected ${expected}\n actual ${actual}`
|
|
1353
|
+
)
|
|
1354
|
+
}
|
|
1355
|
+
console.log('Checksum verified')
|
|
1356
|
+
}
|
|
1357
|
+
} else {
|
|
1358
|
+
console.warn('No checksum file published for this release; skipping verification')
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const unzipResult = spawnSync('unzip', ['-o', zipPath, '-d', destDir], {
|
|
1362
|
+
encoding: 'utf8',
|
|
1363
|
+
timeout: 60000
|
|
1364
|
+
})
|
|
1365
|
+
if (unzipResult.status !== 0) {
|
|
1366
|
+
throw new Error(`Failed to extract ${assetName}: ${unzipResult.stderr}`)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
const binPath = path.join(destDir, 'rpk')
|
|
1370
|
+
if (!fs.existsSync(binPath)) {
|
|
1371
|
+
throw new Error(`Extracted archive did not contain an rpk binary: ${zipPath}`)
|
|
1372
|
+
}
|
|
1373
|
+
fs.chmodSync(binPath, 0o755)
|
|
1374
|
+
return binPath
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Get an rpk binary matching the given version.
|
|
1379
|
+
* Prefers the official release download (published stable releases only;
|
|
1380
|
+
* RC releases are drafts, so their assets are not publicly downloadable).
|
|
1381
|
+
* Falls back to building from source at the tag.
|
|
1382
|
+
* @param {string} rpkVersion - Version tag from the snapshot (e.g., v26.1.12, v26.2.1-rc2)
|
|
1383
|
+
* @param {Object} [options]
|
|
1384
|
+
* @param {string} [options.rpkBin] - Existing binary to use, skipping download/build
|
|
1385
|
+
* @returns {string} Path to an rpk binary
|
|
1386
|
+
*/
|
|
1387
|
+
function acquireRpkBinary(rpkVersion, options = {}) {
|
|
1388
|
+
const { rpkBin } = options
|
|
1389
|
+
|
|
1390
|
+
if (rpkBin) {
|
|
1391
|
+
const binPath = path.resolve(rpkBin)
|
|
1392
|
+
if (!fs.existsSync(binPath)) {
|
|
1393
|
+
throw new Error(`rpk binary not found: ${binPath}`)
|
|
1394
|
+
}
|
|
1395
|
+
console.log(`Using provided rpk binary: ${binPath}`)
|
|
1396
|
+
return binPath
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
const tag = rpkVersion.startsWith('v') ? rpkVersion : `v${rpkVersion}`
|
|
1400
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpk-bin-'))
|
|
1401
|
+
|
|
1402
|
+
// Only published (non-draft) releases have downloadable assets
|
|
1403
|
+
if (/^v\d+\.\d+\.\d+$/.test(tag)) {
|
|
1404
|
+
const downloaded = downloadRpkRelease(tag, workDir)
|
|
1405
|
+
if (downloaded) {
|
|
1406
|
+
return downloaded
|
|
1407
|
+
}
|
|
1408
|
+
console.log('Falling back to building rpk from source...')
|
|
1409
|
+
} else {
|
|
1410
|
+
console.log(`No published release binary for ${tag}; building from source...`)
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
if (!/^v\d+\.\d+\.\d+(-rc\d+)?$/.test(tag) && tag !== 'vdev') {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`Cannot acquire an rpk binary for version '${rpkVersion}'.\n` +
|
|
1416
|
+
'The snapshot\'s rpk_version is not a release tag. ' +
|
|
1417
|
+
'Pass --rpk-bin <path> to use a local rpk binary instead.'
|
|
1418
|
+
)
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
const sourceRef = tag === 'vdev' ? 'dev' : tag
|
|
1422
|
+
const sourcePath = prepareSourceFromRef(sourceRef, null)
|
|
1423
|
+
try {
|
|
1424
|
+
return buildRpkBinary(sourcePath, path.join(workDir, 'rpk'))
|
|
1425
|
+
} catch (nativeErr) {
|
|
1426
|
+
// Local Go older than go.mod requires is common on laptops; the --ref
|
|
1427
|
+
// path already tolerates this by building in a container, so do the same
|
|
1428
|
+
// here when Docker is available
|
|
1429
|
+
const dockerCheck = spawnSync('docker', ['info'], { encoding: 'utf8', timeout: 10000 })
|
|
1430
|
+
if (dockerCheck.status !== 0) {
|
|
1431
|
+
throw new Error(
|
|
1432
|
+
`${nativeErr.message}\n` +
|
|
1433
|
+
'Docker is not available for a container build either. ' +
|
|
1434
|
+
'Update Go, start Docker, or pass --rpk-bin <path> to use an existing rpk binary.'
|
|
1435
|
+
)
|
|
1436
|
+
}
|
|
1437
|
+
console.warn(`Native build failed (${nativeErr.message.split('\n')[0]}); building in a container...`)
|
|
1438
|
+
const goVersion = getRequiredGoVersion(sourcePath)
|
|
1439
|
+
const goImage = goVersion ? `golang:${goVersion}` : 'golang:1'
|
|
1440
|
+
const buildResult = spawnSync('docker', [
|
|
1441
|
+
'run', '--rm',
|
|
1442
|
+
'-v', `${path.resolve(sourcePath)}:/rpk-source:ro`,
|
|
1443
|
+
'-v', `${workDir}:/out`,
|
|
1444
|
+
'-w', '/rpk-source',
|
|
1445
|
+
goImage,
|
|
1446
|
+
'go', 'build', '-o', '/out/rpk', './cmd/rpk'
|
|
1447
|
+
], { encoding: 'utf8', timeout: 600000 })
|
|
1448
|
+
const binPath = path.join(workDir, 'rpk')
|
|
1449
|
+
if (buildResult.status !== 0 || !fs.existsSync(binPath)) {
|
|
1450
|
+
throw new Error(`Container build failed: ${buildResult.stderr || 'no binary produced'}`)
|
|
1451
|
+
}
|
|
1452
|
+
return binPath
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
/**
|
|
1457
|
+
* Look up the latest published version of a plugin from its manifest.
|
|
1458
|
+
* @param {string} plugin - rpk command name (ai, connect, k8s, check)
|
|
1459
|
+
* @returns {string|null} Latest version, or null if unavailable
|
|
1460
|
+
*/
|
|
1461
|
+
function fetchLatestPluginVersion(plugin) {
|
|
1462
|
+
const slug = PLUGIN_MANIFEST_SLUGS[plugin] || plugin
|
|
1463
|
+
const result = spawnSync('curl', [
|
|
1464
|
+
'-fsSL', '--retry', '3', '--connect-timeout', '15', '--max-time', '30',
|
|
1465
|
+
`${PLUGIN_MANIFEST_HOST}/${slug}/manifest.json`
|
|
1466
|
+
], { encoding: 'utf8', timeout: 60000 })
|
|
1467
|
+
|
|
1468
|
+
if (result.status !== 0) {
|
|
1469
|
+
console.warn(`Could not fetch plugin manifest for ${plugin} (slug: ${slug})`)
|
|
1470
|
+
return null
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
try {
|
|
1474
|
+
const manifest = JSON.parse(result.stdout)
|
|
1475
|
+
const latest = (manifest.archives || []).find(a => a.is_latest)
|
|
1476
|
+
return latest ? latest.version : null
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
console.warn(`Could not parse plugin manifest for ${plugin}: ${err.message}`)
|
|
1479
|
+
return null
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* Check whether a plugin node contains real plugin commands, as opposed to
|
|
1485
|
+
* only the install/uninstall/upgrade shim compiled into rpk itself.
|
|
1486
|
+
* @param {Object} node - Top-level command node for the plugin
|
|
1487
|
+
* @returns {boolean}
|
|
1488
|
+
*/
|
|
1489
|
+
function pluginNodeHasRealCommands(node) {
|
|
1490
|
+
return (node.commands || []).some(c => !PLUGIN_SHIM_SUBCOMMANDS.has(c.name))
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Replace a plugin's top-level node in a command tree.
|
|
1495
|
+
* Replace-only: throws if the plugin is not already present, so a refresh
|
|
1496
|
+
* can never introduce a command that the snapshot's rpk version lacks.
|
|
1497
|
+
* @param {Object} tree - Full command tree (root node)
|
|
1498
|
+
* @param {string} plugin - rpk command name
|
|
1499
|
+
* @param {Object} freshNode - Replacement node
|
|
1500
|
+
* @returns {Object} New tree with the node replaced
|
|
1501
|
+
*/
|
|
1502
|
+
function splicePluginNode(tree, plugin, freshNode) {
|
|
1503
|
+
const commands = tree.commands || []
|
|
1504
|
+
if (!commands.some(c => c.name === plugin)) {
|
|
1505
|
+
throw new Error(`Cannot splice plugin '${plugin}': not present in the base tree`)
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
...tree,
|
|
1509
|
+
commands: commands.map(c => (c.name === plugin ? freshNode : c))
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
/**
|
|
1514
|
+
* Carry a snapshot's recorded Linux-only command list onto a working tree.
|
|
1515
|
+
* The list comes from scanning Go build tags in the rpk source, which
|
|
1516
|
+
* from-json runs (including --plugin refreshes) never see, so it can only be
|
|
1517
|
+
* inherited from the existing snapshot. Without it the refreshed snapshot
|
|
1518
|
+
* would lose its platform markers for every future from-json run.
|
|
1519
|
+
* @param {Object} tree - Working command tree (root node)
|
|
1520
|
+
* @param {Object} [fallbackTree] - Tree to inherit linux_only_commands from
|
|
1521
|
+
* when the working tree does not carry the field itself
|
|
1522
|
+
* @returns {Object} Tree that carries linux_only_commands when available
|
|
1523
|
+
*/
|
|
1524
|
+
function preserveLinuxOnlyCommands(tree, fallbackTree) {
|
|
1525
|
+
if (!tree || tree.linux_only_commands) return tree
|
|
1526
|
+
const inherited = fallbackTree && fallbackTree.linux_only_commands
|
|
1527
|
+
if (!inherited) return tree
|
|
1528
|
+
return { ...tree, linux_only_commands: [...inherited] }
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
/**
|
|
1532
|
+
* Parse the local "Flags:" section of cobra --help output into flag objects
|
|
1533
|
+
* matching the shape rpk --print-tree emits for compiled-in commands.
|
|
1534
|
+
* The "Global Flags:" section is ignored: rpk-core globals are documented by
|
|
1535
|
+
* the shared global-flags table, and plugin-global flags are captured from
|
|
1536
|
+
* the plugin root command's own Flags section.
|
|
1537
|
+
* @param {string} helpText - Output of `rpk <command> --help`
|
|
1538
|
+
* @returns {Array<Object>} Flags: { name, shorthand?, type, description, default? }
|
|
1539
|
+
*/
|
|
1540
|
+
function parseCobraFlags(helpText) {
|
|
1541
|
+
const lines = (helpText || '').split('\n')
|
|
1542
|
+
const start = lines.findIndex(l => /^Flags:\s*$/.test(l))
|
|
1543
|
+
if (start === -1) return []
|
|
1544
|
+
|
|
1545
|
+
const flags = []
|
|
1546
|
+
let flagIndent = null
|
|
1547
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1548
|
+
const line = lines[i]
|
|
1549
|
+
if (/^\S/.test(line)) break // next section (Global Flags:, Use "...", ...)
|
|
1550
|
+
if (line.trim() === '') break
|
|
1551
|
+
|
|
1552
|
+
const m = line.match(/^\s+(?:-(\w),\s+)?--([\w.-]+)(?:\s+(\S+))?\s{2,}(.*)$/)
|
|
1553
|
+
if (m) {
|
|
1554
|
+
const [, shorthand, name, valueType, desc] = m
|
|
1555
|
+
if (name === 'help') continue
|
|
1556
|
+
flagIndent = line.search(/\S/)
|
|
1557
|
+
const flag = {
|
|
1558
|
+
name,
|
|
1559
|
+
type: valueType || 'bool',
|
|
1560
|
+
description: desc.trim()
|
|
1561
|
+
}
|
|
1562
|
+
if (shorthand) flag.shorthand = shorthand
|
|
1563
|
+
const def = flag.description.match(/\s*\(default (.+)\)$/)
|
|
1564
|
+
if (def) {
|
|
1565
|
+
flag.default = def[1]
|
|
1566
|
+
flag.description = flag.description.slice(0, def.index).trim()
|
|
1567
|
+
}
|
|
1568
|
+
flags.push(flag)
|
|
1569
|
+
} else if (flags.length > 0 && flagIndent !== null && line.search(/\S/) > flagIndent) {
|
|
1570
|
+
// Wrapped description continuation: any line indented deeper than the
|
|
1571
|
+
// flag column that did not parse as a flag. Continuations can begin
|
|
1572
|
+
// with flag-looking tokens ("(alias: --x-ref)" wraps to "--x-ref)"),
|
|
1573
|
+
// so no dash guard — a genuine flag line always matches the regex
|
|
1574
|
+
// above first.
|
|
1575
|
+
const last = flags[flags.length - 1]
|
|
1576
|
+
last.description = `${last.description} ${line.trim()}`.trim()
|
|
1577
|
+
const def = last.description.match(/\s*\(default (.+)\)$/)
|
|
1578
|
+
if (def) {
|
|
1579
|
+
last.default = def[1]
|
|
1580
|
+
last.description = last.description.slice(0, def.index).trim()
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
return flags
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Parse the OPTIONS section of urfave/cli --help output (Redpanda Connect)
|
|
1589
|
+
* into the same flag shape as parseCobraFlags. Entries look like:
|
|
1590
|
+
* --log.level value override the configured log level
|
|
1591
|
+
* --set value, -s value [ --set value, -s value ] set a field ...
|
|
1592
|
+
* --chilled continue ... (default: false)
|
|
1593
|
+
* The GLOBAL OPTIONS section is skipped, matching the cobra handling.
|
|
1594
|
+
* @param {string} helpText - Output of `rpk <command> --help`
|
|
1595
|
+
* @returns {Array<Object>} Flags: { name, shorthand?, type, description, default? }
|
|
1596
|
+
*/
|
|
1597
|
+
function parseUrfaveFlags(helpText) {
|
|
1598
|
+
const lines = (helpText || '').split('\n')
|
|
1599
|
+
const start = lines.findIndex(l => /^OPTIONS:\s*$/.test(l))
|
|
1600
|
+
if (start === -1) return []
|
|
1601
|
+
|
|
1602
|
+
const flags = []
|
|
1603
|
+
let urfaveIndent = null
|
|
1604
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1605
|
+
const line = lines[i]
|
|
1606
|
+
if (/^\S/.test(line)) break // next section (GLOBAL OPTIONS:, ...)
|
|
1607
|
+
if (line.trim() === '') break
|
|
1608
|
+
|
|
1609
|
+
const m = line.match(/^\s{2,}(--\S[^\s]*(?:[^ ]| (?!\s))*)\s{2,}(.*)$/)
|
|
1610
|
+
if (m) {
|
|
1611
|
+
const [, spec, desc] = m
|
|
1612
|
+
const nameMatch = spec.match(/^--([\w.-]+)/)
|
|
1613
|
+
if (!nameMatch) continue
|
|
1614
|
+
const name = nameMatch[1]
|
|
1615
|
+
if (name === 'help') continue
|
|
1616
|
+
urfaveIndent = line.search(/\S/)
|
|
1617
|
+
|
|
1618
|
+
const flag = { name, description: desc.trim() }
|
|
1619
|
+
const shorthandMatch = spec.match(/,\s+-(\w)\b/)
|
|
1620
|
+
if (shorthandMatch) flag.shorthand = shorthandMatch[1]
|
|
1621
|
+
|
|
1622
|
+
const repeatable = spec.includes('[ --')
|
|
1623
|
+
const hasValue = new RegExp(`^--${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+\\S`).test(spec)
|
|
1624
|
+
flag.type = repeatable ? 'strings' : (hasValue ? 'string' : 'bool')
|
|
1625
|
+
|
|
1626
|
+
const def = flag.description.match(/\s*\(default:\s*(.+?)\)$/)
|
|
1627
|
+
if (def) {
|
|
1628
|
+
flag.default = def[1]
|
|
1629
|
+
flag.description = flag.description.slice(0, def.index).trim()
|
|
1630
|
+
}
|
|
1631
|
+
flags.push(flag)
|
|
1632
|
+
} else if (flags.length > 0 && urfaveIndent !== null && line.search(/\S/) > urfaveIndent) {
|
|
1633
|
+
// Continuation lines may begin with flag-looking tokens; see the
|
|
1634
|
+
// cobra parser above for why there is no dash guard
|
|
1635
|
+
const last = flags[flags.length - 1]
|
|
1636
|
+
last.description = `${last.description} ${line.trim()}`.trim()
|
|
1637
|
+
const def = last.description.match(/\s*\(default:\s*(.+?)\)$/)
|
|
1638
|
+
if (def) {
|
|
1639
|
+
last.default = def[1]
|
|
1640
|
+
last.description = last.description.slice(0, def.index).trim()
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
return flags
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/**
|
|
1648
|
+
* Parse flags from --help output regardless of CLI framework: cobra prints a
|
|
1649
|
+
* "Flags:" section (rpk ai, k8s, check), urfave/cli prints "OPTIONS:"
|
|
1650
|
+
* (Redpanda Connect).
|
|
1651
|
+
* @param {string} helpText
|
|
1652
|
+
* @returns {Array<Object>}
|
|
1653
|
+
*/
|
|
1654
|
+
function parseHelpFlags(helpText) {
|
|
1655
|
+
if (/^Flags:\s*$/m.test(helpText || '')) return parseCobraFlags(helpText)
|
|
1656
|
+
if (/^OPTIONS:\s*$/m.test(helpText || '')) return parseUrfaveFlags(helpText)
|
|
1657
|
+
return []
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Fill in flags for plugin subtree commands by running `--help` per command.
|
|
1662
|
+
* Plugin subcommand nodes come from the plugin's --help-autocomplete output,
|
|
1663
|
+
* which carries no flag information, so without this every plugin command
|
|
1664
|
+
* page renders with an empty flags section. Nodes that already have flags
|
|
1665
|
+
* (the install/uninstall/upgrade shim compiled into rpk) are left alone.
|
|
1666
|
+
* Help failures are non-fatal: the affected command keeps an empty list.
|
|
1667
|
+
* @param {Object} node - The plugin's top-level command node (mutated)
|
|
1668
|
+
* @param {Function} execHelp - (argPath: string[]) => string|null, returns
|
|
1669
|
+
* the help text for `rpk <argPath...> --help`
|
|
1670
|
+
* @returns {number} Number of commands enriched
|
|
1671
|
+
*/
|
|
1672
|
+
function enrichPluginTreeWithFlags(node, execHelp) {
|
|
1673
|
+
let enriched = 0
|
|
1674
|
+
const walk = (cmd, argPath) => {
|
|
1675
|
+
if (!cmd.flags || cmd.flags.length === 0) {
|
|
1676
|
+
const helpText = execHelp(argPath)
|
|
1677
|
+
if (helpText) {
|
|
1678
|
+
const flags = parseHelpFlags(helpText)
|
|
1679
|
+
if (flags.length > 0) {
|
|
1680
|
+
cmd.flags = flags
|
|
1681
|
+
enriched++
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
for (const child of cmd.commands || []) {
|
|
1686
|
+
walk(child, [...argPath, child.name])
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
walk(node, [node.name])
|
|
1690
|
+
return enriched
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Install a single plugin and capture its fresh command subtree.
|
|
1695
|
+
* Runs with an isolated HOME so the install never touches the caller's
|
|
1696
|
+
* rpk plugin state, and the caller's installed plugins never leak in.
|
|
1697
|
+
* @param {Object} params
|
|
1698
|
+
* @param {string} params.plugin - rpk command name (ai, connect, k8s, check)
|
|
1699
|
+
* @param {string} [params.pluginVersion] - Version to pin; defaults to latest
|
|
1700
|
+
* @param {string} params.rpkBinPath - rpk binary to run
|
|
1701
|
+
* @returns {Object} { node, version } - Fresh plugin node and the version recorded
|
|
1702
|
+
*/
|
|
1703
|
+
function fetchPluginSubtree({ plugin, pluginVersion, rpkBinPath }) {
|
|
1704
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rpk-plugin-home-'))
|
|
1705
|
+
const env = { ...process.env, HOME: tmpHome }
|
|
1706
|
+
|
|
1707
|
+
const runInstall = (pinned) => {
|
|
1708
|
+
const args = [plugin, 'install']
|
|
1709
|
+
const versionFlag = PLUGIN_INSTALL_VERSION_FLAGS[plugin]
|
|
1710
|
+
if (pinned && pluginVersion && versionFlag) {
|
|
1711
|
+
args.push(versionFlag, pluginVersion)
|
|
1712
|
+
}
|
|
1713
|
+
console.log(`Installing plugin: rpk ${args.join(' ')}`)
|
|
1714
|
+
return spawnSync(rpkBinPath, args, { encoding: 'utf8', env, timeout: 300000 })
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
let pinApplied = Boolean(pluginVersion)
|
|
1718
|
+
let installResult = runInstall(true)
|
|
1719
|
+
if (installResult.status !== 0 && pluginVersion) {
|
|
1720
|
+
const output = `${installResult.stderr || ''}${installResult.stdout || ''}`
|
|
1721
|
+
// 'unknown flag': this rpk predates the pin flag.
|
|
1722
|
+
// 'is not valid': rpk's version validation rejected the pin (its regex
|
|
1723
|
+
// caps each segment at two digits, so connect versions like 4.102.0
|
|
1724
|
+
// fail). In both cases installing latest is the right recovery: the
|
|
1725
|
+
// dispatch fires right after a release, when latest IS the release.
|
|
1726
|
+
if (output.includes('unknown flag') || output.includes('is not valid')) {
|
|
1727
|
+
console.warn(
|
|
1728
|
+
`rpk rejected the version pin for '${plugin}' ` +
|
|
1729
|
+
`(${output.trim().split('\n')[0]}); retrying without the pin (installs latest)`
|
|
1730
|
+
)
|
|
1731
|
+
pinApplied = false
|
|
1732
|
+
installResult = runInstall(false)
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
if (installResult.status !== 0) {
|
|
1736
|
+
throw new Error(
|
|
1737
|
+
`Failed to install plugin '${plugin}':\n` +
|
|
1738
|
+
`${installResult.stderr || installResult.stdout || 'no output'}`
|
|
1739
|
+
)
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
const treeResult = spawnSync(rpkBinPath, ['--print-tree'], {
|
|
1743
|
+
encoding: 'utf8',
|
|
1744
|
+
env,
|
|
1745
|
+
timeout: 120000,
|
|
1746
|
+
maxBuffer: 50 * 1024 * 1024
|
|
1747
|
+
})
|
|
1748
|
+
if (treeResult.status !== 0) {
|
|
1749
|
+
const stderr = treeResult.stderr || ''
|
|
1750
|
+
if (stderr.includes('unknown flag')) {
|
|
1751
|
+
throw new Error(
|
|
1752
|
+
'This rpk binary does not support --print-tree (requires rpk >= v26.2.0).\n' +
|
|
1753
|
+
'Pass --rpk-bin with a newer binary.'
|
|
1754
|
+
)
|
|
1755
|
+
}
|
|
1756
|
+
throw new Error(`Failed to run rpk --print-tree: ${stderr}`)
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
const freshTree = JSON.parse(treeResult.stdout)
|
|
1760
|
+
const node = (freshTree.commands || []).find(c => c.name === plugin)
|
|
1761
|
+
if (!node) {
|
|
1762
|
+
throw new Error(`Plugin '${plugin}' not found in rpk command tree after install`)
|
|
1763
|
+
}
|
|
1764
|
+
if (!pluginNodeHasRealCommands(node)) {
|
|
1765
|
+
throw new Error(
|
|
1766
|
+
`Plugin '${plugin}' installed but exposed no commands beyond the ` +
|
|
1767
|
+
'install/uninstall/upgrade shim.\n' +
|
|
1768
|
+
'The install likely resolved nothing. For pre-GA plugins (no promoted ' +
|
|
1769
|
+
'latest version in the manifest), a version pin is required: ' +
|
|
1770
|
+
`--plugin-version <version>`
|
|
1771
|
+
)
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// Fill in per-command flags: autocomplete trees carry none, but the plugin
|
|
1775
|
+
// binary is installed, so each command's --help documents them
|
|
1776
|
+
console.log(`Extracting flags from ${plugin} command help output...`)
|
|
1777
|
+
const enriched = enrichPluginTreeWithFlags(node, (argPath) => {
|
|
1778
|
+
const helpResult = spawnSync(rpkBinPath, [...argPath, '--help'], {
|
|
1779
|
+
encoding: 'utf8',
|
|
1780
|
+
env,
|
|
1781
|
+
timeout: 30000
|
|
1782
|
+
})
|
|
1783
|
+
return helpResult.status === 0 ? helpResult.stdout : null
|
|
1784
|
+
})
|
|
1785
|
+
console.log(` Extracted flags for ${enriched} command(s)`)
|
|
1786
|
+
|
|
1787
|
+
// When the pin was applied, record it. When we fell back to latest (or no
|
|
1788
|
+
// pin was given), record what the manifest says latest is: that is what
|
|
1789
|
+
// actually installed.
|
|
1790
|
+
const version = pinApplied ? pluginVersion : fetchLatestPluginVersion(plugin)
|
|
1791
|
+
if (!version) {
|
|
1792
|
+
console.warn(
|
|
1793
|
+
`Could not determine the installed version of '${plugin}'; ` +
|
|
1794
|
+
'plugin_versions will not be updated for this run'
|
|
1795
|
+
)
|
|
1796
|
+
} else if (pluginVersion && version !== pluginVersion) {
|
|
1797
|
+
console.warn(
|
|
1798
|
+
`Requested ${plugin} ${pluginVersion} but installed latest (${version}) ` +
|
|
1799
|
+
'because rpk rejected the version pin'
|
|
1800
|
+
)
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
return { node, version }
|
|
1027
1804
|
}
|
|
1028
1805
|
|
|
1029
1806
|
/**
|
|
@@ -1042,6 +1819,10 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1042
1819
|
overrides: overridesPath,
|
|
1043
1820
|
fromSource, // Path to local rpk Go source directory
|
|
1044
1821
|
fromJson, // Path to existing versioned JSON file to regenerate from
|
|
1822
|
+
plugin, // Refresh a single plugin's subtree (requires fromJson)
|
|
1823
|
+
pluginVersion, // Version to pin for the plugin install (defaults to latest)
|
|
1824
|
+
pluginPins = {}, // Per-plugin version pins for full generation (pre-GA plugins)
|
|
1825
|
+
rpkBin, // Existing rpk binary to use for the plugin refresh
|
|
1045
1826
|
ref, // Git ref (branch or tag) to document
|
|
1046
1827
|
sourceRef, // Alias for ref
|
|
1047
1828
|
diff: diffVersion,
|
|
@@ -1058,6 +1839,15 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1058
1839
|
// Normalize ref/sourceRef
|
|
1059
1840
|
const effectiveRef = ref || sourceRef
|
|
1060
1841
|
|
|
1842
|
+
for (const pinnedPlugin of Object.keys(pluginPins)) {
|
|
1843
|
+
if (!REFRESHABLE_PLUGINS.includes(pinnedPlugin)) {
|
|
1844
|
+
console.warn(
|
|
1845
|
+
`Warning: --plugin-pin for unknown plugin '${pinnedPlugin}' ` +
|
|
1846
|
+
`(known plugins: ${REFRESHABLE_PLUGINS.join(', ')}); the pin will have no effect`
|
|
1847
|
+
)
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1061
1851
|
const repoRoot = findRepoRoot()
|
|
1062
1852
|
const dataDir = customDataDir || path.join(repoRoot, 'docs-data')
|
|
1063
1853
|
const defaultOutputDir = path.join(repoRoot, 'modules', 'reference', 'pages', 'rpk')
|
|
@@ -1094,6 +1884,10 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1094
1884
|
let tree
|
|
1095
1885
|
let rpkVersion
|
|
1096
1886
|
let sourcePath
|
|
1887
|
+
// Managed plugins whose install failed this run. Passed to generateRpkDocs
|
|
1888
|
+
// as explicit protectedPlugins (merged there with auto-detection) so a
|
|
1889
|
+
// failed install never deletes or rewrites that plugin's existing docs.
|
|
1890
|
+
let failedPlugins = []
|
|
1097
1891
|
|
|
1098
1892
|
try {
|
|
1099
1893
|
// Fast path: regenerate from existing JSON file
|
|
@@ -1105,6 +1899,10 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1105
1899
|
console.log(`Loading command tree from ${jsonPath}`)
|
|
1106
1900
|
const jsonData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
1107
1901
|
tree = jsonData.raw_tree || jsonData.tree
|
|
1902
|
+
// Inherit the Linux-only command list from whichever stored tree
|
|
1903
|
+
// carries it: it cannot be re-detected without the rpk source, and
|
|
1904
|
+
// both rendering and the re-saved snapshot below depend on it.
|
|
1905
|
+
tree = preserveLinuxOnlyCommands(tree, jsonData.tree || jsonData.raw_tree)
|
|
1108
1906
|
rpkVersion = jsonData.rpk_version || 'local'
|
|
1109
1907
|
|
|
1110
1908
|
if (!tree) {
|
|
@@ -1113,16 +1911,154 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1113
1911
|
|
|
1114
1912
|
console.log(`Loaded tree with rpk version: ${rpkVersion}`)
|
|
1115
1913
|
|
|
1914
|
+
// Refresh a single plugin's subtree in the snapshot before rendering.
|
|
1915
|
+
// Plugins release on their own cadence, so their commands can change
|
|
1916
|
+
// without a new rpk release. Splice the fresh subtree into the full
|
|
1917
|
+
// tree and re-render everything: only the plugin's pages actually
|
|
1918
|
+
// change, and stale-file cleanup, nav rebuild, and override
|
|
1919
|
+
// validation all see a complete tree.
|
|
1920
|
+
let pluginVersions = jsonData.plugin_versions || {}
|
|
1921
|
+
let pluginDiffData = null
|
|
1922
|
+
if (plugin) {
|
|
1923
|
+
if (!REFRESHABLE_PLUGINS.includes(plugin)) {
|
|
1924
|
+
throw new Error(
|
|
1925
|
+
`Unknown plugin '${plugin}'. Supported plugins: ${REFRESHABLE_PLUGINS.join(', ')}\n` +
|
|
1926
|
+
'Use the rpk command name (for example "ai"), not the manifest slug ("rpai").'
|
|
1927
|
+
)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
const oldNode = (tree.commands || []).find(c => c.name === plugin)
|
|
1931
|
+
if (!oldNode) {
|
|
1932
|
+
console.log(
|
|
1933
|
+
`rpk ${rpkVersion} has no '${plugin}' command; nothing to update. ` +
|
|
1934
|
+
'This is expected when the plugin only exists in a newer rpk line.'
|
|
1935
|
+
)
|
|
1936
|
+
return {
|
|
1937
|
+
success: true,
|
|
1938
|
+
skipped: true,
|
|
1939
|
+
reason: `plugin '${plugin}' not present in rpk ${rpkVersion}`,
|
|
1940
|
+
rpkVersion
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
const rpkBinPath = acquireRpkBinary(rpkVersion, { rpkBin })
|
|
1945
|
+
const { node: rawNode, version: resolvedVersion } = fetchPluginSubtree({
|
|
1946
|
+
plugin,
|
|
1947
|
+
pluginVersion,
|
|
1948
|
+
rpkBinPath
|
|
1949
|
+
})
|
|
1950
|
+
|
|
1951
|
+
// Reapply platform markers: the fresh subtree comes from a plain
|
|
1952
|
+
// --print-tree run, but every command in the snapshot's tree carries
|
|
1953
|
+
// a platforms field. Reuse the snapshot's recorded Linux-only list.
|
|
1954
|
+
const linuxOnly = new Set(tree.linux_only_commands || [])
|
|
1955
|
+
const freshNode = addPlatformMarkersFromSource(
|
|
1956
|
+
{ commands: [rawNode] },
|
|
1957
|
+
linuxOnly
|
|
1958
|
+
).commands[0]
|
|
1959
|
+
|
|
1960
|
+
// Plugin-scoped diff for the PR summary: compare only the plugin's
|
|
1961
|
+
// subtree, old snapshot state vs fresh install
|
|
1962
|
+
pluginDiffData = generateRpkDiff(
|
|
1963
|
+
{ ...tree, commands: [oldNode] },
|
|
1964
|
+
{ ...tree, commands: [freshNode] },
|
|
1965
|
+
{
|
|
1966
|
+
oldVersion: pluginVersions[plugin] || 'previous snapshot',
|
|
1967
|
+
newVersion: resolvedVersion || 'latest'
|
|
1968
|
+
}
|
|
1969
|
+
)
|
|
1970
|
+
printDiffReport(pluginDiffData)
|
|
1971
|
+
|
|
1972
|
+
tree = splicePluginNode(tree, plugin, freshNode)
|
|
1973
|
+
if (resolvedVersion) {
|
|
1974
|
+
pluginVersions = { ...pluginVersions, [plugin]: resolvedVersion }
|
|
1975
|
+
}
|
|
1976
|
+
console.log(
|
|
1977
|
+
`Spliced fresh '${plugin}' subtree` +
|
|
1978
|
+
(resolvedVersion ? ` (version ${resolvedVersion})` : '') +
|
|
1979
|
+
` into snapshot for rpk ${rpkVersion}`
|
|
1980
|
+
)
|
|
1981
|
+
|
|
1982
|
+
// Stamp new plugin commands and flags with the plugin's own version
|
|
1983
|
+
// so pages render "This command was introduced in <plugin> version X".
|
|
1984
|
+
// Runs before the overrides load below, so the stamps apply to this
|
|
1985
|
+
// run's rendering, not just the next one.
|
|
1986
|
+
if (resolvedVersion) {
|
|
1987
|
+
updateOverridesWithIntroducedVersions(
|
|
1988
|
+
pluginDiffData,
|
|
1989
|
+
overridesPath || path.join(dataDir, 'rpk-overrides.json'),
|
|
1990
|
+
resolvedVersion,
|
|
1991
|
+
{ [plugin]: resolvedVersion }
|
|
1992
|
+
)
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1995
|
+
if (diffVersion) {
|
|
1996
|
+
console.warn('Note: --diff is ignored in --plugin mode (the summary uses a plugin-scoped diff)')
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// Plugin releases land in What's new too when requested: their
|
|
2000
|
+
// changes never arrive through a Redpanda release diff. Entries
|
|
2001
|
+
// render without xrefs because plugin subtrees may render as
|
|
2002
|
+
// partials with no linkable pages.
|
|
2003
|
+
if (whatsNewPath) {
|
|
2004
|
+
const label = resolvedVersion
|
|
2005
|
+
? `${plugin} plugin ${resolvedVersion}`
|
|
2006
|
+
: `${plugin} plugin`
|
|
2007
|
+
updateWhatsNewFile(pluginDiffData, whatsNewPath, label, { xrefs: false, sectionHeading: '== rpk plugins' })
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
|
|
1116
2011
|
// Skip to documentation generation (after the source building steps)
|
|
1117
2012
|
// Load and validate overrides
|
|
1118
2013
|
const defaultOverridesPath = path.join(dataDir, 'rpk-overrides.json')
|
|
1119
2014
|
const effectiveOverridesPath = overridesPath || defaultOverridesPath
|
|
1120
|
-
|
|
2015
|
+
|
|
2016
|
+
// Stamp new commands and flags with introducedInVersion BEFORE the
|
|
2017
|
+
// overrides load so this run's pages render the labels. Without this,
|
|
2018
|
+
// a --from-json --diff run emits unlabeled pages and the labels only
|
|
2019
|
+
// appear on the next regeneration (the from-source path stamps before
|
|
2020
|
+
// rendering already).
|
|
2021
|
+
if (diffVersion && !plugin) {
|
|
2022
|
+
const oldDataForStamp = loadVersionedJson(diffVersion, dataDir)
|
|
2023
|
+
if (oldDataForStamp) {
|
|
2024
|
+
const stampDiff = generateRpkDiff(oldDataForStamp.raw_tree || oldDataForStamp.tree, tree, {
|
|
2025
|
+
oldVersion: diffVersion,
|
|
2026
|
+
newVersion: rpkVersion,
|
|
2027
|
+
oldDeprecatedCommands: oldDataForStamp.deprecated_commands || {},
|
|
2028
|
+
newDeprecatedCommands: jsonData.deprecated_commands || {}
|
|
2029
|
+
})
|
|
2030
|
+
if (stampDiff.details.newCommands.length > 0 || stampDiff.details.newFlags.length > 0) {
|
|
2031
|
+
updateOverridesWithIntroducedVersions(stampDiff, effectiveOverridesPath, rpkVersion, pluginVersions)
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const { overrides: overridesData, validation: overrideValidation } = loadOverrides(effectiveOverridesPath, tree, { strict: false }) || {}
|
|
1121
2037
|
|
|
1122
2038
|
if (overridesData) {
|
|
1123
2039
|
console.log(`Loaded overrides from ${effectiveOverridesPath}`)
|
|
1124
2040
|
}
|
|
1125
2041
|
|
|
2042
|
+
// Persist the merged snapshot so the refreshed plugin subtree and its
|
|
2043
|
+
// recorded version become the new baseline (mirrors the from-source
|
|
2044
|
+
// path's augmentedData structure)
|
|
2045
|
+
if (plugin) {
|
|
2046
|
+
// The Linux-only list can't be re-detected during a plugin refresh
|
|
2047
|
+
// (it comes from Go build-tag scanning of the rpk source), so make
|
|
2048
|
+
// sure the saved snapshot's trees still carry it before persisting.
|
|
2049
|
+
tree = preserveLinuxOnlyCommands(tree, jsonData.raw_tree || jsonData.tree)
|
|
2050
|
+
let enhancedTree = tree
|
|
2051
|
+
if (overridesData) {
|
|
2052
|
+
const resolvedOverrides = resolveReferences(overridesData, overridesData)
|
|
2053
|
+
enhancedTree = applyOverridesToTree(tree, resolvedOverrides, '')
|
|
2054
|
+
}
|
|
2055
|
+
jsonData.raw_tree = tree
|
|
2056
|
+
jsonData.tree = enhancedTree
|
|
2057
|
+
jsonData.plugin_versions = pluginVersions
|
|
2058
|
+
jsonData.generated_at = new Date().toISOString()
|
|
2059
|
+
saveVersionedJson(jsonData, rpkVersion, dataDir)
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1126
2062
|
// Generate AsciiDoc documentation
|
|
1127
2063
|
console.log(`\nGenerating AsciiDoc files to ${finalOutputDir}...`)
|
|
1128
2064
|
|
|
@@ -1132,10 +2068,11 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1132
2068
|
outputDir: finalOutputDir,
|
|
1133
2069
|
cloudSecretDir: finalCloudSecretDir,
|
|
1134
2070
|
rpkVersion,
|
|
1135
|
-
pluginVersions
|
|
2071
|
+
pluginVersions,
|
|
1136
2072
|
draftMissing,
|
|
1137
2073
|
preservationsDir: preserveFrom,
|
|
1138
|
-
navFile
|
|
2074
|
+
navFile,
|
|
2075
|
+
protectedPlugins: failedPlugins
|
|
1139
2076
|
})
|
|
1140
2077
|
|
|
1141
2078
|
console.log(`\nGeneration complete!`)
|
|
@@ -1154,16 +2091,19 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1154
2091
|
showInfo
|
|
1155
2092
|
})
|
|
1156
2093
|
|
|
1157
|
-
// Generate diff if requested
|
|
2094
|
+
// Generate diff if requested (rpk-version diff; not used in --plugin
|
|
2095
|
+
// mode, which produces its own plugin-scoped diff)
|
|
1158
2096
|
let diffData = null
|
|
1159
|
-
if (diffVersion) {
|
|
2097
|
+
if (diffVersion && !plugin) {
|
|
1160
2098
|
const oldData = loadVersionedJson(diffVersion, dataDir)
|
|
1161
2099
|
if (oldData) {
|
|
1162
2100
|
// Use raw_tree for diffing (falls back to tree for backward compatibility)
|
|
1163
2101
|
const oldTree = oldData.raw_tree || oldData.tree
|
|
1164
2102
|
diffData = generateRpkDiff(oldTree, tree, {
|
|
1165
2103
|
oldVersion: diffVersion,
|
|
1166
|
-
newVersion: rpkVersion
|
|
2104
|
+
newVersion: rpkVersion,
|
|
2105
|
+
oldDeprecatedCommands: oldData.deprecated_commands || {},
|
|
2106
|
+
newDeprecatedCommands: jsonData.deprecated_commands || {}
|
|
1167
2107
|
})
|
|
1168
2108
|
|
|
1169
2109
|
// Save diff
|
|
@@ -1177,7 +2117,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1177
2117
|
|
|
1178
2118
|
// Update what's-new file if requested
|
|
1179
2119
|
if (whatsNewPath) {
|
|
1180
|
-
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion)
|
|
2120
|
+
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
1181
2121
|
}
|
|
1182
2122
|
} else {
|
|
1183
2123
|
console.warn(`Warning: Could not load previous version ${diffVersion} for diff`)
|
|
@@ -1187,11 +2127,15 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1187
2127
|
// Generate PR summary
|
|
1188
2128
|
const prSummary = generatePRSummary({
|
|
1189
2129
|
rpkVersion,
|
|
2130
|
+
plugin,
|
|
2131
|
+
pluginVersion: plugin ? pluginVersions[plugin] : undefined,
|
|
1190
2132
|
commandCount: result.commandCount,
|
|
1191
2133
|
filesGenerated: result.filesGenerated,
|
|
1192
2134
|
filesSkipped: result.filesSkipped,
|
|
1193
|
-
diffData,
|
|
2135
|
+
diffData: plugin ? pluginDiffData : diffData,
|
|
1194
2136
|
validationResult,
|
|
2137
|
+
overrideValidation,
|
|
2138
|
+
descriptionCoverage: computeDescriptionCoverage(tree, overridesData),
|
|
1195
2139
|
outputDir: finalOutputDir
|
|
1196
2140
|
})
|
|
1197
2141
|
|
|
@@ -1211,12 +2155,21 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1211
2155
|
filesSkipped: result.filesSkipped,
|
|
1212
2156
|
outputDir: finalOutputDir,
|
|
1213
2157
|
rpkVersion,
|
|
1214
|
-
|
|
2158
|
+
plugin,
|
|
2159
|
+
pluginVersion: plugin ? pluginVersions[plugin] : undefined,
|
|
2160
|
+
diffData: plugin ? pluginDiffData : diffData,
|
|
1215
2161
|
validationResult,
|
|
1216
2162
|
prSummary
|
|
1217
2163
|
}
|
|
1218
2164
|
}
|
|
1219
2165
|
|
|
2166
|
+
if (plugin) {
|
|
2167
|
+
throw new Error(
|
|
2168
|
+
'--plugin requires --from-json <snapshot>.\n' +
|
|
2169
|
+
'A plugin refresh splices the fresh subtree into an existing committed snapshot.'
|
|
2170
|
+
)
|
|
2171
|
+
}
|
|
2172
|
+
|
|
1220
2173
|
// Step 1: Get source code
|
|
1221
2174
|
if (effectiveRef) {
|
|
1222
2175
|
// Clone/checkout from ref
|
|
@@ -1284,26 +2237,38 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1284
2237
|
try {
|
|
1285
2238
|
// Build on Linux (in container) - has all commands
|
|
1286
2239
|
console.log('Building rpk in Linux container...')
|
|
1287
|
-
const linuxTree = fetchRpkTreeFromLinuxSource(sourcePath)
|
|
1288
|
-
|
|
1289
|
-
// Build natively (on Darwin)
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
2240
|
+
const { tree: linuxTree, failedPlugins: linuxFailedPlugins } = fetchRpkTreeFromLinuxSource(sourcePath, pluginPins)
|
|
2241
|
+
|
|
2242
|
+
// Build natively (on Darwin) for comparison. The Linux tree is
|
|
2243
|
+
// authoritative, so a comparison-build failure (for example, local
|
|
2244
|
+
// Go older than go.mod requires) only skips dynamic platform
|
|
2245
|
+
// detection - it must not discard the Linux tree.
|
|
2246
|
+
let darwinTree = null
|
|
2247
|
+
try {
|
|
2248
|
+
console.log('Building rpk natively for comparison...')
|
|
2249
|
+
darwinTree = fetchRpkTreeFromSource(sourcePath)
|
|
2250
|
+
} catch (nativeErr) {
|
|
2251
|
+
console.warn(`⚠ Native comparison build failed: ${nativeErr.message}`)
|
|
2252
|
+
console.log('Skipping dynamic platform detection; using source scanning only.')
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
if (darwinTree) {
|
|
2256
|
+
// Compare trees to find Linux-only commands
|
|
2257
|
+
const dynamicLinuxOnly = detectLinuxOnlyByComparison(linuxTree, darwinTree)
|
|
2258
|
+
if (dynamicLinuxOnly.size > 0) {
|
|
2259
|
+
console.log(`Dynamic detection found ${dynamicLinuxOnly.size} Linux-only command(s):`)
|
|
2260
|
+
for (const cmd of dynamicLinuxOnly) {
|
|
2261
|
+
console.log(` - ${cmd}`)
|
|
2262
|
+
linuxOnlyCommands.add(cmd)
|
|
2263
|
+
}
|
|
1300
2264
|
}
|
|
1301
2265
|
}
|
|
1302
2266
|
|
|
1303
2267
|
// Use the Linux tree (has all commands)
|
|
1304
2268
|
tree = linuxTree
|
|
2269
|
+
failedPlugins = linuxFailedPlugins
|
|
1305
2270
|
} catch (dockerErr) {
|
|
1306
|
-
// Docker build failed
|
|
2271
|
+
// Docker build failed - fall back to native build
|
|
1307
2272
|
console.warn(`\n⚠ Docker build failed: ${dockerErr.message}`)
|
|
1308
2273
|
console.log('Falling back to native Go build...')
|
|
1309
2274
|
console.log('Note: Linux-only commands will be detected via source scanning only.\n')
|
|
@@ -1312,7 +2277,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1312
2277
|
} else if (canBuildLinux) {
|
|
1313
2278
|
console.log('\nBuilding rpk in Linux container...')
|
|
1314
2279
|
try {
|
|
1315
|
-
tree = fetchRpkTreeFromLinuxSource(sourcePath)
|
|
2280
|
+
({ tree, failedPlugins } = fetchRpkTreeFromLinuxSource(sourcePath, pluginPins))
|
|
1316
2281
|
} catch (dockerErr) {
|
|
1317
2282
|
if (canBuildNative) {
|
|
1318
2283
|
console.warn(`\n⚠ Docker build failed: ${dockerErr.message}`)
|
|
@@ -1343,8 +2308,23 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1343
2308
|
const plugins = detectPlugins(tree)
|
|
1344
2309
|
console.log(`Detected plugins: ${plugins.join(', ') || 'none'}`)
|
|
1345
2310
|
|
|
1346
|
-
//
|
|
2311
|
+
// Record the version of each plugin that actually installed during the
|
|
2312
|
+
// build (`rpk <plugin> install` resolves the manifest's latest, so the
|
|
2313
|
+
// manifest lookup reflects what the tree contains). Shim-only plugins
|
|
2314
|
+
// (failed installs) are skipped: their subtree carries no plugin commands.
|
|
1347
2315
|
const pluginVersions = {}
|
|
2316
|
+
for (const pluginName of REFRESHABLE_PLUGINS) {
|
|
2317
|
+
const node = (tree.commands || []).find(c => c.name === pluginName)
|
|
2318
|
+
if (node && pluginNodeHasRealCommands(node)) {
|
|
2319
|
+
const installedVersion = pluginPins[pluginName] || fetchLatestPluginVersion(pluginName)
|
|
2320
|
+
if (installedVersion) {
|
|
2321
|
+
pluginVersions[pluginName] = installedVersion
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
}
|
|
2325
|
+
if (Object.keys(pluginVersions).length > 0) {
|
|
2326
|
+
console.log(`Plugin versions: ${JSON.stringify(pluginVersions)}`)
|
|
2327
|
+
}
|
|
1348
2328
|
|
|
1349
2329
|
// Step 5: Scan source for deprecated/hidden commands
|
|
1350
2330
|
let deprecatedCommands = {}
|
|
@@ -1365,7 +2345,13 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1365
2345
|
// Load and validate overrides
|
|
1366
2346
|
const defaultOverridesPath = path.join(dataDir, 'rpk-overrides.json')
|
|
1367
2347
|
const effectiveOverridesPath = overridesPath || defaultOverridesPath
|
|
1368
|
-
|
|
2348
|
+
|
|
2349
|
+
// Annotate deprecated commands that are still visible in the tree so
|
|
2350
|
+
// their pages render deprecation banners without manual curation. Runs
|
|
2351
|
+
// before the overrides load so the annotations apply to this run.
|
|
2352
|
+
mergeVisibleDeprecationsIntoOverrides(deprecatedCommands, tree, effectiveOverridesPath)
|
|
2353
|
+
|
|
2354
|
+
const { overrides: overridesData, validation: overrideValidation } = loadOverrides(effectiveOverridesPath, tree, { strict: false }) || {}
|
|
1369
2355
|
|
|
1370
2356
|
if (overridesData) {
|
|
1371
2357
|
console.log(`Loaded overrides from ${effectiveOverridesPath}`)
|
|
@@ -1403,7 +2389,9 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1403
2389
|
const oldTree = oldData.raw_tree || oldData.tree
|
|
1404
2390
|
diffData = generateRpkDiff(oldTree, tree, {
|
|
1405
2391
|
oldVersion: diffVersion,
|
|
1406
|
-
newVersion: rpkVersion
|
|
2392
|
+
newVersion: rpkVersion,
|
|
2393
|
+
oldDeprecatedCommands: oldData.deprecated_commands || {},
|
|
2394
|
+
newDeprecatedCommands: deprecatedCommands
|
|
1407
2395
|
})
|
|
1408
2396
|
|
|
1409
2397
|
// Save diff
|
|
@@ -1415,14 +2403,17 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1415
2403
|
// Print diff report
|
|
1416
2404
|
printDiffReport(diffData)
|
|
1417
2405
|
|
|
1418
|
-
// Update overrides with introducedInVersion for new commands
|
|
1419
|
-
|
|
1420
|
-
|
|
2406
|
+
// Update overrides with introducedInVersion for new commands. Plugin
|
|
2407
|
+
// commands get the plugin's own version (plugins release on their own
|
|
2408
|
+
// cadence, so the rpk version would be wrong and the page note would
|
|
2409
|
+
// render "introduced in <plugin> version <rpk version>").
|
|
2410
|
+
if ((diffData.details.newCommands.length > 0 || diffData.details.newFlags.length > 0) && effectiveOverridesPath) {
|
|
2411
|
+
updateOverridesWithIntroducedVersions(diffData, effectiveOverridesPath, rpkVersion, pluginVersions)
|
|
1421
2412
|
}
|
|
1422
2413
|
|
|
1423
2414
|
// Update what's-new file if requested
|
|
1424
2415
|
if (whatsNewPath) {
|
|
1425
|
-
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion)
|
|
2416
|
+
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
1426
2417
|
}
|
|
1427
2418
|
} else {
|
|
1428
2419
|
console.warn(`Warning: Could not load previous version ${diffVersion} for diff`)
|
|
@@ -1441,7 +2432,8 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1441
2432
|
pluginVersions,
|
|
1442
2433
|
draftMissing,
|
|
1443
2434
|
preservationsDir: preserveFrom,
|
|
1444
|
-
navFile
|
|
2435
|
+
navFile,
|
|
2436
|
+
protectedPlugins: failedPlugins
|
|
1445
2437
|
})
|
|
1446
2438
|
|
|
1447
2439
|
console.log(`\nGeneration complete!`)
|
|
@@ -1463,11 +2455,13 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1463
2455
|
// Generate PR summary
|
|
1464
2456
|
const prSummary = generatePRSummary({
|
|
1465
2457
|
rpkVersion,
|
|
2458
|
+
overrideValidation,
|
|
1466
2459
|
commandCount: result.commandCount,
|
|
1467
2460
|
filesGenerated: result.filesGenerated,
|
|
1468
2461
|
filesSkipped: result.filesSkipped,
|
|
1469
2462
|
diffData,
|
|
1470
2463
|
validationResult,
|
|
2464
|
+
descriptionCoverage: computeDescriptionCoverage(tree, overridesData),
|
|
1471
2465
|
outputDir: finalOutputDir
|
|
1472
2466
|
})
|
|
1473
2467
|
|
|
@@ -1506,26 +2500,75 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1506
2500
|
* @param {Object} options - Summary options
|
|
1507
2501
|
* @returns {string} Markdown formatted summary
|
|
1508
2502
|
*/
|
|
2503
|
+
/**
|
|
2504
|
+
* Find description overrides that replace substantially longer source help.
|
|
2505
|
+
* Curation is legitimate, but silent replacement is how stale or wrong
|
|
2506
|
+
* content survives regeneration, so every regen PR lists what is hidden.
|
|
2507
|
+
* @param {Object} tree - Raw command tree
|
|
2508
|
+
* @param {Object} overridesData - Parsed overrides
|
|
2509
|
+
* @returns {Array<{commandPath: string, overrideChars: number, sourceChars: number}>}
|
|
2510
|
+
*/
|
|
2511
|
+
function computeDescriptionCoverage(tree, overridesData) {
|
|
2512
|
+
if (!tree || !overridesData?.commands) return []
|
|
2513
|
+
const commandMap = flattenToMap(tree)
|
|
2514
|
+
const results = []
|
|
2515
|
+
for (const [cmdPath, override] of Object.entries(overridesData.commands)) {
|
|
2516
|
+
if (typeof override?.description !== 'string') continue
|
|
2517
|
+
const cmd = commandMap.get(cmdPath)
|
|
2518
|
+
if (!cmd || typeof cmd.description !== 'string') continue
|
|
2519
|
+
const sourceChars = cmd.description.trim().length
|
|
2520
|
+
// appendToDescription content still reaches the page, so count it
|
|
2521
|
+
const appended = typeof override.appendToDescription === 'string' ? override.appendToDescription.trim().length : 0
|
|
2522
|
+
const overrideChars = override.description.trim().length + appended
|
|
2523
|
+
if (sourceChars - overrideChars > 500) {
|
|
2524
|
+
results.push({ commandPath: cmdPath, overrideChars, sourceChars })
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
return results.sort((a, b) => (b.sourceChars - b.overrideChars) - (a.sourceChars - a.overrideChars))
|
|
2528
|
+
}
|
|
2529
|
+
|
|
1509
2530
|
function generatePRSummary(options) {
|
|
1510
2531
|
const {
|
|
1511
2532
|
rpkVersion,
|
|
2533
|
+
plugin,
|
|
2534
|
+
pluginVersion,
|
|
1512
2535
|
commandCount,
|
|
1513
2536
|
filesGenerated,
|
|
1514
2537
|
filesSkipped = 0,
|
|
1515
2538
|
diffData,
|
|
1516
2539
|
validationResult,
|
|
2540
|
+
overrideValidation,
|
|
2541
|
+
descriptionCoverage,
|
|
1517
2542
|
outputDir
|
|
1518
2543
|
} = options
|
|
1519
2544
|
|
|
1520
2545
|
const lines = []
|
|
1521
2546
|
|
|
1522
2547
|
// Header
|
|
1523
|
-
|
|
1524
|
-
|
|
2548
|
+
if (plugin) {
|
|
2549
|
+
lines.push(`## rpk ${plugin} Plugin Documentation Update`)
|
|
2550
|
+
lines.push('')
|
|
2551
|
+
if (pluginVersion) {
|
|
2552
|
+
lines.push(`**Plugin version:** ${pluginVersion}`)
|
|
2553
|
+
}
|
|
2554
|
+
lines.push(`**Base rpk snapshot:** ${rpkVersion}`)
|
|
2555
|
+
lines.push('')
|
|
2556
|
+
lines.push(
|
|
2557
|
+
`The \`rpk ${plugin}\` subtree was refreshed in the snapshot, then the ` +
|
|
2558
|
+
'full rpk reference was re-rendered from it as a converge. Pages outside ' +
|
|
2559
|
+
`\`rpk ${plugin}\` can carry template-level or consistency updates, and ` +
|
|
2560
|
+
'stale generated files are cleaned up, so the change list may be wider ' +
|
|
2561
|
+
'than the plugin itself.'
|
|
2562
|
+
)
|
|
2563
|
+
lines.push('')
|
|
2564
|
+
} else {
|
|
2565
|
+
lines.push('## rpk Documentation Generation Summary')
|
|
2566
|
+
lines.push('')
|
|
1525
2567
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2568
|
+
// Version info
|
|
2569
|
+
lines.push(`**Version:** ${rpkVersion}`)
|
|
2570
|
+
lines.push('')
|
|
2571
|
+
}
|
|
1529
2572
|
|
|
1530
2573
|
// Generation stats
|
|
1531
2574
|
lines.push('### Generation Statistics')
|
|
@@ -1544,52 +2587,118 @@ function generatePRSummary(options) {
|
|
|
1544
2587
|
lines.push('### Changes from Previous Version')
|
|
1545
2588
|
lines.push('')
|
|
1546
2589
|
|
|
1547
|
-
const {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2590
|
+
const {
|
|
2591
|
+
newCommands,
|
|
2592
|
+
removedCommands,
|
|
2593
|
+
newFlags,
|
|
2594
|
+
removedFlags,
|
|
2595
|
+
changedDefaults = 0,
|
|
2596
|
+
changedFlagTypes = 0,
|
|
2597
|
+
changedFlagRequirements = 0,
|
|
2598
|
+
changedFlagDescriptions = 0,
|
|
2599
|
+
descriptionChanges = 0,
|
|
2600
|
+
newlyDeprecatedCommands = 0
|
|
2601
|
+
} = diffData.summary
|
|
2602
|
+
|
|
2603
|
+
const totalChanges = newCommands + removedCommands + newFlags + removedFlags +
|
|
2604
|
+
changedDefaults + changedFlagTypes + changedFlagRequirements +
|
|
2605
|
+
changedFlagDescriptions + descriptionChanges + newlyDeprecatedCommands
|
|
2606
|
+
|
|
2607
|
+
if (totalChanges === 0) {
|
|
2608
|
+
lines.push('No command, flag, or default changes detected.')
|
|
1551
2609
|
} else {
|
|
1552
2610
|
lines.push(`| Change Type | Count |`)
|
|
1553
2611
|
lines.push(`|-------------|-------|`)
|
|
1554
2612
|
if (newCommands > 0) lines.push(`| New commands | ${newCommands} |`)
|
|
2613
|
+
if (newlyDeprecatedCommands > 0) lines.push(`| Deprecated commands | ${newlyDeprecatedCommands} |`)
|
|
1555
2614
|
if (removedCommands > 0) lines.push(`| Removed commands | ${removedCommands} |`)
|
|
1556
2615
|
if (newFlags > 0) lines.push(`| New flags | ${newFlags} |`)
|
|
1557
2616
|
if (removedFlags > 0) lines.push(`| Removed flags | ${removedFlags} |`)
|
|
1558
|
-
if (
|
|
2617
|
+
if (changedDefaults > 0) lines.push(`| Changed flag defaults | ${changedDefaults} |`)
|
|
2618
|
+
if (changedFlagTypes > 0) lines.push(`| Changed flag types | ${changedFlagTypes} |`)
|
|
2619
|
+
if (changedFlagRequirements > 0) lines.push(`| Changed flag requirements | ${changedFlagRequirements} |`)
|
|
2620
|
+
if (changedFlagDescriptions > 0) lines.push(`| Changed flag descriptions | ${changedFlagDescriptions} |`)
|
|
2621
|
+
if (descriptionChanges > 0) lines.push(`| Changed command descriptions | ${descriptionChanges} |`)
|
|
2622
|
+
if (diffData.summary?.flagDataBackfilled > 0) lines.push(`| Flag docs backfilled (not new flags) | ${diffData.summary.flagDataBackfilled} commands |`)
|
|
1559
2623
|
}
|
|
1560
2624
|
lines.push('')
|
|
1561
2625
|
|
|
1562
|
-
//
|
|
1563
|
-
|
|
2626
|
+
// Collapsible list helper: itemized up to a cap, with an overflow note
|
|
2627
|
+
const pushDetailsList = (title, items, renderItem) => {
|
|
2628
|
+
if (!items || items.length === 0) return
|
|
1564
2629
|
lines.push('<details>')
|
|
1565
|
-
lines.push(
|
|
2630
|
+
lines.push(`<summary>${title}</summary>`)
|
|
1566
2631
|
lines.push('')
|
|
1567
|
-
for (const
|
|
1568
|
-
lines.push(
|
|
2632
|
+
for (const item of items.slice(0, 30)) {
|
|
2633
|
+
lines.push(renderItem(item))
|
|
1569
2634
|
}
|
|
1570
|
-
if (
|
|
1571
|
-
lines.push(`- ... and ${
|
|
2635
|
+
if (items.length > 30) {
|
|
2636
|
+
lines.push(`- ... and ${items.length - 30} more (see the diff JSON in docs-data/)`)
|
|
1572
2637
|
}
|
|
1573
2638
|
lines.push('')
|
|
1574
2639
|
lines.push('</details>')
|
|
1575
2640
|
lines.push('')
|
|
1576
2641
|
}
|
|
1577
2642
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
for (const cmd of diffData.details.removedCommands.slice(0, 20)) {
|
|
1584
|
-
lines.push(`- \`${cmd.path}\``)
|
|
1585
|
-
}
|
|
1586
|
-
if (diffData.details.removedCommands.length > 20) {
|
|
1587
|
-
lines.push(`- ... and ${diffData.details.removedCommands.length - 20} more`)
|
|
1588
|
-
}
|
|
1589
|
-
lines.push('')
|
|
1590
|
-
lines.push('</details>')
|
|
1591
|
-
lines.push('')
|
|
2643
|
+
const formatValue = (value) => {
|
|
2644
|
+
if (value === undefined) return 'unset'
|
|
2645
|
+
if (value === null) return 'null'
|
|
2646
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
2647
|
+
return String(value)
|
|
1592
2648
|
}
|
|
2649
|
+
|
|
2650
|
+
pushDetailsList('New Commands', diffData.details?.newCommands,
|
|
2651
|
+
cmd => `- \`${cmd.path}\``)
|
|
2652
|
+
pushDetailsList('Deprecated Commands', diffData.details?.newlyDeprecatedCommands,
|
|
2653
|
+
cmd => `- \`${cmd.path}\`${cmd.message ? ` — ${cmd.message}` : ''}${cmd.hidden ? ' _(hidden from help output)_' : ''}`)
|
|
2654
|
+
pushDetailsList('Removed Commands', diffData.details?.removedCommands,
|
|
2655
|
+
cmd => `- \`${cmd.path}\``)
|
|
2656
|
+
pushDetailsList('New Flags', diffData.details?.newFlags,
|
|
2657
|
+
flag => `- \`${flag.commandPath}\`: \`--${flag.flagName}\` (${flag.type})`)
|
|
2658
|
+
pushDetailsList('Removed Flags', diffData.details?.removedFlags,
|
|
2659
|
+
flag => `- \`${flag.commandPath}\`: \`--${flag.flagName}\``)
|
|
2660
|
+
pushDetailsList('Changed Flag Defaults', diffData.details?.changedDefaults,
|
|
2661
|
+
change => `- \`${change.commandPath}\`: \`--${change.flagName}\` default \`${formatValue(change.oldDefault)}\` → \`${formatValue(change.newDefault)}\``)
|
|
2662
|
+
pushDetailsList('Changed Flag Types', diffData.details?.changedFlagTypes,
|
|
2663
|
+
change => `- \`${change.commandPath}\`: \`--${change.flagName}\` type \`${change.oldType}\` → \`${change.newType}\``)
|
|
2664
|
+
pushDetailsList('Changed Flag Requirements', diffData.details?.changedFlagRequirements,
|
|
2665
|
+
change => `- \`${change.commandPath}\`: \`--${change.flagName}\` required \`${change.oldRequired}\` → \`${change.newRequired}\``)
|
|
2666
|
+
pushDetailsList('Changed Command Descriptions', diffData.details?.descriptionChanges,
|
|
2667
|
+
change => `- \`${change.path}\``)
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
// Override validation: entries referencing commands the tree lacks do
|
|
2671
|
+
// nothing silently, so surface them where they become actionable
|
|
2672
|
+
if (overrideValidation && overrideValidation.errors && overrideValidation.errors.length > 0) {
|
|
2673
|
+
lines.push('### Override Validation')
|
|
2674
|
+
lines.push('')
|
|
2675
|
+
lines.push(`⚠ ${overrideValidation.errors.length} override entr${overrideValidation.errors.length === 1 ? 'y' : 'ies'} did not apply (unknown command paths — stale entries in the overrides file):`)
|
|
2676
|
+
lines.push('')
|
|
2677
|
+
for (const err of overrideValidation.errors.slice(0, 10)) {
|
|
2678
|
+
lines.push(`- ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`)
|
|
2679
|
+
}
|
|
2680
|
+
if (overrideValidation.errors.length > 10) {
|
|
2681
|
+
lines.push(`- ... and ${overrideValidation.errors.length - 10} more`)
|
|
2682
|
+
}
|
|
2683
|
+
lines.push('')
|
|
2684
|
+
}
|
|
2685
|
+
|
|
2686
|
+
// Description overrides that hide most of the source help. Curation is
|
|
2687
|
+
// fine, but this is where stale content hides, so keep it reviewable.
|
|
2688
|
+
if (descriptionCoverage && descriptionCoverage.length > 0) {
|
|
2689
|
+
lines.push('### Curated descriptions replacing source help')
|
|
2690
|
+
lines.push('')
|
|
2691
|
+
lines.push('These overrides replace substantially longer help text from the rpk source. Confirm the curated version still carries every operational detail (or intentionally omits it):')
|
|
2692
|
+
lines.push('')
|
|
2693
|
+
lines.push('| Command | Override | Source help |')
|
|
2694
|
+
lines.push('|---------|----------|-------------|')
|
|
2695
|
+
for (const entry of descriptionCoverage.slice(0, 15)) {
|
|
2696
|
+
lines.push(`| \`${entry.commandPath}\` | ${entry.overrideChars} chars | ${entry.sourceChars} chars |`)
|
|
2697
|
+
}
|
|
2698
|
+
if (descriptionCoverage.length > 15) {
|
|
2699
|
+
lines.push(`| ... and ${descriptionCoverage.length - 15} more | | |`)
|
|
2700
|
+
}
|
|
2701
|
+
lines.push('')
|
|
1593
2702
|
}
|
|
1594
2703
|
|
|
1595
2704
|
// Validation results
|
|
@@ -1686,6 +2795,23 @@ module.exports = {
|
|
|
1686
2795
|
handleRpkDocsGeneration,
|
|
1687
2796
|
fetchRpkTreeFromSource,
|
|
1688
2797
|
fetchRpkTreeFromLinuxSource,
|
|
2798
|
+
acquireRpkBinary,
|
|
2799
|
+
buildRpkBinary,
|
|
2800
|
+
downloadRpkRelease,
|
|
2801
|
+
fetchPluginSubtree,
|
|
2802
|
+
parseCobraFlags,
|
|
2803
|
+
parseUrfaveFlags,
|
|
2804
|
+
parseHelpFlags,
|
|
2805
|
+
mergeVisibleDeprecationsIntoOverrides,
|
|
2806
|
+
makeLinkablePredicate,
|
|
2807
|
+
enrichPluginTreeWithFlags,
|
|
2808
|
+
fetchLatestPluginVersion,
|
|
2809
|
+
pluginNodeHasRealCommands,
|
|
2810
|
+
splicePluginNode,
|
|
2811
|
+
preserveLinuxOnlyCommands,
|
|
2812
|
+
REFRESHABLE_PLUGINS,
|
|
2813
|
+
PLUGIN_INSTALL_VERSION_FLAGS,
|
|
2814
|
+
PLUGIN_MANIFEST_SLUGS,
|
|
1689
2815
|
getRequiredGoVersion,
|
|
1690
2816
|
prepareSourceFromRef,
|
|
1691
2817
|
detectLinuxOnlyFromSource,
|
|
@@ -1701,6 +2827,8 @@ module.exports = {
|
|
|
1701
2827
|
getPlatformDescription,
|
|
1702
2828
|
getCurrentPlatform,
|
|
1703
2829
|
updateOverridesWithIntroducedVersions,
|
|
2830
|
+
computeDescriptionCoverage,
|
|
2831
|
+
updateWhatsNewFile,
|
|
1704
2832
|
KNOWN_PLUGINS,
|
|
1705
2833
|
extractCommandPaths,
|
|
1706
2834
|
detectLinuxOnlyByComparison,
|