@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +159 -0
- package/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +18 -1
- package/tools/property-extractor/transformers.py +29 -13
- package/tools/rpk-docs/generate-plugin-stubs.js +291 -0
- package/tools/rpk-docs/generate-rpk-docs.js +440 -25
- package/tools/rpk-docs/helpers/index.js +13 -3
- package/tools/rpk-docs/report-delta.js +314 -32
- package/tools/rpk-docs/rpk-docs-handler.js +1230 -102
- package/tools/rpk-docs/templates/command.hbs +5 -0
- package/tools/rpk-docs/templates/content-item.hbs +2 -2
|
@@ -52,16 +52,26 @@ function shortDescription(str) {
|
|
|
52
52
|
})
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
// Protect decimal points (OAuth 2.0) so version numbers neither split a
|
|
56
|
+
// sentence nor cause the leading fragment to be dropped
|
|
57
|
+
normalized = normalized.replace(/(\d)\.(\d)/g, '$1__DECIMAL__$2')
|
|
58
|
+
|
|
59
|
+
let sentences = normalized.match(/[^.!?]+[.!?]+(?:\s|$)/g)
|
|
56
60
|
if (!sentences || sentences.length === 0) {
|
|
57
|
-
let result = normalized
|
|
61
|
+
let result = normalized.replace(/__DECIMAL__/g, '.')
|
|
58
62
|
placeholders.forEach(({ ph, original }) => {
|
|
59
63
|
result = result.replace(ph, original)
|
|
60
64
|
})
|
|
61
65
|
return result.trim()
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
// Never drop an unterminated leading fragment
|
|
69
|
+
const firstIdx = normalized.indexOf(sentences[0])
|
|
70
|
+
if (firstIdx > 0) {
|
|
71
|
+
sentences = [normalized.slice(0, firstIdx) + sentences[0], ...sentences.slice(1)]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let result = sentences.slice(0, 2).join('').replace(/__DECIMAL__/g, '.')
|
|
65
75
|
placeholders.forEach(({ ph, original }) => {
|
|
66
76
|
result = result.replace(ph, original)
|
|
67
77
|
})
|
|
@@ -132,7 +132,14 @@ function compareFlags(oldFlag, newFlag) {
|
|
|
132
132
|
* @returns {Object} Diff report
|
|
133
133
|
*/
|
|
134
134
|
function generateRpkDiff(oldTree, newTree, options = {}) {
|
|
135
|
-
const {
|
|
135
|
+
const {
|
|
136
|
+
oldVersion = 'old',
|
|
137
|
+
newVersion = 'new',
|
|
138
|
+
// deprecated_commands maps from the snapshots (path -> metadata), as
|
|
139
|
+
// produced by scan-deprecated-commands.js
|
|
140
|
+
oldDeprecatedCommands = {},
|
|
141
|
+
newDeprecatedCommands = {}
|
|
142
|
+
} = options
|
|
136
143
|
|
|
137
144
|
const oldCommands = flattenToMap(oldTree)
|
|
138
145
|
const newCommands = flattenToMap(newTree)
|
|
@@ -152,23 +159,95 @@ function generateRpkDiff(oldTree, newTree, options = {}) {
|
|
|
152
159
|
}
|
|
153
160
|
})
|
|
154
161
|
|
|
155
|
-
//
|
|
162
|
+
// Newly deprecated commands: in the new snapshot's deprecation map but not
|
|
163
|
+
// the old one. Detected by source scanning, because deprecated commands
|
|
164
|
+
// that are also hidden never appear in --print-tree output.
|
|
165
|
+
const newlyDeprecatedRaw = Object.entries(newDeprecatedCommands)
|
|
166
|
+
.filter(([path]) => !oldDeprecatedCommands[path])
|
|
167
|
+
.map(([path, info]) => ({
|
|
168
|
+
path,
|
|
169
|
+
message: info.deprecatedMessage || '',
|
|
170
|
+
replacement: info.replacement || '',
|
|
171
|
+
hidden: info.hidden === true || /Hidden:\s*true/.test(info._note || ''),
|
|
172
|
+
deprecatedInVersion: newVersion
|
|
173
|
+
}))
|
|
174
|
+
|
|
175
|
+
// Roll deprecated subcommands up into their nearest deprecated ancestor:
|
|
176
|
+
// one entry for `rpk redpanda admin` with its subcommands listed reads
|
|
177
|
+
// better than a dozen sibling entries
|
|
178
|
+
const newlyDeprecatedDetails = newlyDeprecatedRaw.filter(d =>
|
|
179
|
+
!newlyDeprecatedRaw.some(other => other !== d && d.path.startsWith(other.path + ' ')))
|
|
180
|
+
for (const child of newlyDeprecatedRaw) {
|
|
181
|
+
if (newlyDeprecatedDetails.includes(child)) continue
|
|
182
|
+
const root = newlyDeprecatedDetails.find(r => child.path.startsWith(r.path + ' '))
|
|
183
|
+
if (root) {
|
|
184
|
+
root.affectedSubcommands = root.affectedSubcommands || []
|
|
185
|
+
if (!root.affectedSubcommands.includes(child.path)) {
|
|
186
|
+
root.affectedSubcommands.push(child.path)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Find removed commands, then separate genuine removals from commands that
|
|
192
|
+
// disappeared from the tree because they (or an ancestor) were deprecated
|
|
193
|
+
// and hidden — those still work as aliases and should be reported as
|
|
194
|
+
// deprecations, not removals.
|
|
195
|
+
const deprecatedRoots = newlyDeprecatedDetails.map(d => d.path)
|
|
196
|
+
const isUnderNewlyDeprecated = (p) =>
|
|
197
|
+
deprecatedRoots.some(root => p === root || p.startsWith(root + ' '))
|
|
198
|
+
|
|
156
199
|
const removedCommandPaths = [...oldPaths].filter(p => !newPaths.has(p))
|
|
157
|
-
const removedCommandsDetails =
|
|
200
|
+
const removedCommandsDetails = []
|
|
201
|
+
for (const path of removedCommandPaths) {
|
|
202
|
+
if (isUnderNewlyDeprecated(path)) {
|
|
203
|
+
const root = deprecatedRoots.find(r => path === r || path.startsWith(r + ' '))
|
|
204
|
+
const entry = newlyDeprecatedDetails.find(d => d.path === root)
|
|
205
|
+
if (path !== root) {
|
|
206
|
+
entry.affectedSubcommands = entry.affectedSubcommands || []
|
|
207
|
+
if (!entry.affectedSubcommands.includes(path)) {
|
|
208
|
+
entry.affectedSubcommands.push(path)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
continue
|
|
212
|
+
}
|
|
158
213
|
const cmd = oldCommands.get(path)
|
|
159
|
-
|
|
214
|
+
removedCommandsDetails.push({
|
|
160
215
|
path,
|
|
161
216
|
name: cmd.name,
|
|
162
217
|
description: cmd.description || '',
|
|
163
218
|
removedInVersion: newVersion
|
|
164
|
-
}
|
|
165
|
-
}
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
for (const dep of newlyDeprecatedDetails) {
|
|
223
|
+
if (dep.affectedSubcommands) dep.affectedSubcommands.sort()
|
|
224
|
+
}
|
|
166
225
|
|
|
167
226
|
// Find flag changes in existing commands
|
|
168
227
|
const newFlags = []
|
|
169
228
|
const removedFlags = []
|
|
170
229
|
const changedDefaults = []
|
|
230
|
+
const changedFlagTypes = []
|
|
231
|
+
const changedFlagRequirements = []
|
|
232
|
+
const changedFlagDescriptions = []
|
|
171
233
|
const descriptionChanges = []
|
|
234
|
+
const flagDataBackfilled = []
|
|
235
|
+
|
|
236
|
+
// Baseline gap detection: plugin subtrees captured before flag extraction
|
|
237
|
+
// existed record no flags on any of their own commands (the install/
|
|
238
|
+
// uninstall/upgrade shims are rpk-native and do carry flags). Flag
|
|
239
|
+
// "additions" inside such a group are newly captured documentation, not
|
|
240
|
+
// newly introduced flags, and stamping them "New in <version>" would
|
|
241
|
+
// mislabel long-standing flags. Core groups have baseline flag data, so a
|
|
242
|
+
// zero-flag command there that gains a flag is a genuine addition.
|
|
243
|
+
const groupsWithBaselineFlagData = new Set()
|
|
244
|
+
for (const [path, cmd] of oldCommands) {
|
|
245
|
+
const parts = path.split(' ')
|
|
246
|
+
if (parts.length < 2) continue
|
|
247
|
+
// "rpk <plugin> install|uninstall|upgrade" shims are rpk-native
|
|
248
|
+
if (parts.length === 3 && /^(install|uninstall|upgrade)$/.test(parts[2])) continue
|
|
249
|
+
if ((cmd.flags || []).length > 0) groupsWithBaselineFlagData.add(parts[1])
|
|
250
|
+
}
|
|
172
251
|
|
|
173
252
|
for (const path of newPaths) {
|
|
174
253
|
if (!oldPaths.has(path)) continue // Skip new commands
|
|
@@ -179,8 +258,16 @@ function generateRpkDiff(oldTree, newTree, options = {}) {
|
|
|
179
258
|
const oldFlags = getFlagsMap(oldCmd)
|
|
180
259
|
const newFlagsMap = getFlagsMap(newCmd)
|
|
181
260
|
|
|
182
|
-
|
|
183
|
-
|
|
261
|
+
const topLevel = path.split(' ')[1]
|
|
262
|
+
const isFlagBackfill = topLevel !== undefined &&
|
|
263
|
+
!groupsWithBaselineFlagData.has(topLevel) &&
|
|
264
|
+
oldFlags.size === 0 && newFlagsMap.size > 0
|
|
265
|
+
if (isFlagBackfill) {
|
|
266
|
+
flagDataBackfilled.push({ commandPath: path, flagCount: newFlagsMap.size })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Find new flags (skipped entirely for backfilled commands)
|
|
270
|
+
for (const [flagName, flag] of isFlagBackfill ? [] : newFlagsMap) {
|
|
184
271
|
if (!oldFlags.has(flagName)) {
|
|
185
272
|
newFlags.push({
|
|
186
273
|
commandPath: path,
|
|
@@ -222,6 +309,30 @@ function generateRpkDiff(oldTree, newTree, options = {}) {
|
|
|
222
309
|
newDefault: changes.default.new
|
|
223
310
|
})
|
|
224
311
|
}
|
|
312
|
+
if (changes.type) {
|
|
313
|
+
changedFlagTypes.push({
|
|
314
|
+
commandPath: path,
|
|
315
|
+
flagName,
|
|
316
|
+
oldType: changes.type.old,
|
|
317
|
+
newType: changes.type.new
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
if (changes.required) {
|
|
321
|
+
changedFlagRequirements.push({
|
|
322
|
+
commandPath: path,
|
|
323
|
+
flagName,
|
|
324
|
+
oldRequired: changes.required.old,
|
|
325
|
+
newRequired: changes.required.new
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
if (changes.description) {
|
|
329
|
+
changedFlagDescriptions.push({
|
|
330
|
+
commandPath: path,
|
|
331
|
+
flagName,
|
|
332
|
+
oldDescription: changes.description.old,
|
|
333
|
+
newDescription: changes.description.new
|
|
334
|
+
})
|
|
335
|
+
}
|
|
225
336
|
}
|
|
226
337
|
}
|
|
227
338
|
|
|
@@ -244,19 +355,29 @@ function generateRpkDiff(oldTree, newTree, options = {}) {
|
|
|
244
355
|
},
|
|
245
356
|
summary: {
|
|
246
357
|
newCommands: newCommandsDetails.length,
|
|
358
|
+
newlyDeprecatedCommands: newlyDeprecatedDetails.length,
|
|
247
359
|
removedCommands: removedCommandsDetails.length,
|
|
248
360
|
newFlags: newFlags.length,
|
|
249
361
|
removedFlags: removedFlags.length,
|
|
250
362
|
changedDefaults: changedDefaults.length,
|
|
251
|
-
|
|
363
|
+
changedFlagTypes: changedFlagTypes.length,
|
|
364
|
+
changedFlagRequirements: changedFlagRequirements.length,
|
|
365
|
+
changedFlagDescriptions: changedFlagDescriptions.length,
|
|
366
|
+
descriptionChanges: descriptionChanges.length,
|
|
367
|
+
flagDataBackfilled: flagDataBackfilled.length
|
|
252
368
|
},
|
|
253
369
|
details: {
|
|
254
370
|
newCommands: newCommandsDetails,
|
|
371
|
+
newlyDeprecatedCommands: newlyDeprecatedDetails,
|
|
255
372
|
removedCommands: removedCommandsDetails,
|
|
256
373
|
newFlags,
|
|
257
374
|
removedFlags,
|
|
258
375
|
changedDefaults,
|
|
259
|
-
|
|
376
|
+
changedFlagTypes,
|
|
377
|
+
changedFlagRequirements,
|
|
378
|
+
changedFlagDescriptions,
|
|
379
|
+
descriptionChanges,
|
|
380
|
+
flagDataBackfilled
|
|
260
381
|
}
|
|
261
382
|
}
|
|
262
383
|
}
|
|
@@ -272,11 +393,18 @@ function printDiffReport(diff) {
|
|
|
272
393
|
|
|
273
394
|
console.log('Summary:')
|
|
274
395
|
console.log(` New commands: ${diff.summary.newCommands}`)
|
|
275
|
-
console.log(` Deprecated commands: ${diff.summary.
|
|
396
|
+
console.log(` Deprecated commands: ${diff.summary.newlyDeprecatedCommands || 0}`)
|
|
397
|
+
console.log(` Removed commands: ${diff.summary.removedCommands}`)
|
|
276
398
|
console.log(` New flags: ${diff.summary.newFlags}`)
|
|
277
|
-
|
|
399
|
+
if (diff.summary.flagDataBackfilled) {
|
|
400
|
+
console.log(` Flag documentation backfilled: ${diff.summary.flagDataBackfilled} command(s) (baseline had no flag data; not reported as new flags)`)
|
|
401
|
+
}
|
|
402
|
+
console.log(` Removed flags: ${diff.summary.removedFlags}`)
|
|
278
403
|
console.log(` Changed defaults: ${diff.summary.changedDefaults}`)
|
|
279
|
-
console.log(`
|
|
404
|
+
console.log(` Changed flag types: ${diff.summary.changedFlagTypes || 0}`)
|
|
405
|
+
console.log(` Changed flag requirements: ${diff.summary.changedFlagRequirements || 0}`)
|
|
406
|
+
console.log(` Changed flag descriptions: ${diff.summary.changedFlagDescriptions || 0}`)
|
|
407
|
+
console.log(` Command description changes: ${diff.summary.descriptionChanges}`)
|
|
280
408
|
|
|
281
409
|
if (diff.details.newCommands.length > 0) {
|
|
282
410
|
console.log('\nNew Commands:')
|
|
@@ -289,8 +417,19 @@ function printDiffReport(diff) {
|
|
|
289
417
|
}
|
|
290
418
|
}
|
|
291
419
|
|
|
420
|
+
if ((diff.details.newlyDeprecatedCommands || []).length > 0) {
|
|
421
|
+
console.log('\nDeprecated Commands (still work, hidden or discouraged):')
|
|
422
|
+
for (const cmd of diff.details.newlyDeprecatedCommands) {
|
|
423
|
+
console.log(` ⚠ ${cmd.path}${cmd.hidden ? ' (hidden)' : ''}`)
|
|
424
|
+
if (cmd.message) console.log(` ${cmd.message}`)
|
|
425
|
+
if ((cmd.affectedSubcommands || []).length > 0) {
|
|
426
|
+
console.log(` affects ${cmd.affectedSubcommands.length} subcommand(s)`)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
292
431
|
if (diff.details.removedCommands.length > 0) {
|
|
293
|
-
console.log('\
|
|
432
|
+
console.log('\nRemoved Commands (no longer in command tree):')
|
|
294
433
|
for (const cmd of diff.details.removedCommands) {
|
|
295
434
|
console.log(` ⚠ ${cmd.path}`)
|
|
296
435
|
}
|
|
@@ -304,7 +443,7 @@ function printDiffReport(diff) {
|
|
|
304
443
|
}
|
|
305
444
|
|
|
306
445
|
if (diff.details.removedFlags.length > 0) {
|
|
307
|
-
console.log('\
|
|
446
|
+
console.log('\nRemoved Flags (no longer in command tree):')
|
|
308
447
|
for (const flag of diff.details.removedFlags) {
|
|
309
448
|
console.log(` ⚠ ${flag.commandPath} --${flag.flagName}`)
|
|
310
449
|
}
|
|
@@ -318,6 +457,20 @@ function printDiffReport(diff) {
|
|
|
318
457
|
}
|
|
319
458
|
}
|
|
320
459
|
|
|
460
|
+
if ((diff.details.changedFlagTypes || []).length > 0) {
|
|
461
|
+
console.log('\nChanged Flag Types:')
|
|
462
|
+
for (const change of diff.details.changedFlagTypes) {
|
|
463
|
+
console.log(` ~ ${change.commandPath} --${change.flagName}: ${change.oldType} → ${change.newType}`)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if ((diff.details.changedFlagRequirements || []).length > 0) {
|
|
468
|
+
console.log('\nChanged Flag Requirements:')
|
|
469
|
+
for (const change of diff.details.changedFlagRequirements) {
|
|
470
|
+
console.log(` ~ ${change.commandPath} --${change.flagName}: required ${change.oldRequired} → ${change.newRequired}`)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
321
474
|
console.log('')
|
|
322
475
|
}
|
|
323
476
|
|
|
@@ -339,10 +492,13 @@ function generateMarkdownSummary(diff) {
|
|
|
339
492
|
lines.push(`| Category | Count |`)
|
|
340
493
|
lines.push(`|----------|-------|`)
|
|
341
494
|
lines.push(`| New commands | ${diff.summary.newCommands} |`)
|
|
342
|
-
lines.push(`|
|
|
495
|
+
lines.push(`| Removed commands | ${diff.summary.removedCommands} |`)
|
|
343
496
|
lines.push(`| New flags | ${diff.summary.newFlags} |`)
|
|
344
|
-
lines.push(`|
|
|
497
|
+
lines.push(`| Removed flags | ${diff.summary.removedFlags} |`)
|
|
345
498
|
lines.push(`| Changed defaults | ${diff.summary.changedDefaults} |`)
|
|
499
|
+
if (diff.summary.flagDataBackfilled) {
|
|
500
|
+
lines.push(`| Flag docs backfilled (baseline had no flag data) | ${diff.summary.flagDataBackfilled} commands |`)
|
|
501
|
+
}
|
|
346
502
|
lines.push(``)
|
|
347
503
|
|
|
348
504
|
if (diff.details.newCommands.length > 0) {
|
|
@@ -355,9 +511,9 @@ function generateMarkdownSummary(diff) {
|
|
|
355
511
|
}
|
|
356
512
|
|
|
357
513
|
if (diff.details.removedCommands.length > 0) {
|
|
358
|
-
lines.push(`###
|
|
514
|
+
lines.push(`### Removed Commands`)
|
|
359
515
|
lines.push(``)
|
|
360
|
-
lines.push(`> Commands no longer in the active command tree
|
|
516
|
+
lines.push(`> Commands no longer in the active command tree.`)
|
|
361
517
|
lines.push(``)
|
|
362
518
|
for (const cmd of diff.details.removedCommands) {
|
|
363
519
|
lines.push(`- ~~\`${cmd.path}\`~~`)
|
|
@@ -411,32 +567,67 @@ function commandPathToXref(commandPath) {
|
|
|
411
567
|
function generateWhatsNewSection(diff, options = {}) {
|
|
412
568
|
const lines = []
|
|
413
569
|
const version = options.version || diff.comparison.newVersion
|
|
570
|
+
// Plugin subtrees may render as partials with no linkable pages, so plugin
|
|
571
|
+
// runs disable xrefs and render plain command names instead.
|
|
572
|
+
const useXrefs = options.xrefs !== false
|
|
573
|
+
// Commands that render as partials or are excluded have no linkable page
|
|
574
|
+
const linkable = typeof options.linkable === 'function' ? options.linkable : () => true
|
|
575
|
+
// Section heading for the page ("== Redpanda CLI" for core rpk changes,
|
|
576
|
+
// "== rpk plugins" for plugin releases). When blockLabel is set, the block
|
|
577
|
+
// opens with a "=== <label>" heading and category headings nest one level
|
|
578
|
+
// deeper, so accumulated blocks never produce colliding section ids.
|
|
579
|
+
const sectionHeading = options.sectionHeading || '== Redpanda CLI'
|
|
580
|
+
const blockLabel = options.blockLabel || null
|
|
581
|
+
const h = blockLabel ? '====' : '==='
|
|
582
|
+
// Command groups (two-part commands with subcommands) render into their own
|
|
583
|
+
// directory: rpk check -> rpk-check/rpk-check.adoc, not rpk-check.adoc
|
|
584
|
+
const hasSubcommands = typeof options.hasSubcommands === 'function'
|
|
585
|
+
? options.hasSubcommands
|
|
586
|
+
: () => false
|
|
587
|
+
const cmdRef = (commandPath) => {
|
|
588
|
+
if (!useXrefs || !linkable(commandPath)) return `\`${commandPath}\``
|
|
589
|
+
let xrefPath = commandPathToXref(commandPath)
|
|
590
|
+
if (commandPath.split(' ').length === 2 && hasSubcommands(commandPath)) {
|
|
591
|
+
const dashified = commandPath.replace(/ /g, '-')
|
|
592
|
+
xrefPath = `${dashified}/${dashified}.adoc`
|
|
593
|
+
}
|
|
594
|
+
return `xref:reference:rpk/${xrefPath}[\`${commandPath}\`]`
|
|
595
|
+
}
|
|
414
596
|
|
|
415
597
|
// Check if there are any changes worth documenting
|
|
416
598
|
const hasNewCommands = diff.details.newCommands.length > 0
|
|
599
|
+
const hasDeprecatedCommands = (diff.details.newlyDeprecatedCommands || []).length > 0
|
|
600
|
+
const hasRemovedCommands = diff.details.removedCommands.length > 0
|
|
417
601
|
const hasNewFlags = diff.details.newFlags.length > 0
|
|
602
|
+
const hasRemovedFlags = diff.details.removedFlags.length > 0
|
|
418
603
|
const hasChangedDefaults = diff.details.changedDefaults.length > 0
|
|
604
|
+
const hasChangedFlagTypes = (diff.details.changedFlagTypes || []).length > 0
|
|
419
605
|
|
|
420
|
-
if (!hasNewCommands && !
|
|
606
|
+
if (!hasNewCommands && !hasDeprecatedCommands && !hasRemovedCommands &&
|
|
607
|
+
!hasNewFlags && !hasRemovedFlags && !hasChangedDefaults && !hasChangedFlagTypes) {
|
|
421
608
|
return '' // No changes to document
|
|
422
609
|
}
|
|
423
610
|
|
|
424
|
-
lines.push(
|
|
611
|
+
lines.push(sectionHeading)
|
|
425
612
|
lines.push(``)
|
|
613
|
+
if (blockLabel) {
|
|
614
|
+
lines.push(`=== ${blockLabel}`)
|
|
615
|
+
lines.push(``)
|
|
616
|
+
}
|
|
426
617
|
|
|
427
618
|
if (hasNewCommands) {
|
|
428
|
-
lines.push(
|
|
619
|
+
lines.push(`${h} New commands`)
|
|
429
620
|
lines.push(``)
|
|
430
621
|
for (const cmd of diff.details.newCommands) {
|
|
431
|
-
const
|
|
432
|
-
const desc =
|
|
433
|
-
lines.push(`*
|
|
622
|
+
const shortDesc = firstSentence(cmd.description || '')
|
|
623
|
+
const desc = shortDesc ? ` - ${shortDesc}` : ''
|
|
624
|
+
lines.push(`* ${cmdRef(cmd.path)}${desc}`)
|
|
434
625
|
}
|
|
435
626
|
lines.push(``)
|
|
436
627
|
}
|
|
437
628
|
|
|
438
629
|
if (hasNewFlags) {
|
|
439
|
-
lines.push(
|
|
630
|
+
lines.push(`${h} New flags`)
|
|
440
631
|
lines.push(``)
|
|
441
632
|
// Group flags by command
|
|
442
633
|
const flagsByCommand = {}
|
|
@@ -448,19 +639,73 @@ function generateWhatsNewSection(diff, options = {}) {
|
|
|
448
639
|
}
|
|
449
640
|
|
|
450
641
|
for (const [cmdPath, flags] of Object.entries(flagsByCommand)) {
|
|
451
|
-
const xrefPath = commandPathToXref(cmdPath)
|
|
452
642
|
const flagList = flags.map(f => `\`--${f.flagName}\``).join(', ')
|
|
453
|
-
lines.push(`*
|
|
643
|
+
lines.push(`* ${cmdRef(cmdPath)}: Added ${flagList}`)
|
|
454
644
|
}
|
|
455
645
|
lines.push(``)
|
|
456
646
|
}
|
|
457
647
|
|
|
458
648
|
if (hasChangedDefaults) {
|
|
459
|
-
lines.push(
|
|
649
|
+
lines.push(`${h} Changed defaults`)
|
|
460
650
|
lines.push(``)
|
|
461
651
|
for (const change of diff.details.changedDefaults) {
|
|
462
|
-
const
|
|
463
|
-
|
|
652
|
+
const oldVal = formatFlagValue(change.oldDefault)
|
|
653
|
+
const newVal = formatFlagValue(change.newDefault)
|
|
654
|
+
lines.push(`* ${cmdRef(change.commandPath)}: \`--${change.flagName}\` default changed from \`${oldVal}\` to \`${newVal}\``)
|
|
655
|
+
}
|
|
656
|
+
lines.push(``)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (hasChangedFlagTypes) {
|
|
660
|
+
lines.push(`${h} Changed flag types`)
|
|
661
|
+
lines.push(``)
|
|
662
|
+
for (const change of diff.details.changedFlagTypes) {
|
|
663
|
+
lines.push(`* ${cmdRef(change.commandPath)}: \`--${change.flagName}\` type changed from \`${change.oldType}\` to \`${change.newType}\``)
|
|
664
|
+
}
|
|
665
|
+
lines.push(``)
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
if (hasDeprecatedCommands) {
|
|
669
|
+
lines.push(`${h} Deprecated commands`)
|
|
670
|
+
lines.push(``)
|
|
671
|
+
for (const cmd of diff.details.newlyDeprecatedCommands) {
|
|
672
|
+
let entry = `* \`${cmd.path}\``
|
|
673
|
+
if (cmd.replacement) {
|
|
674
|
+
entry += `: ${cmd.replacement}`
|
|
675
|
+
} else if (cmd.message) {
|
|
676
|
+
entry += `: ${cmd.message}`
|
|
677
|
+
}
|
|
678
|
+
lines.push(entry)
|
|
679
|
+
if ((cmd.affectedSubcommands || []).length > 0) {
|
|
680
|
+
lines.push(`+`)
|
|
681
|
+
lines.push(`Includes its subcommands: ${cmd.affectedSubcommands.map(s => `\`${s}\``).join(', ')}.`)
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
lines.push(``)
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
if (hasRemovedCommands) {
|
|
688
|
+
lines.push(`${h} Removed commands`)
|
|
689
|
+
lines.push(``)
|
|
690
|
+
for (const cmd of diff.details.removedCommands) {
|
|
691
|
+
lines.push(`* \`${cmd.path}\``)
|
|
692
|
+
}
|
|
693
|
+
lines.push(``)
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (hasRemovedFlags) {
|
|
697
|
+
lines.push(`${h} Removed flags`)
|
|
698
|
+
lines.push(``)
|
|
699
|
+
const removedByCommand = {}
|
|
700
|
+
for (const flag of diff.details.removedFlags) {
|
|
701
|
+
if (!removedByCommand[flag.commandPath]) {
|
|
702
|
+
removedByCommand[flag.commandPath] = []
|
|
703
|
+
}
|
|
704
|
+
removedByCommand[flag.commandPath].push(flag)
|
|
705
|
+
}
|
|
706
|
+
for (const [cmdPath, flags] of Object.entries(removedByCommand)) {
|
|
707
|
+
const flagList = flags.map(f => `\`--${f.flagName}\``).join(', ')
|
|
708
|
+
lines.push(`* ${cmdRef(cmdPath)}: Removed ${flagList}`)
|
|
464
709
|
}
|
|
465
710
|
lines.push(``)
|
|
466
711
|
}
|
|
@@ -468,6 +713,41 @@ function generateWhatsNewSection(diff, options = {}) {
|
|
|
468
713
|
return lines.join('\n')
|
|
469
714
|
}
|
|
470
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Extract the first sentence of a description for a one-line bullet.
|
|
718
|
+
* Newlines are collapsed first (cobra help wraps mid-sentence), and decimal
|
|
719
|
+
* points in version numbers do not end a sentence. Falls back to the whole
|
|
720
|
+
* collapsed text when no sentence boundary exists.
|
|
721
|
+
* @param {string} str - Command description
|
|
722
|
+
* @returns {string}
|
|
723
|
+
*/
|
|
724
|
+
function firstSentence(str) {
|
|
725
|
+
if (!str) return ''
|
|
726
|
+
// A blank line separates the summary from the long text even when the
|
|
727
|
+
// summary has no terminal period ("Install Redpanda Check\n\nThis
|
|
728
|
+
// command..."), so cut at the paragraph break before joining the
|
|
729
|
+
// hard-wrapped lines within it.
|
|
730
|
+
const firstParagraph = String(str).split(/\n\s*\n/, 1)[0]
|
|
731
|
+
const singleLine = firstParagraph.replace(/\s*\n+\s*/g, ' ').trim()
|
|
732
|
+
const protectedText = singleLine.replace(/(\d)\.(\d)/g, '$1__DECIMAL__$2')
|
|
733
|
+
const match = protectedText.match(/^.*?[.!?](?=\s|$)/)
|
|
734
|
+
const sentence = match ? match[0] : protectedText
|
|
735
|
+
return sentence.replace(/__DECIMAL__/g, '.').trim()
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Render a flag value for display. Objects and arrays are JSON-encoded so
|
|
740
|
+
* they never render as [object Object].
|
|
741
|
+
* @param {*} value - Flag default value
|
|
742
|
+
* @returns {string}
|
|
743
|
+
*/
|
|
744
|
+
function formatFlagValue(value) {
|
|
745
|
+
if (value === undefined) return 'unset'
|
|
746
|
+
if (value === null) return 'null'
|
|
747
|
+
if (typeof value === 'object') return JSON.stringify(value)
|
|
748
|
+
return String(value)
|
|
749
|
+
}
|
|
750
|
+
|
|
471
751
|
module.exports = {
|
|
472
752
|
generateRpkDiff,
|
|
473
753
|
printDiffReport,
|
|
@@ -475,5 +755,7 @@ module.exports = {
|
|
|
475
755
|
generateWhatsNewSection,
|
|
476
756
|
flattenToMap,
|
|
477
757
|
getFlagsMap,
|
|
478
|
-
compareFlags
|
|
758
|
+
compareFlags,
|
|
759
|
+
// Exported for testing
|
|
760
|
+
firstSentence
|
|
479
761
|
}
|