@redpanda-data/docs-extensions-and-macros 5.31.0 → 5.35.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.
@@ -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
- return parseSharedValuesFile(content, { attachRealKeys: true }).map((r) => {
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 = { kind: 'dead-marker', unverifiable: true }
66
- } else if (r.undocumented) {
67
- // Nothing to lint for content: the rule fires on the absence itself.
68
- meta = { kind: 'key', top_level: true, undocumented: true, unverifiable: true }
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 = { kind: 'key', commented_out: true, top_level: r.topLevel, default_annotation: r.default || null }
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: r.undocumented ? null : 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
- return [{ message: 'This "# --" description is buried where neither helm-docs nor the helm-spec commented-values pass can attach it (inside another commented-out key\'s subtree, or separated from any key), so it silently never ships. Move it directly above the key it documents, use "# @doc full.path -- ...", or delete it.' }]
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-top-level-key',
143
- description: 'Top-level user-visible key with no # -- 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: `Top-level key "${decl.name}" has no "# --" description, so helm-docs ships it undocumented. Add a marker comment directly above it (or "# @ignored" if it is not user-facing).` }]
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
  }
@@ -4,7 +4,7 @@ const { spawnSync } = require('child_process')
4
4
  const fs = require('fs')
5
5
  const os = require('os')
6
6
  const path = require('path')
7
- const { getGitHubToken } = require('../../cli-utils/github-token')
7
+ const { getGitHubToken, getGitHubApiToken } = require('../../cli-utils/github-token')
8
8
 
9
9
  /**
10
10
  * rp_util's config schema JSON, for any streaming-enterprise ref (tag,
@@ -287,7 +287,7 @@ const RELEASE_ASSET_TO_SCHEMA_KEY = {
287
287
  * no release exists for this tag (caller should fall back to building)
288
288
  */
289
289
  async function fetchPublishedSchema(tag) {
290
- const token = getGitHubToken()
290
+ const token = getGitHubApiToken()
291
291
  const headers = token ? { Authorization: `Bearer ${token}` } : {}
292
292
 
293
293
  const releaseResp = await fetch(
@@ -1324,8 +1324,8 @@ function downloadRpkRelease(tag, destDir) {
1324
1324
  // redpanda-data/redpanda. This function already has a fallback (the
1325
1325
  // caller builds from source instead), so a missing token warns and
1326
1326
  // returns null here rather than throwing the way the clone-only paths do.
1327
- const { getGitHubToken } = require('../../cli-utils/github-token')
1328
- const token = getGitHubToken()
1327
+ const { getGitHubApiToken } = require('../../cli-utils/github-token')
1328
+ const token = getGitHubApiToken()
1329
1329
  if (!token) {
1330
1330
  console.warn('No GitHub token available for the private streaming-enterprise release download; falling back to a source build.')
1331
1331
  return null