diff --git a/__tests__/tools/rpk-docs/override-features.test.js b/__tests__/tools/rpk-docs/override-features.test.js index 83c287aa..5a0b3353 100644 --- a/__tests__/tools/rpk-docs/override-features.test.js +++ b/__tests__/tools/rpk-docs/override-features.test.js @@ -977,6 +977,25 @@ Original fields content that should be replaced.`, expect(page).toContain('|xref:reference:rpk/rpk-cluster/rpk-cluster-info.adoc[`rpk cluster info`]') }, 30000) + // Availability never wraps the page itself: includes only extract the + // single-source tag region (a page-level conditional outside it is dead + // code), and a build that did consume it would publish an empty, + // untitled page. Row and flag gating inside the tag does the real work. + test('selfHostedOnly never wraps the command page in a conditional', async () => { + await generateRpkDocs({ + tree: clusterTree(), + overrides: { commands: { 'rpk cluster health': { selfHostedOnly: true } } }, + outputDir, + rpkVersion: 'test', + pluginVersions: {} + }) + + const page = fs.readFileSync(path.join(outputDir, 'rpk-cluster', 'rpk-cluster-health.adoc'), 'utf8') + expect(page.startsWith('= rpk cluster health')).toBe(true) + expect(page).not.toMatch(/^ifndef::env-cloud/m) + expect(page.trimEnd().endsWith('// end::single-source[]')).toBe(true) + }, 30000) + test('wraps cloudOnly subcommand rows in ifdef::env-cloud', async () => { await generateRpkDocs({ tree: clusterTree(), diff --git a/__tests__/tools/rpk-docs/rpk-docs-handler.test.js b/__tests__/tools/rpk-docs/rpk-docs-handler.test.js index fcecc29e..89c93461 100644 --- a/__tests__/tools/rpk-docs/rpk-docs-handler.test.js +++ b/__tests__/tools/rpk-docs/rpk-docs-handler.test.js @@ -6,6 +6,9 @@ const os = require('os') const { updateOverridesWithIntroducedVersions, + isPluginStampAttributable, + attributablePluginSet, + pluginManifestVersionsCache, detectLinuxOnlyFromSource, addPlatformMarkersFromSource, countCommands, @@ -105,6 +108,83 @@ describe('rpk Docs Handler', () => { }) }) + describe('introduction-version attribution', () => { + // Mirrors a real incident: 30 rpk ai commands shipped in 0.2.26 and + // 0.2.28 were stamped "introduced in 0.2.32" because the baseline + // snapshot was several plugin releases stale. + afterEach(() => pluginManifestVersionsCache.clear()) + + test('skips plugin entries when the plugin is not attributable, stamps core', () => { + fs.writeFileSync(overridesPath, JSON.stringify({ commands: {} })) + + const diffData = { + summary: { newCommands: 2 }, + details: { + newCommands: [ + { path: 'rpk ai policy create' }, + { path: 'rpk cluster new-command' } + ], + newFlags: [{ commandPath: 'rpk ai policy', flagName: 'new-flag' }], + removedCommands: [], + removedFlags: [], + changedDefaults: [] + } + } + + updateOverridesWithIntroducedVersions(diffData, overridesPath, 'v26.2.1', { ai: '0.2.32' }, { + attributablePlugins: [] + }) + + const result = JSON.parse(fs.readFileSync(overridesPath, 'utf8')) + expect(result.commands['rpk ai policy create']).toBeUndefined() + expect(result.commands['rpk ai policy']).toBeUndefined() + expect(result.commands['rpk cluster new-command'].introducedInVersion).toBe('v26.2.1') + }) + + test('stamps plugin entries when the plugin is attributable', () => { + fs.writeFileSync(overridesPath, JSON.stringify({ commands: {} })) + + const diffData = { + summary: { newCommands: 1 }, + details: { + newCommands: [{ path: 'rpk ai policy create' }], + newFlags: [], + removedCommands: [], + removedFlags: [], + changedDefaults: [] + } + } + + updateOverridesWithIntroducedVersions(diffData, overridesPath, 'v26.2.1', { ai: '0.2.32' }, { + attributablePlugins: ['ai'] + }) + + const result = JSON.parse(fs.readFileSync(overridesPath, 'utf8')) + expect(result.commands['rpk ai policy create'].introducedInVersion).toBe('0.2.32') + }) + + test('isPluginStampAttributable requires a manifest-adjacent baseline', () => { + pluginManifestVersionsCache.set('ai', ['0.2.30', '0.2.31', '0.2.32']) + expect(isPluginStampAttributable('ai', '0.2.31', '0.2.32')).toBe(true) + expect(isPluginStampAttributable('ai', '0.2.32', '0.2.32')).toBe(true) + // A gap means intermediate releases may own the "new" commands + expect(isPluginStampAttributable('ai', '0.2.30', '0.2.32')).toBe(false) + // Unknown baseline can never be attributed + expect(isPluginStampAttributable('ai', undefined, '0.2.32')).toBe(false) + }) + + test('attributablePluginSet evaluates each plugin independently', () => { + pluginManifestVersionsCache.set('ai', ['0.2.31', '0.2.32']) + pluginManifestVersionsCache.set('connect', ['4.101.0', '4.102.0', '4.103.1']) + const set = attributablePluginSet( + { ai: '0.2.31', connect: '4.101.0' }, + { ai: '0.2.32', connect: '4.103.1' } + ) + expect(set.has('ai')).toBe(true) + expect(set.has('connect')).toBe(false) + }) + }) + describe('flag version tracking', () => { test('adds introducedInVersion for new flags', () => { fs.writeFileSync(overridesPath, JSON.stringify({ commands: {} })) diff --git a/__tests__/tools/rpk-docs/text-transformations.test.js b/__tests__/tools/rpk-docs/text-transformations.test.js index 76caba74..fe3cb529 100644 --- a/__tests__/tools/rpk-docs/text-transformations.test.js +++ b/__tests__/tools/rpk-docs/text-transformations.test.js @@ -146,6 +146,99 @@ describe('applyToCode rules in early code blocks', () => { }) }) +describe('applyToCode rules in inline code spans', () => { + const { formatDescription } = require('../../../tools/rpk-docs/generate-rpk-docs.js') + + const transforms = { + replacements: [ + { pattern: '\\brpai\\b', replacement: 'rpk ai', flags: 'g', applyToCode: true }, + { pattern: '(^|\\n\\s*)Note:\\s', replacement: '$1NOTE: ', flags: 'g' } + ] + } + + test('rewrites the binary name inside protected inline code spans', () => { + const out = formatDescription('Run `rpai auth token` to authenticate first.', transforms) + expect(out).toContain('`rpk ai auth token`') + expect(out).not.toContain('rpai') + }) + + test('rules without applyToCode never touch inline code spans', () => { + const out = formatDescription('The literal `Note: keep this` stays verbatim.', transforms) + expect(out).toContain('`Note: keep this`') + expect(out).not.toContain('NOTE: keep this') + }) +}) + +describe('known command path formatting', () => { + const { formatDescription, registerKnownCommandPaths } = require('../../../tools/rpk-docs/generate-rpk-docs.js') + + afterEach(() => registerKnownCommandPaths([])) + + test('wraps a full multi-word command path as a unit', () => { + registerKnownCommandPaths(['rpk', 'rpk ai', 'rpk ai run', 'rpk ai run codex']) + const out = formatDescription('Use rpk ai run codex to start a session.', null) + expect(out).toContain('`rpk ai run codex` to start a session.') + expect(out).not.toContain('`rpk` ai') + }) + + test('prefers the longest registered path over a shorter prefix', () => { + registerKnownCommandPaths(['rpk', 'rpk ai', 'rpk ai run', 'rpk ai run claude']) + const out = formatDescription('Then rpk ai run claude resumes the session.', null) + expect(out).toContain('`rpk ai run claude` resumes') + }) + + test('leaves prose that resembles a command alone when not in the tree', () => { + registerKnownCommandPaths(['rpk', 'rpk cloud']) + const out = formatDescription('Manage rpk cloud authentications for details.', null) + // A registered single-token prefix ("rpk cloud") never matches: the + // known-path pass requires two tokens after rpk, so the phrase falls + // through to the context-aware heuristic, which wraps rpk alone. + expect(out).toBe('Manage `rpk` cloud authentications for details.') + }) + + test('is inert when no paths are registered', () => { + const out = formatDescription('Use rpk ai run codex to start.', null) + expect(out).not.toContain('`rpk ai run codex`') + }) +}) + +describe('mid-token periods in summaries', () => { + const { formatDescription, capToTwoSentences } = require('../../../tools/rpk-docs/generate-rpk-docs.js') + + test('dotted topic names never split or drop a sentence', () => { + const src = "View logs for a transform.\n\nData transform's STDOUT and STDERR are captured during runtime and written to \nan internally managed topic _redpanda.transform_logs.\nThis command outputs logs for a single transform." + const out = capToTwoSentences(formatDescription(src, null, { skipTableConversion: true, skipListConversion: true })) + expect(out).not.toBe('View logs for a transform. transform_logs.') + expect(out).toContain('`_redpanda.transform_logs`') + expect(out).toContain('STDOUT and STDERR are captured') + }) + + test('URLs never split or drop a sentence', () => { + const out = capToTwoSentences('Generate a license. To get one, contact us at redpanda.com/contact for details. The license is saved locally.') + expect(out).toBe('Generate a license. To get one, contact us at redpanda.com/contact for details.') + }) + + test('an unterminated paragraph is a sentence boundary', () => { + const out = capToTwoSentences('Generate a trial license\n\nThis command generates a license for a 30-day trial. The license is saved locally.') + expect(out).toBe('Generate a trial license. This command generates a license for a 30-day trial.') + }) +}) + +describe('internal topic name backticking', () => { + const { formatDescription } = require('../../../tools/rpk-docs/generate-rpk-docs.js') + + test('wraps _redpanda.* topic names in inline code', () => { + const out = formatDescription('Logs are written to an internally managed topic _redpanda.transform_logs.\nRead them with the logs command.', null) + expect(out).toContain('`_redpanda.transform_logs`.') + }) + + test('leaves already-backticked topic names alone', () => { + const out = formatDescription('Logs go to `_redpanda.transform_logs` always.', null) + expect(out).toContain('`_redpanda.transform_logs`') + expect(out).not.toContain('``') + }) +}) + describe('applyTextTransformationsToExamples', () => { const { applyTextTransformationsToExamples } = require('../../../tools/rpk-docs/generate-rpk-docs.js') diff --git a/docs-data/rpk-overrides.schema.json b/docs-data/rpk-overrides.schema.json index 7bd6d734..c0a0b001 100644 --- a/docs-data/rpk-overrides.schema.json +++ b/docs-data/rpk-overrides.schema.json @@ -39,7 +39,7 @@ }, "applyToCode": { "type": "boolean", - "description": "Also apply this replacement inside code blocks captured from help text (verbatim command examples). Default false: code blocks are protected from prose transformations. Use for binary-name rewrites like rpai to rpk ai." + "description": "Also apply this replacement inside code contexts: code blocks captured from help text (verbatim command examples) and inline code spans. Default false: code contexts are protected from prose transformations. Use for binary-name rewrites like rpai to rpk ai." } }, "required": ["pattern", "replacement"], diff --git a/tools/rpk-docs/generate-rpk-docs.js b/tools/rpk-docs/generate-rpk-docs.js index 3130e424..aa605a93 100644 --- a/tools/rpk-docs/generate-rpk-docs.js +++ b/tools/rpk-docs/generate-rpk-docs.js @@ -15,6 +15,19 @@ Object.entries(helpers).forEach(([name, fn]) => { // Template paths const TEMPLATES_DIR = path.resolve(__dirname, './templates') +// Full command paths from the tree being generated (e.g. "rpk ai run codex"). +// Registered by generateRpkDocs so formatDescription can wrap real multi-word +// command paths as a unit instead of heuristically wrapping `rpk` alone. +let knownCommandPaths = new Set() + +/** + * Register the set of real command paths for the current generation run. + * @param {string[]} paths - Full command paths (e.g. "rpk ai run codex") + */ +function registerKnownCommandPaths(paths) { + knownCommandPaths = new Set(paths) +} + /** * Register a Handlebars partial from file * @param {string} name - Partial name @@ -1607,6 +1620,25 @@ function formatDescription(desc, customTransformations = null, options = {}) { // Fix product name: "Redpanda cloud" → "Redpanda Cloud" (product name) .replace(/Redpanda\s+cloud\b/g, 'Redpanda Cloud') + // === RPK COMMAND FORMATTING (known paths first, ground truth) === + // Wrap full multi-word command paths that exist in the generated tree + // (e.g. "rpk ai run codex" -> `rpk ai run codex`). The heuristic formatter + // below only ever matches "rpk ", so without this pass it wraps + // `rpk` alone and splits the command in half. Matching against the real + // tree means prose that merely resembles a command is never wrapped. + if (knownCommandPaths.size > 0) { + result = result.replace(/(? { + const tokens = rest.trim().split(' ') + for (let n = tokens.length; n >= 2; n--) { + const path = `rpk ${tokens.slice(0, n).join(' ')}` + if (knownCommandPaths.has(path)) { + return `\`${path}\`` + match.slice(path.length) + } + } + return match + }) + } + // === RPK COMMAND FORMATTING (context-aware) === // Process "rpk X" patterns based on what follows result = result.replace(/(? { @@ -1702,6 +1734,11 @@ function formatDescription(desc, customTransformations = null, options = {}) { // Add backticks around standalone file names (not inside paths) result = result.replace(/(? transformed) }) - // Restore inline code + // Restore inline code. Spans are verbatim, but rules flagged applyToCode + // (like the rpai -> rpk ai binary-name rewrite) must reach them the same + // way they reach code blocks, or the internal binary name survives in + // published spans like `rpai auth token`. inlineCode.forEach((code, i) => { - result = result.replace(`__INLINE_CODE_${i}__`, () => code) + const transformed = applyTextTransformations(code, customTransformations, { code: true }) + result = result.replace(`__INLINE_CODE_${i}__`, () => transformed) }) // Restore xrefs @@ -2173,6 +2214,12 @@ function capToTwoSentences(desc) { // The pattern matches: colon, optional whitespace/newlines, then indented content cleaned = cleaned.replace(/:\s*\n+[ \t]+.+$/s, ':') + // A paragraph break is a sentence boundary even when the paragraph has no + // terminal punctuation (cobra short descriptions often lack one: "Generate + // a trial license\n\nThis command..."). Without this, flattening newlines + // glues the paragraphs into one run-on "sentence". + cleaned = cleaned.replace(/([^.!?:\s])[ \t]*\n[ \t]*\n/g, '$1.\n\n') + // Normalize newlines to spaces for inline use (like :description: attribute) const singleLine = cleaned.replace(/\s*\n+\s*/g, ' ').trim() @@ -2202,6 +2249,14 @@ function capToTwoSentences(desc) { // dropped by the sentence matcher normalized = normalized.replace(/(\d)\.(\d)/g, '$1__DECIMAL__$2') + // Protect ALL mid-token periods (no whitespace after): dotted names like + // _redpanda.transform_logs or URLs like redpanda.com/contact are not + // sentence boundaries. Without this the sentence matcher below cannot + // match the sentence containing the token, silently drops everything up + // to the mid-token period, and emits the tail fragment as a "sentence" + // ("View logs for a transform. transform_logs."). + normalized = normalized.replace(/\.(?=\S)/g, '__MIDDOT__') + // Match sentences let sentences = normalized.match(/[^.!?]+[.!?]+(?:\s|$)/g) @@ -2217,6 +2272,7 @@ function capToTwoSentences(desc) { if (!sentences || sentences.length === 0) { // Restore and return let result = normalized.replace(/__DECIMAL__/g, '.') + result = result.replace(/__MIDDOT__/g, '.') placeholders.forEach(({ ph, original }) => { result = result.replace(ph, original) }) @@ -2230,6 +2286,7 @@ function capToTwoSentences(desc) { // Restore decimal points result = result.replace(/__DECIMAL__/g, '.') + result = result.replace(/__MIDDOT__/g, '.') // Restore abbreviations placeholders.forEach(({ ph, original }) => { @@ -2578,6 +2635,9 @@ async function generateRpkDocs(options = {}) { // Flatten command tree const commands = flattenCommands(tree) + // Let formatDescription wrap real multi-word command paths as a unit + registerKnownCommandPaths(commands.map(c => c.path)) + // Determine which managed plugins are protected this run: explicitly // passed by the caller (failed installs) merged with auto-detection of // absent or shim-only plugin subtrees. Protection covers the whole @@ -3121,5 +3181,6 @@ module.exports = { // Exported for testing filterExamples, formatExamples, - applyTextTransformationsToExamples + applyTextTransformationsToExamples, + registerKnownCommandPaths } diff --git a/tools/rpk-docs/rpk-docs-handler.js b/tools/rpk-docs/rpk-docs-handler.js index 68e6ceb2..ef29a514 100644 --- a/tools/rpk-docs/rpk-docs-handler.js +++ b/tools/rpk-docs/rpk-docs-handler.js @@ -894,7 +894,7 @@ function loadVersionedJson(version, dataDir) { * version (the page note renders "introduced in version X"), not * the rpk version, because plugins release on their own cadence. */ -function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, pluginVersions = {}) { +function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, pluginVersions = {}, options = {}) { const hasNewCommands = diffData.details.newCommands && diffData.details.newCommands.length > 0 const hasNewFlags = diffData.details.newFlags && diffData.details.newFlags.length > 0 @@ -908,6 +908,23 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, return pluginVersions[topLevel] || version } + // Plugin-owned entries are only stamped when the caller vouches that the + // baseline is manifest-adjacent to this run's plugin version (see + // isPluginStampAttributable). "New relative to the snapshot" is not "new + // in this release" when the snapshot skipped releases. Callers that omit + // attributablePlugins keep legacy stamp-everything behavior. + const attributable = options.attributablePlugins + ? new Set(options.attributablePlugins) + : null + const skippedByPlugin = new Map() + const stampable = (cmdPath) => { + const topLevel = cmdPath.split(' ')[1] + if (!KNOWN_PLUGINS.includes(topLevel) || !attributable) return true + if (attributable.has(topLevel)) return true + skippedByPlugin.set(topLevel, (skippedByPlugin.get(topLevel) || 0) + 1) + return false + } + let overrides = {} if (fs.existsSync(overridesPath)) { try { @@ -929,6 +946,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, if (hasNewCommands) { for (const newCmd of diffData.details.newCommands) { const cmdPath = newCmd.path + if (!stampable(cmdPath)) continue if (!overrides.commands[cmdPath]) { overrides.commands[cmdPath] = {} } @@ -945,6 +963,7 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, for (const newFlag of diffData.details.newFlags) { const cmdPath = newFlag.commandPath const flagName = newFlag.flagName + if (!stampable(cmdPath)) continue if (!overrides.commands[cmdPath]) { overrides.commands[cmdPath] = {} @@ -964,6 +983,15 @@ function updateOverridesWithIntroducedVersions(diffData, overridesPath, version, } } + for (const [plugin, count] of skippedByPlugin) { + console.warn( + `\u26a0 Skipped stamping introducedInVersion for ${count} new '${plugin}' entr${count === 1 ? 'y' : 'ies'}: ` + + `the baseline snapshot's ${plugin} version is not the release immediately before ` + + `${pluginVersions[plugin] || version} in the plugin manifest, so the introduction ` + + 'version cannot be attributed automatically. Attribute manually against released binaries.' + ) + } + if (commandsUpdated > 0 || flagsUpdated > 0) { fs.writeFileSync(overridesPath, JSON.stringify(overrides, null, 2), 'utf8') const updates = [] @@ -1444,6 +1472,75 @@ function fetchLatestPluginVersion(plugin) { } } +/** + * Fetch the ordered list of published versions for a plugin from its + * manifest. Cached per plugin for the process lifetime. + * @param {string} plugin - rpk command name (ai, connect, k8s, check) + * @returns {string[]|null} Versions in manifest (release) order, or null + */ +const pluginManifestVersionsCache = new Map() +function fetchPluginManifestVersions(plugin) { + if (pluginManifestVersionsCache.has(plugin)) { + return pluginManifestVersionsCache.get(plugin) + } + const slug = PLUGIN_MANIFEST_SLUGS[plugin] || plugin + const result = spawnSync('curl', [ + '-fsSL', '--retry', '3', '--connect-timeout', '15', '--max-time', '30', + `${PLUGIN_MANIFEST_HOST}/${slug}/manifest.json` + ], { encoding: 'utf8', timeout: 60000 }) + let versions = null + if (result.status === 0) { + try { + versions = (JSON.parse(result.stdout).archives || []).map(a => a.version) + } catch (err) { + console.warn(`Could not parse plugin manifest for ${plugin}: ${err.message}`) + } + } else { + console.warn(`Could not fetch plugin manifest for ${plugin} (slug: ${slug})`) + } + pluginManifestVersionsCache.set(plugin, versions) + return versions +} + +/** + * Decide whether new commands under a plugin can truthfully be attributed + * to newVersion: only when the baseline snapshot recorded the plugin + * version AND that version is the release immediately before newVersion in + * the plugin's manifest. Any gap means a "new" command may have shipped in + * an intermediate release, so a stamp would fabricate history (30 rpk ai + * commands were once labeled 0.2.32 when they shipped in 0.2.26/0.2.28). + * @param {string} plugin - rpk command name + * @param {string|undefined} oldVersion - Plugin version recorded in the + * baseline snapshot's plugin_versions, if any + * @param {string} newVersion - Plugin version in this run + * @returns {boolean} + */ +function isPluginStampAttributable(plugin, oldVersion, newVersion) { + if (!oldVersion || !newVersion) return false + if (oldVersion === newVersion) return true + const versions = fetchPluginManifestVersions(plugin) + if (!versions) return false + const oldIdx = versions.indexOf(oldVersion) + const newIdx = versions.indexOf(newVersion) + return oldIdx !== -1 && newIdx === oldIdx + 1 +} + +/** + * Compute the set of plugins whose new commands may be stamped this run. + * @param {Object} oldPluginVersions - plugin_versions from the baseline snapshot + * @param {Object} newPluginVersions - plugin_versions for this run + * @returns {Set} + */ +function attributablePluginSet(oldPluginVersions = {}, newPluginVersions = {}) { + const attributable = new Set() + for (const plugin of Object.keys(newPluginVersions)) { + if (isPluginStampAttributable(plugin, oldPluginVersions[plugin], newPluginVersions[plugin])) { + attributable.add(plugin) + } + } + return attributable +} + /** * Check whether a plugin node contains real plugin commands, as opposed to * only the install/uninstall/upgrade shim compiled into rpk itself. @@ -1952,7 +2049,12 @@ async function handleRpkDocsGeneration(options = {}) { pluginDiffData, overridesPath || path.join(dataDir, 'rpk-overrides.json'), resolvedVersion, - { [plugin]: resolvedVersion } + { [plugin]: resolvedVersion }, + { + attributablePlugins: isPluginStampAttributable( + plugin, (jsonData.plugin_versions || {})[plugin], resolvedVersion + ) ? [plugin] : [] + } ) } @@ -1996,7 +2098,9 @@ async function handleRpkDocsGeneration(options = {}) { newDeprecatedCommands: jsonData.deprecated_commands || {} }) if (stampDiff.details.newCommands.length > 0 || stampDiff.details.newFlags.length > 0) { - updateOverridesWithIntroducedVersions(stampDiff, effectiveOverridesPath, rpkVersion, pluginVersions) + updateOverridesWithIntroducedVersions(stampDiff, effectiveOverridesPath, rpkVersion, pluginVersions, { + attributablePlugins: attributablePluginSet(oldDataForStamp.plugin_versions, pluginVersions) + }) } } } @@ -2400,7 +2504,9 @@ async function handleRpkDocsGeneration(options = {}) { // cadence, so the rpk version would be wrong and the page note would // render "introduced in version "). if ((diffData.details.newCommands.length > 0 || diffData.details.newFlags.length > 0) && effectiveOverridesPath) { - updateOverridesWithIntroducedVersions(diffData, effectiveOverridesPath, rpkVersion, pluginVersions) + updateOverridesWithIntroducedVersions(diffData, effectiveOverridesPath, rpkVersion, pluginVersions, { + attributablePlugins: attributablePluginSet(oldData.plugin_versions, pluginVersions) + }) } // Update what's-new file if requested @@ -2819,6 +2925,11 @@ module.exports = { getPlatformDescription, getCurrentPlatform, updateOverridesWithIntroducedVersions, + isPluginStampAttributable, + attributablePluginSet, + fetchPluginManifestVersions, + // Exported for tests to seed manifest data without network access + pluginManifestVersionsCache, filterDiffForWhatsNew, computeDescriptionCoverage, updateWhatsNewFile, diff --git a/tools/rpk-docs/templates/command.hbs b/tools/rpk-docs/templates/command.hbs index 1df742eb..afb76adb 100644 --- a/tools/rpk-docs/templates/command.hbs +++ b/tools/rpk-docs/templates/command.hbs @@ -1,11 +1,10 @@ -{{!-- Cloud-only wrapper start --}} -{{#if cloudOnly}} -ifdef::env-cloud[] -{{/if}} -{{!-- Self-hosted-only wrapper start --}} -{{#if selfHostedOnly}} -ifndef::env-cloud[] -{{/if}} +{{!-- cloudOnly/selfHostedOnly deliberately do NOT wrap the page. The + gating that matters happens inside the single-source tag region + (subcommand rows, flags, content items): includes only extract that + region, and the docs component never builds with env-cloud set, so a + page-level conditional is dead code at best and an empty, untitled + page at worst. Availability pages are excluded from cloud by not + generating a stub for them in cloud-docs. --}} = {{commandPath}} :description: {{{wrapDescriptionPassthrough shortDesc}}} {{#if pageAliases}} @@ -583,11 +582,3 @@ include::{{{this}}}[] {{> content-item this commandPath=../commandPath}} {{/each}} // end::single-source[] -{{!-- Self-hosted-only wrapper end --}} -{{#if selfHostedOnly}} -endif::[] -{{/if}} -{{!-- Cloud-only wrapper end --}} -{{#if cloudOnly}} -endif::[] -{{/if}}