Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8fd5a01
feat(rpk-docs): refresh a single plugin's docs with --plugin
JakeSCahill Jul 28, 2026
c7c785c
fix(rpk-docs): preserve platform markers and survive rejected version…
JakeSCahill Jul 28, 2026
ab3f102
docs: Auto-update CLI reference documentation (PR #225)
github-actions[bot] Jul 28, 2026
cd21b9c
feat(rpk-docs): stamp new plugin commands with the plugin's own version
JakeSCahill Jul 28, 2026
e66e57d
feat(rpk-docs): support per-plugin version pins during full generation
JakeSCahill Jul 29, 2026
2cfb7f6
docs: Auto-update CLI reference documentation (PR #225)
github-actions[bot] Jul 29, 2026
af8a754
fix(rpk-docs): report every detected change category in diffs and PR …
JakeSCahill Jul 29, 2026
d09c554
feat(rpk-docs): detect and report command deprecations
JakeSCahill Jul 29, 2026
2fba250
feat(rpk-docs): merge What's-new updates instead of one-shot writes
JakeSCahill Jul 29, 2026
dd139c7
feat(rpk-docs): separate plugin changes into their own What's-new sec…
JakeSCahill Jul 29, 2026
4c3d065
fix(rpk-docs): extract plugin command flags and fix generator output …
JakeSCahill Jul 29, 2026
8f57fec
ci: retrigger Netlify preview with raised Node heap
JakeSCahill Jul 29, 2026
eb43f7c
fix: persist linux_only_commands through plugin snapshot refreshes (C…
JakeSCahill Jul 29, 2026
a31eac0
fix(rpk-docs): correct What's-new xrefs and cap bullets at sentence b…
JakeSCahill Jul 30, 2026
36583fe
fix(rpk-docs): retry container builds and verify the binary exists
JakeSCahill Jul 30, 2026
de1a44d
fix(rpk-docs): keep the Linux tree when the Darwin comparison build f…
JakeSCahill Jul 30, 2026
41953a9
fix(rpk-docs): retry phantom-success container builds
JakeSCahill Jul 30, 2026
717ad8f
feat(rpk-docs): stub reconciler for single-source consumer repos
JakeSCahill Jul 30, 2026
678b86e
docs: Auto-update CLI reference documentation (PR #225)
github-actions[bot] Jul 30, 2026
1ad5f65
fix(rpk-docs): stop linking partial-routed subcommands from parent pages
JakeSCahill Jul 30, 2026
1f1bb5f
feat(rpk-docs): parse urfave/cli help so Redpanda Connect flags extract
JakeSCahill Jul 30, 2026
0fe00b9
feat(rpk-docs): support page-family sources in the stub reconciler
JakeSCahill Jul 30, 2026
7970f90
fix(rpk-docs): never delete pages for plugins absent from the tree
JakeSCahill Jul 27, 2026
f03d0f5
fix(rpk-docs): stop help-text underlines from breaking generated pages
JakeSCahill Jul 27, 2026
b6d8179
fix: add regression tests for protected-plugin cleanup and nav dedup …
JakeSCahill Jul 29, 2026
830aaa7
fix(rpk-docs): stop shim-only runs from rewriting plugin pages and mi…
JakeSCahill Jul 30, 2026
21eb0ce
fix(rpk-docs): pass failed plugin installs to generation as protected…
JakeSCahill Jul 30, 2026
31bcd44
fix(rpk-docs): treat open-block delimiters as content boundaries in b…
JakeSCahill Jul 30, 2026
f5e3280
fix(rpk-docs): address second-round review on flag parsing, page conf…
JakeSCahill Jul 31, 2026
98a1e65
fix: clean dangling example fragments and warn on duplicate rendered …
JakeSCahill Jul 31, 2026
19e09cc
fix: keep rpk root page in nav and cut What's-new summaries at paragr…
JakeSCahill Jul 31, 2026
73ff8f0
fix: do not label backfilled plugin flags as newly introduced
JakeSCahill Jul 31, 2026
3c2954b
fix: scope the backfill guard to plugin groups and stamp before rende…
JakeSCahill Jul 31, 2026
f9fe6ac
fix: capture unindented shell examples and colon-introduced code samp…
JakeSCahill Jul 31, 2026
a1c0da8
chore: allow applyToCode in the overrides schema
JakeSCahill Jul 31, 2026
338ba80
fix: protect example command lines from caption transforms and add de…
JakeSCahill Jul 31, 2026
610e2d9
fix: render deeply indented help literals as code blocks and escape p…
JakeSCahill Jul 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CLI_REFERENCE.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,7 @@ display help for command
* `migrate-rpcn-metadata` - One-time migration of inline connector == Metadata blocks into regenerated partials. Dry run unless --write is given.
* `property-docs` - Generate JSON and consolidated AsciiDoc partials for Redpanda configuration properties. Defaults to branch "dev" if neither --tag nor --branch is specified.
* `rpk-docs` - Generate rpk CLI documentation from source. Builds rpk and parses source for platform detection.
* `rpk-plugin-stubs` - Reconcile single-source stub pages and nav against the docs repo's rpk plugin partials. Run from the consumer repo root.
* `rpk-overrides` - Validate rpk-overrides.json against schema and check for common issues
* `helm-spec` - Generate AsciiDoc documentation for Helm charts. Requires either --tag or --branch for GitHub URLs.
* `cloud-regions` - Generate Markdown table of cloud regions and tiers from GitHub YAML file
Expand Down Expand Up @@ -693,6 +694,18 @@ Path to local rpk source (src/go/rpk directory)
`--from-json <path>`::
Regenerate docs from an existing versioned JSON file (skips building)

`--plugin <name>`::
Refresh a single rpk plugin's docs (ai, connect, k8s, check). Requires --from-json. Installs the plugin, splices its fresh subtree into the snapshot, and re-renders.

`--plugin-version <version>`::
Plugin version to install and record (for example, 4.102.0). Defaults to the latest published version.

`--plugin-pin <name=version>`::
Pin a plugin version for the installs during full generation (repeatable, for example --plugin-pin k8s=26.3.1-beta.1). Required for pre-GA plugins with no promoted latest version. (default: {})

`--rpk-bin <path>`::
Path to an existing rpk binary for the plugin refresh (skips download/build)

`--overrides <path>`::
Path to overrides JSON file (default: "docs-data/rpk-overrides.json")

Expand Down
404 changes: 400 additions & 4 deletions __tests__/tools/rpk-docs/generate-rpk-docs.test.js

Large diffs are not rendered by default.

180 changes: 180 additions & 0 deletions __tests__/tools/rpk-docs/plugin-flags.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
'use strict'

const { parseCobraFlags, enrichPluginTreeWithFlags } = require('../../../tools/rpk-docs/rpk-docs-handler.js')

describe('parseCobraFlags', () => {
const HELP = [
'Reconcile LLM providers from one or more YAML manifests.',
'',
'Usage:',
' rpk ai llm-provider apply [flags]',
'',
'Flags:',
' --allow-empty Allow applying zero manifests',
' -f, --file strings Manifest paths, - for stdin (default [])',
' -h, --help help for apply',
' --timeout duration How long to wait for the reconcile to',
' complete before giving up (default 30s)',
' -o, --format string Output format (default "text")',
'',
'Global Flags:',
' -c, --config string rpk config file',
' -v, --verbose enable verbose logging',
'',
'Use "rpk ai llm-provider apply [command] --help" for more information.'
].join('\n')

test('parses local flags with shorthand, type, and default', () => {
const flags = parseCobraFlags(HELP)
const byName = Object.fromEntries(flags.map(f => [f.name, f]))

expect(byName['allow-empty']).toMatchObject({ type: 'bool' })
expect(byName['file']).toMatchObject({ shorthand: 'f', type: 'strings', default: '[]' })
expect(byName['format']).toMatchObject({ shorthand: 'o', type: 'string', default: '"text"' })
})

test('joins wrapped descriptions and extracts trailing defaults', () => {
const flags = parseCobraFlags(HELP)
const timeout = flags.find(f => f.name === 'timeout')
expect(timeout.description).toBe('How long to wait for the reconcile to complete before giving up')
expect(timeout.default).toBe('30s')
})

test('skips --help and the Global Flags section', () => {
const flags = parseCobraFlags(HELP)
expect(flags.map(f => f.name)).not.toContain('help')
expect(flags.map(f => f.name)).not.toContain('config')
expect(flags.map(f => f.name)).not.toContain('verbose')
})

test('returns empty for help without a Flags section', () => {
expect(parseCobraFlags('Usage:\n rpk ai\n\nUse "rpk ai --help".')).toEqual([])
expect(parseCobraFlags('')).toEqual([])
})
})

describe('enrichPluginTreeWithFlags', () => {
test('fills flagless commands and leaves shim flags alone', () => {
const node = {
name: 'ai',
commands: [
{ name: 'install', flags: [{ name: 'ai-version', type: 'string' }], commands: [] },
{ name: 'auth', commands: [{ name: 'login', commands: [] }] }
]
}
const calls = []
const enriched = enrichPluginTreeWithFlags(node, (argPath) => {
calls.push(argPath.join(' '))
return 'Flags:\n --no-browser Print the URL instead of opening it\n'
})

// install already has flags: not queried
expect(calls).not.toContain('ai install')
expect(calls).toContain('ai auth login')
const login = node.commands[1].commands[0]
expect(login.flags).toHaveLength(1)
expect(login.flags[0]).toMatchObject({ name: 'no-browser', type: 'bool' })
expect(node.commands[0].flags[0].name).toBe('ai-version')
expect(enriched).toBeGreaterThanOrEqual(2) // root + auth + login minus empties
})

test('help failures are non-fatal', () => {
const node = { name: 'ai', commands: [{ name: 'run', commands: [] }] }
const enriched = enrichPluginTreeWithFlags(node, () => null)
expect(enriched).toBe(0)
expect(node.commands[0].flags).toBeUndefined()
})
})

describe('mergeVisibleDeprecationsIntoOverrides', () => {
const fs = require('fs')
const path = require('path')
const os = require('os')
const { mergeVisibleDeprecationsIntoOverrides } = require('../../../tools/rpk-docs/rpk-docs-handler.js')

const tree = {
name: 'rpk',
commands: [
{ name: 'oldcmd', commands: [] },
{ name: 'topic', commands: [] }
]
}

let dir, overridesPath
beforeEach(() => {
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dep-merge-'))
overridesPath = path.join(dir, 'overrides.json')
})
afterEach(() => fs.rmSync(dir, { recursive: true, force: true }))

test('annotates visible deprecated commands, skips hidden ones', () => {
fs.writeFileSync(overridesPath, JSON.stringify({ commands: {} }))
mergeVisibleDeprecationsIntoOverrides({
'rpk oldcmd': { deprecated: true, deprecatedMessage: 'use rpk newcmd', replacement: 'See `rpk newcmd`.' },
'rpk hiddencmd': { deprecated: true, _note: 'Hidden: true' }
}, tree, overridesPath)

const result = JSON.parse(fs.readFileSync(overridesPath, 'utf8'))
expect(result.commands['rpk oldcmd']).toMatchObject({
deprecated: true,
deprecatedMessage: 'use rpk newcmd',
replacement: 'See `rpk newcmd`.'
})
// hiddencmd is not in the tree: no page to annotate
expect(result.commands['rpk hiddencmd']).toBeUndefined()
})

test('never overwrites curated deprecation overrides', () => {
fs.writeFileSync(overridesPath, JSON.stringify({
commands: { 'rpk oldcmd': { deprecated: false, deprecatedMessage: 'curated text' } }
}))
mergeVisibleDeprecationsIntoOverrides({
'rpk oldcmd': { deprecated: true, deprecatedMessage: 'scanner text' }
}, tree, overridesPath)

const result = JSON.parse(fs.readFileSync(overridesPath, 'utf8'))
expect(result.commands['rpk oldcmd'].deprecated).toBe(false)
expect(result.commands['rpk oldcmd'].deprecatedMessage).toBe('curated text')
})

test('no-op when nothing to annotate', () => {
fs.writeFileSync(overridesPath, JSON.stringify({ commands: {} }))
const before = fs.readFileSync(overridesPath, 'utf8')
mergeVisibleDeprecationsIntoOverrides({}, tree, overridesPath)
expect(fs.readFileSync(overridesPath, 'utf8')).toBe(before)
})
})

describe('parseUrfaveFlags (Redpanda Connect help format)', () => {
const { parseUrfaveFlags, parseHelpFlags } = require('../../../tools/rpk-docs/rpk-docs-handler.js')

const HELP = [
'NAME:',
' redpanda-connect run - Run',
'',
'OPTIONS:',
' --log.level value override the log level',
' --set value, -s value [ --set value, -s value ] set a field',
' --chilled continue on lint errors (default: false)',
' --watcher, -w watch config files (default: false)',
'',
'GLOBAL OPTIONS:',
' --verbose noisy'
].join('\n')

test('parses names, shorthands, types, and defaults', () => {
const flags = parseUrfaveFlags(HELP)
const byName = Object.fromEntries(flags.map(f => [f.name, f]))
expect(byName['log.level']).toMatchObject({ type: 'string' })
expect(byName['set']).toMatchObject({ shorthand: 's', type: 'strings' })
expect(byName['chilled']).toMatchObject({ type: 'bool', default: 'false' })
expect(byName['watcher']).toMatchObject({ shorthand: 'w', type: 'bool' })
expect(flags.map(f => f.name)).not.toContain('verbose')
})

test('parseHelpFlags dispatches by section header', () => {
expect(parseHelpFlags(HELP).length).toBe(4)
expect(parseHelpFlags('Flags:\n --no-browser print URL\n').length).toBe(1)
expect(parseHelpFlags('Usage: nothing here')).toEqual([])
})
})
154 changes: 154 additions & 0 deletions __tests__/tools/rpk-docs/plugin-refresh.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
'use strict'

const {
splicePluginNode,
preserveLinuxOnlyCommands,
pluginNodeHasRealCommands,
REFRESHABLE_PLUGINS,
PLUGIN_INSTALL_VERSION_FLAGS,
PLUGIN_MANIFEST_SLUGS
} = require('../../../tools/rpk-docs/rpk-docs-handler.js')

describe('Plugin refresh (--plugin mode)', () => {
const shimOnlyNode = {
name: 'k8s',
description: 'Kubernetes plugin',
commands: [
{ name: 'install' },
{ name: 'uninstall' },
{ name: 'upgrade' }
]
}

const installedNode = {
name: 'connect',
description: 'Redpanda Connect plugin',
commands: [
{ name: 'install' },
{ name: 'uninstall' },
{ name: 'upgrade' },
{ name: 'run', description: 'Run a pipeline' },
{ name: 'lint', description: 'Lint a config' }
]
}

const baseTree = {
name: 'rpk',
global_flags: [{ name: '--config' }],
commands: [
{ name: 'topic', commands: [{ name: 'create' }] },
{ name: 'connect', commands: [{ name: 'install' }, { name: 'run', description: 'old run' }] },
{ name: 'cluster', commands: [{ name: 'health' }] }
]
}

describe('pluginNodeHasRealCommands', () => {
test('false for a shim-only node (install/uninstall/upgrade)', () => {
expect(pluginNodeHasRealCommands(shimOnlyNode)).toBe(false)
})

test('true when real plugin commands are present', () => {
expect(pluginNodeHasRealCommands(installedNode)).toBe(true)
})

test('false for a node with no subcommands', () => {
expect(pluginNodeHasRealCommands({ name: 'ai' })).toBe(false)
})
})

describe('splicePluginNode', () => {
test('replaces the plugin node and keeps everything else', () => {
const result = splicePluginNode(baseTree, 'connect', installedNode)

expect(result.commands.map(c => c.name)).toEqual(['topic', 'connect', 'cluster'])
const connect = result.commands.find(c => c.name === 'connect')
expect(connect.commands.map(c => c.name)).toContain('lint')
expect(result.commands.find(c => c.name === 'topic')).toBe(baseTree.commands[0])
expect(result.global_flags).toEqual(baseTree.global_flags)
})

test('does not mutate the input tree', () => {
splicePluginNode(baseTree, 'connect', installedNode)
const connect = baseTree.commands.find(c => c.name === 'connect')
expect(connect.commands.find(c => c.name === 'run').description).toBe('old run')
})

test('throws when the plugin is not in the base tree', () => {
expect(() => splicePluginNode(baseTree, 'k8s', shimOnlyNode))
.toThrow(/not present in the base tree/)
})

test('keeps the tree linux_only_commands list through a splice', () => {
const treeWithMarkers = { ...baseTree, linux_only_commands: ['rpk debug bundle', 'rpk iotune'] }
const result = splicePluginNode(treeWithMarkers, 'connect', installedNode)
expect(result.linux_only_commands).toEqual(['rpk debug bundle', 'rpk iotune'])
})
})

describe('preserveLinuxOnlyCommands', () => {
const linuxOnly = ['rpk debug bundle', 'rpk iotune']

test('inherits the snapshot list when the working tree lacks it', () => {
const result = preserveLinuxOnlyCommands(baseTree, { ...baseTree, linux_only_commands: linuxOnly })
expect(result.linux_only_commands).toEqual(linuxOnly)
// Copies, not aliases: mutating the result must not touch the snapshot
expect(result.linux_only_commands).not.toBe(linuxOnly)
expect(result.commands).toBe(baseTree.commands)
})

test('keeps the working tree list when it already has one', () => {
const tree = { ...baseTree, linux_only_commands: linuxOnly }
const result = preserveLinuxOnlyCommands(tree, { ...baseTree, linux_only_commands: ['rpk other'] })
expect(result).toBe(tree)
expect(result.linux_only_commands).toEqual(linuxOnly)
})

test('is a no-op when neither tree carries the list', () => {
expect(preserveLinuxOnlyCommands(baseTree, baseTree)).toBe(baseTree)
expect(preserveLinuxOnlyCommands(baseTree, undefined)).toBe(baseTree)
expect(preserveLinuxOnlyCommands(null, baseTree)).toBe(null)
})

test('refresh persistence chain keeps markers for the saved snapshot', () => {
// Mirrors the --plugin save path: derive the working tree from the
// snapshot, splice the fresh subtree, then preserve before persisting.
const snapshot = {
raw_tree: { ...baseTree, linux_only_commands: linuxOnly },
tree: { ...baseTree, linux_only_commands: linuxOnly }
}
let tree = snapshot.raw_tree || snapshot.tree
tree = preserveLinuxOnlyCommands(tree, snapshot.tree || snapshot.raw_tree)
tree = splicePluginNode(tree, 'connect', installedNode)
tree = preserveLinuxOnlyCommands(tree, snapshot.raw_tree || snapshot.tree)
expect(tree.linux_only_commands).toEqual(linuxOnly)
})

test('refresh persistence chain restores markers when only the enhanced tree has them', () => {
// Older snapshots may carry the field on only one stored tree; the
// derivation step must inherit it so the re-saved snapshot keeps it.
const snapshot = {
raw_tree: { ...baseTree },
tree: { ...baseTree, linux_only_commands: linuxOnly }
}
let tree = snapshot.raw_tree || snapshot.tree
tree = preserveLinuxOnlyCommands(tree, snapshot.tree || snapshot.raw_tree)
expect(tree.linux_only_commands).toEqual(linuxOnly)
})
})

describe('plugin constants', () => {
test('every refreshable plugin has a version pin flag', () => {
for (const plugin of REFRESHABLE_PLUGINS) {
expect(PLUGIN_INSTALL_VERSION_FLAGS[plugin]).toMatch(/^--[a-z-]+$/)
}
})

test('oxla is not refreshable (stub with no installable binary)', () => {
expect(REFRESHABLE_PLUGINS).not.toContain('oxla')
})

test('ai maps to the rpai manifest slug', () => {
expect(PLUGIN_MANIFEST_SLUGS.ai).toBe('rpai')
})
})
})
Loading
Loading