@redpanda-data/docs-extensions-and-macros 5.30.0 → 5.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools-mcp.js +1 -1
- package/bin/doc-tools.js +5 -1
- package/bin/mcp-tools/lint-doc-strings.js +2 -1
- package/package.json +1 -1
- package/tools/lint-strings/diff.js +15 -1
- package/tools/lint-strings/fixtures/api/lint_admin_v2.proto +114 -0
- package/tools/lint-strings/fixtures/api/lint_api.proto +123 -0
- package/tools/lint-strings/fixtures/connect/lint_connect.go +1 -1
- package/tools/lint-strings/fixtures/crd/lint_types.go +70 -0
- package/tools/lint-strings/fixtures/helm/lint_values.yaml +16 -3
- package/tools/lint-strings/index.js +2 -1
- package/tools/lint-strings/rules/common.js +87 -1
- package/tools/lint-strings/surfaces/api.js +786 -0
- package/tools/lint-strings/surfaces/crd.js +204 -7
- package/tools/lint-strings/surfaces/helm.js +338 -12
- package/tools/property-extractor/Makefile +11 -0
- package/tools/property-extractor/merge-rp-util.js +208 -0
- package/tools/property-extractor/rp-util-fetch.js +447 -0
- package/tools/property-extractor/rp_util_merge.py +599 -0
- package/tools/property-extractor/templates/property.hbs +12 -5
- package/tools/property-extractor/templates/topic-property.hbs +7 -4
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const path = require('path')
|
|
5
|
+
const yaml = require('js-yaml')
|
|
5
6
|
|
|
6
7
|
const { SourceCache } = require('../source-text')
|
|
7
|
-
const { parseValuesFile: parseSharedValuesFile } = require('../../../cli-utils/helm-commented-values')
|
|
8
|
+
const { parseValuesFile: parseSharedValuesFile, PATTERNS } = require('../../../cli-utils/helm-commented-values')
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Helm surface: helm-docs description comments in chart values.yaml files
|
|
@@ -33,6 +34,71 @@ const { parseValuesFile: parseSharedValuesFile } = require('../../../cli-utils/h
|
|
|
33
34
|
* description silently never ships - that is the error this surface exists
|
|
34
35
|
* to catch.
|
|
35
36
|
*
|
|
37
|
+
* A MISSING description has to be judged at the level helm-docs actually
|
|
38
|
+
* publishes, which is not the level a values.yaml author thinks in.
|
|
39
|
+
* helm-docs renders one row per key that either carries a `# --` marker or
|
|
40
|
+
* is a leaf with no marked ancestor; a marked key documents its whole
|
|
41
|
+
* subtree, so its unmarked children are never rendered at all. Verified by
|
|
42
|
+
* running helm-docs 1.14.2 over all three charts and diffing the predicted
|
|
43
|
+
* blank set against the real output: exact on redpanda (20) and connectors
|
|
44
|
+
* (30), and one known divergence on console, below.
|
|
45
|
+
*
|
|
46
|
+
* The consequences for detection:
|
|
47
|
+
*
|
|
48
|
+
* * An unmarked PARENT (`statefulset:`, `podTemplate:`) is not published
|
|
49
|
+
* as a row, so telling anyone to document it is wrong - helm-docs never
|
|
50
|
+
* shows it. The old undocumented-top-level-key rule reported exactly
|
|
51
|
+
* these, and all four of its findings on the redpanda chart were keys
|
|
52
|
+
* absent from the published reference.
|
|
53
|
+
* * The keys that DO ship blank are unmarked leaves under unmarked
|
|
54
|
+
* parents (`statefulset.budget.maxUnavailable`, `tests.enabled`). There
|
|
55
|
+
* were 20 of them in the redpanda chart and the old rule, being
|
|
56
|
+
* top-level only, could not report one.
|
|
57
|
+
*
|
|
58
|
+
* Leaf-ness follows helm-docs: a non-empty array of objects is descended by
|
|
59
|
+
* index (`ingress.hosts[0].host`), while an array of scalars and an empty
|
|
60
|
+
* array are single rows. `# @ignored` drops a key and its subtree.
|
|
61
|
+
*
|
|
62
|
+
* The two pipelines can also attach ONE marker to TWO different keys, which
|
|
63
|
+
* is its own defect and needs its own rule. helm-docs uses the nearest `# --`
|
|
64
|
+
* above a real key and does not care that the marker was written for a
|
|
65
|
+
* commented-out key in between, so
|
|
66
|
+
*
|
|
67
|
+
* # -- Override the value in `console.config.server.listenPort`
|
|
68
|
+
* # targetPort:
|
|
69
|
+
* annotations: {}
|
|
70
|
+
*
|
|
71
|
+
* publishes that text as `service.annotations`' description, while the
|
|
72
|
+
* helm-spec pass injects the same text under `service.targetPort`. The text
|
|
73
|
+
* ships twice, once under a key it does not describe. `misattached-marker`
|
|
74
|
+
* reports it on the marker itself and names both keys.
|
|
75
|
+
*
|
|
76
|
+
* Two things about helm-docs' attachment are easy to get wrong, and both were
|
|
77
|
+
* wrong here until helm-docs 1.14.2 was run on isolated fixtures to settle
|
|
78
|
+
* them:
|
|
79
|
+
*
|
|
80
|
+
* * A BLANK LINE DOES NOT BREAK ATTACHMENT. A `# --` attaches to the next
|
|
81
|
+
* real key in document order however many blank lines intervene; only a
|
|
82
|
+
* later `# --` supersedes it. (A marker at the very top of a file, before
|
|
83
|
+
* any key, attaches to nothing.) Predicting attachment this way agrees
|
|
84
|
+
* with helm-docs on 227 of the 228 markers in the three charts, the
|
|
85
|
+
* single difference being an `@raw` annotation line it strips.
|
|
86
|
+
* * helm-docs' MARKER FORM IS STRICTER than this repo's. It needs exactly
|
|
87
|
+
* one space, `# -- text`; an indented `# -- text` is invisible to it,
|
|
88
|
+
* while `DESC_MARKER_RE` here accepts both. That gap is the whole reason
|
|
89
|
+
* the walk can attribute a marker helm-docs never sees, and it is why the
|
|
90
|
+
* markers buried in a commented-out subtree really are dead: they are all
|
|
91
|
+
* written in the indented form.
|
|
92
|
+
*
|
|
93
|
+
* So a marker the walk calls dead is only dead when helm-docs cannot see it
|
|
94
|
+
* or no key follows it; a visible one separated by blank lines is not dead,
|
|
95
|
+
* it is misattached, and `misattached-marker` says so instead.
|
|
96
|
+
*
|
|
97
|
+
* Modelling all of this is also what makes the blank-key set exact: a
|
|
98
|
+
* misattached target is NOT blank, so it must not be reported as
|
|
99
|
+
* undocumented. With it, the predicted blank set equals helm-docs' own output
|
|
100
|
+
* on all three charts (20/20, 36/36, 30/30).
|
|
101
|
+
*
|
|
36
102
|
* helm-docs output is markdown converted via pandoc, not verbatim AsciiDoc,
|
|
37
103
|
* so the verbatim escaping rules do not apply. Terminal periods are
|
|
38
104
|
* optional prose style here; only capitalization is enforced (via the
|
|
@@ -45,6 +111,183 @@ const CONVENTION = {
|
|
|
45
111
|
verbatim_asciidoc: false
|
|
46
112
|
}
|
|
47
113
|
|
|
114
|
+
// Block-scalar and real-key recognition come from the shared walk's own
|
|
115
|
+
// patterns rather than a second copy here; a divergence between the two is
|
|
116
|
+
// exactly the drift this surface's guard test forbids.
|
|
117
|
+
const { BLOCK_SCALAR_RE, REAL_KEY_RE } = PATTERNS
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* The marker form helm-docs itself recognizes: exactly one space between the
|
|
121
|
+
* hash and the dashes. Verified against helm-docs 1.14.2 - `# -- text`
|
|
122
|
+
* attaches, `# -- text` produces no description at all.
|
|
123
|
+
*
|
|
124
|
+
* Deliberately stricter than the shared `DESC_MARKER_RE`, which this repo's
|
|
125
|
+
* own pass uses. A marker only the loose pattern matches is attributed by
|
|
126
|
+
* doc-tools and ignored by helm-docs.
|
|
127
|
+
*/
|
|
128
|
+
const HELM_DOCS_MARKER_RE = /^\s*#\s--\s/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Walk the raw text for every real key's dotted path, its line, and whether
|
|
132
|
+
* its comment block carries `# @ignored`.
|
|
133
|
+
*
|
|
134
|
+
* Done on the text rather than the parsed tree because findings need line
|
|
135
|
+
* numbers, and because a block scalar's BODY must not be mistaken for keys -
|
|
136
|
+
* `config: |` followed by indented `some: content` is content, not structure.
|
|
137
|
+
*
|
|
138
|
+
* @returns {{lines: Map<string, number>, ignored: Set<string>}} 0-indexed lines
|
|
139
|
+
*/
|
|
140
|
+
function scanKeyLines (content) {
|
|
141
|
+
const lines = new Map()
|
|
142
|
+
const ignored = new Set()
|
|
143
|
+
const stack = []
|
|
144
|
+
let block = []
|
|
145
|
+
let blockScalarIndent = null
|
|
146
|
+
const rows = content.split('\n')
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < rows.length; i++) {
|
|
149
|
+
const row = rows[i]
|
|
150
|
+
if (blockScalarIndent !== null) {
|
|
151
|
+
const indent = row.search(/\S/)
|
|
152
|
+
if (row.trim() === '' || indent > blockScalarIndent) continue
|
|
153
|
+
blockScalarIndent = null
|
|
154
|
+
}
|
|
155
|
+
if (/^\s*#/.test(row)) {
|
|
156
|
+
block.push(row)
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
const match = REAL_KEY_RE.exec(row)
|
|
160
|
+
if (!match) {
|
|
161
|
+
if (row.trim() !== '') block = []
|
|
162
|
+
continue
|
|
163
|
+
}
|
|
164
|
+
const indent = match[1].length
|
|
165
|
+
while (stack.length > 0 && stack[stack.length - 1].indent >= indent) stack.pop()
|
|
166
|
+
stack.push({ indent, key: match[2] })
|
|
167
|
+
const dotted = stack.map((entry) => entry.key).join('.')
|
|
168
|
+
lines.set(dotted, i)
|
|
169
|
+
if (block.some((b) => /^\s*#\s*@ignored\b/.test(b))) ignored.add(dotted)
|
|
170
|
+
block = []
|
|
171
|
+
// The shared pattern matches the value side, so test the captured value.
|
|
172
|
+
if (BLOCK_SCALAR_RE.test(match[3].trim())) blockScalarIndent = indent
|
|
173
|
+
}
|
|
174
|
+
return { lines, ignored }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Markers that helm-docs will attach to a different key than the helm-spec
|
|
179
|
+
* pass does: `commentedOutPath -> realKeyPath`.
|
|
180
|
+
*
|
|
181
|
+
* The walk decides what documents a commented-out key, so this only has to
|
|
182
|
+
* answer what helm-docs does with the same marker. Walking down from the
|
|
183
|
+
* commented-out key line: a blank line ends the comment block (helm-docs then
|
|
184
|
+
* sees no marker), a further `# --` gives the real key its own description,
|
|
185
|
+
* and anything else lands on the next real key.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} content - values.yaml text
|
|
188
|
+
* @param {Array} records - shared-walk records
|
|
189
|
+
* @param {Map<number, string>} pathByLine - 0-indexed line -> dotted path
|
|
190
|
+
* @param {Set<string>} ignored - paths carrying `# @ignored`
|
|
191
|
+
* @returns {Map<string, string>}
|
|
192
|
+
*/
|
|
193
|
+
function misattachedMarkers (content, records, pathByLine, ignored = new Set()) {
|
|
194
|
+
const rows = content.split('\n')
|
|
195
|
+
const out = new Map()
|
|
196
|
+
for (const record of records) {
|
|
197
|
+
// Both shapes go wrong the same way: a marker this repo attributes to a
|
|
198
|
+
// commented-out key, and one it calls dead, are each handed by helm-docs
|
|
199
|
+
// to the next real key below.
|
|
200
|
+
const isCommentedOut = record.commentedOut && record.path
|
|
201
|
+
const isDead = record.kind === 'dead-marker'
|
|
202
|
+
if (!isCommentedOut && !isDead) continue
|
|
203
|
+
// helm-docs has to be able to see the marker in the first place.
|
|
204
|
+
if (!HELM_DOCS_MARKER_RE.test(rows[record.lineStart] || '')) continue
|
|
205
|
+
|
|
206
|
+
let i = record.lineEnd + 1
|
|
207
|
+
let superseded = false
|
|
208
|
+
while (i < rows.length) {
|
|
209
|
+
const row = rows[i]
|
|
210
|
+
// A blank line does NOT end attachment; only a later visible marker
|
|
211
|
+
// does, by taking the real key for itself.
|
|
212
|
+
if (row.trim() === '') { i++; continue }
|
|
213
|
+
if (/^\s*#/.test(row)) {
|
|
214
|
+
if (HELM_DOCS_MARKER_RE.test(row)) { superseded = true; break }
|
|
215
|
+
i++
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
break
|
|
219
|
+
}
|
|
220
|
+
if (superseded || i >= rows.length) continue
|
|
221
|
+
if (!REAL_KEY_RE.test(rows[i])) continue
|
|
222
|
+
const target = pathByLine.get(i)
|
|
223
|
+
if (!target) continue
|
|
224
|
+
// An `@ignored` key is not published at all, so a marker landing there
|
|
225
|
+
// still ships nowhere: that is dead-marker's finding, not this one.
|
|
226
|
+
if (ignored.has(target) || ancestorIn(target, ignored)) continue
|
|
227
|
+
out.set(isCommentedOut ? record.path : `@marker:${record.lineStart}`, target)
|
|
228
|
+
}
|
|
229
|
+
return out
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** True when any strict ancestor of `dotted` is in `set`. */
|
|
233
|
+
function ancestorIn (dotted, set) {
|
|
234
|
+
const parts = dotted.replace(/\[\d+\]/g, '').split('.')
|
|
235
|
+
for (let i = 1; i < parts.length; i++) {
|
|
236
|
+
if (set.has(parts.slice(0, i).join('.'))) return true
|
|
237
|
+
}
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The keys helm-docs will publish with an EMPTY description: unmarked leaves
|
|
243
|
+
* that no marked (or `@ignored`) ancestor covers.
|
|
244
|
+
*
|
|
245
|
+
* @param {string} content - values.yaml text
|
|
246
|
+
* @param {Set<string>} marked - paths carrying a `# --` description
|
|
247
|
+
* @param {Set<string>} ignored - paths carrying `# @ignored`
|
|
248
|
+
* @returns {string[]} dotted paths, array elements indexed
|
|
249
|
+
*/
|
|
250
|
+
function publishableBlankKeys (content, marked, ignored, misattachedTo = new Set()) {
|
|
251
|
+
let tree
|
|
252
|
+
try {
|
|
253
|
+
tree = yaml.load(content)
|
|
254
|
+
} catch (err) {
|
|
255
|
+
// A values.yaml this surface cannot parse is not a doc-string defect;
|
|
256
|
+
// the chart's own CI owns that. Report nothing rather than guess.
|
|
257
|
+
return []
|
|
258
|
+
}
|
|
259
|
+
if (!tree || typeof tree !== 'object') return []
|
|
260
|
+
|
|
261
|
+
const leaves = []
|
|
262
|
+
const visit = (node, dotted) => {
|
|
263
|
+
if (Array.isArray(node)) {
|
|
264
|
+
// helm-docs descends an array by index only when its elements are
|
|
265
|
+
// themselves structured; an array of scalars is a single row.
|
|
266
|
+
if (!node.some((el) => el && typeof el === 'object')) {
|
|
267
|
+
if (dotted) leaves.push(dotted)
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
node.forEach((el, index) => visit(el, `${dotted}[${index}]`))
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
if (node && typeof node === 'object' && Object.keys(node).length > 0) {
|
|
274
|
+
for (const key of Object.keys(node)) visit(node[key], dotted ? `${dotted}.${key}` : key)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
if (dotted) leaves.push(dotted)
|
|
278
|
+
}
|
|
279
|
+
visit(tree, '')
|
|
280
|
+
|
|
281
|
+
return leaves.filter((leaf) => {
|
|
282
|
+
const plain = leaf.replace(/\[\d+\]/g, '')
|
|
283
|
+
if (marked.has(plain) || ignored.has(plain)) return false
|
|
284
|
+
// A key helm-docs hands a misattached marker to is not blank - it ships
|
|
285
|
+
// the wrong description, which misattached-marker reports instead.
|
|
286
|
+
if (misattachedTo.has(plain)) return false
|
|
287
|
+
return !ancestorIn(leaf, marked) && !ancestorIn(leaf, ignored)
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
48
291
|
/**
|
|
49
292
|
* Parse one values.yaml into lint declarations. Exported for tests.
|
|
50
293
|
*
|
|
@@ -56,18 +299,44 @@ const CONVENTION = {
|
|
|
56
299
|
* @returns {Array} declarations (without declaration_text)
|
|
57
300
|
*/
|
|
58
301
|
function parseValuesFile (content, file) {
|
|
59
|
-
|
|
302
|
+
const records = parseSharedValuesFile(content, { attachRealKeys: true })
|
|
303
|
+
const { lines: keyLines, ignored } = scanKeyLines(content)
|
|
304
|
+
const marked = new Set(
|
|
305
|
+
records
|
|
306
|
+
.filter((r) => r.kind !== 'dead-marker' && !r.undocumented && r.path)
|
|
307
|
+
.map((r) => r.path)
|
|
308
|
+
)
|
|
309
|
+
const pathByLine = new Map([...keyLines].map(([dotted, line]) => [line, dotted]))
|
|
310
|
+
const misattached = misattachedMarkers(content, records, pathByLine, ignored)
|
|
311
|
+
const misattachedTo = new Set(misattached.values())
|
|
312
|
+
|
|
313
|
+
const declarations = records
|
|
314
|
+
// The walk's own undocumented records are top-level parents, which
|
|
315
|
+
// helm-docs does not publish as rows. They are replaced below by the
|
|
316
|
+
// leaves it does publish.
|
|
317
|
+
.filter((r) => !(r.kind === 'key' && r.undocumented))
|
|
318
|
+
.map((r) => {
|
|
60
319
|
// The generator keeps newlines in a description; helm-docs renders one
|
|
61
320
|
// paragraph, and the rules read one string, so flatten here.
|
|
62
321
|
const string = r.descLines.map((l) => l.trim()).filter(Boolean).join(' ') || null
|
|
63
322
|
let meta
|
|
64
323
|
if (r.kind === 'dead-marker') {
|
|
65
|
-
meta = {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
324
|
+
meta = {
|
|
325
|
+
kind: 'dead-marker',
|
|
326
|
+
unverifiable: true,
|
|
327
|
+
// A marker helm-docs CAN see is not dead: it lands on the next real
|
|
328
|
+
// key rather than nowhere.
|
|
329
|
+
misattached_to: misattached.get(`@marker:${r.lineStart}`) || null
|
|
330
|
+
}
|
|
69
331
|
} else if (r.commentedOut) {
|
|
70
|
-
meta = {
|
|
332
|
+
meta = {
|
|
333
|
+
kind: 'key',
|
|
334
|
+
commented_out: true,
|
|
335
|
+
top_level: r.topLevel,
|
|
336
|
+
default_annotation: r.default || null,
|
|
337
|
+
// Set when helm-docs hands this same marker to a real key below.
|
|
338
|
+
misattached_to: misattached.get(r.path) || null
|
|
339
|
+
}
|
|
71
340
|
} else {
|
|
72
341
|
meta = { kind: 'key', top_level: r.topLevel, raw: Boolean(r.annotations.raw), default_annotation: r.default || null }
|
|
73
342
|
}
|
|
@@ -77,12 +346,45 @@ function parseValuesFile (content, file) {
|
|
|
77
346
|
file,
|
|
78
347
|
line_start: r.lineStart + 1, // 0-indexed -> 1-indexed
|
|
79
348
|
line_end: r.lineEnd + 1,
|
|
80
|
-
string
|
|
349
|
+
string,
|
|
81
350
|
declaration_text: null,
|
|
82
351
|
convention: CONVENTION,
|
|
83
352
|
meta
|
|
84
353
|
}
|
|
85
354
|
})
|
|
355
|
+
|
|
356
|
+
// One declaration per key helm-docs will render with an empty description.
|
|
357
|
+
for (const key of publishableBlankKeys(content, marked, ignored, misattachedTo)) {
|
|
358
|
+
// An indexed path has no key line of its own; anchor on the nearest
|
|
359
|
+
// ancestor that does, so the finding still points into the file.
|
|
360
|
+
let anchor = keyLines.get(key.replace(/\[\d+\]/g, ''))
|
|
361
|
+
if (anchor === undefined) {
|
|
362
|
+
const parts = key.replace(/\[\d+\]/g, '').split('.')
|
|
363
|
+
for (let i = parts.length - 1; i > 0 && anchor === undefined; i--) {
|
|
364
|
+
anchor = keyLines.get(parts.slice(0, i).join('.'))
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (anchor === undefined) continue
|
|
368
|
+
declarations.push({
|
|
369
|
+
surface: 'helm',
|
|
370
|
+
name: key,
|
|
371
|
+
file,
|
|
372
|
+
line_start: anchor + 1,
|
|
373
|
+
line_end: anchor + 1,
|
|
374
|
+
// Nothing to lint for content: the rule fires on the absence itself.
|
|
375
|
+
string: null,
|
|
376
|
+
declaration_text: null,
|
|
377
|
+
convention: CONVENTION,
|
|
378
|
+
meta: {
|
|
379
|
+
kind: 'key',
|
|
380
|
+
undocumented: true,
|
|
381
|
+
top_level: !key.includes('.'),
|
|
382
|
+
unverifiable: true
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return declarations
|
|
86
388
|
}
|
|
87
389
|
|
|
88
390
|
/**
|
|
@@ -135,17 +437,37 @@ const RULES = [
|
|
|
135
437
|
runOnUnverifiable: true,
|
|
136
438
|
check: (decl) => {
|
|
137
439
|
if (decl.meta.kind !== 'dead-marker') return []
|
|
138
|
-
|
|
440
|
+
// Only dead if helm-docs cannot see it either. A visible marker
|
|
441
|
+
// separated from its key still lands somewhere, which is
|
|
442
|
+
// misattached-marker's finding, not this one.
|
|
443
|
+
if (decl.meta.misattached_to) return []
|
|
444
|
+
return [{ message: 'This "# --" description is buried where neither helm-docs nor the helm-spec commented-values pass can attach it: it sits inside another commented-out key\'s subtree, in the indented `# -- ` form that helm-docs does not recognize as a marker at all. It silently never ships. Move it directly above the key it documents, use "# @doc full.path -- ...", or delete it.' }]
|
|
445
|
+
}
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: 'misattached-marker',
|
|
449
|
+
description: 'One # -- marker is published under two different keys',
|
|
450
|
+
severity: 'error',
|
|
451
|
+
runOnUnverifiable: true,
|
|
452
|
+
check: (decl) => {
|
|
453
|
+
if (!decl.meta.misattached_to) return []
|
|
454
|
+
const target = decl.meta.misattached_to
|
|
455
|
+
const written = decl.meta.kind === 'dead-marker'
|
|
456
|
+
? 'is not attached to any key of its own'
|
|
457
|
+
: `documents the commented-out key "${decl.name}"`
|
|
458
|
+
return [{
|
|
459
|
+
message: `This "# --" ${written}, but helm-docs attaches a marker to the next REAL key below it - blank lines and commented-out keys in between make no difference - so it publishes this text as "${target}"'s description. Give "${target}" its own "# --" description, which supersedes this one, or delete this marker.`
|
|
460
|
+
}]
|
|
139
461
|
}
|
|
140
462
|
},
|
|
141
463
|
{
|
|
142
|
-
name: 'undocumented-
|
|
143
|
-
description: '
|
|
464
|
+
name: 'undocumented-key',
|
|
465
|
+
description: 'Key helm-docs publishes with an empty description',
|
|
144
466
|
severity: 'info',
|
|
145
467
|
runOnUnverifiable: true,
|
|
146
468
|
check: (decl) => {
|
|
147
469
|
if (decl.meta.kind !== 'key' || !decl.meta.undocumented) return []
|
|
148
|
-
return [{ message: `
|
|
470
|
+
return [{ message: `"${decl.name}" has no "# --" description of its own, so helm-docs publishes it with an empty description. Add a marker comment directly above the key (or "# @ignored" if it is not user-facing). Note that documenting an ancestor instead removes the row entirely rather than filling it in.` }]
|
|
149
471
|
}
|
|
150
472
|
}
|
|
151
473
|
]
|
|
@@ -155,5 +477,9 @@ module.exports = {
|
|
|
155
477
|
convention: CONVENTION,
|
|
156
478
|
extract,
|
|
157
479
|
parseValuesFile,
|
|
480
|
+
publishableBlankKeys,
|
|
481
|
+
scanKeyLines,
|
|
482
|
+
misattachedMarkers,
|
|
483
|
+
HELM_DOCS_MARKER_RE,
|
|
158
484
|
rules: RULES
|
|
159
485
|
}
|
|
@@ -57,6 +57,17 @@ build: venv redpanda-git treesitter
|
|
|
57
57
|
$(if $(CLOUD_SUPPORT),--cloud-support,)
|
|
58
58
|
@echo "✅ JSON generated at $(TOOL_ROOT)/gen/properties-output.json"
|
|
59
59
|
@echo "✅ Enhanced JSON (with overrides) generated at $(TOOL_ROOT)/gen/$(TAG)-properties.json"
|
|
60
|
+
@if [ -z "$(SKIP_RP_UTIL)" ]; then \
|
|
61
|
+
echo "🔀 Merging rp_util schema for cluster/broker scope…"; \
|
|
62
|
+
cd $(TOOL_ROOT) && node merge-rp-util.js \
|
|
63
|
+
--tag $(TAG) \
|
|
64
|
+
--enhanced gen/$(TAG)-properties.json \
|
|
65
|
+
--output gen/$(TAG)-properties.json \
|
|
66
|
+
$(if $(OVERRIDES),$(if $(shell [ -f "$(OVERRIDES)" ] && echo exists),--overrides $(OVERRIDES),),) \
|
|
67
|
+
$(if $(CLOUD_SUPPORT),--cloud-support,); \
|
|
68
|
+
else \
|
|
69
|
+
echo "⏭️ Skipping rp_util merge (SKIP_RP_UTIL set) -- cluster/broker scope stays Tree-sitter-only"; \
|
|
70
|
+
fi
|
|
60
71
|
@$(MAKE) generate-docs
|
|
61
72
|
|
|
62
73
|
# --- Ensure Python venv & dependencies ---
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Overwrite cluster/broker scope in an already-extracted --enhanced-output
|
|
6
|
+
* file with rp_util's runtime-introspected data, in place.
|
|
7
|
+
*
|
|
8
|
+
* Called from the Makefile's `build` target, after property_extractor.py's
|
|
9
|
+
* Tree-sitter extraction and before generate-docs renders the result. Topic
|
|
10
|
+
* properties (rp_util has no equivalent) and anything rp_util doesn't cover
|
|
11
|
+
* pass through untouched -- see rp_util_merge.py.
|
|
12
|
+
*
|
|
13
|
+
* Failure behavior depends on what kind of ref is being built:
|
|
14
|
+
*
|
|
15
|
+
* Branch refs (dev, feature branches): never fails the build. If rp_util's
|
|
16
|
+
* schema can't be obtained (no published release for this ref, no GitHub
|
|
17
|
+
* token, no Docker/Bazel available to build from source) or the merge
|
|
18
|
+
* itself errors, this logs a warning and leaves the Tree-sitter-only
|
|
19
|
+
* extraction as the enhanced file, exactly as it already was before this
|
|
20
|
+
* step ran. In-progress content tolerates the degraded data; it gets
|
|
21
|
+
* regenerated constantly.
|
|
22
|
+
*
|
|
23
|
+
* Release tags (v26.3.0, v26.3.1-rc1, ...): fails the build. A release run
|
|
24
|
+
* is the one that publishes the property reference, and shipping the
|
|
25
|
+
* Tree-sitter-only extraction there is not a degradation readers can see
|
|
26
|
+
* past: post streaming-enterprise#63, it silently drops enum_set
|
|
27
|
+
* properties (http_authentication) and misparses others behind a green
|
|
28
|
+
* job. The fix is to publish the schema first (the publish-rp-util-schema
|
|
29
|
+
* workflow, which also runs on a schedule) and re-run; --skip-rp-util /
|
|
30
|
+
* SKIP_RP_UTIL=1 remains the explicit opt-out.
|
|
31
|
+
*
|
|
32
|
+
* Usage:
|
|
33
|
+
* node merge-rp-util.js --tag <ref> --enhanced <path> [--overrides <path>] [--output <path>]
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const fs = require('fs')
|
|
37
|
+
const os = require('os')
|
|
38
|
+
const path = require('path')
|
|
39
|
+
const { spawnSync } = require('child_process')
|
|
40
|
+
const { getRpUtilSchema, SCHEMA_FLAGS } = require('./rp-util-fetch')
|
|
41
|
+
const bigIntJson = require('../../cli-utils/big-int-json')
|
|
42
|
+
|
|
43
|
+
function getArg(args, flag) {
|
|
44
|
+
const i = args.indexOf(flag)
|
|
45
|
+
return i === -1 ? undefined : args[i + 1]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// A ref this pipeline treats as a release build: vX.Y.Z, with or without an
|
|
49
|
+
// -rcN suffix (RC tags feed the docs beta branch, which publishes too). Same
|
|
50
|
+
// release/RC shape streaming-enterprise's own dispatch-docs-updates.yml keys
|
|
51
|
+
// on. Everything else (dev, feature branches, bare SHAs) is in-progress
|
|
52
|
+
// content where a degraded merge is tolerable.
|
|
53
|
+
const RELEASE_TAG_RX = /^v?\d+\.\d+\.\d+(-rc\d+)?$/
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Handle a step of the rp_util merge being unavailable: fatal for a release
|
|
57
|
+
* tag (see the header comment), a warning plus the Tree-sitter fallback for
|
|
58
|
+
* everything else.
|
|
59
|
+
*/
|
|
60
|
+
function handleMergeUnavailable(tag, enhanced, reason) {
|
|
61
|
+
if (RELEASE_TAG_RX.test(tag)) {
|
|
62
|
+
// Print the canonical v-prefixed tag in the remediation command:
|
|
63
|
+
// streaming-enterprise tags (and therefore schema release names) are
|
|
64
|
+
// always v-prefixed, so telling the operator to publish a v-less name
|
|
65
|
+
// would send them to a workflow run that can never help.
|
|
66
|
+
const releaseTag = tag.startsWith('v') ? tag : `v${tag}`
|
|
67
|
+
console.error(`Error: ${reason}`)
|
|
68
|
+
console.error(
|
|
69
|
+
`Refusing to publish Tree-sitter-only property data for release ${tag}: ` +
|
|
70
|
+
'it drops or misparses enum_set properties. Publish the rp_util schema ' +
|
|
71
|
+
`first (gh workflow run publish-rp-util-schema.yaml -f tag=${releaseTag}, or wait ` +
|
|
72
|
+
'for its scheduled run) and retry, or pass --skip-rp-util to override.'
|
|
73
|
+
)
|
|
74
|
+
process.exitCode = 1
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
console.warn(`Warning: ${reason}`)
|
|
78
|
+
console.warn('Skipping rp_util merge -- keeping the Tree-sitter-only extraction for cluster/broker scope.')
|
|
79
|
+
markRpUtilMergeUnavailable(enhanced)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// rp_util covers every cluster/broker-scope property (see this file's own
|
|
83
|
+
// header comment), so when its merge is skipped or fails, any cluster/
|
|
84
|
+
// broker property still missing gets_restored isn't "no annotation exists"
|
|
85
|
+
// (the pre-existing, legitimate case property.hbs already renders as an
|
|
86
|
+
// absent row) -- it's "we don't know, because the merge that would have
|
|
87
|
+
// told us didn't run". Mark those explicitly so the template can render
|
|
88
|
+
// that as a visible "Unknown" state instead of silently rendering nothing,
|
|
89
|
+
// which previously made a flaky rp_util fetch indistinguishable from a
|
|
90
|
+
// real upstream change in the generated docs.
|
|
91
|
+
const RP_UTIL_COVERED_SCOPES = new Set(['cluster', 'broker'])
|
|
92
|
+
|
|
93
|
+
function markRpUtilMergeUnavailable(enhancedPath) {
|
|
94
|
+
let data
|
|
95
|
+
try {
|
|
96
|
+
data = bigIntJson.parse(fs.readFileSync(enhancedPath, 'utf8'))
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.warn(`Warning: could not mark rp_util merge as unavailable in ${enhancedPath}: ${err.message}`)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const properties = data.properties || {}
|
|
102
|
+
let marked = 0
|
|
103
|
+
for (const prop of Object.values(properties)) {
|
|
104
|
+
if (!prop || !RP_UTIL_COVERED_SCOPES.has(prop.config_scope)) continue
|
|
105
|
+
if (prop.gets_restored !== undefined) continue
|
|
106
|
+
prop.rp_util_merge_status = 'unavailable'
|
|
107
|
+
marked++
|
|
108
|
+
}
|
|
109
|
+
if (marked === 0) return
|
|
110
|
+
try {
|
|
111
|
+
fs.writeFileSync(enhancedPath, bigIntJson.stringify(data))
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.warn(`Warning: could not write rp_util-unavailable marker to ${enhancedPath}: ${err.message}`)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function main() {
|
|
118
|
+
const args = process.argv.slice(2)
|
|
119
|
+
const tag = getArg(args, '--tag')
|
|
120
|
+
const enhanced = getArg(args, '--enhanced')
|
|
121
|
+
const overrides = getArg(args, '--overrides')
|
|
122
|
+
const output = getArg(args, '--output') || enhanced
|
|
123
|
+
const sourcePath = getArg(args, '--source-path')
|
|
124
|
+
// Cluster properties rp_util reports but the Tree-sitter pass never saw
|
|
125
|
+
// have no existing entry to carry cloud metadata forward from, so the
|
|
126
|
+
// merge needs its own CloudConfig to annotate them -- see rp_util_merge.py.
|
|
127
|
+
// Honours CLOUD_SUPPORT=1 as well because that is how doc-tools passes the
|
|
128
|
+
// request down to `make` in the first place.
|
|
129
|
+
const cloudSupport = args.includes('--cloud-support') || process.env.CLOUD_SUPPORT === '1'
|
|
130
|
+
|
|
131
|
+
if (!tag || !enhanced) {
|
|
132
|
+
console.error(
|
|
133
|
+
'Usage: node merge-rp-util.js --tag <ref> --enhanced <path> ' +
|
|
134
|
+
'[--overrides <path>] [--output <path>] [--source-path <dir>] [--cloud-support]'
|
|
135
|
+
)
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`Fetching rp_util schema for ${tag}...`)
|
|
140
|
+
let schemas
|
|
141
|
+
try {
|
|
142
|
+
// --source-path builds from an existing local streaming-enterprise
|
|
143
|
+
// checkout instead of fetching a published release or cloning+building
|
|
144
|
+
// fresh -- same option rp-util-fetch.js itself exposes, useful for
|
|
145
|
+
// local iteration against a branch that has no published schema yet.
|
|
146
|
+
schemas = await getRpUtilSchema(tag, sourcePath ? { sourcePath } : undefined)
|
|
147
|
+
} catch (err) {
|
|
148
|
+
handleMergeUnavailable(tag, enhanced, `could not get rp_util schema for ${tag}: ${err.message}`)
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const schemaDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rp-util-schemas-'))
|
|
153
|
+
try {
|
|
154
|
+
let anySchema = false
|
|
155
|
+
for (const { key } of SCHEMA_FLAGS) {
|
|
156
|
+
if (schemas[key]) {
|
|
157
|
+
fs.writeFileSync(path.join(schemaDir, `${key}.json`), bigIntJson.stringify(schemas[key]))
|
|
158
|
+
anySchema = true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!anySchema) {
|
|
162
|
+
handleMergeUnavailable(tag, enhanced, `rp_util schema for ${tag} came back empty.`)
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const venvPython = path.join(__dirname, 'tmp', 'redpanda-property-extractor-venv', 'bin', 'python')
|
|
167
|
+
const pythonBin = fs.existsSync(venvPython) ? venvPython : 'python3'
|
|
168
|
+
|
|
169
|
+
// Write to a temp path and only rename over the real output on success.
|
|
170
|
+
// output frequently equals enhanced (an in-place update), so a partial
|
|
171
|
+
// write from a mid-json.dump failure must never be able to leave that
|
|
172
|
+
// file truncated -- generate-docs reads it right after this returns.
|
|
173
|
+
const tempOutput = `${output}.rp-util-merge-tmp`
|
|
174
|
+
const mergeArgs = [
|
|
175
|
+
'rp_util_merge.py',
|
|
176
|
+
'--enhanced', enhanced,
|
|
177
|
+
'--rp-util-dir', schemaDir,
|
|
178
|
+
'--output', tempOutput
|
|
179
|
+
]
|
|
180
|
+
if (overrides && fs.existsSync(overrides)) {
|
|
181
|
+
mergeArgs.push('--overrides', overrides)
|
|
182
|
+
}
|
|
183
|
+
if (cloudSupport) {
|
|
184
|
+
mergeArgs.push('--cloud-support')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log('Merging rp_util schema into extracted properties...')
|
|
188
|
+
const result = spawnSync(pythonBin, mergeArgs, { cwd: __dirname, stdio: 'inherit' })
|
|
189
|
+
if (result.error || result.status !== 0) {
|
|
190
|
+
fs.rmSync(tempOutput, { force: true })
|
|
191
|
+
handleMergeUnavailable(
|
|
192
|
+
tag,
|
|
193
|
+
enhanced,
|
|
194
|
+
`rp_util merge failed (${result.error ? result.error.message : `exit ${result.status}`}).`
|
|
195
|
+
)
|
|
196
|
+
} else {
|
|
197
|
+
fs.renameSync(tempOutput, output)
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
fs.rmSync(schemaDir, { recursive: true, force: true })
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = { main, markRpUtilMergeUnavailable }
|
|
205
|
+
|
|
206
|
+
if (require.main === module) {
|
|
207
|
+
main()
|
|
208
|
+
}
|