@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +159 -0
- package/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +39 -2
- package/tools/property-extractor/transformers.py +29 -13
- package/tools/rpk-docs/detect-platform-commands.js +566 -0
- package/tools/rpk-docs/generate-plugin-stubs.js +291 -0
- package/tools/rpk-docs/generate-rpk-docs.js +466 -30
- package/tools/rpk-docs/helpers/index.js +13 -3
- package/tools/rpk-docs/report-delta.js +314 -32
- package/tools/rpk-docs/rpk-docs-handler.js +1329 -208
- package/tools/rpk-docs/templates/command.hbs +17 -0
- package/tools/rpk-docs/templates/content-item.hbs +2 -2
- package/tools/rpk-docs/validate-overrides.js +16 -0
|
@@ -1,13 +1,15 @@
|
|
|
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 {
|
|
10
|
+
const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir } = require('./generate-rpk-docs')
|
|
11
|
+
const { detectLinuxOnlyFromSource, warnIfDetectionLooksBroken } = require('./detect-platform-commands')
|
|
12
|
+
const { generateRpkDiff, printDiffReport, generateWhatsNewSection, flattenToMap } = require('./report-delta')
|
|
11
13
|
const { loadAndValidateOverrides, ValidationResult } = require('./validate-overrides')
|
|
12
14
|
const { validateDirectory, formatResults } = require('./validate-output')
|
|
13
15
|
|
|
@@ -16,6 +18,37 @@ const { validateDirectory, formatResults } = require('./validate-output')
|
|
|
16
18
|
*/
|
|
17
19
|
const KNOWN_PLUGINS = ['ai', 'check', 'connect', 'k8s', 'oxla']
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Plugins whose docs can be refreshed individually with --plugin.
|
|
23
|
+
* oxla is excluded: it is a "Coming Soon" stub with no installable binary.
|
|
24
|
+
*/
|
|
25
|
+
const REFRESHABLE_PLUGINS = ['ai', 'check', 'connect', 'k8s']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Per-plugin install flags that pin a version (rpk <plugin> install <flag> <version>).
|
|
29
|
+
* k8s uses the generic flag name; the others embed the plugin name.
|
|
30
|
+
*/
|
|
31
|
+
const PLUGIN_INSTALL_VERSION_FLAGS = {
|
|
32
|
+
ai: '--ai-version',
|
|
33
|
+
check: '--check-version',
|
|
34
|
+
connect: '--connect-version',
|
|
35
|
+
k8s: '--plugin-version'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Manifest slugs at https://rpk-plugins.redpanda.com/<slug>/manifest.json
|
|
40
|
+
* where the slug differs from the rpk command name.
|
|
41
|
+
*/
|
|
42
|
+
const PLUGIN_MANIFEST_SLUGS = { ai: 'rpai' }
|
|
43
|
+
|
|
44
|
+
const PLUGIN_MANIFEST_HOST = 'https://rpk-plugins.redpanda.com'
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Subcommands compiled into rpk itself for managed plugins. A plugin node
|
|
48
|
+
* whose children are only these never actually installed.
|
|
49
|
+
*/
|
|
50
|
+
const PLUGIN_SHIM_SUBCOMMANDS = new Set(['install', 'uninstall', 'upgrade'])
|
|
51
|
+
|
|
19
52
|
/**
|
|
20
53
|
* Parse Go version from 'go version' output
|
|
21
54
|
* @param {string} versionOutput - Output from 'go version' command
|
|
@@ -347,9 +380,12 @@ function fetchRpkTreeFromSource(sourcePath) {
|
|
|
347
380
|
* Builds rpk binary, installs plugins, then runs --print-tree for complete command coverage.
|
|
348
381
|
* Falls back to native Go build if Docker is unavailable.
|
|
349
382
|
* @param {string} sourcePath - Path to rpk Go source directory (e.g., ~/redpanda/src/go/rpk)
|
|
350
|
-
* @returns {Object}
|
|
383
|
+
* @returns {Object} { tree, failedPlugins } — the parsed JSON tree plus the
|
|
384
|
+
* names of managed plugins whose install failed this run. Callers pass
|
|
385
|
+
* failedPlugins to generateRpkDocs as protectedPlugins so those plugins'
|
|
386
|
+
* existing pages and nav entries are preserved instead of treated as stale.
|
|
351
387
|
*/
|
|
352
|
-
function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
388
|
+
function fetchRpkTreeFromLinuxSource(sourcePath, pluginPins = {}) {
|
|
353
389
|
// Resolve to absolute path
|
|
354
390
|
const absoluteSourcePath = path.resolve(sourcePath)
|
|
355
391
|
|
|
@@ -455,18 +491,46 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
455
491
|
try {
|
|
456
492
|
// Step 1: Build rpk binary
|
|
457
493
|
console.log('Building rpk binary...')
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
494
|
+
// Module downloads from proxy.golang.org fail transiently (stream
|
|
495
|
+
// INTERNAL_ERROR), especially on a cold module cache. Retry: the second
|
|
496
|
+
// attempt reuses whatever the first already downloaded.
|
|
497
|
+
let buildResult
|
|
498
|
+
let binaryExists = false
|
|
499
|
+
for (let attempt = 1; attempt <= 4; attempt++) {
|
|
500
|
+
buildResult = spawnSync('docker', [
|
|
501
|
+
'exec', containerId,
|
|
502
|
+
'go', 'build', '-o', '/tmp/rpk', './cmd/rpk'
|
|
503
|
+
], {
|
|
504
|
+
encoding: 'utf8',
|
|
505
|
+
timeout: 300000 // 5 minutes per attempt
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
if (buildResult.status === 0) {
|
|
509
|
+
// Trust but verify: docker exec has been observed returning zero for
|
|
510
|
+
// a build that produced nothing, and everything downstream execs
|
|
511
|
+
// /tmp/rpk. Treat a phantom success as a failed attempt.
|
|
512
|
+
const binCheck = spawnSync('docker', [
|
|
513
|
+
'exec', containerId, 'test', '-x', '/tmp/rpk'
|
|
514
|
+
], { encoding: 'utf8', timeout: 15000 })
|
|
515
|
+
if (binCheck.status === 0) {
|
|
516
|
+
binaryExists = true
|
|
517
|
+
break
|
|
518
|
+
}
|
|
519
|
+
if (attempt < 4) {
|
|
520
|
+
console.warn(` Build attempt ${attempt} reported success but produced no binary; retrying...`)
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
} else if (attempt < 4) {
|
|
524
|
+
const firstError = (buildResult.stderr || buildResult.signal || 'unknown error')
|
|
525
|
+
.toString().trim().split('\n').slice(-1)[0]
|
|
526
|
+
console.warn(` Build attempt ${attempt} failed (${firstError}); retrying...`)
|
|
527
|
+
}
|
|
528
|
+
}
|
|
465
529
|
|
|
466
|
-
if (
|
|
530
|
+
if (!binaryExists) {
|
|
467
531
|
const stderr = buildResult.stderr || ''
|
|
468
532
|
throw new Error(
|
|
469
|
-
`Failed to build rpk in Linux container: ${stderr}\n` +
|
|
533
|
+
`Failed to build rpk in Linux container: ${stderr || 'build produced no binary'}\n` +
|
|
470
534
|
'Common causes:\n' +
|
|
471
535
|
' 1. Source code is out of date - run: git pull origin dev\n' +
|
|
472
536
|
' 2. Go module issues - the container will download dependencies automatically\n' +
|
|
@@ -477,15 +541,31 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
477
541
|
|
|
478
542
|
// Step 2: Install plugins
|
|
479
543
|
console.log('Installing plugins for complete command coverage...')
|
|
544
|
+
const failedPlugins = []
|
|
480
545
|
for (const plugin of KNOWN_PLUGINS) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
]
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
546
|
+
// A pin installs a specific version instead of the manifest's latest.
|
|
547
|
+
// This is how pre-GA plugins (no version promoted to latest) get into a
|
|
548
|
+
// full regeneration at all — without a pin their install resolves
|
|
549
|
+
// nothing and their commands are absent from the tree.
|
|
550
|
+
const pin = pluginPins[plugin]
|
|
551
|
+
const versionFlag = PLUGIN_INSTALL_VERSION_FLAGS[plugin]
|
|
552
|
+
const runInstall = (pinned) => {
|
|
553
|
+
const args = ['exec', containerId, '/tmp/rpk', plugin, 'install']
|
|
554
|
+
if (pinned && pin && versionFlag) {
|
|
555
|
+
args.push(versionFlag, pin)
|
|
556
|
+
}
|
|
557
|
+
return spawnSync('docker', args, { encoding: 'utf8', timeout: 120000 })
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
console.log(` Installing plugin: ${plugin}${pin ? ` (pinned to ${pin})` : ''}...`)
|
|
561
|
+
let installResult = runInstall(true)
|
|
562
|
+
if (installResult.status !== 0 && pin) {
|
|
563
|
+
const output = `${installResult.stderr || ''}${installResult.stdout || ''}`
|
|
564
|
+
if (output.includes('unknown flag') || output.includes('is not valid')) {
|
|
565
|
+
console.warn(` rpk rejected the version pin for ${plugin}; retrying without the pin (installs latest)`)
|
|
566
|
+
installResult = runInstall(false)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
489
569
|
|
|
490
570
|
if (installResult.status === 0) {
|
|
491
571
|
console.log(` ✓ ${plugin} installed`)
|
|
@@ -498,13 +578,14 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
498
578
|
console.log(` - ${plugin} is not an installable plugin`)
|
|
499
579
|
} else {
|
|
500
580
|
// 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
|
-
//
|
|
581
|
+
// plugin's commands will be absent from the tree. Expected for
|
|
582
|
+
// beta-only plugins with no version pin — `rpk <plugin> install`
|
|
583
|
+
// finds no `latest` release because the plugin publisher's
|
|
584
|
+
// stableVersionRe only promotes pure X.Y.Z versions, so their
|
|
585
|
+
// commands only appear at GA unless the run pins a version.
|
|
506
586
|
// See redpanda-data/docs#1801.
|
|
507
587
|
console.warn(` ✗ Failed to install ${plugin}: ${stderr || stdout}`)
|
|
588
|
+
failedPlugins.push(plugin)
|
|
508
589
|
}
|
|
509
590
|
}
|
|
510
591
|
}
|
|
@@ -536,8 +617,9 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
536
617
|
)
|
|
537
618
|
}
|
|
538
619
|
|
|
620
|
+
let tree
|
|
539
621
|
try {
|
|
540
|
-
|
|
622
|
+
tree = JSON.parse(result.stdout)
|
|
541
623
|
} catch (err) {
|
|
542
624
|
throw new Error(
|
|
543
625
|
`Failed to parse rpk tree JSON from Linux source build: ${err.message}\n` +
|
|
@@ -545,6 +627,26 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
545
627
|
'This may indicate a version mismatch or corrupted source.'
|
|
546
628
|
)
|
|
547
629
|
}
|
|
630
|
+
|
|
631
|
+
// Step 4: Fill in flags for plugin subtrees. Their commands come from
|
|
632
|
+
// --help-autocomplete, which carries no flag data, so without this every
|
|
633
|
+
// plugin command page renders an empty flags section.
|
|
634
|
+
console.log('Extracting flags from plugin command help output...')
|
|
635
|
+
for (const plugin of KNOWN_PLUGINS) {
|
|
636
|
+
const node = (tree.commands || []).find(c => c.name === plugin)
|
|
637
|
+
if (!node || !pluginNodeHasRealCommands(node)) continue
|
|
638
|
+
const enriched = enrichPluginTreeWithFlags(node, (argPath) => {
|
|
639
|
+
const helpResult = spawnSync('docker', [
|
|
640
|
+
'exec', containerId, '/tmp/rpk', ...argPath, '--help'
|
|
641
|
+
], { encoding: 'utf8', timeout: 30000 })
|
|
642
|
+
return helpResult.status === 0 ? helpResult.stdout : null
|
|
643
|
+
})
|
|
644
|
+
if (enriched > 0) {
|
|
645
|
+
console.log(` ${plugin}: extracted flags for ${enriched} command(s)`)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
return { tree, failedPlugins }
|
|
548
650
|
} finally {
|
|
549
651
|
// Clean up container
|
|
550
652
|
console.log('Cleaning up build container...')
|
|
@@ -555,105 +657,6 @@ function fetchRpkTreeFromLinuxSource(sourcePath) {
|
|
|
555
657
|
}
|
|
556
658
|
}
|
|
557
659
|
|
|
558
|
-
/**
|
|
559
|
-
* Scan Go source files for Linux-only build tags
|
|
560
|
-
* Looks for //go:build linux or // +build linux
|
|
561
|
-
* @param {string} sourcePath - Path to rpk Go source directory
|
|
562
|
-
* @returns {Set<string>} Set of Linux-only command paths (e.g., 'rpk redpanda tune')
|
|
563
|
-
*/
|
|
564
|
-
function detectLinuxOnlyFromSource(sourcePath) {
|
|
565
|
-
const linuxOnlyCommands = new Set()
|
|
566
|
-
|
|
567
|
-
// Map of source directory patterns to command paths
|
|
568
|
-
// rpk commands are typically in pkg/cli/cmd/<command>/<subcommand>/
|
|
569
|
-
const scanDirectory = (dir, prefix = '') => {
|
|
570
|
-
if (!fs.existsSync(dir)) return
|
|
571
|
-
|
|
572
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
573
|
-
|
|
574
|
-
for (const entry of entries) {
|
|
575
|
-
const fullPath = path.join(dir, entry.name)
|
|
576
|
-
|
|
577
|
-
if (entry.isDirectory()) {
|
|
578
|
-
// Recurse into subdirectories
|
|
579
|
-
const newPrefix = prefix ? `${prefix} ${entry.name}` : entry.name
|
|
580
|
-
scanDirectory(fullPath, newPrefix)
|
|
581
|
-
} else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
|
|
582
|
-
// Check Go files for build tags
|
|
583
|
-
try {
|
|
584
|
-
const content = fs.readFileSync(fullPath, 'utf8')
|
|
585
|
-
const firstLines = content.split('\n').slice(0, 20).join('\n')
|
|
586
|
-
|
|
587
|
-
// Check for Linux build constraints
|
|
588
|
-
const hasLinuxTag = /\/\/go:build\s+.*linux/.test(firstLines) ||
|
|
589
|
-
/\/\/\s*\+build\s+.*linux/.test(firstLines)
|
|
590
|
-
|
|
591
|
-
if (hasLinuxTag) {
|
|
592
|
-
// Try to determine command path from file location
|
|
593
|
-
const commandPath = inferCommandPath(sourcePath, fullPath, entry.name)
|
|
594
|
-
if (commandPath) {
|
|
595
|
-
linuxOnlyCommands.add(commandPath)
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
} catch (err) {
|
|
599
|
-
// Skip files we can't read
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Scan the cmd/rpk directory for command implementations
|
|
606
|
-
const cmdDir = path.join(sourcePath, 'pkg', 'cli', 'cmd')
|
|
607
|
-
if (fs.existsSync(cmdDir)) {
|
|
608
|
-
scanDirectory(cmdDir, 'rpk')
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Also check the older structure
|
|
612
|
-
const oldCmdDir = path.join(sourcePath, 'cmd', 'rpk')
|
|
613
|
-
if (fs.existsSync(oldCmdDir)) {
|
|
614
|
-
scanDirectory(oldCmdDir, 'rpk')
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
return linuxOnlyCommands
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Infer command path from Go source file location
|
|
622
|
-
* @param {string} sourcePath - Root source path
|
|
623
|
-
* @param {string} filePath - Full path to Go file
|
|
624
|
-
* @param {string} fileName - Name of the Go file
|
|
625
|
-
* @returns {string|null} Command path or null
|
|
626
|
-
*/
|
|
627
|
-
function inferCommandPath(sourcePath, filePath, fileName) {
|
|
628
|
-
// Get relative path from source root
|
|
629
|
-
const relativePath = filePath.replace(sourcePath, '').replace(/^\//, '')
|
|
630
|
-
|
|
631
|
-
// Common patterns:
|
|
632
|
-
// pkg/cli/cmd/redpanda/tune.go -> rpk redpanda tune
|
|
633
|
-
// pkg/cli/cmd/redpanda/start.go -> rpk redpanda start
|
|
634
|
-
// cmd/rpk/iotune/iotune.go -> rpk iotune
|
|
635
|
-
|
|
636
|
-
// Remove common prefixes and file extension
|
|
637
|
-
let parts = relativePath
|
|
638
|
-
.replace(/^pkg\/cli\/cmd\//, '')
|
|
639
|
-
.replace(/^cmd\/rpk\//, '')
|
|
640
|
-
.replace(/\.go$/, '')
|
|
641
|
-
.split('/')
|
|
642
|
-
|
|
643
|
-
// If file name matches directory name, use directory only
|
|
644
|
-
// e.g., iotune/iotune.go -> iotune
|
|
645
|
-
if (parts.length > 1 && parts[parts.length - 1] === parts[parts.length - 2]) {
|
|
646
|
-
parts = parts.slice(0, -1)
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Filter out common non-command directories
|
|
650
|
-
parts = parts.filter(p => !['internal', 'common', 'config', 'cmd'].includes(p))
|
|
651
|
-
|
|
652
|
-
if (parts.length === 0) return null
|
|
653
|
-
|
|
654
|
-
return 'rpk ' + parts.join(' ')
|
|
655
|
-
}
|
|
656
|
-
|
|
657
660
|
/**
|
|
658
661
|
* Add platform markers to command tree based on source analysis
|
|
659
662
|
* @param {Object} tree - Command tree from rpk --print-tree
|
|
@@ -668,11 +671,18 @@ function addPlatformMarkersFromSource(tree, linuxOnlyCommands) {
|
|
|
668
671
|
[...linuxOnlyCommands].some(loc => cmdPath.startsWith(loc + ' '))
|
|
669
672
|
}
|
|
670
673
|
|
|
674
|
+
// Collect every tree path actually marked Linux-only so the persisted
|
|
675
|
+
// linux_only_commands list is fully expanded (each descendant listed),
|
|
676
|
+
// matching what dynamic Linux-vs-Darwin tree comparison produces.
|
|
677
|
+
const markedPaths = new Set()
|
|
678
|
+
|
|
671
679
|
const markCommands = (commands, parentPath = 'rpk') => {
|
|
672
680
|
if (!commands) return commands
|
|
673
681
|
return commands.map(cmd => {
|
|
674
682
|
const fullPath = `${parentPath} ${cmd.name}`
|
|
675
|
-
const
|
|
683
|
+
const linuxOnly = isLinuxOnly(fullPath)
|
|
684
|
+
if (linuxOnly) markedPaths.add(fullPath)
|
|
685
|
+
const platforms = linuxOnly
|
|
676
686
|
? [PLATFORMS.LINUX]
|
|
677
687
|
: [PLATFORMS.LINUX, PLATFORMS.DARWIN]
|
|
678
688
|
return {
|
|
@@ -691,11 +701,15 @@ function addPlatformMarkersFromSource(tree, linuxOnlyCommands) {
|
|
|
691
701
|
}
|
|
692
702
|
}
|
|
693
703
|
|
|
704
|
+
const markedCommands = markCommands(tree.commands)
|
|
705
|
+
|
|
694
706
|
return {
|
|
695
707
|
...tree,
|
|
696
708
|
platforms: [PLATFORMS.LINUX, PLATFORMS.DARWIN],
|
|
697
|
-
|
|
698
|
-
|
|
709
|
+
// Union of detected paths and marked tree paths: keeps detected roots
|
|
710
|
+
// even when the tree was built on a platform where they don't exist
|
|
711
|
+
linux_only_commands: [...new Set([...linuxOnlyCommands, ...markedPaths])].sort(),
|
|
712
|
+
commands: markedCommands
|
|
699
713
|
}
|
|
700
714
|
}
|
|
701
715
|
|
|
@@ -773,7 +787,58 @@ function loadOverrides(overridesPath, commandTree = null, options = {}) {
|
|
|
773
787
|
)
|
|
774
788
|
}
|
|
775
789
|
|
|
776
|
-
return overrides
|
|
790
|
+
return { overrides, validation }
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Merge deprecation metadata for commands still present in the tree into the
|
|
795
|
+
* overrides file, so their pages render deprecation banners without manual
|
|
796
|
+
* curation. Hidden deprecated commands are excluded: they are absent from
|
|
797
|
+
* --print-tree, have no pages, and would only produce unknown-path warnings.
|
|
798
|
+
* Commands whose overrides already carry a `deprecated` value are left alone.
|
|
799
|
+
* @param {Object} deprecatedCommands - Map from scan-deprecated-commands.js
|
|
800
|
+
* @param {Object} tree - Current command tree
|
|
801
|
+
* @param {string} overridesPath - Path to overrides JSON file
|
|
802
|
+
*/
|
|
803
|
+
function mergeVisibleDeprecationsIntoOverrides(deprecatedCommands, tree, overridesPath) {
|
|
804
|
+
const entries = Object.entries(deprecatedCommands || {})
|
|
805
|
+
if (entries.length === 0 || !overridesPath || !fs.existsSync(overridesPath)) {
|
|
806
|
+
return
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const treePaths = new Set(flattenToMap(tree).keys())
|
|
810
|
+
|
|
811
|
+
let overrides
|
|
812
|
+
try {
|
|
813
|
+
overrides = JSON.parse(fs.readFileSync(overridesPath, 'utf8'))
|
|
814
|
+
} catch (err) {
|
|
815
|
+
console.warn(`Warning: Could not parse overrides file for deprecation merge: ${err.message}`)
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
if (!overrides.commands) {
|
|
819
|
+
overrides.commands = {}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
let annotated = 0
|
|
823
|
+
for (const [cmdPath, info] of entries) {
|
|
824
|
+
if (!treePaths.has(cmdPath)) continue
|
|
825
|
+
const existing = overrides.commands[cmdPath] || {}
|
|
826
|
+
if (existing.deprecated !== undefined) continue
|
|
827
|
+
overrides.commands[cmdPath] = {
|
|
828
|
+
...existing,
|
|
829
|
+
deprecated: true,
|
|
830
|
+
...(info.deprecatedMessage && !existing.deprecatedMessage
|
|
831
|
+
? { deprecatedMessage: info.deprecatedMessage } : {}),
|
|
832
|
+
...(info.replacement && !existing.replacement
|
|
833
|
+
? { replacement: info.replacement } : {})
|
|
834
|
+
}
|
|
835
|
+
annotated++
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (annotated > 0) {
|
|
839
|
+
fs.writeFileSync(overridesPath, JSON.stringify(overrides, null, 2), 'utf8')
|
|
840
|
+
console.log(`Annotated ${annotated} visible deprecated command(s) in ${overridesPath}`)
|
|
841
|
+
}
|
|
777
842
|
}
|
|
778
843
|
|
|
779
844
|
/**
|
|
@@ -824,8 +889,12 @@ function loadVersionedJson(version, dataDir) {
|
|
|
824
889
|
* @param {Object} diffData - Diff data with new commands and flags
|
|
825
890
|
* @param {string} overridesPath - Path to overrides JSON file
|
|
826
891
|
* @param {string} version - Version to set as introducedInVersion
|
|
892
|
+
* @param {Object} [pluginVersions] - Plugin versions keyed by rpk command
|
|
893
|
+
* name. Commands under a plugin subtree are stamped with the plugin's own
|
|
894
|
+
* version (the page note renders "introduced in <plugin> version X"), not
|
|
895
|
+
* the rpk version, because plugins release on their own cadence.
|
|
827
896
|
*/
|
|
828
|
-
function updateOverridesWithIntroducedVersions(diffData, overridesPath, version) {
|
|
897
|
+
function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, pluginVersions = {}) {
|
|
829
898
|
const hasNewCommands = diffData.details.newCommands && diffData.details.newCommands.length > 0
|
|
830
899
|
const hasNewFlags = diffData.details.newFlags && diffData.details.newFlags.length > 0
|
|
831
900
|
|
|
@@ -833,6 +902,12 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version)
|
|
|
833
902
|
return
|
|
834
903
|
}
|
|
835
904
|
|
|
905
|
+
// "rpk connect lint" -> pluginVersions.connect, else the rpk version
|
|
906
|
+
const versionFor = (cmdPath) => {
|
|
907
|
+
const topLevel = cmdPath.split(' ')[1]
|
|
908
|
+
return pluginVersions[topLevel] || version
|
|
909
|
+
}
|
|
910
|
+
|
|
836
911
|
let overrides = {}
|
|
837
912
|
if (fs.existsSync(overridesPath)) {
|
|
838
913
|
try {
|
|
@@ -859,7 +934,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version)
|
|
|
859
934
|
}
|
|
860
935
|
// Only set if not already set (preserve manual overrides)
|
|
861
936
|
if (!overrides.commands[cmdPath].introducedInVersion) {
|
|
862
|
-
overrides.commands[cmdPath].introducedInVersion =
|
|
937
|
+
overrides.commands[cmdPath].introducedInVersion = versionFor(cmdPath)
|
|
863
938
|
commandsUpdated++
|
|
864
939
|
}
|
|
865
940
|
}
|
|
@@ -883,7 +958,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version)
|
|
|
883
958
|
|
|
884
959
|
// Only set if not already set (preserve manual overrides)
|
|
885
960
|
if (!overrides.commands[cmdPath].flags[flagName].introducedInVersion) {
|
|
886
|
-
overrides.commands[cmdPath].flags[flagName].introducedInVersion =
|
|
961
|
+
overrides.commands[cmdPath].flags[flagName].introducedInVersion = versionFor(cmdPath)
|
|
887
962
|
flagsUpdated++
|
|
888
963
|
}
|
|
889
964
|
}
|
|
@@ -970,8 +1045,80 @@ function countCommands(node) {
|
|
|
970
1045
|
* @param {string} whatsNewPath - Path to what's-new.adoc file
|
|
971
1046
|
* @param {string} version - Version string for display
|
|
972
1047
|
*/
|
|
973
|
-
|
|
974
|
-
|
|
1048
|
+
/**
|
|
1049
|
+
* Build a predicate that reports whether a command path renders as a
|
|
1050
|
+
* linkable page (not excluded and not routed to partials by the overrides).
|
|
1051
|
+
* @param {Object|null} overridesData - Loaded overrides
|
|
1052
|
+
* @returns {Function} (commandPath) => boolean
|
|
1053
|
+
*/
|
|
1054
|
+
/**
|
|
1055
|
+
* Build a predicate that reports whether a command path has subcommands,
|
|
1056
|
+
* which determines its page location (groups render into their own dir).
|
|
1057
|
+
* @param {Object} tree - Full command tree
|
|
1058
|
+
* @returns {Function} (commandPath) => boolean
|
|
1059
|
+
*/
|
|
1060
|
+
function makeSubcommandPredicate(tree) {
|
|
1061
|
+
const commandMap = flattenToMap(tree)
|
|
1062
|
+
return (commandPath) => {
|
|
1063
|
+
const node = commandMap.get(commandPath)
|
|
1064
|
+
return Boolean(node && (node.commands || []).length > 0)
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function makeLinkablePredicate(overridesData) {
|
|
1069
|
+
const resolved = overridesData ? resolveReferences(overridesData, overridesData) : null
|
|
1070
|
+
return (commandPath) => {
|
|
1071
|
+
// rpk cloud and rpk security secret render to partials (single-sourced
|
|
1072
|
+
// into cloud docs), so this repo has no linkable pages for them
|
|
1073
|
+
if (commandPath.startsWith('rpk cloud') || commandPath.startsWith('rpk security secret')) {
|
|
1074
|
+
return false
|
|
1075
|
+
}
|
|
1076
|
+
if (!resolved) return true
|
|
1077
|
+
return !shouldExcludeCommand(resolved, commandPath) &&
|
|
1078
|
+
!shouldUsePartialDir(resolved, commandPath)
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Command subtrees whose changes never belong in the Self-Managed What's
|
|
1083
|
+
// new. rpk ai's documentation home is adp-docs, and the ADP release notes
|
|
1084
|
+
// already cover its CLI changes per release. The plugin-release receiver
|
|
1085
|
+
// workflow excludes ai from --update-whats-new for exactly this reason; the
|
|
1086
|
+
// full-regeneration path must agree, or a full run floods the Self-Managed
|
|
1087
|
+
// release notes with rpk ai entries (a rename release alone produces 21 new
|
|
1088
|
+
// plus 21 removed bullets).
|
|
1089
|
+
const WHATS_NEW_EXCLUDED_SUBTREES = ['rpk ai']
|
|
1090
|
+
|
|
1091
|
+
/**
|
|
1092
|
+
* Return a copy of diffData without entries under the excluded subtrees.
|
|
1093
|
+
* Only the published What's-new block filters; diff reports and PR
|
|
1094
|
+
* summaries keep the full picture.
|
|
1095
|
+
* @param {Object} diffData - Diff from generateRpkDiff
|
|
1096
|
+
* @param {string[]} [excluded] - Command-path prefixes to drop
|
|
1097
|
+
* @returns {Object} Filtered copy
|
|
1098
|
+
*/
|
|
1099
|
+
function filterDiffForWhatsNew(diffData, excluded = WHATS_NEW_EXCLUDED_SUBTREES) {
|
|
1100
|
+
const outside = (cmdPath) => !excluded.some(prefix =>
|
|
1101
|
+
cmdPath === prefix || (typeof cmdPath === 'string' && cmdPath.startsWith(prefix + ' ')))
|
|
1102
|
+
const details = diffData.details || {}
|
|
1103
|
+
const filteredDetails = { ...details }
|
|
1104
|
+
for (const key of ['newCommands', 'newlyDeprecatedCommands', 'removedCommands', 'descriptionChanges']) {
|
|
1105
|
+
if (Array.isArray(details[key])) filteredDetails[key] = details[key].filter(e => outside(e.path))
|
|
1106
|
+
}
|
|
1107
|
+
for (const key of ['newFlags', 'removedFlags', 'changedDefaults', 'changedFlagTypes', 'changedFlagRequirements', 'changedFlagDescriptions']) {
|
|
1108
|
+
if (Array.isArray(details[key])) filteredDetails[key] = details[key].filter(e => outside(e.commandPath))
|
|
1109
|
+
}
|
|
1110
|
+
return { ...diffData, details: filteredDetails }
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function updateWhatsNewFile(diffData, whatsNewPath, version, options = {}) {
|
|
1114
|
+
// Each block opens with a "=== <version>" heading so accumulated blocks
|
|
1115
|
+
// (successive RCs, multiple plugin releases) never collide on section ids
|
|
1116
|
+
const sectionHeading = options.sectionHeading || '== Redpanda CLI'
|
|
1117
|
+
const whatsNewContent = generateWhatsNewSection(diffData, {
|
|
1118
|
+
version,
|
|
1119
|
+
blockLabel: version,
|
|
1120
|
+
...options
|
|
1121
|
+
})
|
|
975
1122
|
|
|
976
1123
|
if (!whatsNewContent) {
|
|
977
1124
|
console.log('No Redpanda CLI changes to add to what\'s new')
|
|
@@ -987,9 +1134,39 @@ function updateWhatsNewFile(diffData, whatsNewPath, version) {
|
|
|
987
1134
|
|
|
988
1135
|
const existingContent = fs.readFileSync(whatsNewPath, 'utf8')
|
|
989
1136
|
|
|
990
|
-
//
|
|
991
|
-
|
|
992
|
-
|
|
1137
|
+
// Version-scoped marker block: re-runs for the same version replace their
|
|
1138
|
+
// own block, and later versions (for example, successive RCs in a beta
|
|
1139
|
+
// cycle, or plugin releases) append their own blocks inside the existing
|
|
1140
|
+
// Redpanda CLI section instead of being dropped. Writers can edit or
|
|
1141
|
+
// remove blocks freely; the automation only ever touches content between
|
|
1142
|
+
// its own markers for the same version label.
|
|
1143
|
+
const startMarker = `// AUTOGEN-RPK-CHANGES ${version} START`
|
|
1144
|
+
const endMarker = `// AUTOGEN-RPK-CHANGES ${version} END`
|
|
1145
|
+
const sectionBody = whatsNewContent.replace(/^== [^\n]*\n+/, '')
|
|
1146
|
+
const block = `${startMarker}\n${sectionBody.trimEnd()}\n${endMarker}`
|
|
1147
|
+
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
1148
|
+
|
|
1149
|
+
if (existingContent.includes(startMarker)) {
|
|
1150
|
+
// Replace this version's existing block (idempotent re-runs)
|
|
1151
|
+
const blockRe = new RegExp(`${escapeRe(startMarker)}[\\s\\S]*?${escapeRe(endMarker)}`)
|
|
1152
|
+
const updatedContent = existingContent.replace(blockRe, block)
|
|
1153
|
+
fs.writeFileSync(whatsNewPath, updatedContent, 'utf8')
|
|
1154
|
+
console.log(`Refreshed existing ${version} block in what's-new file: ${whatsNewPath}`)
|
|
1155
|
+
return
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const headingMatch = existingContent.match(new RegExp(`^${escapeRe(sectionHeading)}[^\\n]*$`, 'm'))
|
|
1159
|
+
if (headingMatch) {
|
|
1160
|
+
// Append this version's block at the end of the existing section, just
|
|
1161
|
+
// before the next level-2 heading (or end of file)
|
|
1162
|
+
const sectionStart = headingMatch.index + headingMatch[0].length
|
|
1163
|
+
const rest = existingContent.slice(sectionStart)
|
|
1164
|
+
const nextHeading = rest.search(/\n== /)
|
|
1165
|
+
const insertAt = nextHeading === -1 ? existingContent.length : sectionStart + nextHeading
|
|
1166
|
+
const before = existingContent.slice(0, insertAt).replace(/\s*$/, '\n\n')
|
|
1167
|
+
const after = existingContent.slice(insertAt).replace(/^\n*/, '\n')
|
|
1168
|
+
fs.writeFileSync(whatsNewPath, `${before}${block}${after}`, 'utf8')
|
|
1169
|
+
console.log(`Appended ${version} block to the "${sectionHeading}" section in: ${whatsNewPath}`)
|
|
993
1170
|
return
|
|
994
1171
|
}
|
|
995
1172
|
|
|
@@ -1011,19 +1188,583 @@ function updateWhatsNewFile(diffData, whatsNewPath, version) {
|
|
|
1011
1188
|
}
|
|
1012
1189
|
}
|
|
1013
1190
|
|
|
1191
|
+
const fullSection = `${sectionHeading}\n\n${block}\n`
|
|
1192
|
+
|
|
1014
1193
|
let updatedContent
|
|
1015
1194
|
if (insertIndex > 0) {
|
|
1016
1195
|
// Insert before the matched section
|
|
1017
1196
|
updatedContent = existingContent.slice(0, insertIndex) +
|
|
1018
|
-
|
|
1197
|
+
fullSection + '\n' +
|
|
1019
1198
|
existingContent.slice(insertIndex)
|
|
1020
1199
|
} else {
|
|
1021
1200
|
// Append at the end
|
|
1022
|
-
updatedContent = existingContent
|
|
1201
|
+
updatedContent = existingContent.replace(/\s*$/, '\n\n') + fullSection
|
|
1023
1202
|
}
|
|
1024
1203
|
|
|
1025
1204
|
fs.writeFileSync(whatsNewPath, updatedContent, 'utf8')
|
|
1026
|
-
console.log(`
|
|
1205
|
+
console.log(`Created "${sectionHeading}" section in what's-new file: ${whatsNewPath}`)
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/**
|
|
1209
|
+
* Build an rpk binary natively from Go source.
|
|
1210
|
+
* @param {string} sourcePath - Path to rpk Go source directory (src/go/rpk)
|
|
1211
|
+
* @param {string} outPath - Where to write the binary
|
|
1212
|
+
* @returns {string} Path to the built binary
|
|
1213
|
+
*/
|
|
1214
|
+
function buildRpkBinary(sourcePath, outPath) {
|
|
1215
|
+
const goCheck = spawnSync('go', ['version'], { encoding: 'utf8', timeout: 5000 })
|
|
1216
|
+
if (goCheck.status !== 0) {
|
|
1217
|
+
throw new Error(
|
|
1218
|
+
'Go is required to build rpk from source but was not found.\n' +
|
|
1219
|
+
'Install Go from https://go.dev/ and ensure it\'s in your PATH.'
|
|
1220
|
+
)
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
const installedGoVersion = parseGoVersion(goCheck.stdout)
|
|
1224
|
+
const requiredGoVersion = getRequiredGoVersion(sourcePath)
|
|
1225
|
+
if (installedGoVersion && requiredGoVersion &&
|
|
1226
|
+
!checkGoVersionSufficient(installedGoVersion, requiredGoVersion)) {
|
|
1227
|
+
throw new Error(
|
|
1228
|
+
`Go version mismatch: installed ${installedGoVersion}, required >= ${requiredGoVersion}\n` +
|
|
1229
|
+
`The rpk source (go.mod) requires Go ${requiredGoVersion} or newer.`
|
|
1230
|
+
)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
console.log(`Building rpk from source at ${sourcePath}...`)
|
|
1234
|
+
const buildResult = spawnSync('go', ['build', '-o', outPath, './cmd/rpk'], {
|
|
1235
|
+
cwd: sourcePath,
|
|
1236
|
+
encoding: 'utf8',
|
|
1237
|
+
timeout: 300000
|
|
1238
|
+
})
|
|
1239
|
+
|
|
1240
|
+
if (buildResult.status !== 0) {
|
|
1241
|
+
throw new Error(`Failed to build rpk from source: ${buildResult.stderr}`)
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
return outPath
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/**
|
|
1248
|
+
* Download an official rpk release binary for the current platform.
|
|
1249
|
+
* @param {string} tag - Release tag (e.g., v26.1.12)
|
|
1250
|
+
* @param {string} destDir - Directory to download and extract into
|
|
1251
|
+
* @returns {string|null} Path to the extracted binary, or null if the
|
|
1252
|
+
* release asset is unavailable (caller falls back to a source build)
|
|
1253
|
+
*/
|
|
1254
|
+
function downloadRpkRelease(tag, destDir) {
|
|
1255
|
+
const osName = { darwin: 'darwin', linux: 'linux', win32: 'windows' }[process.platform]
|
|
1256
|
+
const archName = { arm64: 'arm64', x64: 'amd64' }[process.arch]
|
|
1257
|
+
if (!osName || !archName) {
|
|
1258
|
+
console.warn(`No rpk release asset for platform ${process.platform}/${process.arch}`)
|
|
1259
|
+
return null
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const assetName = `rpk-${osName}-${archName}.zip`
|
|
1263
|
+
const baseUrl = `https://github.com/redpanda-data/redpanda/releases/download/${tag}`
|
|
1264
|
+
const zipPath = path.join(destDir, assetName)
|
|
1265
|
+
|
|
1266
|
+
console.log(`Downloading ${assetName} for ${tag}...`)
|
|
1267
|
+
const curlResult = spawnSync('curl', [
|
|
1268
|
+
'-fL', '--retry', '5', '--retry-all-errors',
|
|
1269
|
+
'--connect-timeout', '30', '--max-time', '300',
|
|
1270
|
+
'-o', zipPath, `${baseUrl}/${assetName}`
|
|
1271
|
+
], { encoding: 'utf8', timeout: 360000 })
|
|
1272
|
+
|
|
1273
|
+
if (curlResult.status !== 0) {
|
|
1274
|
+
console.warn(`Could not download rpk release for ${tag} (draft or missing release asset)`)
|
|
1275
|
+
return null
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Verify against the release checksum file when it exists
|
|
1279
|
+
const checksumAsset = `rpk_${tag.replace(/^v/, '')}_checksums.txt`
|
|
1280
|
+
const checksumPath = path.join(destDir, checksumAsset)
|
|
1281
|
+
const checksumResult = spawnSync('curl', [
|
|
1282
|
+
'-fsSL', '--retry', '3', '--connect-timeout', '30', '--max-time', '60',
|
|
1283
|
+
'-o', checksumPath, `${baseUrl}/${checksumAsset}`
|
|
1284
|
+
], { encoding: 'utf8', timeout: 90000 })
|
|
1285
|
+
|
|
1286
|
+
if (checksumResult.status === 0) {
|
|
1287
|
+
const expectedLine = fs.readFileSync(checksumPath, 'utf8')
|
|
1288
|
+
.split('\n')
|
|
1289
|
+
.find(line => line.trim().endsWith(assetName))
|
|
1290
|
+
if (expectedLine) {
|
|
1291
|
+
const expected = expectedLine.trim().split(/\s+/)[0]
|
|
1292
|
+
const actual = crypto.createHash('sha256').update(fs.readFileSync(zipPath)).digest('hex')
|
|
1293
|
+
if (expected !== actual) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`Checksum mismatch for ${assetName} (${tag}):\n` +
|
|
1296
|
+
` expected ${expected}\n actual ${actual}`
|
|
1297
|
+
)
|
|
1298
|
+
}
|
|
1299
|
+
console.log('Checksum verified')
|
|
1300
|
+
}
|
|
1301
|
+
} else {
|
|
1302
|
+
console.warn('No checksum file published for this release; skipping verification')
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
const unzipResult = spawnSync('unzip', ['-o', zipPath, '-d', destDir], {
|
|
1306
|
+
encoding: 'utf8',
|
|
1307
|
+
timeout: 60000
|
|
1308
|
+
})
|
|
1309
|
+
if (unzipResult.status !== 0) {
|
|
1310
|
+
throw new Error(`Failed to extract ${assetName}: ${unzipResult.stderr}`)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const binPath = path.join(destDir, 'rpk')
|
|
1314
|
+
if (!fs.existsSync(binPath)) {
|
|
1315
|
+
throw new Error(`Extracted archive did not contain an rpk binary: ${zipPath}`)
|
|
1316
|
+
}
|
|
1317
|
+
fs.chmodSync(binPath, 0o755)
|
|
1318
|
+
return binPath
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Get an rpk binary matching the given version.
|
|
1323
|
+
* Prefers the official release download (published stable releases only;
|
|
1324
|
+
* RC releases are drafts, so their assets are not publicly downloadable).
|
|
1325
|
+
* Falls back to building from source at the tag.
|
|
1326
|
+
* @param {string} rpkVersion - Version tag from the snapshot (e.g., v26.1.12, v26.2.1-rc2)
|
|
1327
|
+
* @param {Object} [options]
|
|
1328
|
+
* @param {string} [options.rpkBin] - Existing binary to use, skipping download/build
|
|
1329
|
+
* @returns {string} Path to an rpk binary
|
|
1330
|
+
*/
|
|
1331
|
+
function acquireRpkBinary(rpkVersion, options = {}) {
|
|
1332
|
+
const { rpkBin } = options
|
|
1333
|
+
|
|
1334
|
+
if (rpkBin) {
|
|
1335
|
+
const binPath = path.resolve(rpkBin)
|
|
1336
|
+
if (!fs.existsSync(binPath)) {
|
|
1337
|
+
throw new Error(`rpk binary not found: ${binPath}`)
|
|
1338
|
+
}
|
|
1339
|
+
console.log(`Using provided rpk binary: ${binPath}`)
|
|
1340
|
+
return binPath
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const tag = rpkVersion.startsWith('v') ? rpkVersion : `v${rpkVersion}`
|
|
1344
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpk-bin-'))
|
|
1345
|
+
|
|
1346
|
+
// Only published (non-draft) releases have downloadable assets
|
|
1347
|
+
if (/^v\d+\.\d+\.\d+$/.test(tag)) {
|
|
1348
|
+
const downloaded = downloadRpkRelease(tag, workDir)
|
|
1349
|
+
if (downloaded) {
|
|
1350
|
+
return downloaded
|
|
1351
|
+
}
|
|
1352
|
+
console.log('Falling back to building rpk from source...')
|
|
1353
|
+
} else {
|
|
1354
|
+
console.log(`No published release binary for ${tag}; building from source...`)
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (!/^v\d+\.\d+\.\d+(-rc\d+)?$/.test(tag) && tag !== 'vdev') {
|
|
1358
|
+
throw new Error(
|
|
1359
|
+
`Cannot acquire an rpk binary for version '${rpkVersion}'.\n` +
|
|
1360
|
+
'The snapshot\'s rpk_version is not a release tag. ' +
|
|
1361
|
+
'Pass --rpk-bin <path> to use a local rpk binary instead.'
|
|
1362
|
+
)
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const sourceRef = tag === 'vdev' ? 'dev' : tag
|
|
1366
|
+
const sourcePath = prepareSourceFromRef(sourceRef, null)
|
|
1367
|
+
try {
|
|
1368
|
+
return buildRpkBinary(sourcePath, path.join(workDir, 'rpk'))
|
|
1369
|
+
} catch (nativeErr) {
|
|
1370
|
+
// Local Go older than go.mod requires is common on laptops; the --ref
|
|
1371
|
+
// path already tolerates this by building in a container, so do the same
|
|
1372
|
+
// here when Docker is available
|
|
1373
|
+
const dockerCheck = spawnSync('docker', ['info'], { encoding: 'utf8', timeout: 10000 })
|
|
1374
|
+
if (dockerCheck.status !== 0) {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`${nativeErr.message}\n` +
|
|
1377
|
+
'Docker is not available for a container build either. ' +
|
|
1378
|
+
'Update Go, start Docker, or pass --rpk-bin <path> to use an existing rpk binary.'
|
|
1379
|
+
)
|
|
1380
|
+
}
|
|
1381
|
+
console.warn(`Native build failed (${nativeErr.message.split('\n')[0]}); building in a container...`)
|
|
1382
|
+
const goVersion = getRequiredGoVersion(sourcePath)
|
|
1383
|
+
const goImage = goVersion ? `golang:${goVersion}` : 'golang:1'
|
|
1384
|
+
// Cross-compile for the HOST platform: the container reports
|
|
1385
|
+
// GOOS=linux, and a linux binary dies silently when executed on the
|
|
1386
|
+
// macOS host that needs it for plugin installs (review finding on the
|
|
1387
|
+
// 5.3.0 train). rpk builds with CGO disabled, so cross-compilation
|
|
1388
|
+
// from the linux container is safe.
|
|
1389
|
+
// Only macOS and Linux hosts are supported: mapping anything else to
|
|
1390
|
+
// linux would hand a Windows host an unrunnable binary, the exact bug
|
|
1391
|
+
// class the cross-compile fixes (review finding on #238).
|
|
1392
|
+
if (process.platform !== 'darwin' && process.platform !== 'linux') {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`${nativeErr.message}\n` +
|
|
1395
|
+
`Container fallback does not support host platform "${process.platform}". ` +
|
|
1396
|
+
'Update Go, or pass --rpk-bin <path> to use an existing rpk binary.'
|
|
1397
|
+
)
|
|
1398
|
+
}
|
|
1399
|
+
const hostGoos = process.platform === 'darwin' ? 'darwin' : 'linux'
|
|
1400
|
+
const hostGoarch = process.arch === 'arm64' ? 'arm64' : 'amd64'
|
|
1401
|
+
const buildResult = spawnSync('docker', [
|
|
1402
|
+
'run', '--rm',
|
|
1403
|
+
'-e', `GOOS=${hostGoos}`,
|
|
1404
|
+
'-e', `GOARCH=${hostGoarch}`,
|
|
1405
|
+
'-e', 'CGO_ENABLED=0',
|
|
1406
|
+
'-v', `${path.resolve(sourcePath)}:/rpk-source:ro`,
|
|
1407
|
+
'-v', `${workDir}:/out`,
|
|
1408
|
+
'-w', '/rpk-source',
|
|
1409
|
+
goImage,
|
|
1410
|
+
'go', 'build', '-o', '/out/rpk', './cmd/rpk'
|
|
1411
|
+
], { encoding: 'utf8', timeout: 600000 })
|
|
1412
|
+
const binPath = path.join(workDir, 'rpk')
|
|
1413
|
+
if (buildResult.status !== 0 || !fs.existsSync(binPath)) {
|
|
1414
|
+
throw new Error(`Container build failed: ${buildResult.stderr || 'no binary produced'}`)
|
|
1415
|
+
}
|
|
1416
|
+
return binPath
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* Look up the latest published version of a plugin from its manifest.
|
|
1422
|
+
* @param {string} plugin - rpk command name (ai, connect, k8s, check)
|
|
1423
|
+
* @returns {string|null} Latest version, or null if unavailable
|
|
1424
|
+
*/
|
|
1425
|
+
function fetchLatestPluginVersion(plugin) {
|
|
1426
|
+
const slug = PLUGIN_MANIFEST_SLUGS[plugin] || plugin
|
|
1427
|
+
const result = spawnSync('curl', [
|
|
1428
|
+
'-fsSL', '--retry', '3', '--connect-timeout', '15', '--max-time', '30',
|
|
1429
|
+
`${PLUGIN_MANIFEST_HOST}/${slug}/manifest.json`
|
|
1430
|
+
], { encoding: 'utf8', timeout: 60000 })
|
|
1431
|
+
|
|
1432
|
+
if (result.status !== 0) {
|
|
1433
|
+
console.warn(`Could not fetch plugin manifest for ${plugin} (slug: ${slug})`)
|
|
1434
|
+
return null
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
try {
|
|
1438
|
+
const manifest = JSON.parse(result.stdout)
|
|
1439
|
+
const latest = (manifest.archives || []).find(a => a.is_latest)
|
|
1440
|
+
return latest ? latest.version : null
|
|
1441
|
+
} catch (err) {
|
|
1442
|
+
console.warn(`Could not parse plugin manifest for ${plugin}: ${err.message}`)
|
|
1443
|
+
return null
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Check whether a plugin node contains real plugin commands, as opposed to
|
|
1449
|
+
* only the install/uninstall/upgrade shim compiled into rpk itself.
|
|
1450
|
+
* @param {Object} node - Top-level command node for the plugin
|
|
1451
|
+
* @returns {boolean}
|
|
1452
|
+
*/
|
|
1453
|
+
function pluginNodeHasRealCommands(node) {
|
|
1454
|
+
return (node.commands || []).some(c => !PLUGIN_SHIM_SUBCOMMANDS.has(c.name))
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* Replace a plugin's top-level node in a command tree.
|
|
1459
|
+
* Replace-only: throws if the plugin is not already present, so a refresh
|
|
1460
|
+
* can never introduce a command that the snapshot's rpk version lacks.
|
|
1461
|
+
* @param {Object} tree - Full command tree (root node)
|
|
1462
|
+
* @param {string} plugin - rpk command name
|
|
1463
|
+
* @param {Object} freshNode - Replacement node
|
|
1464
|
+
* @returns {Object} New tree with the node replaced
|
|
1465
|
+
*/
|
|
1466
|
+
function splicePluginNode(tree, plugin, freshNode) {
|
|
1467
|
+
const commands = tree.commands || []
|
|
1468
|
+
if (!commands.some(c => c.name === plugin)) {
|
|
1469
|
+
throw new Error(`Cannot splice plugin '${plugin}': not present in the base tree`)
|
|
1470
|
+
}
|
|
1471
|
+
return {
|
|
1472
|
+
...tree,
|
|
1473
|
+
commands: commands.map(c => (c.name === plugin ? freshNode : c))
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
/**
|
|
1478
|
+
* Carry a snapshot's recorded Linux-only command list onto a working tree.
|
|
1479
|
+
* The list comes from scanning Go build tags in the rpk source, which
|
|
1480
|
+
* from-json runs (including --plugin refreshes) never see, so it can only be
|
|
1481
|
+
* inherited from the existing snapshot. Without it the refreshed snapshot
|
|
1482
|
+
* would lose its platform markers for every future from-json run.
|
|
1483
|
+
* @param {Object} tree - Working command tree (root node)
|
|
1484
|
+
* @param {Object} [fallbackTree] - Tree to inherit linux_only_commands from
|
|
1485
|
+
* when the working tree does not carry the field itself
|
|
1486
|
+
* @returns {Object} Tree that carries linux_only_commands when available
|
|
1487
|
+
*/
|
|
1488
|
+
function preserveLinuxOnlyCommands(tree, fallbackTree) {
|
|
1489
|
+
if (!tree || tree.linux_only_commands) return tree
|
|
1490
|
+
const inherited = fallbackTree && fallbackTree.linux_only_commands
|
|
1491
|
+
if (!inherited) return tree
|
|
1492
|
+
return { ...tree, linux_only_commands: [...inherited] }
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
/**
|
|
1496
|
+
* Parse the local "Flags:" section of cobra --help output into flag objects
|
|
1497
|
+
* matching the shape rpk --print-tree emits for compiled-in commands.
|
|
1498
|
+
* The "Global Flags:" section is ignored: rpk-core globals are documented by
|
|
1499
|
+
* the shared global-flags table, and plugin-global flags are captured from
|
|
1500
|
+
* the plugin root command's own Flags section.
|
|
1501
|
+
* @param {string} helpText - Output of `rpk <command> --help`
|
|
1502
|
+
* @returns {Array<Object>} Flags: { name, shorthand?, type, description, default? }
|
|
1503
|
+
*/
|
|
1504
|
+
function parseCobraFlags(helpText) {
|
|
1505
|
+
const lines = (helpText || '').split('\n')
|
|
1506
|
+
const start = lines.findIndex(l => /^Flags:\s*$/.test(l))
|
|
1507
|
+
if (start === -1) return []
|
|
1508
|
+
|
|
1509
|
+
const flags = []
|
|
1510
|
+
let flagIndent = null
|
|
1511
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1512
|
+
const line = lines[i]
|
|
1513
|
+
if (/^\S/.test(line)) break // next section (Global Flags:, Use "...", ...)
|
|
1514
|
+
if (line.trim() === '') break
|
|
1515
|
+
|
|
1516
|
+
const m = line.match(/^\s+(?:-(\w),\s+)?--([\w.-]+)(?:\s+(\S+))?\s{2,}(.*)$/)
|
|
1517
|
+
if (m) {
|
|
1518
|
+
const [, shorthand, name, valueType, desc] = m
|
|
1519
|
+
if (name === 'help') continue
|
|
1520
|
+
flagIndent = line.search(/\S/)
|
|
1521
|
+
const flag = {
|
|
1522
|
+
name,
|
|
1523
|
+
type: valueType || 'bool',
|
|
1524
|
+
description: desc.trim()
|
|
1525
|
+
}
|
|
1526
|
+
if (shorthand) flag.shorthand = shorthand
|
|
1527
|
+
const def = flag.description.match(/\s*\(default (.+)\)$/)
|
|
1528
|
+
if (def) {
|
|
1529
|
+
flag.default = def[1]
|
|
1530
|
+
flag.description = flag.description.slice(0, def.index).trim()
|
|
1531
|
+
}
|
|
1532
|
+
flags.push(flag)
|
|
1533
|
+
} else if (flags.length > 0 && flagIndent !== null && line.search(/\S/) > flagIndent) {
|
|
1534
|
+
// Wrapped description continuation: any line indented deeper than the
|
|
1535
|
+
// flag column that did not parse as a flag. Continuations can begin
|
|
1536
|
+
// with flag-looking tokens ("(alias: --x-ref)" wraps to "--x-ref)"),
|
|
1537
|
+
// so no dash guard — a genuine flag line always matches the regex
|
|
1538
|
+
// above first.
|
|
1539
|
+
const last = flags[flags.length - 1]
|
|
1540
|
+
last.description = `${last.description} ${line.trim()}`.trim()
|
|
1541
|
+
const def = last.description.match(/\s*\(default (.+)\)$/)
|
|
1542
|
+
if (def) {
|
|
1543
|
+
last.default = def[1]
|
|
1544
|
+
last.description = last.description.slice(0, def.index).trim()
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
return flags
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/**
|
|
1552
|
+
* Parse the OPTIONS section of urfave/cli --help output (Redpanda Connect)
|
|
1553
|
+
* into the same flag shape as parseCobraFlags. Entries look like:
|
|
1554
|
+
* --log.level value override the configured log level
|
|
1555
|
+
* --set value, -s value [ --set value, -s value ] set a field ...
|
|
1556
|
+
* --chilled continue ... (default: false)
|
|
1557
|
+
* The GLOBAL OPTIONS section is skipped, matching the cobra handling.
|
|
1558
|
+
* @param {string} helpText - Output of `rpk <command> --help`
|
|
1559
|
+
* @returns {Array<Object>} Flags: { name, shorthand?, type, description, default? }
|
|
1560
|
+
*/
|
|
1561
|
+
function parseUrfaveFlags(helpText) {
|
|
1562
|
+
const lines = (helpText || '').split('\n')
|
|
1563
|
+
const start = lines.findIndex(l => /^OPTIONS:\s*$/.test(l))
|
|
1564
|
+
if (start === -1) return []
|
|
1565
|
+
|
|
1566
|
+
const flags = []
|
|
1567
|
+
let urfaveIndent = null
|
|
1568
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1569
|
+
const line = lines[i]
|
|
1570
|
+
if (/^\S/.test(line)) break // next section (GLOBAL OPTIONS:, ...)
|
|
1571
|
+
if (line.trim() === '') break
|
|
1572
|
+
|
|
1573
|
+
const m = line.match(/^\s{2,}(--\S[^\s]*(?:[^ ]| (?!\s))*)\s{2,}(.*)$/)
|
|
1574
|
+
if (m) {
|
|
1575
|
+
const [, spec, desc] = m
|
|
1576
|
+
const nameMatch = spec.match(/^--([\w.-]+)/)
|
|
1577
|
+
if (!nameMatch) continue
|
|
1578
|
+
const name = nameMatch[1]
|
|
1579
|
+
if (name === 'help') continue
|
|
1580
|
+
urfaveIndent = line.search(/\S/)
|
|
1581
|
+
|
|
1582
|
+
const flag = { name, description: desc.trim() }
|
|
1583
|
+
const shorthandMatch = spec.match(/,\s+-(\w)\b/)
|
|
1584
|
+
if (shorthandMatch) flag.shorthand = shorthandMatch[1]
|
|
1585
|
+
|
|
1586
|
+
const repeatable = spec.includes('[ --')
|
|
1587
|
+
const hasValue = new RegExp(`^--${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s+\\S`).test(spec)
|
|
1588
|
+
flag.type = repeatable ? 'strings' : (hasValue ? 'string' : 'bool')
|
|
1589
|
+
|
|
1590
|
+
const def = flag.description.match(/\s*\(default:\s*(.+?)\)$/)
|
|
1591
|
+
if (def) {
|
|
1592
|
+
flag.default = def[1]
|
|
1593
|
+
flag.description = flag.description.slice(0, def.index).trim()
|
|
1594
|
+
}
|
|
1595
|
+
flags.push(flag)
|
|
1596
|
+
} else if (flags.length > 0 && urfaveIndent !== null && line.search(/\S/) > urfaveIndent) {
|
|
1597
|
+
// Continuation lines may begin with flag-looking tokens; see the
|
|
1598
|
+
// cobra parser above for why there is no dash guard
|
|
1599
|
+
const last = flags[flags.length - 1]
|
|
1600
|
+
last.description = `${last.description} ${line.trim()}`.trim()
|
|
1601
|
+
const def = last.description.match(/\s*\(default:\s*(.+?)\)$/)
|
|
1602
|
+
if (def) {
|
|
1603
|
+
last.default = def[1]
|
|
1604
|
+
last.description = last.description.slice(0, def.index).trim()
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
return flags
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
/**
|
|
1612
|
+
* Parse flags from --help output regardless of CLI framework: cobra prints a
|
|
1613
|
+
* "Flags:" section (rpk ai, k8s, check), urfave/cli prints "OPTIONS:"
|
|
1614
|
+
* (Redpanda Connect).
|
|
1615
|
+
* @param {string} helpText
|
|
1616
|
+
* @returns {Array<Object>}
|
|
1617
|
+
*/
|
|
1618
|
+
function parseHelpFlags(helpText) {
|
|
1619
|
+
if (/^Flags:\s*$/m.test(helpText || '')) return parseCobraFlags(helpText)
|
|
1620
|
+
if (/^OPTIONS:\s*$/m.test(helpText || '')) return parseUrfaveFlags(helpText)
|
|
1621
|
+
return []
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
/**
|
|
1625
|
+
* Fill in flags for plugin subtree commands by running `--help` per command.
|
|
1626
|
+
* Plugin subcommand nodes come from the plugin's --help-autocomplete output,
|
|
1627
|
+
* which carries no flag information, so without this every plugin command
|
|
1628
|
+
* page renders with an empty flags section. Nodes that already have flags
|
|
1629
|
+
* (the install/uninstall/upgrade shim compiled into rpk) are left alone.
|
|
1630
|
+
* Help failures are non-fatal: the affected command keeps an empty list.
|
|
1631
|
+
* @param {Object} node - The plugin's top-level command node (mutated)
|
|
1632
|
+
* @param {Function} execHelp - (argPath: string[]) => string|null, returns
|
|
1633
|
+
* the help text for `rpk <argPath...> --help`
|
|
1634
|
+
* @returns {number} Number of commands enriched
|
|
1635
|
+
*/
|
|
1636
|
+
function enrichPluginTreeWithFlags(node, execHelp) {
|
|
1637
|
+
let enriched = 0
|
|
1638
|
+
const walk = (cmd, argPath) => {
|
|
1639
|
+
if (!cmd.flags || cmd.flags.length === 0) {
|
|
1640
|
+
const helpText = execHelp(argPath)
|
|
1641
|
+
if (helpText) {
|
|
1642
|
+
const flags = parseHelpFlags(helpText)
|
|
1643
|
+
if (flags.length > 0) {
|
|
1644
|
+
cmd.flags = flags
|
|
1645
|
+
enriched++
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
for (const child of cmd.commands || []) {
|
|
1650
|
+
walk(child, [...argPath, child.name])
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
walk(node, [node.name])
|
|
1654
|
+
return enriched
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Install a single plugin and capture its fresh command subtree.
|
|
1659
|
+
* Runs with an isolated HOME so the install never touches the caller's
|
|
1660
|
+
* rpk plugin state, and the caller's installed plugins never leak in.
|
|
1661
|
+
* @param {Object} params
|
|
1662
|
+
* @param {string} params.plugin - rpk command name (ai, connect, k8s, check)
|
|
1663
|
+
* @param {string} [params.pluginVersion] - Version to pin; defaults to latest
|
|
1664
|
+
* @param {string} params.rpkBinPath - rpk binary to run
|
|
1665
|
+
* @returns {Object} { node, version } - Fresh plugin node and the version recorded
|
|
1666
|
+
*/
|
|
1667
|
+
function fetchPluginSubtree({ plugin, pluginVersion, rpkBinPath }) {
|
|
1668
|
+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rpk-plugin-home-'))
|
|
1669
|
+
const env = { ...process.env, HOME: tmpHome }
|
|
1670
|
+
|
|
1671
|
+
const runInstall = (pinned) => {
|
|
1672
|
+
const args = [plugin, 'install']
|
|
1673
|
+
const versionFlag = PLUGIN_INSTALL_VERSION_FLAGS[plugin]
|
|
1674
|
+
if (pinned && pluginVersion && versionFlag) {
|
|
1675
|
+
args.push(versionFlag, pluginVersion)
|
|
1676
|
+
}
|
|
1677
|
+
console.log(`Installing plugin: rpk ${args.join(' ')}`)
|
|
1678
|
+
return spawnSync(rpkBinPath, args, { encoding: 'utf8', env, timeout: 300000 })
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
let pinApplied = Boolean(pluginVersion)
|
|
1682
|
+
let installResult = runInstall(true)
|
|
1683
|
+
if (installResult.status !== 0 && pluginVersion) {
|
|
1684
|
+
const output = `${installResult.stderr || ''}${installResult.stdout || ''}`
|
|
1685
|
+
// 'unknown flag': this rpk predates the pin flag.
|
|
1686
|
+
// 'is not valid': rpk's version validation rejected the pin (its regex
|
|
1687
|
+
// caps each segment at two digits, so connect versions like 4.102.0
|
|
1688
|
+
// fail). In both cases installing latest is the right recovery: the
|
|
1689
|
+
// dispatch fires right after a release, when latest IS the release.
|
|
1690
|
+
if (output.includes('unknown flag') || output.includes('is not valid')) {
|
|
1691
|
+
console.warn(
|
|
1692
|
+
`rpk rejected the version pin for '${plugin}' ` +
|
|
1693
|
+
`(${output.trim().split('\n')[0]}); retrying without the pin (installs latest)`
|
|
1694
|
+
)
|
|
1695
|
+
pinApplied = false
|
|
1696
|
+
installResult = runInstall(false)
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (installResult.status !== 0) {
|
|
1700
|
+
throw new Error(
|
|
1701
|
+
`Failed to install plugin '${plugin}':\n` +
|
|
1702
|
+
`${installResult.stderr || installResult.stdout || 'no output'}`
|
|
1703
|
+
)
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const treeResult = spawnSync(rpkBinPath, ['--print-tree'], {
|
|
1707
|
+
encoding: 'utf8',
|
|
1708
|
+
env,
|
|
1709
|
+
timeout: 120000,
|
|
1710
|
+
maxBuffer: 50 * 1024 * 1024
|
|
1711
|
+
})
|
|
1712
|
+
if (treeResult.status !== 0) {
|
|
1713
|
+
const stderr = treeResult.stderr || ''
|
|
1714
|
+
if (stderr.includes('unknown flag')) {
|
|
1715
|
+
throw new Error(
|
|
1716
|
+
'This rpk binary does not support --print-tree (requires rpk >= v26.2.0).\n' +
|
|
1717
|
+
'Pass --rpk-bin with a newer binary.'
|
|
1718
|
+
)
|
|
1719
|
+
}
|
|
1720
|
+
throw new Error(`Failed to run rpk --print-tree: ${stderr}`)
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
const freshTree = JSON.parse(treeResult.stdout)
|
|
1724
|
+
const node = (freshTree.commands || []).find(c => c.name === plugin)
|
|
1725
|
+
if (!node) {
|
|
1726
|
+
throw new Error(`Plugin '${plugin}' not found in rpk command tree after install`)
|
|
1727
|
+
}
|
|
1728
|
+
if (!pluginNodeHasRealCommands(node)) {
|
|
1729
|
+
throw new Error(
|
|
1730
|
+
`Plugin '${plugin}' installed but exposed no commands beyond the ` +
|
|
1731
|
+
'install/uninstall/upgrade shim.\n' +
|
|
1732
|
+
'The install likely resolved nothing. For pre-GA plugins (no promoted ' +
|
|
1733
|
+
'latest version in the manifest), a version pin is required: ' +
|
|
1734
|
+
`--plugin-version <version>`
|
|
1735
|
+
)
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Fill in per-command flags: autocomplete trees carry none, but the plugin
|
|
1739
|
+
// binary is installed, so each command's --help documents them
|
|
1740
|
+
console.log(`Extracting flags from ${plugin} command help output...`)
|
|
1741
|
+
const enriched = enrichPluginTreeWithFlags(node, (argPath) => {
|
|
1742
|
+
const helpResult = spawnSync(rpkBinPath, [...argPath, '--help'], {
|
|
1743
|
+
encoding: 'utf8',
|
|
1744
|
+
env,
|
|
1745
|
+
timeout: 30000
|
|
1746
|
+
})
|
|
1747
|
+
return helpResult.status === 0 ? helpResult.stdout : null
|
|
1748
|
+
})
|
|
1749
|
+
console.log(` Extracted flags for ${enriched} command(s)`)
|
|
1750
|
+
|
|
1751
|
+
// When the pin was applied, record it. When we fell back to latest (or no
|
|
1752
|
+
// pin was given), record what the manifest says latest is: that is what
|
|
1753
|
+
// actually installed.
|
|
1754
|
+
const version = pinApplied ? pluginVersion : fetchLatestPluginVersion(plugin)
|
|
1755
|
+
if (!version) {
|
|
1756
|
+
console.warn(
|
|
1757
|
+
`Could not determine the installed version of '${plugin}'; ` +
|
|
1758
|
+
'plugin_versions will not be updated for this run'
|
|
1759
|
+
)
|
|
1760
|
+
} else if (pluginVersion && version !== pluginVersion) {
|
|
1761
|
+
console.warn(
|
|
1762
|
+
`Requested ${plugin} ${pluginVersion} but installed latest (${version}) ` +
|
|
1763
|
+
'because rpk rejected the version pin'
|
|
1764
|
+
)
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
return { node, version }
|
|
1027
1768
|
}
|
|
1028
1769
|
|
|
1029
1770
|
/**
|
|
@@ -1042,6 +1783,10 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1042
1783
|
overrides: overridesPath,
|
|
1043
1784
|
fromSource, // Path to local rpk Go source directory
|
|
1044
1785
|
fromJson, // Path to existing versioned JSON file to regenerate from
|
|
1786
|
+
plugin, // Refresh a single plugin's subtree (requires fromJson)
|
|
1787
|
+
pluginVersion, // Version to pin for the plugin install (defaults to latest)
|
|
1788
|
+
pluginPins = {}, // Per-plugin version pins for full generation (pre-GA plugins)
|
|
1789
|
+
rpkBin, // Existing rpk binary to use for the plugin refresh
|
|
1045
1790
|
ref, // Git ref (branch or tag) to document
|
|
1046
1791
|
sourceRef, // Alias for ref
|
|
1047
1792
|
diff: diffVersion,
|
|
@@ -1058,6 +1803,15 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1058
1803
|
// Normalize ref/sourceRef
|
|
1059
1804
|
const effectiveRef = ref || sourceRef
|
|
1060
1805
|
|
|
1806
|
+
for (const pinnedPlugin of Object.keys(pluginPins)) {
|
|
1807
|
+
if (!REFRESHABLE_PLUGINS.includes(pinnedPlugin)) {
|
|
1808
|
+
console.warn(
|
|
1809
|
+
`Warning: --plugin-pin for unknown plugin '${pinnedPlugin}' ` +
|
|
1810
|
+
`(known plugins: ${REFRESHABLE_PLUGINS.join(', ')}); the pin will have no effect`
|
|
1811
|
+
)
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1061
1815
|
const repoRoot = findRepoRoot()
|
|
1062
1816
|
const dataDir = customDataDir || path.join(repoRoot, 'docs-data')
|
|
1063
1817
|
const defaultOutputDir = path.join(repoRoot, 'modules', 'reference', 'pages', 'rpk')
|
|
@@ -1094,6 +1848,10 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1094
1848
|
let tree
|
|
1095
1849
|
let rpkVersion
|
|
1096
1850
|
let sourcePath
|
|
1851
|
+
// Managed plugins whose install failed this run. Passed to generateRpkDocs
|
|
1852
|
+
// as explicit protectedPlugins (merged there with auto-detection) so a
|
|
1853
|
+
// failed install never deletes or rewrites that plugin's existing docs.
|
|
1854
|
+
let failedPlugins = []
|
|
1097
1855
|
|
|
1098
1856
|
try {
|
|
1099
1857
|
// Fast path: regenerate from existing JSON file
|
|
@@ -1105,6 +1863,10 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1105
1863
|
console.log(`Loading command tree from ${jsonPath}`)
|
|
1106
1864
|
const jsonData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'))
|
|
1107
1865
|
tree = jsonData.raw_tree || jsonData.tree
|
|
1866
|
+
// Inherit the Linux-only command list from whichever stored tree
|
|
1867
|
+
// carries it: it cannot be re-detected without the rpk source, and
|
|
1868
|
+
// both rendering and the re-saved snapshot below depend on it.
|
|
1869
|
+
tree = preserveLinuxOnlyCommands(tree, jsonData.tree || jsonData.raw_tree)
|
|
1108
1870
|
rpkVersion = jsonData.rpk_version || 'local'
|
|
1109
1871
|
|
|
1110
1872
|
if (!tree) {
|
|
@@ -1113,16 +1875,158 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1113
1875
|
|
|
1114
1876
|
console.log(`Loaded tree with rpk version: ${rpkVersion}`)
|
|
1115
1877
|
|
|
1878
|
+
// Refresh a single plugin's subtree in the snapshot before rendering.
|
|
1879
|
+
// Plugins release on their own cadence, so their commands can change
|
|
1880
|
+
// without a new rpk release. Splice the fresh subtree into the full
|
|
1881
|
+
// tree and re-render everything: only the plugin's pages actually
|
|
1882
|
+
// change, and stale-file cleanup, nav rebuild, and override
|
|
1883
|
+
// validation all see a complete tree.
|
|
1884
|
+
let pluginVersions = jsonData.plugin_versions || {}
|
|
1885
|
+
let pluginDiffData = null
|
|
1886
|
+
if (plugin) {
|
|
1887
|
+
if (!REFRESHABLE_PLUGINS.includes(plugin)) {
|
|
1888
|
+
throw new Error(
|
|
1889
|
+
`Unknown plugin '${plugin}'. Supported plugins: ${REFRESHABLE_PLUGINS.join(', ')}\n` +
|
|
1890
|
+
'Use the rpk command name (for example "ai"), not the manifest slug ("rpai").'
|
|
1891
|
+
)
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const oldNode = (tree.commands || []).find(c => c.name === plugin)
|
|
1895
|
+
if (!oldNode) {
|
|
1896
|
+
console.log(
|
|
1897
|
+
`rpk ${rpkVersion} has no '${plugin}' command; nothing to update. ` +
|
|
1898
|
+
'This is expected when the plugin only exists in a newer rpk line.'
|
|
1899
|
+
)
|
|
1900
|
+
return {
|
|
1901
|
+
success: true,
|
|
1902
|
+
skipped: true,
|
|
1903
|
+
reason: `plugin '${plugin}' not present in rpk ${rpkVersion}`,
|
|
1904
|
+
rpkVersion
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const rpkBinPath = acquireRpkBinary(rpkVersion, { rpkBin })
|
|
1909
|
+
const { node: rawNode, version: resolvedVersion } = fetchPluginSubtree({
|
|
1910
|
+
plugin,
|
|
1911
|
+
pluginVersion,
|
|
1912
|
+
rpkBinPath
|
|
1913
|
+
})
|
|
1914
|
+
|
|
1915
|
+
// Reapply platform markers: the fresh subtree comes from a plain
|
|
1916
|
+
// --print-tree run, but every command in the snapshot's tree carries
|
|
1917
|
+
// a platforms field. Reuse the snapshot's recorded Linux-only list.
|
|
1918
|
+
const linuxOnly = new Set(tree.linux_only_commands || [])
|
|
1919
|
+
const freshNode = addPlatformMarkersFromSource(
|
|
1920
|
+
{ commands: [rawNode] },
|
|
1921
|
+
linuxOnly
|
|
1922
|
+
).commands[0]
|
|
1923
|
+
|
|
1924
|
+
// Plugin-scoped diff for the PR summary: compare only the plugin's
|
|
1925
|
+
// subtree, old snapshot state vs fresh install
|
|
1926
|
+
pluginDiffData = generateRpkDiff(
|
|
1927
|
+
{ ...tree, commands: [oldNode] },
|
|
1928
|
+
{ ...tree, commands: [freshNode] },
|
|
1929
|
+
{
|
|
1930
|
+
oldVersion: pluginVersions[plugin] || 'previous snapshot',
|
|
1931
|
+
newVersion: resolvedVersion || 'latest'
|
|
1932
|
+
}
|
|
1933
|
+
)
|
|
1934
|
+
printDiffReport(pluginDiffData)
|
|
1935
|
+
|
|
1936
|
+
tree = splicePluginNode(tree, plugin, freshNode)
|
|
1937
|
+
if (resolvedVersion) {
|
|
1938
|
+
pluginVersions = { ...pluginVersions, [plugin]: resolvedVersion }
|
|
1939
|
+
}
|
|
1940
|
+
console.log(
|
|
1941
|
+
`Spliced fresh '${plugin}' subtree` +
|
|
1942
|
+
(resolvedVersion ? ` (version ${resolvedVersion})` : '') +
|
|
1943
|
+
` into snapshot for rpk ${rpkVersion}`
|
|
1944
|
+
)
|
|
1945
|
+
|
|
1946
|
+
// Stamp new plugin commands and flags with the plugin's own version
|
|
1947
|
+
// so pages render "This command was introduced in <plugin> version X".
|
|
1948
|
+
// Runs before the overrides load below, so the stamps apply to this
|
|
1949
|
+
// run's rendering, not just the next one.
|
|
1950
|
+
if (resolvedVersion) {
|
|
1951
|
+
updateOverridesWithIntroducedVersions(
|
|
1952
|
+
pluginDiffData,
|
|
1953
|
+
overridesPath || path.join(dataDir, 'rpk-overrides.json'),
|
|
1954
|
+
resolvedVersion,
|
|
1955
|
+
{ [plugin]: resolvedVersion }
|
|
1956
|
+
)
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
if (diffVersion) {
|
|
1960
|
+
console.warn('Note: --diff is ignored in --plugin mode (the summary uses a plugin-scoped diff)')
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1963
|
+
// Plugin releases land in What's new too when requested: their
|
|
1964
|
+
// changes never arrive through a Redpanda release diff. Entries
|
|
1965
|
+
// render without xrefs because plugin subtrees may render as
|
|
1966
|
+
// partials with no linkable pages.
|
|
1967
|
+
if (whatsNewPath) {
|
|
1968
|
+
if (WHATS_NEW_EXCLUDED_SUBTREES.includes(`rpk ${plugin}`)) {
|
|
1969
|
+
console.log(`Skipping What's new for rpk ${plugin}: its documentation home covers CLI changes in its own release notes`)
|
|
1970
|
+
} else {
|
|
1971
|
+
const label = resolvedVersion
|
|
1972
|
+
? `${plugin} plugin ${resolvedVersion}`
|
|
1973
|
+
: `${plugin} plugin`
|
|
1974
|
+
updateWhatsNewFile(pluginDiffData, whatsNewPath, label, { xrefs: false, sectionHeading: '== rpk plugins' })
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1116
1979
|
// Skip to documentation generation (after the source building steps)
|
|
1117
1980
|
// Load and validate overrides
|
|
1118
1981
|
const defaultOverridesPath = path.join(dataDir, 'rpk-overrides.json')
|
|
1119
1982
|
const effectiveOverridesPath = overridesPath || defaultOverridesPath
|
|
1120
|
-
|
|
1983
|
+
|
|
1984
|
+
// Stamp new commands and flags with introducedInVersion BEFORE the
|
|
1985
|
+
// overrides load so this run's pages render the labels. Without this,
|
|
1986
|
+
// a --from-json --diff run emits unlabeled pages and the labels only
|
|
1987
|
+
// appear on the next regeneration (the from-source path stamps before
|
|
1988
|
+
// rendering already).
|
|
1989
|
+
if (diffVersion && !plugin) {
|
|
1990
|
+
const oldDataForStamp = loadVersionedJson(diffVersion, dataDir)
|
|
1991
|
+
if (oldDataForStamp) {
|
|
1992
|
+
const stampDiff = generateRpkDiff(oldDataForStamp.raw_tree || oldDataForStamp.tree, tree, {
|
|
1993
|
+
oldVersion: diffVersion,
|
|
1994
|
+
newVersion: rpkVersion,
|
|
1995
|
+
oldDeprecatedCommands: oldDataForStamp.deprecated_commands || {},
|
|
1996
|
+
newDeprecatedCommands: jsonData.deprecated_commands || {}
|
|
1997
|
+
})
|
|
1998
|
+
if (stampDiff.details.newCommands.length > 0 || stampDiff.details.newFlags.length > 0) {
|
|
1999
|
+
updateOverridesWithIntroducedVersions(stampDiff, effectiveOverridesPath, rpkVersion, pluginVersions)
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const { overrides: overridesData, validation: overrideValidation } = loadOverrides(effectiveOverridesPath, tree, { strict: false }) || {}
|
|
1121
2005
|
|
|
1122
2006
|
if (overridesData) {
|
|
1123
2007
|
console.log(`Loaded overrides from ${effectiveOverridesPath}`)
|
|
1124
2008
|
}
|
|
1125
2009
|
|
|
2010
|
+
// Persist the merged snapshot so the refreshed plugin subtree and its
|
|
2011
|
+
// recorded version become the new baseline (mirrors the from-source
|
|
2012
|
+
// path's augmentedData structure)
|
|
2013
|
+
if (plugin) {
|
|
2014
|
+
// The Linux-only list can't be re-detected during a plugin refresh
|
|
2015
|
+
// (it comes from Go build-tag scanning of the rpk source), so make
|
|
2016
|
+
// sure the saved snapshot's trees still carry it before persisting.
|
|
2017
|
+
tree = preserveLinuxOnlyCommands(tree, jsonData.raw_tree || jsonData.tree)
|
|
2018
|
+
let enhancedTree = tree
|
|
2019
|
+
if (overridesData) {
|
|
2020
|
+
const resolvedOverrides = resolveReferences(overridesData, overridesData)
|
|
2021
|
+
enhancedTree = applyOverridesToTree(tree, resolvedOverrides, '')
|
|
2022
|
+
}
|
|
2023
|
+
jsonData.raw_tree = tree
|
|
2024
|
+
jsonData.tree = enhancedTree
|
|
2025
|
+
jsonData.plugin_versions = pluginVersions
|
|
2026
|
+
jsonData.generated_at = new Date().toISOString()
|
|
2027
|
+
saveVersionedJson(jsonData, rpkVersion, dataDir)
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1126
2030
|
// Generate AsciiDoc documentation
|
|
1127
2031
|
console.log(`\nGenerating AsciiDoc files to ${finalOutputDir}...`)
|
|
1128
2032
|
|
|
@@ -1132,10 +2036,11 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1132
2036
|
outputDir: finalOutputDir,
|
|
1133
2037
|
cloudSecretDir: finalCloudSecretDir,
|
|
1134
2038
|
rpkVersion,
|
|
1135
|
-
pluginVersions
|
|
2039
|
+
pluginVersions,
|
|
1136
2040
|
draftMissing,
|
|
1137
2041
|
preservationsDir: preserveFrom,
|
|
1138
|
-
navFile
|
|
2042
|
+
navFile,
|
|
2043
|
+
protectedPlugins: failedPlugins
|
|
1139
2044
|
})
|
|
1140
2045
|
|
|
1141
2046
|
console.log(`\nGeneration complete!`)
|
|
@@ -1154,16 +2059,19 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1154
2059
|
showInfo
|
|
1155
2060
|
})
|
|
1156
2061
|
|
|
1157
|
-
// Generate diff if requested
|
|
2062
|
+
// Generate diff if requested (rpk-version diff; not used in --plugin
|
|
2063
|
+
// mode, which produces its own plugin-scoped diff)
|
|
1158
2064
|
let diffData = null
|
|
1159
|
-
if (diffVersion) {
|
|
2065
|
+
if (diffVersion && !plugin) {
|
|
1160
2066
|
const oldData = loadVersionedJson(diffVersion, dataDir)
|
|
1161
2067
|
if (oldData) {
|
|
1162
2068
|
// Use raw_tree for diffing (falls back to tree for backward compatibility)
|
|
1163
2069
|
const oldTree = oldData.raw_tree || oldData.tree
|
|
1164
2070
|
diffData = generateRpkDiff(oldTree, tree, {
|
|
1165
2071
|
oldVersion: diffVersion,
|
|
1166
|
-
newVersion: rpkVersion
|
|
2072
|
+
newVersion: rpkVersion,
|
|
2073
|
+
oldDeprecatedCommands: oldData.deprecated_commands || {},
|
|
2074
|
+
newDeprecatedCommands: jsonData.deprecated_commands || {}
|
|
1167
2075
|
})
|
|
1168
2076
|
|
|
1169
2077
|
// Save diff
|
|
@@ -1177,7 +2085,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1177
2085
|
|
|
1178
2086
|
// Update what's-new file if requested
|
|
1179
2087
|
if (whatsNewPath) {
|
|
1180
|
-
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion)
|
|
2088
|
+
updateWhatsNewFile(filterDiffForWhatsNew(diffData), whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
1181
2089
|
}
|
|
1182
2090
|
} else {
|
|
1183
2091
|
console.warn(`Warning: Could not load previous version ${diffVersion} for diff`)
|
|
@@ -1187,11 +2095,15 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1187
2095
|
// Generate PR summary
|
|
1188
2096
|
const prSummary = generatePRSummary({
|
|
1189
2097
|
rpkVersion,
|
|
2098
|
+
plugin,
|
|
2099
|
+
pluginVersion: plugin ? pluginVersions[plugin] : undefined,
|
|
1190
2100
|
commandCount: result.commandCount,
|
|
1191
2101
|
filesGenerated: result.filesGenerated,
|
|
1192
2102
|
filesSkipped: result.filesSkipped,
|
|
1193
|
-
diffData,
|
|
2103
|
+
diffData: plugin ? pluginDiffData : diffData,
|
|
1194
2104
|
validationResult,
|
|
2105
|
+
overrideValidation,
|
|
2106
|
+
descriptionCoverage: computeDescriptionCoverage(tree, overridesData),
|
|
1195
2107
|
outputDir: finalOutputDir
|
|
1196
2108
|
})
|
|
1197
2109
|
|
|
@@ -1211,12 +2123,21 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1211
2123
|
filesSkipped: result.filesSkipped,
|
|
1212
2124
|
outputDir: finalOutputDir,
|
|
1213
2125
|
rpkVersion,
|
|
1214
|
-
|
|
2126
|
+
plugin,
|
|
2127
|
+
pluginVersion: plugin ? pluginVersions[plugin] : undefined,
|
|
2128
|
+
diffData: plugin ? pluginDiffData : diffData,
|
|
1215
2129
|
validationResult,
|
|
1216
2130
|
prSummary
|
|
1217
2131
|
}
|
|
1218
2132
|
}
|
|
1219
2133
|
|
|
2134
|
+
if (plugin) {
|
|
2135
|
+
throw new Error(
|
|
2136
|
+
'--plugin requires --from-json <snapshot>.\n' +
|
|
2137
|
+
'A plugin refresh splices the fresh subtree into an existing committed snapshot.'
|
|
2138
|
+
)
|
|
2139
|
+
}
|
|
2140
|
+
|
|
1220
2141
|
// Step 1: Get source code
|
|
1221
2142
|
if (effectiveRef) {
|
|
1222
2143
|
// Clone/checkout from ref
|
|
@@ -1264,10 +2185,17 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1264
2185
|
rpkVersion = effectiveRef || 'local'
|
|
1265
2186
|
|
|
1266
2187
|
// Step 2: Detect Linux-only commands
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
2188
|
+
// Static detection (build-constraint analysis) works on any platform,
|
|
2189
|
+
// including Linux CI runners. Dynamic detection (comparing Linux vs
|
|
2190
|
+
// Darwin builds) supplements it below when the host can run both.
|
|
1269
2191
|
console.log('\nAnalyzing source for Linux-only commands...')
|
|
1270
2192
|
let linuxOnlyCommands = detectLinuxOnlyFromSource(sourcePath)
|
|
2193
|
+
if (linuxOnlyCommands.size > 0) {
|
|
2194
|
+
console.log(`Static detection found ${linuxOnlyCommands.size} Linux-only command path(s):`)
|
|
2195
|
+
for (const cmd of [...linuxOnlyCommands].sort()) {
|
|
2196
|
+
console.log(` - ${cmd}`)
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
1271
2199
|
|
|
1272
2200
|
// Step 3: Build rpk and get command tree
|
|
1273
2201
|
// Use Docker if available (for running rpk plugins in Linux)
|
|
@@ -1277,33 +2205,56 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1277
2205
|
const canBuildNative = goCheck.status === 0
|
|
1278
2206
|
const canBuildLinux = dockerCheck.status === 0
|
|
1279
2207
|
|
|
1280
|
-
// Dynamic detection: if we can build on both platforms, compare the trees
|
|
1281
|
-
|
|
2208
|
+
// Dynamic detection: if we can build on both platforms, compare the trees.
|
|
2209
|
+
// Only possible on non-Linux hosts: a Linux runner can cross-compile a
|
|
2210
|
+
// darwin rpk but cannot execute it to get its command tree, so Linux CI
|
|
2211
|
+
// relies on the static detection above.
|
|
2212
|
+
// RPK_DOCS_SKIP_DYNAMIC_DETECTION=1 forces the static-only path (useful
|
|
2213
|
+
// for testing what a Linux CI runner would produce).
|
|
2214
|
+
const skipDynamicDetection = ['1', 'true'].includes(
|
|
2215
|
+
String(process.env.RPK_DOCS_SKIP_DYNAMIC_DETECTION || '').toLowerCase()
|
|
2216
|
+
)
|
|
2217
|
+
if (skipDynamicDetection) {
|
|
2218
|
+
console.log('\nDynamic platform detection disabled via RPK_DOCS_SKIP_DYNAMIC_DETECTION')
|
|
2219
|
+
}
|
|
2220
|
+
if (canBuildLinux && canBuildNative && os.platform() !== 'linux' && !skipDynamicDetection) {
|
|
1282
2221
|
console.log('\nBuilding rpk on both Linux and Darwin for dynamic platform detection...')
|
|
1283
2222
|
|
|
1284
2223
|
try {
|
|
1285
2224
|
// Build on Linux (in container) - has all commands
|
|
1286
2225
|
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
|
-
|
|
2226
|
+
const { tree: linuxTree, failedPlugins: linuxFailedPlugins } = fetchRpkTreeFromLinuxSource(sourcePath, pluginPins)
|
|
2227
|
+
|
|
2228
|
+
// Build natively (on Darwin) for comparison. The Linux tree is
|
|
2229
|
+
// authoritative, so a comparison-build failure (for example, local
|
|
2230
|
+
// Go older than go.mod requires) only skips dynamic platform
|
|
2231
|
+
// detection - it must not discard the Linux tree.
|
|
2232
|
+
let darwinTree = null
|
|
2233
|
+
try {
|
|
2234
|
+
console.log('Building rpk natively for comparison...')
|
|
2235
|
+
darwinTree = fetchRpkTreeFromSource(sourcePath)
|
|
2236
|
+
} catch (nativeErr) {
|
|
2237
|
+
console.warn(`⚠ Native comparison build failed: ${nativeErr.message}`)
|
|
2238
|
+
console.log('Skipping dynamic platform detection; using source scanning only.')
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
if (darwinTree) {
|
|
2242
|
+
// Compare trees to find Linux-only commands
|
|
2243
|
+
const dynamicLinuxOnly = detectLinuxOnlyByComparison(linuxTree, darwinTree)
|
|
2244
|
+
if (dynamicLinuxOnly.size > 0) {
|
|
2245
|
+
console.log(`Dynamic detection found ${dynamicLinuxOnly.size} Linux-only command(s):`)
|
|
2246
|
+
for (const cmd of dynamicLinuxOnly) {
|
|
2247
|
+
console.log(` - ${cmd}`)
|
|
2248
|
+
linuxOnlyCommands.add(cmd)
|
|
2249
|
+
}
|
|
1300
2250
|
}
|
|
1301
2251
|
}
|
|
1302
2252
|
|
|
1303
2253
|
// Use the Linux tree (has all commands)
|
|
1304
2254
|
tree = linuxTree
|
|
2255
|
+
failedPlugins = linuxFailedPlugins
|
|
1305
2256
|
} catch (dockerErr) {
|
|
1306
|
-
// Docker build failed
|
|
2257
|
+
// Docker build failed - fall back to native build
|
|
1307
2258
|
console.warn(`\n⚠ Docker build failed: ${dockerErr.message}`)
|
|
1308
2259
|
console.log('Falling back to native Go build...')
|
|
1309
2260
|
console.log('Note: Linux-only commands will be detected via source scanning only.\n')
|
|
@@ -1312,7 +2263,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1312
2263
|
} else if (canBuildLinux) {
|
|
1313
2264
|
console.log('\nBuilding rpk in Linux container...')
|
|
1314
2265
|
try {
|
|
1315
|
-
tree = fetchRpkTreeFromLinuxSource(sourcePath)
|
|
2266
|
+
({ tree, failedPlugins } = fetchRpkTreeFromLinuxSource(sourcePath, pluginPins))
|
|
1316
2267
|
} catch (dockerErr) {
|
|
1317
2268
|
if (canBuildNative) {
|
|
1318
2269
|
console.warn(`\n⚠ Docker build failed: ${dockerErr.message}`)
|
|
@@ -1332,6 +2283,12 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1332
2283
|
)
|
|
1333
2284
|
}
|
|
1334
2285
|
|
|
2286
|
+
// Tripwire: an empty combined detection result while the source tree
|
|
2287
|
+
// demonstrably contains Linux-gated files means the scan is broken and
|
|
2288
|
+
// Linux-only pages are about to be published as cross-platform
|
|
2289
|
+
// (see redpanda-data/docs#1831).
|
|
2290
|
+
warnIfDetectionLooksBroken(sourcePath, linuxOnlyCommands)
|
|
2291
|
+
|
|
1335
2292
|
// Step 4: Add platform markers based on detection
|
|
1336
2293
|
// This includes both source scanning results and static fallback lists
|
|
1337
2294
|
tree = addPlatformMarkersFromSource(tree, linuxOnlyCommands)
|
|
@@ -1343,8 +2300,23 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1343
2300
|
const plugins = detectPlugins(tree)
|
|
1344
2301
|
console.log(`Detected plugins: ${plugins.join(', ') || 'none'}`)
|
|
1345
2302
|
|
|
1346
|
-
//
|
|
2303
|
+
// Record the version of each plugin that actually installed during the
|
|
2304
|
+
// build (`rpk <plugin> install` resolves the manifest's latest, so the
|
|
2305
|
+
// manifest lookup reflects what the tree contains). Shim-only plugins
|
|
2306
|
+
// (failed installs) are skipped: their subtree carries no plugin commands.
|
|
1347
2307
|
const pluginVersions = {}
|
|
2308
|
+
for (const pluginName of REFRESHABLE_PLUGINS) {
|
|
2309
|
+
const node = (tree.commands || []).find(c => c.name === pluginName)
|
|
2310
|
+
if (node && pluginNodeHasRealCommands(node)) {
|
|
2311
|
+
const installedVersion = pluginPins[pluginName] || fetchLatestPluginVersion(pluginName)
|
|
2312
|
+
if (installedVersion) {
|
|
2313
|
+
pluginVersions[pluginName] = installedVersion
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
if (Object.keys(pluginVersions).length > 0) {
|
|
2318
|
+
console.log(`Plugin versions: ${JSON.stringify(pluginVersions)}`)
|
|
2319
|
+
}
|
|
1348
2320
|
|
|
1349
2321
|
// Step 5: Scan source for deprecated/hidden commands
|
|
1350
2322
|
let deprecatedCommands = {}
|
|
@@ -1365,7 +2337,13 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1365
2337
|
// Load and validate overrides
|
|
1366
2338
|
const defaultOverridesPath = path.join(dataDir, 'rpk-overrides.json')
|
|
1367
2339
|
const effectiveOverridesPath = overridesPath || defaultOverridesPath
|
|
1368
|
-
|
|
2340
|
+
|
|
2341
|
+
// Annotate deprecated commands that are still visible in the tree so
|
|
2342
|
+
// their pages render deprecation banners without manual curation. Runs
|
|
2343
|
+
// before the overrides load so the annotations apply to this run.
|
|
2344
|
+
mergeVisibleDeprecationsIntoOverrides(deprecatedCommands, tree, effectiveOverridesPath)
|
|
2345
|
+
|
|
2346
|
+
const { overrides: overridesData, validation: overrideValidation } = loadOverrides(effectiveOverridesPath, tree, { strict: false }) || {}
|
|
1369
2347
|
|
|
1370
2348
|
if (overridesData) {
|
|
1371
2349
|
console.log(`Loaded overrides from ${effectiveOverridesPath}`)
|
|
@@ -1403,7 +2381,9 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1403
2381
|
const oldTree = oldData.raw_tree || oldData.tree
|
|
1404
2382
|
diffData = generateRpkDiff(oldTree, tree, {
|
|
1405
2383
|
oldVersion: diffVersion,
|
|
1406
|
-
newVersion: rpkVersion
|
|
2384
|
+
newVersion: rpkVersion,
|
|
2385
|
+
oldDeprecatedCommands: oldData.deprecated_commands || {},
|
|
2386
|
+
newDeprecatedCommands: deprecatedCommands
|
|
1407
2387
|
})
|
|
1408
2388
|
|
|
1409
2389
|
// Save diff
|
|
@@ -1415,14 +2395,17 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1415
2395
|
// Print diff report
|
|
1416
2396
|
printDiffReport(diffData)
|
|
1417
2397
|
|
|
1418
|
-
// Update overrides with introducedInVersion for new commands
|
|
1419
|
-
|
|
1420
|
-
|
|
2398
|
+
// Update overrides with introducedInVersion for new commands. Plugin
|
|
2399
|
+
// commands get the plugin's own version (plugins release on their own
|
|
2400
|
+
// cadence, so the rpk version would be wrong and the page note would
|
|
2401
|
+
// render "introduced in <plugin> version <rpk version>").
|
|
2402
|
+
if ((diffData.details.newCommands.length > 0 || diffData.details.newFlags.length > 0) && effectiveOverridesPath) {
|
|
2403
|
+
updateOverridesWithIntroducedVersions(diffData, effectiveOverridesPath, rpkVersion, pluginVersions)
|
|
1421
2404
|
}
|
|
1422
2405
|
|
|
1423
2406
|
// Update what's-new file if requested
|
|
1424
2407
|
if (whatsNewPath) {
|
|
1425
|
-
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion)
|
|
2408
|
+
updateWhatsNewFile(filterDiffForWhatsNew(diffData), whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
1426
2409
|
}
|
|
1427
2410
|
} else {
|
|
1428
2411
|
console.warn(`Warning: Could not load previous version ${diffVersion} for diff`)
|
|
@@ -1441,7 +2424,8 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1441
2424
|
pluginVersions,
|
|
1442
2425
|
draftMissing,
|
|
1443
2426
|
preservationsDir: preserveFrom,
|
|
1444
|
-
navFile
|
|
2427
|
+
navFile,
|
|
2428
|
+
protectedPlugins: failedPlugins
|
|
1445
2429
|
})
|
|
1446
2430
|
|
|
1447
2431
|
console.log(`\nGeneration complete!`)
|
|
@@ -1463,11 +2447,13 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1463
2447
|
// Generate PR summary
|
|
1464
2448
|
const prSummary = generatePRSummary({
|
|
1465
2449
|
rpkVersion,
|
|
2450
|
+
overrideValidation,
|
|
1466
2451
|
commandCount: result.commandCount,
|
|
1467
2452
|
filesGenerated: result.filesGenerated,
|
|
1468
2453
|
filesSkipped: result.filesSkipped,
|
|
1469
2454
|
diffData,
|
|
1470
2455
|
validationResult,
|
|
2456
|
+
descriptionCoverage: computeDescriptionCoverage(tree, overridesData),
|
|
1471
2457
|
outputDir: finalOutputDir
|
|
1472
2458
|
})
|
|
1473
2459
|
|
|
@@ -1506,26 +2492,75 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
1506
2492
|
* @param {Object} options - Summary options
|
|
1507
2493
|
* @returns {string} Markdown formatted summary
|
|
1508
2494
|
*/
|
|
2495
|
+
/**
|
|
2496
|
+
* Find description overrides that replace substantially longer source help.
|
|
2497
|
+
* Curation is legitimate, but silent replacement is how stale or wrong
|
|
2498
|
+
* content survives regeneration, so every regen PR lists what is hidden.
|
|
2499
|
+
* @param {Object} tree - Raw command tree
|
|
2500
|
+
* @param {Object} overridesData - Parsed overrides
|
|
2501
|
+
* @returns {Array<{commandPath: string, overrideChars: number, sourceChars: number}>}
|
|
2502
|
+
*/
|
|
2503
|
+
function computeDescriptionCoverage(tree, overridesData) {
|
|
2504
|
+
if (!tree || !overridesData?.commands) return []
|
|
2505
|
+
const commandMap = flattenToMap(tree)
|
|
2506
|
+
const results = []
|
|
2507
|
+
for (const [cmdPath, override] of Object.entries(overridesData.commands)) {
|
|
2508
|
+
if (typeof override?.description !== 'string') continue
|
|
2509
|
+
const cmd = commandMap.get(cmdPath)
|
|
2510
|
+
if (!cmd || typeof cmd.description !== 'string') continue
|
|
2511
|
+
const sourceChars = cmd.description.trim().length
|
|
2512
|
+
// appendToDescription content still reaches the page, so count it
|
|
2513
|
+
const appended = typeof override.appendToDescription === 'string' ? override.appendToDescription.trim().length : 0
|
|
2514
|
+
const overrideChars = override.description.trim().length + appended
|
|
2515
|
+
if (sourceChars - overrideChars > 500) {
|
|
2516
|
+
results.push({ commandPath: cmdPath, overrideChars, sourceChars })
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
return results.sort((a, b) => (b.sourceChars - b.overrideChars) - (a.sourceChars - a.overrideChars))
|
|
2520
|
+
}
|
|
2521
|
+
|
|
1509
2522
|
function generatePRSummary(options) {
|
|
1510
2523
|
const {
|
|
1511
2524
|
rpkVersion,
|
|
2525
|
+
plugin,
|
|
2526
|
+
pluginVersion,
|
|
1512
2527
|
commandCount,
|
|
1513
2528
|
filesGenerated,
|
|
1514
2529
|
filesSkipped = 0,
|
|
1515
2530
|
diffData,
|
|
1516
2531
|
validationResult,
|
|
2532
|
+
overrideValidation,
|
|
2533
|
+
descriptionCoverage,
|
|
1517
2534
|
outputDir
|
|
1518
2535
|
} = options
|
|
1519
2536
|
|
|
1520
2537
|
const lines = []
|
|
1521
2538
|
|
|
1522
2539
|
// Header
|
|
1523
|
-
|
|
1524
|
-
|
|
2540
|
+
if (plugin) {
|
|
2541
|
+
lines.push(`## rpk ${plugin} Plugin Documentation Update`)
|
|
2542
|
+
lines.push('')
|
|
2543
|
+
if (pluginVersion) {
|
|
2544
|
+
lines.push(`**Plugin version:** ${pluginVersion}`)
|
|
2545
|
+
}
|
|
2546
|
+
lines.push(`**Base rpk snapshot:** ${rpkVersion}`)
|
|
2547
|
+
lines.push('')
|
|
2548
|
+
lines.push(
|
|
2549
|
+
`The \`rpk ${plugin}\` subtree was refreshed in the snapshot, then the ` +
|
|
2550
|
+
'full rpk reference was re-rendered from it as a converge. Pages outside ' +
|
|
2551
|
+
`\`rpk ${plugin}\` can carry template-level or consistency updates, and ` +
|
|
2552
|
+
'stale generated files are cleaned up, so the change list may be wider ' +
|
|
2553
|
+
'than the plugin itself.'
|
|
2554
|
+
)
|
|
2555
|
+
lines.push('')
|
|
2556
|
+
} else {
|
|
2557
|
+
lines.push('## rpk Documentation Generation Summary')
|
|
2558
|
+
lines.push('')
|
|
1525
2559
|
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2560
|
+
// Version info
|
|
2561
|
+
lines.push(`**Version:** ${rpkVersion}`)
|
|
2562
|
+
lines.push('')
|
|
2563
|
+
}
|
|
1529
2564
|
|
|
1530
2565
|
// Generation stats
|
|
1531
2566
|
lines.push('### Generation Statistics')
|
|
@@ -1544,52 +2579,118 @@ function generatePRSummary(options) {
|
|
|
1544
2579
|
lines.push('### Changes from Previous Version')
|
|
1545
2580
|
lines.push('')
|
|
1546
2581
|
|
|
1547
|
-
const {
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2582
|
+
const {
|
|
2583
|
+
newCommands,
|
|
2584
|
+
removedCommands,
|
|
2585
|
+
newFlags,
|
|
2586
|
+
removedFlags,
|
|
2587
|
+
changedDefaults = 0,
|
|
2588
|
+
changedFlagTypes = 0,
|
|
2589
|
+
changedFlagRequirements = 0,
|
|
2590
|
+
changedFlagDescriptions = 0,
|
|
2591
|
+
descriptionChanges = 0,
|
|
2592
|
+
newlyDeprecatedCommands = 0
|
|
2593
|
+
} = diffData.summary
|
|
2594
|
+
|
|
2595
|
+
const totalChanges = newCommands + removedCommands + newFlags + removedFlags +
|
|
2596
|
+
changedDefaults + changedFlagTypes + changedFlagRequirements +
|
|
2597
|
+
changedFlagDescriptions + descriptionChanges + newlyDeprecatedCommands
|
|
2598
|
+
|
|
2599
|
+
if (totalChanges === 0) {
|
|
2600
|
+
lines.push('No command, flag, or default changes detected.')
|
|
1551
2601
|
} else {
|
|
1552
2602
|
lines.push(`| Change Type | Count |`)
|
|
1553
2603
|
lines.push(`|-------------|-------|`)
|
|
1554
2604
|
if (newCommands > 0) lines.push(`| New commands | ${newCommands} |`)
|
|
2605
|
+
if (newlyDeprecatedCommands > 0) lines.push(`| Deprecated commands | ${newlyDeprecatedCommands} |`)
|
|
1555
2606
|
if (removedCommands > 0) lines.push(`| Removed commands | ${removedCommands} |`)
|
|
1556
2607
|
if (newFlags > 0) lines.push(`| New flags | ${newFlags} |`)
|
|
1557
2608
|
if (removedFlags > 0) lines.push(`| Removed flags | ${removedFlags} |`)
|
|
1558
|
-
if (
|
|
2609
|
+
if (changedDefaults > 0) lines.push(`| Changed flag defaults | ${changedDefaults} |`)
|
|
2610
|
+
if (changedFlagTypes > 0) lines.push(`| Changed flag types | ${changedFlagTypes} |`)
|
|
2611
|
+
if (changedFlagRequirements > 0) lines.push(`| Changed flag requirements | ${changedFlagRequirements} |`)
|
|
2612
|
+
if (changedFlagDescriptions > 0) lines.push(`| Changed flag descriptions | ${changedFlagDescriptions} |`)
|
|
2613
|
+
if (descriptionChanges > 0) lines.push(`| Changed command descriptions | ${descriptionChanges} |`)
|
|
2614
|
+
if (diffData.summary?.flagDataBackfilled > 0) lines.push(`| Flag docs backfilled (not new flags) | ${diffData.summary.flagDataBackfilled} commands |`)
|
|
1559
2615
|
}
|
|
1560
2616
|
lines.push('')
|
|
1561
2617
|
|
|
1562
|
-
//
|
|
1563
|
-
|
|
2618
|
+
// Collapsible list helper: itemized up to a cap, with an overflow note
|
|
2619
|
+
const pushDetailsList = (title, items, renderItem) => {
|
|
2620
|
+
if (!items || items.length === 0) return
|
|
1564
2621
|
lines.push('<details>')
|
|
1565
|
-
lines.push(
|
|
2622
|
+
lines.push(`<summary>${title}</summary>`)
|
|
1566
2623
|
lines.push('')
|
|
1567
|
-
for (const
|
|
1568
|
-
lines.push(
|
|
2624
|
+
for (const item of items.slice(0, 30)) {
|
|
2625
|
+
lines.push(renderItem(item))
|
|
1569
2626
|
}
|
|
1570
|
-
if (
|
|
1571
|
-
lines.push(`- ... and ${
|
|
2627
|
+
if (items.length > 30) {
|
|
2628
|
+
lines.push(`- ... and ${items.length - 30} more (see the diff JSON in docs-data/)`)
|
|
1572
2629
|
}
|
|
1573
2630
|
lines.push('')
|
|
1574
2631
|
lines.push('</details>')
|
|
1575
2632
|
lines.push('')
|
|
1576
2633
|
}
|
|
1577
2634
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2635
|
+
const formatValue = (value) => {
|
|
2636
|
+
if (value === undefined) return 'unset'
|
|
2637
|
+
if (value === null) return 'null'
|
|
2638
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
2639
|
+
return String(value)
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
pushDetailsList('New Commands', diffData.details?.newCommands,
|
|
2643
|
+
cmd => `- \`${cmd.path}\``)
|
|
2644
|
+
pushDetailsList('Deprecated Commands', diffData.details?.newlyDeprecatedCommands,
|
|
2645
|
+
cmd => `- \`${cmd.path}\`${cmd.message ? ` — ${cmd.message}` : ''}${cmd.hidden ? ' _(hidden from help output)_' : ''}`)
|
|
2646
|
+
pushDetailsList('Removed Commands', diffData.details?.removedCommands,
|
|
2647
|
+
cmd => `- \`${cmd.path}\``)
|
|
2648
|
+
pushDetailsList('New Flags', diffData.details?.newFlags,
|
|
2649
|
+
flag => `- \`${flag.commandPath}\`: \`--${flag.flagName}\` (${flag.type})`)
|
|
2650
|
+
pushDetailsList('Removed Flags', diffData.details?.removedFlags,
|
|
2651
|
+
flag => `- \`${flag.commandPath}\`: \`--${flag.flagName}\``)
|
|
2652
|
+
pushDetailsList('Changed Flag Defaults', diffData.details?.changedDefaults,
|
|
2653
|
+
change => `- \`${change.commandPath}\`: \`--${change.flagName}\` default \`${formatValue(change.oldDefault)}\` → \`${formatValue(change.newDefault)}\``)
|
|
2654
|
+
pushDetailsList('Changed Flag Types', diffData.details?.changedFlagTypes,
|
|
2655
|
+
change => `- \`${change.commandPath}\`: \`--${change.flagName}\` type \`${change.oldType}\` → \`${change.newType}\``)
|
|
2656
|
+
pushDetailsList('Changed Flag Requirements', diffData.details?.changedFlagRequirements,
|
|
2657
|
+
change => `- \`${change.commandPath}\`: \`--${change.flagName}\` required \`${change.oldRequired}\` → \`${change.newRequired}\``)
|
|
2658
|
+
pushDetailsList('Changed Command Descriptions', diffData.details?.descriptionChanges,
|
|
2659
|
+
change => `- \`${change.path}\``)
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
// Override validation: entries referencing commands the tree lacks do
|
|
2663
|
+
// nothing silently, so surface them where they become actionable
|
|
2664
|
+
if (overrideValidation && overrideValidation.errors && overrideValidation.errors.length > 0) {
|
|
2665
|
+
lines.push('### Override Validation')
|
|
2666
|
+
lines.push('')
|
|
2667
|
+
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):`)
|
|
2668
|
+
lines.push('')
|
|
2669
|
+
for (const err of overrideValidation.errors.slice(0, 10)) {
|
|
2670
|
+
lines.push(`- ${typeof err === 'string' ? err : err.message || JSON.stringify(err)}`)
|
|
2671
|
+
}
|
|
2672
|
+
if (overrideValidation.errors.length > 10) {
|
|
2673
|
+
lines.push(`- ... and ${overrideValidation.errors.length - 10} more`)
|
|
2674
|
+
}
|
|
2675
|
+
lines.push('')
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
// Description overrides that hide most of the source help. Curation is
|
|
2679
|
+
// fine, but this is where stale content hides, so keep it reviewable.
|
|
2680
|
+
if (descriptionCoverage && descriptionCoverage.length > 0) {
|
|
2681
|
+
lines.push('### Curated descriptions replacing source help')
|
|
2682
|
+
lines.push('')
|
|
2683
|
+
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):')
|
|
2684
|
+
lines.push('')
|
|
2685
|
+
lines.push('| Command | Override | Source help |')
|
|
2686
|
+
lines.push('|---------|----------|-------------|')
|
|
2687
|
+
for (const entry of descriptionCoverage.slice(0, 15)) {
|
|
2688
|
+
lines.push(`| \`${entry.commandPath}\` | ${entry.overrideChars} chars | ${entry.sourceChars} chars |`)
|
|
2689
|
+
}
|
|
2690
|
+
if (descriptionCoverage.length > 15) {
|
|
2691
|
+
lines.push(`| ... and ${descriptionCoverage.length - 15} more | | |`)
|
|
1592
2692
|
}
|
|
2693
|
+
lines.push('')
|
|
1593
2694
|
}
|
|
1594
2695
|
|
|
1595
2696
|
// Validation results
|
|
@@ -1686,6 +2787,23 @@ module.exports = {
|
|
|
1686
2787
|
handleRpkDocsGeneration,
|
|
1687
2788
|
fetchRpkTreeFromSource,
|
|
1688
2789
|
fetchRpkTreeFromLinuxSource,
|
|
2790
|
+
acquireRpkBinary,
|
|
2791
|
+
buildRpkBinary,
|
|
2792
|
+
downloadRpkRelease,
|
|
2793
|
+
fetchPluginSubtree,
|
|
2794
|
+
parseCobraFlags,
|
|
2795
|
+
parseUrfaveFlags,
|
|
2796
|
+
parseHelpFlags,
|
|
2797
|
+
mergeVisibleDeprecationsIntoOverrides,
|
|
2798
|
+
makeLinkablePredicate,
|
|
2799
|
+
enrichPluginTreeWithFlags,
|
|
2800
|
+
fetchLatestPluginVersion,
|
|
2801
|
+
pluginNodeHasRealCommands,
|
|
2802
|
+
splicePluginNode,
|
|
2803
|
+
preserveLinuxOnlyCommands,
|
|
2804
|
+
REFRESHABLE_PLUGINS,
|
|
2805
|
+
PLUGIN_INSTALL_VERSION_FLAGS,
|
|
2806
|
+
PLUGIN_MANIFEST_SLUGS,
|
|
1689
2807
|
getRequiredGoVersion,
|
|
1690
2808
|
prepareSourceFromRef,
|
|
1691
2809
|
detectLinuxOnlyFromSource,
|
|
@@ -1701,6 +2819,9 @@ module.exports = {
|
|
|
1701
2819
|
getPlatformDescription,
|
|
1702
2820
|
getCurrentPlatform,
|
|
1703
2821
|
updateOverridesWithIntroducedVersions,
|
|
2822
|
+
filterDiffForWhatsNew,
|
|
2823
|
+
computeDescriptionCoverage,
|
|
2824
|
+
updateWhatsNewFile,
|
|
1704
2825
|
KNOWN_PLUGINS,
|
|
1705
2826
|
extractCommandPaths,
|
|
1706
2827
|
detectLinuxOnlyByComparison,
|