@redpanda-data/docs-extensions-and-macros 5.3.0 → 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/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +21 -1
- package/tools/rpk-docs/detect-platform-commands.js +566 -0
- package/tools/rpk-docs/generate-rpk-docs.js +33 -12
- package/tools/rpk-docs/rpk-docs-handler.js +105 -112
- package/tools/rpk-docs/templates/command.hbs +12 -0
- package/tools/rpk-docs/validate-overrides.js +16 -0
package/package.json
CHANGED
|
@@ -79,7 +79,7 @@ ENUM_PATTERN = re.compile(r'^[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)$') # Match full qu
|
|
|
79
79
|
CONSTRUCTOR_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\((.*)\)')
|
|
80
80
|
BRACED_CONSTRUCTOR_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\{(.*)\}')
|
|
81
81
|
DIGIT_SEPARATOR_PATTERN = re.compile(r"(?<=\d)'(?=\d)")
|
|
82
|
-
FUNCTION_CALL_PATTERN = re.compile(r'([a-zA-Z0-9_
|
|
82
|
+
FUNCTION_CALL_PATTERN = re.compile(r'([a-zA-Z0-9_:.]+)\(\)$')
|
|
83
83
|
CHRONO_PATTERN = re.compile(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}')
|
|
84
84
|
CHRONO_PAREN_PATTERN = re.compile(r'(?:std::)?chrono::([a-zA-Z]+)\s*\(\s*([^)]+)\s*\)')
|
|
85
85
|
TIME_UNIT_PATTERN = re.compile(r'(\d+)\s*(min|s|ms|h)')
|
|
@@ -159,6 +159,15 @@ class ConstexprCache:
|
|
|
159
159
|
re.compile(r'const\s+model::ns\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*"([^"]+)"\s*\)'),
|
|
160
160
|
]
|
|
161
161
|
|
|
162
|
+
# Member accessors on inline constants used in property defaults,
|
|
163
|
+
# for example model::schema_registry_internal_tp.topic() -> "_schemas".
|
|
164
|
+
# Each entry maps a variable declaration to the member name whose call
|
|
165
|
+
# returns the captured string, cached under "<variable>.<member>".
|
|
166
|
+
member_accessor_patterns = [
|
|
167
|
+
# Pattern: inline const model::topic_partition name{model::topic{"value"}, ...}
|
|
168
|
+
(re.compile(r'inline\s+const\s+(?:model::)?topic_partition\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\{\s*(?:model::)?topic\{\s*"([^"]+)"\s*\}'), 'topic'),
|
|
169
|
+
]
|
|
170
|
+
|
|
162
171
|
# Legacy specific patterns (kept for compatibility, but general patterns should cover these)
|
|
163
172
|
function_patterns = {}
|
|
164
173
|
|
|
@@ -206,6 +215,17 @@ class ConstexprCache:
|
|
|
206
215
|
qualified_name = f"{namespace}::{func_name}"
|
|
207
216
|
self.function_cache[qualified_name] = func_value
|
|
208
217
|
|
|
218
|
+
# Extract member accessors on inline constants
|
|
219
|
+
for pattern, member in member_accessor_patterns:
|
|
220
|
+
for match in pattern.finditer(content):
|
|
221
|
+
var_name = match.group(1)
|
|
222
|
+
value = match.group(2)
|
|
223
|
+
key = f"{var_name}.{member}"
|
|
224
|
+
namespace = self._extract_namespace_for_function(content, match.start())
|
|
225
|
+
self.function_cache[key] = value
|
|
226
|
+
if namespace:
|
|
227
|
+
self.function_cache[f"{namespace}::{key}"] = value
|
|
228
|
+
|
|
209
229
|
# Legacy: Extract function definitions from hardcoded patterns (if any)
|
|
210
230
|
for func_name, patterns in function_patterns.items():
|
|
211
231
|
for pattern in patterns:
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static (source-based) detection of Linux-only rpk commands.
|
|
5
|
+
*
|
|
6
|
+
* Works on any platform (including Linux CI runners, where the dynamic
|
|
7
|
+
* "build on Linux and Darwin and diff the trees" approach is impossible
|
|
8
|
+
* because a cross-compiled darwin rpk cannot be executed).
|
|
9
|
+
*
|
|
10
|
+
* How it works
|
|
11
|
+
* ------------
|
|
12
|
+
* rpk gates platform-specific commands with Go build constraints:
|
|
13
|
+
*
|
|
14
|
+
* 1. Whole-package gating: every file in a command's package carries a
|
|
15
|
+
* Linux constraint (explicit `//go:build linux` tag or a `_linux.go`
|
|
16
|
+
* filename suffix) and no darwin-buildable counterpart exists.
|
|
17
|
+
* Example: pkg/cli/iotune, pkg/cli/redpanda/tune.
|
|
18
|
+
*
|
|
19
|
+
* 2. Dual registration: a package provides two variants of the same
|
|
20
|
+
* command constructor, one per platform, registering different
|
|
21
|
+
* subcommand sets. Example: pkg/cli/redpanda/redpanda.go
|
|
22
|
+
* (`//go:build linux`, registers start/stop/check/mode/config/tune)
|
|
23
|
+
* vs pkg/cli/redpanda/redpanda_darwin.go (`//go:build darwin`,
|
|
24
|
+
* registers only the hidden admin command). Same pattern at the root:
|
|
25
|
+
* pkg/cli/root_linux.go vs pkg/cli/root_darwin.go.
|
|
26
|
+
*
|
|
27
|
+
* This module detects both patterns:
|
|
28
|
+
*
|
|
29
|
+
* - For every scanned package, it computes which files build on linux
|
|
30
|
+
* and which build on darwin (from `//go:build` / `// +build` tags AND
|
|
31
|
+
* filename-implied constraints like `_linux.go`).
|
|
32
|
+
* - Packages whose files all fail to build on darwin (but build on
|
|
33
|
+
* linux) and that define a cobra command constructor are Linux-only.
|
|
34
|
+
* - Packages containing platform-differential files get a registration
|
|
35
|
+
* diff: subcommand constructors referenced from linux-buildable files
|
|
36
|
+
* but not from darwin-buildable files are Linux-only. Deprecated and
|
|
37
|
+
* hidden constructors are ignored because `rpk --print-tree` (the
|
|
38
|
+
* basis of the generated docs) excludes hidden commands.
|
|
39
|
+
*
|
|
40
|
+
* Scope: built-in commands only. Plugin commands (rpk connect, rpk ai, ...)
|
|
41
|
+
* are not part of the rpk source tree, so their platform availability
|
|
42
|
+
* cannot be derived here. Plugins keep whatever platform behavior the
|
|
43
|
+
* caller's other detection paths provide (for example, dynamic detection
|
|
44
|
+
* marks plugin-only commands when plugins are only installed in the Linux
|
|
45
|
+
* build container).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const fs = require('fs')
|
|
49
|
+
const path = require('path')
|
|
50
|
+
|
|
51
|
+
/** GOOS values recognized in filename suffixes and build tags */
|
|
52
|
+
const KNOWN_GOOS = new Set([
|
|
53
|
+
'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
|
|
54
|
+
'ios', 'js', 'linux', 'nacl', 'netbsd', 'openbsd', 'plan9', 'solaris',
|
|
55
|
+
'wasip1', 'windows', 'zos'
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/** GOOS values that satisfy the `unix` build tag */
|
|
59
|
+
const UNIX_GOOS = new Set([
|
|
60
|
+
'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
|
|
61
|
+
'ios', 'linux', 'netbsd', 'openbsd', 'solaris'
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
/** GOARCH values recognized in filename suffixes */
|
|
65
|
+
const KNOWN_GOARCH = new Set([
|
|
66
|
+
'386', 'amd64', 'amd64p32', 'arm', 'arm64', 'arm64be', 'armbe', 'loong64',
|
|
67
|
+
'mips', 'mips64', 'mips64le', 'mips64p32', 'mips64p32le', 'mipsle',
|
|
68
|
+
'ppc', 'ppc64', 'ppc64le', 'riscv', 'riscv64', 's390', 's390x',
|
|
69
|
+
'sparc', 'sparc64', 'wasm'
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Directories (relative to the rpk source root) scanned for command
|
|
74
|
+
* implementations. Modern rpk keeps commands in pkg/cli/<command>/;
|
|
75
|
+
* the other roots cover older source layouts.
|
|
76
|
+
*/
|
|
77
|
+
const SCAN_ROOTS = ['pkg/cli', 'pkg/cli/cmd', 'cmd/rpk']
|
|
78
|
+
|
|
79
|
+
/** Directory names that never correspond to a command path segment */
|
|
80
|
+
const NON_COMMAND_SEGMENTS = new Set(['internal', 'common', 'testdata'])
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Determine the GOOS constraint implied by a Go filename.
|
|
84
|
+
* Follows Go's rules: *_GOOS.go, *_GOOS_GOARCH.go constrain the OS;
|
|
85
|
+
* *_GOARCH.go alone does not.
|
|
86
|
+
* @param {string} fileName - e.g. 'redpanda_darwin.go', 'bundle_k8s_linux.go'
|
|
87
|
+
* @returns {string|null} GOOS name or null when unconstrained
|
|
88
|
+
*/
|
|
89
|
+
function goosFromFileName(fileName) {
|
|
90
|
+
const base = fileName.replace(/\.go$/, '')
|
|
91
|
+
const parts = base.split('_')
|
|
92
|
+
if (parts.length < 2) return null
|
|
93
|
+
|
|
94
|
+
const last = parts[parts.length - 1]
|
|
95
|
+
if (KNOWN_GOOS.has(last)) return last
|
|
96
|
+
if (KNOWN_GOARCH.has(last) && parts.length >= 3) {
|
|
97
|
+
const secondLast = parts[parts.length - 2]
|
|
98
|
+
if (KNOWN_GOOS.has(secondLast)) return secondLast
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Evaluate a `//go:build` constraint expression for a target GOOS.
|
|
105
|
+
* Supports identifiers, !, &&, || and parentheses.
|
|
106
|
+
* Unknown identifiers (custom build tags) evaluate to false, matching a
|
|
107
|
+
* default `go build` with no -tags flag. GOARCH identifiers evaluate to
|
|
108
|
+
* true because we only care about OS-level availability.
|
|
109
|
+
* @param {string} expr - Expression after `//go:build`
|
|
110
|
+
* @param {string} goos - Target GOOS ('linux' or 'darwin')
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function evaluateBuildExpr(expr, goos) {
|
|
114
|
+
const tokens = expr.match(/[A-Za-z0-9_.]+|&&|\|\||!|\(|\)/g) || []
|
|
115
|
+
let pos = 0
|
|
116
|
+
|
|
117
|
+
const peek = () => tokens[pos]
|
|
118
|
+
const next = () => tokens[pos++]
|
|
119
|
+
|
|
120
|
+
function evalIdent(ident) {
|
|
121
|
+
if (KNOWN_GOOS.has(ident)) return ident === goos
|
|
122
|
+
if (ident === 'unix') return UNIX_GOOS.has(goos)
|
|
123
|
+
if (KNOWN_GOARCH.has(ident)) return true
|
|
124
|
+
if (ident === 'cgo') return true
|
|
125
|
+
// Custom build tags (e.g. withasan, integration) are unset by default
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePrimary() {
|
|
130
|
+
const tok = next()
|
|
131
|
+
if (tok === '(') {
|
|
132
|
+
const val = parseOr()
|
|
133
|
+
if (peek() === ')') next()
|
|
134
|
+
return val
|
|
135
|
+
}
|
|
136
|
+
if (tok === '!') return !parsePrimary()
|
|
137
|
+
if (tok === undefined) return false
|
|
138
|
+
return evalIdent(tok)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseAnd() {
|
|
142
|
+
let val = parsePrimary()
|
|
143
|
+
while (peek() === '&&') {
|
|
144
|
+
next()
|
|
145
|
+
val = parsePrimary() && val
|
|
146
|
+
}
|
|
147
|
+
return val
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseOr() {
|
|
151
|
+
let val = parseAnd()
|
|
152
|
+
while (peek() === '||') {
|
|
153
|
+
next()
|
|
154
|
+
val = parseAnd() || val
|
|
155
|
+
}
|
|
156
|
+
return val
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return parseOr()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Evaluate legacy `// +build` lines for a target GOOS.
|
|
164
|
+
* Multiple lines AND together; within a line, space-separated options OR
|
|
165
|
+
* and comma-separated terms AND.
|
|
166
|
+
* @param {string[]} lines - Contents after `// +build` (one per line)
|
|
167
|
+
* @param {string} goos - Target GOOS
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
function evaluatePlusBuildLines(lines, goos) {
|
|
171
|
+
const evalTerm = (term) => {
|
|
172
|
+
let negate = false
|
|
173
|
+
while (term.startsWith('!')) {
|
|
174
|
+
negate = !negate
|
|
175
|
+
term = term.slice(1)
|
|
176
|
+
}
|
|
177
|
+
let val
|
|
178
|
+
if (KNOWN_GOOS.has(term)) val = term === goos
|
|
179
|
+
else if (term === 'unix') val = UNIX_GOOS.has(goos)
|
|
180
|
+
else if (KNOWN_GOARCH.has(term) || term === 'cgo') val = true
|
|
181
|
+
else val = false
|
|
182
|
+
return negate ? !val : val
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.every(line => {
|
|
186
|
+
const options = line.trim().split(/\s+/).filter(Boolean)
|
|
187
|
+
if (options.length === 0) return true
|
|
188
|
+
return options.some(opt => opt.split(',').every(evalTerm))
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extract the build constraint (if any) from a Go file's header.
|
|
194
|
+
* Only lines before the `package` declaration are considered.
|
|
195
|
+
* @param {string} content - Go file content
|
|
196
|
+
* @returns {{goBuildExpr: string|null, plusBuildLines: string[]}}
|
|
197
|
+
*/
|
|
198
|
+
function extractBuildConstraint(content) {
|
|
199
|
+
const header = content.split(/^package\s/m)[0]
|
|
200
|
+
const goBuildMatch = header.match(/^\/\/go:build\s+(.+)$/m)
|
|
201
|
+
const plusBuildLines = []
|
|
202
|
+
const plusBuildRe = /^\/\/\s*\+build\s+(.+)$/gm
|
|
203
|
+
let m
|
|
204
|
+
while ((m = plusBuildRe.exec(header)) !== null) {
|
|
205
|
+
plusBuildLines.push(m[1])
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
goBuildExpr: goBuildMatch ? goBuildMatch[1].trim() : null,
|
|
209
|
+
plusBuildLines
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Determine whether a Go file builds for a target GOOS, taking both the
|
|
215
|
+
* filename-implied constraint and explicit build tags into account.
|
|
216
|
+
* @param {string} fileName - Base name of the file
|
|
217
|
+
* @param {string} content - File content
|
|
218
|
+
* @param {string} goos - Target GOOS ('linux' or 'darwin')
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
function fileBuildsOn(fileName, content, goos) {
|
|
222
|
+
const fileGoos = goosFromFileName(fileName)
|
|
223
|
+
if (fileGoos && fileGoos !== goos) return false
|
|
224
|
+
|
|
225
|
+
const { goBuildExpr, plusBuildLines } = extractBuildConstraint(content)
|
|
226
|
+
if (goBuildExpr) return evaluateBuildExpr(goBuildExpr, goos)
|
|
227
|
+
if (plusBuildLines.length > 0) return evaluatePlusBuildLines(plusBuildLines, goos)
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Derive a command name from a cobra constructor function name.
|
|
233
|
+
* NewStartCommand -> start, newTuneCommand -> tune, NewCommand -> null.
|
|
234
|
+
* @param {string} funcName
|
|
235
|
+
* @returns {string|null}
|
|
236
|
+
*/
|
|
237
|
+
function commandNameFromConstructor(funcName) {
|
|
238
|
+
const m = funcName.match(/^(?:New|new)(\w+?)(?:Command|Cmd)$/)
|
|
239
|
+
if (m && m[1]) return m[1].toLowerCase()
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse cobra command constructors defined in a Go file.
|
|
245
|
+
* A constructor is any `func XxxYyy(...) *cobra.Command`. For each one we
|
|
246
|
+
* record the cobra `Use:` name (first token) and whether the command is
|
|
247
|
+
* deprecated or hidden (such commands never appear in `rpk --print-tree`).
|
|
248
|
+
* @param {string} content - Go file content
|
|
249
|
+
* @returns {Object<string, {useName: string|null, excluded: boolean}>}
|
|
250
|
+
*/
|
|
251
|
+
function parseConstructors(content) {
|
|
252
|
+
const constructors = {}
|
|
253
|
+
const funcRe = /^func\s+(\w+)\s*\([^)]*\)\s*\*cobra\.Command\s*\{/gm
|
|
254
|
+
const matches = []
|
|
255
|
+
let m
|
|
256
|
+
while ((m = funcRe.exec(content)) !== null) {
|
|
257
|
+
matches.push({ name: m[1], start: m.index })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < matches.length; i++) {
|
|
261
|
+
const bodyEnd = i + 1 < matches.length ? matches[i + 1].start : content.length
|
|
262
|
+
const body = content.slice(matches[i].start, bodyEnd)
|
|
263
|
+
|
|
264
|
+
const useMatch = body.match(/\bUse:\s*"([^"]+)"/)
|
|
265
|
+
const useName = useMatch ? useMatch[1].trim().split(/\s+/)[0] : null
|
|
266
|
+
|
|
267
|
+
// Deprecated or hidden commands are excluded from `rpk --print-tree`
|
|
268
|
+
const excluded = /\bHidden:\s*true\b/.test(body) ||
|
|
269
|
+
/\bDeprecated:\s*"/.test(body) ||
|
|
270
|
+
/\bDeprecateCmd\s*\(/.test(body)
|
|
271
|
+
|
|
272
|
+
constructors[matches[i].name] = { useName, excluded }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return constructors
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract constructor references from all AddCommand(...) calls in a file.
|
|
280
|
+
* Handles both same-package (`NewStartCommand(...)`) and cross-package
|
|
281
|
+
* (`tune.NewCommand(...)`) references. Only functions starting with
|
|
282
|
+
* New/new are considered, which skips wrappers like cobraext.DeprecateCmd.
|
|
283
|
+
* @param {string} content - Go file content
|
|
284
|
+
* @returns {Array<{qualifier: string|null, funcName: string}>}
|
|
285
|
+
*/
|
|
286
|
+
function parseAddCommandRefs(content) {
|
|
287
|
+
const refs = []
|
|
288
|
+
const addRe = /\bAddCommand\s*\(/g
|
|
289
|
+
let m
|
|
290
|
+
while ((m = addRe.exec(content)) !== null) {
|
|
291
|
+
// Find the span of this AddCommand(...) call by balancing parentheses
|
|
292
|
+
let depth = 1
|
|
293
|
+
let i = m.index + m[0].length
|
|
294
|
+
while (i < content.length && depth > 0) {
|
|
295
|
+
if (content[i] === '(') depth++
|
|
296
|
+
else if (content[i] === ')') depth--
|
|
297
|
+
i++
|
|
298
|
+
}
|
|
299
|
+
const span = content.slice(m.index + m[0].length, i - 1)
|
|
300
|
+
|
|
301
|
+
const callRe = /(?:(\w+)\.)?((?:New|new)\w*)\s*\(/g
|
|
302
|
+
let c
|
|
303
|
+
while ((c = callRe.exec(span)) !== null) {
|
|
304
|
+
refs.push({ qualifier: c[1] || null, funcName: c[2] })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return refs
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Recursively collect non-test Go files under a directory.
|
|
312
|
+
* @param {string} dir - Absolute directory path
|
|
313
|
+
* @param {Array} out - Accumulator of {absPath, dir, name}
|
|
314
|
+
*/
|
|
315
|
+
function collectGoFiles(dir, out) {
|
|
316
|
+
let entries
|
|
317
|
+
try {
|
|
318
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const fullPath = path.join(dir, entry.name)
|
|
324
|
+
if (entry.isDirectory()) {
|
|
325
|
+
if (entry.name === 'testdata' || entry.name === 'vendor') continue
|
|
326
|
+
collectGoFiles(fullPath, out)
|
|
327
|
+
} else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
|
|
328
|
+
out.push({ absPath: fullPath, dir, name: entry.name })
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build the command path prefix for a package directory.
|
|
335
|
+
* pkg/cli -> 'rpk'; pkg/cli/redpanda -> 'rpk redpanda';
|
|
336
|
+
* pkg/cli/cmd/redpanda (legacy) -> 'rpk redpanda'.
|
|
337
|
+
* @param {string} scanRoot - Absolute path of the scan root
|
|
338
|
+
* @param {string} dir - Absolute directory path
|
|
339
|
+
* @returns {string|null} Command path prefix, or null for non-command dirs
|
|
340
|
+
*/
|
|
341
|
+
function commandPrefixForDir(scanRoot, dir) {
|
|
342
|
+
const rel = path.relative(scanRoot, dir)
|
|
343
|
+
let segments = rel === '' ? [] : rel.split(path.sep)
|
|
344
|
+
// Legacy layouts nested commands under an extra cmd/ level
|
|
345
|
+
if (segments[0] === 'cmd') segments = segments.slice(1)
|
|
346
|
+
if (segments.some(s => NON_COMMAND_SEGMENTS.has(s))) return null
|
|
347
|
+
return ['rpk', ...segments].join(' ')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Analyze one scan root and add detected Linux-only command paths.
|
|
352
|
+
* @param {string} scanRoot - Absolute path of the scan root
|
|
353
|
+
* @param {Set<string>} linuxOnlyCommands - Accumulator
|
|
354
|
+
*/
|
|
355
|
+
function scanRootForLinuxOnly(scanRoot, linuxOnlyCommands) {
|
|
356
|
+
const files = []
|
|
357
|
+
collectGoFiles(scanRoot, files)
|
|
358
|
+
if (files.length === 0) return
|
|
359
|
+
|
|
360
|
+
// Group files (with parsed metadata) by directory
|
|
361
|
+
const dirs = new Map()
|
|
362
|
+
for (const file of files) {
|
|
363
|
+
let content
|
|
364
|
+
try {
|
|
365
|
+
content = fs.readFileSync(file.absPath, 'utf8')
|
|
366
|
+
} catch (err) {
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
const info = {
|
|
370
|
+
name: file.name,
|
|
371
|
+
content,
|
|
372
|
+
buildsOnLinux: fileBuildsOn(file.name, content, 'linux'),
|
|
373
|
+
buildsOnDarwin: fileBuildsOn(file.name, content, 'darwin')
|
|
374
|
+
}
|
|
375
|
+
if (!dirs.has(file.dir)) dirs.set(file.dir, [])
|
|
376
|
+
dirs.get(file.dir).push(info)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Per-directory constructor index: dir -> {funcName: {useName, excluded}}
|
|
380
|
+
const constructorIndex = new Map()
|
|
381
|
+
for (const [dir, dirFiles] of dirs) {
|
|
382
|
+
const index = {}
|
|
383
|
+
for (const f of dirFiles) {
|
|
384
|
+
Object.assign(index, parseConstructors(f.content))
|
|
385
|
+
}
|
|
386
|
+
constructorIndex.set(dir, index)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolve an AddCommand constructor reference to a visible command name.
|
|
391
|
+
* Returns null for deprecated/hidden/unresolvable commands.
|
|
392
|
+
*/
|
|
393
|
+
const resolveRef = (dir, ref) => {
|
|
394
|
+
let target = null
|
|
395
|
+
let fallback = commandNameFromConstructor(ref.funcName)
|
|
396
|
+
|
|
397
|
+
if (ref.qualifier) {
|
|
398
|
+
const subDir = path.join(dir, ref.qualifier)
|
|
399
|
+
target = constructorIndex.get(subDir) || null
|
|
400
|
+
if (!fallback) fallback = ref.qualifier
|
|
401
|
+
} else {
|
|
402
|
+
target = constructorIndex.get(dir) || null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (target && target[ref.funcName]) {
|
|
406
|
+
const ctor = target[ref.funcName]
|
|
407
|
+
if (ctor.excluded) return null
|
|
408
|
+
return ctor.useName || fallback
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Unresolvable reference: only trust it if the function name follows
|
|
412
|
+
// the cobra constructor convention (New[Xxx]Command). This skips
|
|
413
|
+
// helper arguments captured inside AddCommand spans, such as the
|
|
414
|
+
// rp.NewLauncher() argument in redpanda.NewCommand(fs, p, rp.NewLauncher()).
|
|
415
|
+
if (!/^(?:New|new)(?:\w*(?:Command|Cmd))?$/.test(ref.funcName)) return null
|
|
416
|
+
return fallback
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const [dir, dirFiles] of dirs) {
|
|
420
|
+
const prefix = commandPrefixForDir(scanRoot, dir)
|
|
421
|
+
if (!prefix) continue
|
|
422
|
+
|
|
423
|
+
const hasDifferentialFile = dirFiles.some(f => f.buildsOnLinux !== f.buildsOnDarwin)
|
|
424
|
+
|
|
425
|
+
// Pattern 1: whole package is Linux-gated (and defines a command)
|
|
426
|
+
if (prefix !== 'rpk') {
|
|
427
|
+
const anyLinux = dirFiles.some(f => f.buildsOnLinux)
|
|
428
|
+
const anyDarwin = dirFiles.some(f => f.buildsOnDarwin)
|
|
429
|
+
const definesCommand = Object.keys(constructorIndex.get(dir)).length > 0
|
|
430
|
+
if (anyLinux && !anyDarwin && definesCommand) {
|
|
431
|
+
linuxOnlyCommands.add(prefix)
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Pattern 2: dual registration - diff subcommands registered from
|
|
437
|
+
// linux-buildable files vs darwin-buildable files
|
|
438
|
+
if (!hasDifferentialFile) continue
|
|
439
|
+
|
|
440
|
+
const refsFor = (goos) => {
|
|
441
|
+
const names = new Set()
|
|
442
|
+
for (const f of dirFiles) {
|
|
443
|
+
const buildable = goos === 'linux' ? f.buildsOnLinux : f.buildsOnDarwin
|
|
444
|
+
if (!buildable) continue
|
|
445
|
+
for (const ref of parseAddCommandRefs(f.content)) {
|
|
446
|
+
const name = resolveRef(dir, ref)
|
|
447
|
+
if (name) names.add(name)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return names
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const linuxRefs = refsFor('linux')
|
|
454
|
+
const darwinRefs = refsFor('darwin')
|
|
455
|
+
|
|
456
|
+
for (const name of linuxRefs) {
|
|
457
|
+
if (!darwinRefs.has(name)) {
|
|
458
|
+
linuxOnlyCommands.add(`${prefix} ${name}`)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Detect Linux-only rpk commands by statically analyzing Go source.
|
|
466
|
+
* Works on any platform, including Linux CI runners.
|
|
467
|
+
* @param {string} sourcePath - Path to the rpk Go source root (src/go/rpk)
|
|
468
|
+
* @returns {Set<string>} Linux-only command paths (e.g. 'rpk iotune').
|
|
469
|
+
* Descendants of a returned path are implicitly Linux-only too.
|
|
470
|
+
*/
|
|
471
|
+
function detectLinuxOnlyFromSource(sourcePath) {
|
|
472
|
+
const linuxOnlyCommands = new Set()
|
|
473
|
+
|
|
474
|
+
const scannedRoots = new Set()
|
|
475
|
+
for (const root of SCAN_ROOTS) {
|
|
476
|
+
const absRoot = path.join(sourcePath, root)
|
|
477
|
+
if (!fs.existsSync(absRoot)) continue
|
|
478
|
+
// pkg/cli/cmd is nested inside pkg/cli; avoid scanning it twice
|
|
479
|
+
if ([...scannedRoots].some(r => absRoot.startsWith(r + path.sep))) continue
|
|
480
|
+
scannedRoots.add(absRoot)
|
|
481
|
+
scanRootForLinuxOnly(absRoot, linuxOnlyCommands)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Drop any detected path that is a descendant of another detected path;
|
|
485
|
+
// platform markers already propagate to descendants.
|
|
486
|
+
for (const cmd of [...linuxOnlyCommands]) {
|
|
487
|
+
if ([...linuxOnlyCommands].some(other => other !== cmd && cmd.startsWith(other + ' '))) {
|
|
488
|
+
linuxOnlyCommands.delete(cmd)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return linuxOnlyCommands
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* List Go files under the scanned roots that carry a Linux-only build
|
|
497
|
+
* constraint (explicit tag or filename suffix). Used as a tripwire: if
|
|
498
|
+
* detection returns an empty set while this list is non-empty, the scan
|
|
499
|
+
* is almost certainly broken (wrong directory layout, changed tag style).
|
|
500
|
+
* @param {string} sourcePath - Path to the rpk Go source root
|
|
501
|
+
* @returns {string[]} Relative paths of Linux-constrained files
|
|
502
|
+
*/
|
|
503
|
+
function findLinuxConstrainedFiles(sourcePath) {
|
|
504
|
+
const results = []
|
|
505
|
+
for (const root of SCAN_ROOTS) {
|
|
506
|
+
const absRoot = path.join(sourcePath, root)
|
|
507
|
+
if (!fs.existsSync(absRoot)) continue
|
|
508
|
+
const files = []
|
|
509
|
+
collectGoFiles(absRoot, files)
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
let content
|
|
512
|
+
try {
|
|
513
|
+
content = fs.readFileSync(file.absPath, 'utf8')
|
|
514
|
+
} catch (err) {
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
if (fileBuildsOn(file.name, content, 'linux') && !fileBuildsOn(file.name, content, 'darwin')) {
|
|
518
|
+
results.push(path.relative(sourcePath, file.absPath))
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return [...new Set(results)]
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Emit a loud warning when platform detection came back empty even though
|
|
527
|
+
* the source demonstrably contains Linux-gated files. Never let a scan
|
|
528
|
+
* failure silently mark every command as cross-platform.
|
|
529
|
+
* @param {string} sourcePath - Path to the rpk Go source root
|
|
530
|
+
* @param {Set<string>} linuxOnlyCommands - Combined detection result
|
|
531
|
+
* @returns {boolean} True when the tripwire fired
|
|
532
|
+
*/
|
|
533
|
+
function warnIfDetectionLooksBroken(sourcePath, linuxOnlyCommands) {
|
|
534
|
+
if (linuxOnlyCommands.size > 0) return false
|
|
535
|
+
|
|
536
|
+
const constrained = findLinuxConstrainedFiles(sourcePath)
|
|
537
|
+
if (constrained.length === 0) return false
|
|
538
|
+
|
|
539
|
+
console.warn('\n' + '!'.repeat(70))
|
|
540
|
+
console.warn('⚠ PLATFORM DETECTION TRIPWIRE: no Linux-only commands were detected,')
|
|
541
|
+
console.warn(`⚠ but the rpk source contains ${constrained.length} Linux-constrained file(s), e.g.:`)
|
|
542
|
+
for (const file of constrained.slice(0, 5)) {
|
|
543
|
+
console.warn(`⚠ - ${file}`)
|
|
544
|
+
}
|
|
545
|
+
console.warn('⚠ This usually means the static source scan failed (unexpected')
|
|
546
|
+
console.warn('⚠ directory layout or build-tag style) and every command is about')
|
|
547
|
+
console.warn('⚠ to be marked as available on both Linux and macOS, which is wrong.')
|
|
548
|
+
console.warn('⚠ Fix tools/rpk-docs/detect-platform-commands.js before publishing')
|
|
549
|
+
console.warn('⚠ these docs. See redpanda-data/docs#1831 for the impact of this.')
|
|
550
|
+
console.warn('!'.repeat(70) + '\n')
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = {
|
|
555
|
+
detectLinuxOnlyFromSource,
|
|
556
|
+
findLinuxConstrainedFiles,
|
|
557
|
+
warnIfDetectionLooksBroken,
|
|
558
|
+
// Exported for unit tests
|
|
559
|
+
fileBuildsOn,
|
|
560
|
+
goosFromFileName,
|
|
561
|
+
evaluateBuildExpr,
|
|
562
|
+
evaluatePlusBuildLines,
|
|
563
|
+
parseConstructors,
|
|
564
|
+
parseAddCommandRefs,
|
|
565
|
+
commandNameFromConstructor
|
|
566
|
+
}
|
|
@@ -291,6 +291,24 @@ function shouldUsePartialDir(overrides, commandPath) {
|
|
|
291
291
|
return false
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Check if a command belongs to the rpk cloud or rpk security secret families.
|
|
296
|
+
* Their pages are routed to the cloud partials directory (cloudSecretDir) and
|
|
297
|
+
* published by cloud-docs, so links to them from regular pages must cross to
|
|
298
|
+
* the cloud component.
|
|
299
|
+
* @param {string} commandPath - Full command path (e.g., "rpk cloud login")
|
|
300
|
+
* @returns {boolean}
|
|
301
|
+
*/
|
|
302
|
+
function isCloudSecretCommand(commandPath) {
|
|
303
|
+
return commandPath === 'rpk cloud' || commandPath.startsWith('rpk cloud ') ||
|
|
304
|
+
commandPath === 'rpk security secret' || commandPath.startsWith('rpk security secret ')
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Antora component that publishes the rpk cloud and rpk security secret pages.
|
|
309
|
+
*/
|
|
310
|
+
const CLOUD_DOCS_COMPONENT = 'cloud-data-platform'
|
|
311
|
+
|
|
294
312
|
/**
|
|
295
313
|
* Get command metadata from overrides
|
|
296
314
|
* @param {Object} overrides - Overrides object (resolved)
|
|
@@ -2420,8 +2438,7 @@ function updateNavFile(navFile, commands, resolvedOverrides, topLevelWithSubcomm
|
|
|
2420
2438
|
if (isProtectedCommandPath(commandPath, protectedPlugins)) continue
|
|
2421
2439
|
if (shouldExcludeCommand(resolvedOverrides, commandPath)) continue
|
|
2422
2440
|
if (shouldUsePartialDir(resolvedOverrides, commandPath)) continue
|
|
2423
|
-
if (commandPath
|
|
2424
|
-
if (commandPath.startsWith('rpk security secret')) continue
|
|
2441
|
+
if (isCloudSecretCommand(commandPath)) continue
|
|
2425
2442
|
|
|
2426
2443
|
const parts = commandPath.split(' ')
|
|
2427
2444
|
const stars = '*'.repeat(parts.length + 1)
|
|
@@ -2725,16 +2742,13 @@ async function generateRpkDocs(options = {}) {
|
|
|
2725
2742
|
// Build subcommands with correct xref paths
|
|
2726
2743
|
// Filter out excluded and asPartial subcommands — excluded have no file,
|
|
2727
2744
|
// asPartial ones live in the partials directory with no linkable xref.
|
|
2728
|
-
// rpk cloud and rpk security secret
|
|
2729
|
-
//
|
|
2730
|
-
//
|
|
2731
|
-
//
|
|
2745
|
+
// rpk cloud and rpk security secret rows stay in the table: their pages
|
|
2746
|
+
// are routed to the cloud partials directory and published by
|
|
2747
|
+
// cloud-docs, so the xref below links across to the cloud component
|
|
2748
|
+
// instead of dropping the row (which hid the subcommand entirely).
|
|
2732
2749
|
const subcommands = (command.commands || [])
|
|
2733
2750
|
.filter(sub => {
|
|
2734
2751
|
const subPath = `${commandPath} ${sub.name}`
|
|
2735
|
-
if (subPath.startsWith('rpk cloud') || subPath.startsWith('rpk security secret')) {
|
|
2736
|
-
return false
|
|
2737
|
-
}
|
|
2738
2752
|
return !shouldExcludeCommand(resolvedOverrides, subPath) && !shouldUsePartialDir(resolvedOverrides, subPath)
|
|
2739
2753
|
})
|
|
2740
2754
|
.map(sub => {
|
|
@@ -2756,6 +2770,13 @@ async function generateRpkDocs(options = {}) {
|
|
|
2756
2770
|
}
|
|
2757
2771
|
}
|
|
2758
2772
|
|
|
2773
|
+
// rpk cloud and rpk security secret pages are routed to the cloud
|
|
2774
|
+
// partials directory and published by cloud-docs, so rows in regular
|
|
2775
|
+
// pages must link across to the cloud component.
|
|
2776
|
+
if (cloudSecretDir && isCloudSecretCommand(subPath) && !isCloudSecretCommand(commandPath)) {
|
|
2777
|
+
xrefPath = `${CLOUD_DOCS_COMPONENT}:${xrefPath}`
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2759
2780
|
// Use overridden description if available
|
|
2760
2781
|
const subOverride = resolvedOverrides?.commands?.[subPath]
|
|
2761
2782
|
const subDescription = subOverride?.description || sub.description || ''
|
|
@@ -2765,6 +2786,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2765
2786
|
fullPath: subPath,
|
|
2766
2787
|
dashifiedPath: dashifiedSubPath,
|
|
2767
2788
|
xrefPath,
|
|
2789
|
+
cloudOnly: subOverride?.cloudOnly === true,
|
|
2790
|
+
selfHostedOnly: subOverride?.selfHostedOnly === true,
|
|
2768
2791
|
shortDesc: ensurePeriod(capToTwoSentences(formatDescription(subDescription, textTransformations, { skipTableConversion: true, skipListConversion: true })))
|
|
2769
2792
|
}
|
|
2770
2793
|
})
|
|
@@ -2949,10 +2972,8 @@ async function generateRpkDocs(options = {}) {
|
|
|
2949
2972
|
|
|
2950
2973
|
// Determine output path
|
|
2951
2974
|
// Check if this command should go to cloudSecretDir
|
|
2952
|
-
const isCloudCommand = commandPath.startsWith('rpk cloud')
|
|
2953
|
-
const isSecuritySecretCommand = commandPath.startsWith('rpk security secret')
|
|
2954
2975
|
const useCloudSecretDir = cloudSecretDir && (
|
|
2955
|
-
|
|
2976
|
+
isCloudSecretCommand(commandPath) || shouldUsePartialDir(resolvedOverrides, commandPath)
|
|
2956
2977
|
)
|
|
2957
2978
|
const effectiveOutputDir = useCloudSecretDir ? cloudSecretDir : outputDir
|
|
2958
2979
|
|
|
@@ -8,6 +8,7 @@ const os = require('os')
|
|
|
8
8
|
const semver = require('semver')
|
|
9
9
|
const { findRepoRoot } = require('../../cli-utils/doc-tools-utils')
|
|
10
10
|
const { generateRpkDocs, applyOverridesToTree, resolveReferences, shouldExcludeCommand, shouldUsePartialDir } = require('./generate-rpk-docs')
|
|
11
|
+
const { detectLinuxOnlyFromSource, warnIfDetectionLooksBroken } = require('./detect-platform-commands')
|
|
11
12
|
const { generateRpkDiff, printDiffReport, generateWhatsNewSection, flattenToMap } = require('./report-delta')
|
|
12
13
|
const { loadAndValidateOverrides, ValidationResult } = require('./validate-overrides')
|
|
13
14
|
const { validateDirectory, formatResults } = require('./validate-output')
|
|
@@ -656,105 +657,6 @@ function fetchRpkTreeFromLinuxSource(sourcePath, pluginPins = {}) {
|
|
|
656
657
|
}
|
|
657
658
|
}
|
|
658
659
|
|
|
659
|
-
/**
|
|
660
|
-
* Scan Go source files for Linux-only build tags
|
|
661
|
-
* Looks for //go:build linux or // +build linux
|
|
662
|
-
* @param {string} sourcePath - Path to rpk Go source directory
|
|
663
|
-
* @returns {Set<string>} Set of Linux-only command paths (e.g., 'rpk redpanda tune')
|
|
664
|
-
*/
|
|
665
|
-
function detectLinuxOnlyFromSource(sourcePath) {
|
|
666
|
-
const linuxOnlyCommands = new Set()
|
|
667
|
-
|
|
668
|
-
// Map of source directory patterns to command paths
|
|
669
|
-
// rpk commands are typically in pkg/cli/cmd/<command>/<subcommand>/
|
|
670
|
-
const scanDirectory = (dir, prefix = '') => {
|
|
671
|
-
if (!fs.existsSync(dir)) return
|
|
672
|
-
|
|
673
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
674
|
-
|
|
675
|
-
for (const entry of entries) {
|
|
676
|
-
const fullPath = path.join(dir, entry.name)
|
|
677
|
-
|
|
678
|
-
if (entry.isDirectory()) {
|
|
679
|
-
// Recurse into subdirectories
|
|
680
|
-
const newPrefix = prefix ? `${prefix} ${entry.name}` : entry.name
|
|
681
|
-
scanDirectory(fullPath, newPrefix)
|
|
682
|
-
} else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
|
|
683
|
-
// Check Go files for build tags
|
|
684
|
-
try {
|
|
685
|
-
const content = fs.readFileSync(fullPath, 'utf8')
|
|
686
|
-
const firstLines = content.split('\n').slice(0, 20).join('\n')
|
|
687
|
-
|
|
688
|
-
// Check for Linux build constraints
|
|
689
|
-
const hasLinuxTag = /\/\/go:build\s+.*linux/.test(firstLines) ||
|
|
690
|
-
/\/\/\s*\+build\s+.*linux/.test(firstLines)
|
|
691
|
-
|
|
692
|
-
if (hasLinuxTag) {
|
|
693
|
-
// Try to determine command path from file location
|
|
694
|
-
const commandPath = inferCommandPath(sourcePath, fullPath, entry.name)
|
|
695
|
-
if (commandPath) {
|
|
696
|
-
linuxOnlyCommands.add(commandPath)
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
} catch (err) {
|
|
700
|
-
// Skip files we can't read
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// Scan the cmd/rpk directory for command implementations
|
|
707
|
-
const cmdDir = path.join(sourcePath, 'pkg', 'cli', 'cmd')
|
|
708
|
-
if (fs.existsSync(cmdDir)) {
|
|
709
|
-
scanDirectory(cmdDir, 'rpk')
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Also check the older structure
|
|
713
|
-
const oldCmdDir = path.join(sourcePath, 'cmd', 'rpk')
|
|
714
|
-
if (fs.existsSync(oldCmdDir)) {
|
|
715
|
-
scanDirectory(oldCmdDir, 'rpk')
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return linuxOnlyCommands
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
/**
|
|
722
|
-
* Infer command path from Go source file location
|
|
723
|
-
* @param {string} sourcePath - Root source path
|
|
724
|
-
* @param {string} filePath - Full path to Go file
|
|
725
|
-
* @param {string} fileName - Name of the Go file
|
|
726
|
-
* @returns {string|null} Command path or null
|
|
727
|
-
*/
|
|
728
|
-
function inferCommandPath(sourcePath, filePath, fileName) {
|
|
729
|
-
// Get relative path from source root
|
|
730
|
-
const relativePath = filePath.replace(sourcePath, '').replace(/^\//, '')
|
|
731
|
-
|
|
732
|
-
// Common patterns:
|
|
733
|
-
// pkg/cli/cmd/redpanda/tune.go -> rpk redpanda tune
|
|
734
|
-
// pkg/cli/cmd/redpanda/start.go -> rpk redpanda start
|
|
735
|
-
// cmd/rpk/iotune/iotune.go -> rpk iotune
|
|
736
|
-
|
|
737
|
-
// Remove common prefixes and file extension
|
|
738
|
-
let parts = relativePath
|
|
739
|
-
.replace(/^pkg\/cli\/cmd\//, '')
|
|
740
|
-
.replace(/^cmd\/rpk\//, '')
|
|
741
|
-
.replace(/\.go$/, '')
|
|
742
|
-
.split('/')
|
|
743
|
-
|
|
744
|
-
// If file name matches directory name, use directory only
|
|
745
|
-
// e.g., iotune/iotune.go -> iotune
|
|
746
|
-
if (parts.length > 1 && parts[parts.length - 1] === parts[parts.length - 2]) {
|
|
747
|
-
parts = parts.slice(0, -1)
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
// Filter out common non-command directories
|
|
751
|
-
parts = parts.filter(p => !['internal', 'common', 'config', 'cmd'].includes(p))
|
|
752
|
-
|
|
753
|
-
if (parts.length === 0) return null
|
|
754
|
-
|
|
755
|
-
return 'rpk ' + parts.join(' ')
|
|
756
|
-
}
|
|
757
|
-
|
|
758
660
|
/**
|
|
759
661
|
* Add platform markers to command tree based on source analysis
|
|
760
662
|
* @param {Object} tree - Command tree from rpk --print-tree
|
|
@@ -769,11 +671,18 @@ function addPlatformMarkersFromSource(tree, linuxOnlyCommands) {
|
|
|
769
671
|
[...linuxOnlyCommands].some(loc => cmdPath.startsWith(loc + ' '))
|
|
770
672
|
}
|
|
771
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
|
+
|
|
772
679
|
const markCommands = (commands, parentPath = 'rpk') => {
|
|
773
680
|
if (!commands) return commands
|
|
774
681
|
return commands.map(cmd => {
|
|
775
682
|
const fullPath = `${parentPath} ${cmd.name}`
|
|
776
|
-
const
|
|
683
|
+
const linuxOnly = isLinuxOnly(fullPath)
|
|
684
|
+
if (linuxOnly) markedPaths.add(fullPath)
|
|
685
|
+
const platforms = linuxOnly
|
|
777
686
|
? [PLATFORMS.LINUX]
|
|
778
687
|
: [PLATFORMS.LINUX, PLATFORMS.DARWIN]
|
|
779
688
|
return {
|
|
@@ -792,11 +701,15 @@ function addPlatformMarkersFromSource(tree, linuxOnlyCommands) {
|
|
|
792
701
|
}
|
|
793
702
|
}
|
|
794
703
|
|
|
704
|
+
const markedCommands = markCommands(tree.commands)
|
|
705
|
+
|
|
795
706
|
return {
|
|
796
707
|
...tree,
|
|
797
708
|
platforms: [PLATFORMS.LINUX, PLATFORMS.DARWIN],
|
|
798
|
-
|
|
799
|
-
|
|
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
|
|
800
713
|
}
|
|
801
714
|
}
|
|
802
715
|
|
|
@@ -1166,6 +1079,37 @@ function makeLinkablePredicate(overridesData) {
|
|
|
1166
1079
|
}
|
|
1167
1080
|
}
|
|
1168
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
|
+
|
|
1169
1113
|
function updateWhatsNewFile(diffData, whatsNewPath, version, options = {}) {
|
|
1170
1114
|
// Each block opens with a "=== <version>" heading so accumulated blocks
|
|
1171
1115
|
// (successive RCs, multiple plugin releases) never collide on section ids
|
|
@@ -1437,8 +1381,28 @@ function acquireRpkBinary(rpkVersion, options = {}) {
|
|
|
1437
1381
|
console.warn(`Native build failed (${nativeErr.message.split('\n')[0]}); building in a container...`)
|
|
1438
1382
|
const goVersion = getRequiredGoVersion(sourcePath)
|
|
1439
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'
|
|
1440
1401
|
const buildResult = spawnSync('docker', [
|
|
1441
1402
|
'run', '--rm',
|
|
1403
|
+
'-e', `GOOS=${hostGoos}`,
|
|
1404
|
+
'-e', `GOARCH=${hostGoarch}`,
|
|
1405
|
+
'-e', 'CGO_ENABLED=0',
|
|
1442
1406
|
'-v', `${path.resolve(sourcePath)}:/rpk-source:ro`,
|
|
1443
1407
|
'-v', `${workDir}:/out`,
|
|
1444
1408
|
'-w', '/rpk-source',
|
|
@@ -2001,10 +1965,14 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2001
1965
|
// render without xrefs because plugin subtrees may render as
|
|
2002
1966
|
// partials with no linkable pages.
|
|
2003
1967
|
if (whatsNewPath) {
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
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
|
+
}
|
|
2008
1976
|
}
|
|
2009
1977
|
}
|
|
2010
1978
|
|
|
@@ -2117,7 +2085,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2117
2085
|
|
|
2118
2086
|
// Update what's-new file if requested
|
|
2119
2087
|
if (whatsNewPath) {
|
|
2120
|
-
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
2088
|
+
updateWhatsNewFile(filterDiffForWhatsNew(diffData), whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
2121
2089
|
}
|
|
2122
2090
|
} else {
|
|
2123
2091
|
console.warn(`Warning: Could not load previous version ${diffVersion} for diff`)
|
|
@@ -2217,10 +2185,17 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2217
2185
|
rpkVersion = effectiveRef || 'local'
|
|
2218
2186
|
|
|
2219
2187
|
// Step 2: Detect Linux-only commands
|
|
2220
|
-
//
|
|
2221
|
-
//
|
|
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.
|
|
2222
2191
|
console.log('\nAnalyzing source for Linux-only commands...')
|
|
2223
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
|
+
}
|
|
2224
2199
|
|
|
2225
2200
|
// Step 3: Build rpk and get command tree
|
|
2226
2201
|
// Use Docker if available (for running rpk plugins in Linux)
|
|
@@ -2230,8 +2205,19 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2230
2205
|
const canBuildNative = goCheck.status === 0
|
|
2231
2206
|
const canBuildLinux = dockerCheck.status === 0
|
|
2232
2207
|
|
|
2233
|
-
// Dynamic detection: if we can build on both platforms, compare the trees
|
|
2234
|
-
|
|
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) {
|
|
2235
2221
|
console.log('\nBuilding rpk on both Linux and Darwin for dynamic platform detection...')
|
|
2236
2222
|
|
|
2237
2223
|
try {
|
|
@@ -2297,6 +2283,12 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2297
2283
|
)
|
|
2298
2284
|
}
|
|
2299
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
|
+
|
|
2300
2292
|
// Step 4: Add platform markers based on detection
|
|
2301
2293
|
// This includes both source scanning results and static fallback lists
|
|
2302
2294
|
tree = addPlatformMarkersFromSource(tree, linuxOnlyCommands)
|
|
@@ -2413,7 +2405,7 @@ async function handleRpkDocsGeneration(options = {}) {
|
|
|
2413
2405
|
|
|
2414
2406
|
// Update what's-new file if requested
|
|
2415
2407
|
if (whatsNewPath) {
|
|
2416
|
-
updateWhatsNewFile(diffData, whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
2408
|
+
updateWhatsNewFile(filterDiffForWhatsNew(diffData), whatsNewPath, rpkVersion, { linkable: makeLinkablePredicate(overridesData), hasSubcommands: makeSubcommandPredicate(tree) })
|
|
2417
2409
|
}
|
|
2418
2410
|
} else {
|
|
2419
2411
|
console.warn(`Warning: Could not load previous version ${diffVersion} for diff`)
|
|
@@ -2827,6 +2819,7 @@ module.exports = {
|
|
|
2827
2819
|
getPlatformDescription,
|
|
2828
2820
|
getCurrentPlatform,
|
|
2829
2821
|
updateOverridesWithIntroducedVersions,
|
|
2822
|
+
filterDiffForWhatsNew,
|
|
2830
2823
|
computeDescriptionCoverage,
|
|
2831
2824
|
updateWhatsNewFile,
|
|
2832
2825
|
KNOWN_PLUGINS,
|
|
@@ -389,8 +389,20 @@ This section provides examples of how to use `{{commandPath}}`.
|
|
|
389
389
|
|Command |Description
|
|
390
390
|
|
|
391
391
|
{{#each subcommands}}
|
|
392
|
+
{{#if cloudOnly}}
|
|
393
|
+
ifdef::env-cloud[]
|
|
394
|
+
{{/if}}
|
|
395
|
+
{{#if selfHostedOnly}}
|
|
396
|
+
ifndef::env-cloud[]
|
|
397
|
+
{{/if}}
|
|
392
398
|
|xref:{{xrefPath}}[`{{fullPath}}`]
|
|
393
399
|
|{{{shortDesc}}}
|
|
400
|
+
{{#if cloudOnly}}
|
|
401
|
+
endif::[]
|
|
402
|
+
{{/if}}
|
|
403
|
+
{{#if selfHostedOnly}}
|
|
404
|
+
endif::[]
|
|
405
|
+
{{/if}}
|
|
394
406
|
|
|
395
407
|
{{/each}}
|
|
396
408
|
|===
|
|
@@ -465,8 +465,24 @@ function validatePlatforms(commandOverride, context) {
|
|
|
465
465
|
function validateFlags(commandOverride, context) {
|
|
466
466
|
const result = new ValidationResult()
|
|
467
467
|
|
|
468
|
+
// cloudOnly and selfHostedOnly are mutually exclusive: setting both wraps
|
|
469
|
+
// the row in an ifdef::env-cloud + ifndef::env-cloud pair that no build
|
|
470
|
+
// can satisfy, so the row silently vanishes from BOTH sites.
|
|
471
|
+
if (commandOverride.cloudOnly && commandOverride.selfHostedOnly) {
|
|
472
|
+
result.addError(
|
|
473
|
+
'cloudOnly and selfHostedOnly are both set: the command row is unsatisfiable and disappears from every build. Set at most one.',
|
|
474
|
+
context
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
468
478
|
if (commandOverride.flags) {
|
|
469
479
|
for (const [flagName, flagOverride] of Object.entries(commandOverride.flags)) {
|
|
480
|
+
if (flagOverride.cloudOnly && flagOverride.selfHostedOnly) {
|
|
481
|
+
result.addError(
|
|
482
|
+
`cloudOnly and selfHostedOnly are both set on flag "${flagName}": the flag row is unsatisfiable and disappears from every build. Set at most one.`,
|
|
483
|
+
`${context}.flags.${flagName}`
|
|
484
|
+
)
|
|
485
|
+
}
|
|
470
486
|
// Validate flag name format
|
|
471
487
|
if (!flagName.match(/^[a-zA-Z][-a-zA-Z0-9]*$/)) {
|
|
472
488
|
result.addWarning(
|