@redpanda-data/docs-extensions-and-macros 5.11.0 → 5.11.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.
package/bin/doc-tools.js CHANGED
@@ -2223,7 +2223,7 @@ const validation = new Command('validate').description('Validate docs data again
2223
2223
  * @why A feature must only be documented as enterprise under its approved
2224
2224
  * external name. This check catches new license-gated features in core that
2225
2225
  * have no registry entry yet, registry pointers that no longer match core,
2226
- * and stale connect enterprise lists.
2226
+ * and registry connect-plugin entries that no longer match info.csv.
2227
2227
  *
2228
2228
  * @example
2229
2229
  * # Check everything against the default remote sources
@@ -2244,58 +2244,27 @@ validation
2244
2244
  .description('Check the enterprise features registry against core, connect, and docs sources of truth')
2245
2245
  .option('--registry <path>', 'Local path to enterprise-features.yml (default: fetch from docs repo main)')
2246
2246
  .option('--tag <ref>', 'Redpanda git ref for the core headers', 'dev')
2247
- .option('--connect-ref <ref>', 'Connect git ref for info.csv', 'main')
2247
+ .option('--connect-ref <ref>', "Connect git ref for info.csv ('latest' resolves the newest release tag; the registry documents released state)", 'latest')
2248
2248
  .option('--docs-ref <ref>', 'Docs repo git ref for remote fetches', 'main')
2249
2249
  .option('--disable-page <path>', 'Local path to disable-enterprise-features.adoc (default: fetch from docs repo)')
2250
- .option('--antora <path>', 'Local path to rp-connect-docs antora.yml (default: fetch from rp-connect-docs main)')
2251
2250
  .option('--properties <path>', 'Local path to a generated cluster-properties JSON; enables existence checks for gating properties')
2252
- .option('--skip-connect', 'Skip the connect info.csv comparison')
2251
+ .option('--skip-connect', 'Skip the connect check (info.csv is not fetched, so registry connect-plugin entries go unverified)')
2253
2252
  .option('--format <format>', 'Output format: text or json', 'text')
2254
2253
  .option('--write-mapping <path>', 'Write the internal-to-external name mapping partial to this path')
2255
2254
  .action(async (options) => {
2256
2255
  const { runChecks, buildMappingPartial } = require('../tools/enterprise-features/verify')
2257
- const { extractAntoraEnterpriseComponents } = require('../tools/enterprise-features/parsers')
2258
- const yamlLib = require('js-yaml')
2259
-
2260
- const RAW = 'https://raw.githubusercontent.com'
2261
- // Sources that fail to load are reported as error-level findings rather
2262
- // than silently skipped, so CI cannot pass on a check that never ran.
2263
- const failedSources = []
2264
- async function fetchText (url, sourceName) {
2265
- const resp = await fetch(url)
2266
- if (!resp.ok) {
2267
- if (sourceName) {
2268
- failedSources.push({ level: 'error', check: 'fetch', message: `Could not load ${sourceName} (${url}): ${resp.status} ${resp.statusText}. The related checks did not run.` })
2269
- return undefined
2270
- }
2271
- throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}`)
2272
- }
2273
- return resp.text()
2274
- }
2275
- const readLocal = (p) => fs.readFileSync(path.resolve(p), 'utf8')
2256
+ // Fetch wiring (GitHub auth, rejected-token retry, transient-failure
2257
+ // retries, --skip-connect short-circuits) lives in
2258
+ // cli-utils/enterprise-sources so it is testable without a network.
2259
+ const { loadEnterpriseSources } = require('../cli-utils/enterprise-sources')
2276
2260
 
2277
2261
  try {
2278
- const registryYaml = options.registry
2279
- ? readLocal(options.registry)
2280
- : await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/shared/modules/ROOT/partials/enterprise-features.yml`)
2281
- const coreHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/features/enterprise_features.h`, 'core enterprise_features.h')
2282
- const configurationHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/config/configuration.h`, 'core configuration.h')
2283
- const infoCsv = options.skipConnect
2284
- ? undefined
2285
- : await fetchText(`${RAW}/redpanda-data/connect/${options.connectRef}/internal/plugins/info.csv`, 'connect info.csv')
2286
- const disablePage = options.disablePage
2287
- ? readLocal(options.disablePage)
2288
- : await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/modules/get-started/pages/licensing/disable-enterprise-features.adoc`, 'disable-enterprise-features.adoc')
2289
- const antoraYaml = options.antora
2290
- ? readLocal(options.antora)
2291
- : await fetchText(`${RAW}/redpanda-data/rp-connect-docs/main/antora.yml`, 'rp-connect-docs antora.yml')
2292
- const antoraEnterpriseComponents = antoraYaml
2293
- ? extractAntoraEnterpriseComponents(yamlLib.load(antoraYaml))
2294
- : undefined
2262
+ const { registryYaml, coreHeader, configurationHeader, infoCsv, connectRef, disablePage, failedSources } =
2263
+ await loadEnterpriseSources(options)
2295
2264
 
2296
2265
  let allPropertyNames
2297
2266
  if (options.properties) {
2298
- const propertyData = JSON.parse(readLocal(options.properties))
2267
+ const propertyData = JSON.parse(fs.readFileSync(path.resolve(options.properties), 'utf8'))
2299
2268
  allPropertyNames = Object.keys(propertyData.properties || propertyData)
2300
2269
  }
2301
2270
 
@@ -2304,7 +2273,7 @@ validation
2304
2273
  coreHeader,
2305
2274
  configurationHeader,
2306
2275
  infoCsv,
2307
- antoraEnterpriseComponents,
2276
+ connectRef,
2308
2277
  disablePage,
2309
2278
  allPropertyNames,
2310
2279
  })
@@ -0,0 +1,244 @@
1
+ 'use strict'
2
+
3
+ /**
4
+ * Source loading for `doc-tools validate enterprise-features`.
5
+ *
6
+ * Assembles the raw text of every source of truth the validator compares
7
+ * against: the enterprise features registry, the core headers, the connect
8
+ * plugin list, and the disable-enterprise-features page. Extracted from
9
+ * bin/doc-tools.js so the fetch wiring (auth, retries, --skip-connect
10
+ * short-circuits) is testable without a network.
11
+ */
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+ const { getGitHubToken } = require('./github-token')
16
+
17
+ const RAW = 'https://raw.githubusercontent.com'
18
+
19
+ // Repos known to be private, so a 404 can legitimately mean "needs auth".
20
+ // Public sources (redpanda core headers, connect info.csv) 404 only when the
21
+ // file is genuinely missing, and suggesting credentials there sends the
22
+ // reader after a problem that does not exist (e.g. during a transient
23
+ // GitHub blip).
24
+ const PRIVATE_REPO_PREFIXES = [
25
+ `${RAW}/redpanda-data/docs/`,
26
+ `${RAW}/redpanda-data/rp-connect-docs/`,
27
+ ]
28
+
29
+ const TOKEN_HINT = ' This source is in a private repository: set GIT_CREDENTIALS (or GITHUB_TOKEN / REDPANDA_GITHUB_TOKEN / ACTIONS_BOT_TOKEN) so the fetch can authenticate.'
30
+ const REJECTED_HINT = ' A GitHub token was sent but rejected, so it may be expired or not grant access to this repository (the default Actions GITHUB_TOKEN is scoped to its own repository).'
31
+
32
+ const TRANSIENT_RETRIES = 3
33
+ const TRANSIENT_RETRY_DELAY_MS = 2000
34
+
35
+ function isPrivateSource (url) {
36
+ return PRIVATE_REPO_PREFIXES.some((prefix) => url.startsWith(prefix))
37
+ }
38
+
39
+ // The response body of a failed fetch is never read; cancel it so the
40
+ // underlying connection is released instead of lingering until GC.
41
+ async function discardBody (resp) {
42
+ await resp.body?.cancel().catch(() => {})
43
+ }
44
+
45
+ /**
46
+ * Build a fetchText function that authenticates raw.githubusercontent.com
47
+ * requests and records failures for named sources.
48
+ *
49
+ * Some sources live in private repos (the docs repo), which
50
+ * raw.githubusercontent serves as 404 rather than 401 when unauthenticated —
51
+ * indistinguishable from a genuinely missing file. A token is sent when one
52
+ * is available so those fetches resolve; public sources are unaffected.
53
+ *
54
+ * raw.githubusercontent also returns 404 (not 401) for a REJECTED token,
55
+ * even on public files, so a stale token in any of the token env vars would
56
+ * break fetches that work tokenless. On a 404 with a token, the fetch is
57
+ * retried once without authentication before the 404 is treated as real.
58
+ *
59
+ * Transient failures (network errors, 429, 5xx) are retried a few times
60
+ * with a delay — the same treatment the CGO test gives curl — because a
61
+ * single blip on any source would otherwise fail the whole run, and the
62
+ * check runs on a weekly cron where nobody is watching live.
63
+ *
64
+ * @param {object} [deps]
65
+ * @param {string|null} [deps.token] - GitHub token (defaults to getGitHubToken())
66
+ * @param {Function} [deps.fetchImpl] - fetch implementation (defaults to global fetch)
67
+ * @param {Function} [deps.warn] - warning sink (defaults to console.warn)
68
+ * @param {Function} [deps.sleep] - delay implementation, for tests
69
+ * @returns {{fetchText: Function, failedSources: Array}} fetchText and the
70
+ * findings array it appends to. Sources that fail to load are reported as
71
+ * error-level findings rather than silently skipped, so CI cannot pass on a
72
+ * check that never ran. Failures without a sourceName throw instead.
73
+ */
74
+ function createSourceFetcher (deps = {}) {
75
+ const ghToken = deps.token !== undefined ? deps.token : getGitHubToken()
76
+ const fetchImpl = deps.fetchImpl || ((...args) => fetch(...args))
77
+ const warn = deps.warn || ((msg) => console.warn(msg))
78
+ const sleep = deps.sleep || ((ms) => new Promise((resolve) => setTimeout(resolve, ms)))
79
+ const failedSources = []
80
+ let warnedRejectedToken = false
81
+
82
+ // One HTTP attempt with transient-failure retries. Returns the final
83
+ // response; network errors that persist through every retry are rethrown.
84
+ async function attemptFetch (url, opts) {
85
+ let lastError
86
+ for (let attempt = 1; attempt <= TRANSIENT_RETRIES; attempt++) {
87
+ let resp
88
+ try {
89
+ resp = await fetchImpl(url, opts)
90
+ } catch (err) {
91
+ lastError = err
92
+ if (attempt < TRANSIENT_RETRIES) await sleep(TRANSIENT_RETRY_DELAY_MS)
93
+ continue
94
+ }
95
+ if (resp.status === 429 || resp.status >= 500) {
96
+ await discardBody(resp)
97
+ lastError = null
98
+ if (attempt < TRANSIENT_RETRIES) {
99
+ await sleep(TRANSIENT_RETRY_DELAY_MS)
100
+ continue
101
+ }
102
+ return resp
103
+ }
104
+ return resp
105
+ }
106
+ throw lastError
107
+ }
108
+
109
+ async function fetchText (url, sourceName) {
110
+ let resp
111
+ try {
112
+ resp = await attemptFetch(url, ghToken ? { headers: { Authorization: `Bearer ${ghToken}` } } : undefined)
113
+ } catch (err) {
114
+ if (sourceName) {
115
+ failedSources.push({ level: 'error', check: 'fetch', message: `Could not load ${sourceName} (${url}): ${err.message}. The related checks did not run.` })
116
+ return undefined
117
+ }
118
+ throw new Error(`Failed to fetch ${url}: ${err.message}`)
119
+ }
120
+ if (resp.ok) return resp.text()
121
+ await discardBody(resp)
122
+
123
+ let hint = ''
124
+ if (resp.status === 404 && ghToken) {
125
+ const retry = await attemptFetch(url)
126
+ if (retry.ok) {
127
+ if (!warnedRejectedToken) {
128
+ warnedRejectedToken = true
129
+ warn('Warning: the configured GitHub token was rejected by raw.githubusercontent.com (it may be expired); fetching without authentication instead.')
130
+ }
131
+ return retry.text()
132
+ }
133
+ await discardBody(retry)
134
+ if (isPrivateSource(url)) hint = REJECTED_HINT
135
+ } else if (resp.status === 404 && isPrivateSource(url)) {
136
+ // A tokenless 404 on a known-private repo is the signature of an
137
+ // unauthenticated fetch, so say so instead of leaving it to look
138
+ // like a missing file. Public sources get no credential hint: their
139
+ // 404s mean the file is missing (or GitHub is having a bad moment).
140
+ hint = TOKEN_HINT
141
+ }
142
+
143
+ if (sourceName) {
144
+ failedSources.push({ level: 'error', check: 'fetch', message: `Could not load ${sourceName} (${url}): ${resp.status} ${resp.statusText}. The related checks did not run.${hint}` })
145
+ return undefined
146
+ }
147
+ throw new Error(`Failed to fetch ${url}: ${resp.status} ${resp.statusText}.${hint}`)
148
+ }
149
+
150
+ return { fetchText, failedSources }
151
+ }
152
+
153
+ /**
154
+ * Resolve the ref to compare connect plugins at.
155
+ *
156
+ * Primary: the newest published release tag from the GitHub API. Fallback
157
+ * when the API is unavailable: the `latest-connect-version` asciidoc
158
+ * attribute in rp-connect-docs antora.yml — the latest version the docs
159
+ * actually document, which is the state the registry describes. (This is a
160
+ * different use of that file than the retired enterprise-components list:
161
+ * the version attribute is actively maintained by the connect version-update
162
+ * automation.) If both routes fail, the connect check is skipped with an
163
+ * error finding rather than silently comparing against main.
164
+ */
165
+ async function resolveLatestConnectRef ({ fetchText, failedSources, warn }) {
166
+ try {
167
+ const releaseJson = await fetchText('https://api.github.com/repos/redpanda-data/connect/releases/latest')
168
+ const tag = JSON.parse(releaseJson).tag_name
169
+ if (tag) return String(tag)
170
+ throw new Error('the GitHub API response has no tag_name')
171
+ } catch (err) {
172
+ warn(`Warning: could not resolve the latest connect release from the GitHub API (${err.message}); falling back to the latest documented connect version in rp-connect-docs antora.yml.`)
173
+ }
174
+ const antoraYaml = await fetchText(`${RAW}/redpanda-data/rp-connect-docs/main/antora.yml`, 'latest documented connect version (rp-connect-docs antora.yml fallback)')
175
+ if (antoraYaml === undefined) return undefined // fetch failure already reported
176
+ const yamlLib = require('js-yaml')
177
+ let documented
178
+ try {
179
+ const attributes = yamlLib.load(antoraYaml)?.asciidoc?.attributes
180
+ documented = attributes && attributes['latest-connect-version']
181
+ } catch (err) {
182
+ failedSources.push({ level: 'error', check: 'fetch', message: `Could not resolve the connect ref: rp-connect-docs antora.yml did not parse (${err.message}). The connect check did not run.` })
183
+ return undefined
184
+ }
185
+ if (!documented) {
186
+ failedSources.push({ level: 'error', check: 'fetch', message: 'Could not resolve the connect ref: rp-connect-docs antora.yml has no latest-connect-version attribute. The connect check did not run.' })
187
+ return undefined
188
+ }
189
+ const version = String(documented).trim()
190
+ return version.startsWith('v') ? version : `v${version}`
191
+ }
192
+
193
+ /**
194
+ * Load every source the enterprise-features validation consumes.
195
+ *
196
+ * The rp-connect-docs antora.yml is deliberately NOT a source anymore:
197
+ * rp-connect-docs#485 removed the enterprise-components list it carried, so
198
+ * fetching it bought a cross-repo credential requirement in exchange for a
199
+ * comparison against nothing. The connect check now validates the registry's
200
+ * connect-plugin entries directly against info.csv.
201
+ *
202
+ * The connect ref defaults to 'latest', which resolves to the latest
203
+ * published connect release tag. The registry documents released state, so
204
+ * comparing it against main would flag plugins renamed or newly marked
205
+ * enterprise ahead of any release. The core headers deliberately stay on
206
+ * 'dev' for the opposite reason: their check exists to give early warning
207
+ * of NEW license-gated features before they ship.
208
+ *
209
+ * @param {object} options - Parsed CLI options: registry, tag, connectRef,
210
+ * docsRef, disablePage, skipConnect.
211
+ * @param {object} [deps] - Injectable dependencies for tests: token,
212
+ * fetchImpl, warn, sleep (see createSourceFetcher), and readLocal.
213
+ * @returns {Promise<object>} { registryYaml, coreHeader, configurationHeader,
214
+ * infoCsv, connectRef, disablePage, failedSources }
215
+ */
216
+ async function loadEnterpriseSources (options, deps = {}) {
217
+ const { fetchText, failedSources } = createSourceFetcher(deps)
218
+ const readLocal = deps.readLocal || ((p) => fs.readFileSync(path.resolve(p), 'utf8'))
219
+
220
+ const registryYaml = options.registry
221
+ ? readLocal(options.registry)
222
+ : await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/shared/modules/ROOT/partials/enterprise-features.yml`)
223
+ const coreHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/features/enterprise_features.h`, 'core enterprise_features.h')
224
+ const configurationHeader = await fetchText(`${RAW}/redpanda-data/redpanda/${options.tag}/src/v/config/configuration.h`, 'core configuration.h')
225
+
226
+ let connectRef = options.connectRef
227
+ let infoCsv
228
+ if (!options.skipConnect) {
229
+ if (!connectRef || connectRef === 'latest') {
230
+ connectRef = await resolveLatestConnectRef({ fetchText, failedSources, warn: deps.warn || ((msg) => console.warn(msg)) })
231
+ }
232
+ if (connectRef) {
233
+ infoCsv = await fetchText(`${RAW}/redpanda-data/connect/${connectRef}/internal/plugins/info.csv`, `connect info.csv (${connectRef})`)
234
+ }
235
+ }
236
+
237
+ const disablePage = options.disablePage
238
+ ? readLocal(options.disablePage)
239
+ : await fetchText(`${RAW}/redpanda-data/docs/${options.docsRef}/modules/get-started/pages/licensing/disable-enterprise-features.adoc`, 'disable-enterprise-features.adoc')
240
+
241
+ return { registryYaml, coreHeader, configurationHeader, infoCsv, connectRef, disablePage, failedSources }
242
+ }
243
+
244
+ module.exports = { createSourceFetcher, loadEnterpriseSources, RAW }
@@ -1,37 +1,45 @@
1
- const https = require('https');
2
1
  const yaml = require('js-yaml');
3
2
  const { spawnSync } = require('child_process');
3
+ const { getGitHubToken } = require('./github-token');
4
+
5
+ const ANTORA_URL = 'https://raw.githubusercontent.com/redpanda-data/docs/main/antora.yml'
6
+
7
+ const TOKEN_HINT = ' The docs repository is private, so set GIT_CREDENTIALS (or GITHUB_TOKEN / REDPANDA_GITHUB_TOKEN / ACTIONS_BOT_TOKEN) so the fetch can authenticate.'
8
+ const REJECTED_HINT = ' A GitHub token was sent but rejected, so it may be expired or not grant access to the docs repository (the default Actions GITHUB_TOKEN is scoped to its own repository).'
4
9
 
5
10
  /**
6
11
  * Retrieves the current Self-Managed documentation version from the remote antora.yml file.
7
12
  *
13
+ * The docs repository is private, and raw.githubusercontent serves private
14
+ * content as 404 rather than 401 when unauthenticated, so the fetch sends a
15
+ * Bearer token when one is available. It also returns 404 (not 401) for a
16
+ * REJECTED token, so a 404 with a token is retried once without
17
+ * authentication before being treated as real.
18
+ *
19
+ * @param {object} [deps] - Injectable for tests: { fetchImpl, token }.
8
20
  * @returns {Promise<string>} Resolves with the version string (for example, "25.1").
9
21
  *
10
22
  * @throws {Error} If the antora.yml file cannot be fetched, parsed, or if the version field is missing.
11
23
  */
12
- function fetchRemoteAntoraVersion() {
13
- const url = 'https://raw.githubusercontent.com/redpanda-data/docs/main/antora.yml'
14
- return new Promise((resolve, reject) => {
15
- https.get(url, res => {
16
- if (res.statusCode !== 200) {
17
- return reject(new Error(`Failed to fetch antora.yml: ${res.statusCode}`))
18
- }
19
- let body = ''
20
- res.on('data', chunk => (body += chunk))
21
- res.on('end', () => {
22
- try {
23
- const cfg = yaml.load(body)
24
- if (cfg.version == null) {
25
- throw new Error('version field missing')
26
- }
27
- const version = String(cfg.version).trim()
28
- resolve(version)
29
- } catch (err) {
30
- reject(err)
31
- }
32
- })
33
- }).on('error', reject)
34
- })
24
+ async function fetchRemoteAntoraVersion(deps = {}) {
25
+ const fetchImpl = deps.fetchImpl || fetch
26
+ const ghToken = 'token' in deps ? deps.token : getGitHubToken()
27
+
28
+ let resp = await fetchImpl(ANTORA_URL, ghToken ? { headers: { Authorization: `Bearer ${ghToken}` } } : undefined)
29
+ const hint = ghToken ? REJECTED_HINT : TOKEN_HINT
30
+ if (resp.status === 404 && ghToken) {
31
+ await resp.body?.cancel().catch(() => {})
32
+ resp = await fetchImpl(ANTORA_URL)
33
+ }
34
+ if (!resp.ok) {
35
+ await resp.body?.cancel().catch(() => {})
36
+ throw new Error(`Failed to fetch antora.yml: ${resp.status}${resp.status === 404 ? hint : ''}`)
37
+ }
38
+ const cfg = yaml.load(await resp.text())
39
+ if (cfg.version == null) {
40
+ throw new Error('version field missing')
41
+ }
42
+ return String(cfg.version).trim()
35
43
  }
36
44
 
37
45
  /**
@@ -141,7 +141,8 @@ async function setupMCP(options = {}) {
141
141
 
142
142
  // Check if configuration matches desired mode
143
143
  const isCorrectNpxConfig = mode === 'npx' && existingCommand === 'npx' &&
144
- existingArgs.includes('doc-tools-mcp');
144
+ existingArgs.includes('doc-tools-mcp') &&
145
+ existingArgs.some(a => a === `--package=${packageName}`);
145
146
  const isCorrectLocalConfig = mode === 'local' && existingCommand === 'node' &&
146
147
  existingArgs[0] === localPath;
147
148
 
@@ -149,7 +150,7 @@ async function setupMCP(options = {}) {
149
150
  needsUpdate = true;
150
151
  console.log(chalk.yellow('⚠ ') + ' MCP server configured but with different setup:');
151
152
  console.log(chalk.gray(` Current: ${existingCommand} ${existingArgs.join(' ')}`));
152
- const newSetup = mode === 'local' ? `node ${localPath}` : 'npx -y doc-tools-mcp';
153
+ const newSetup = mode === 'local' ? `node ${localPath}` : `npx -y --package=${packageName} doc-tools-mcp`;
153
154
  console.log(chalk.gray(` New: ${newSetup}\n`));
154
155
  } else {
155
156
  console.log(chalk.green('✓') + ` MCP server already configured correctly\n`);
@@ -197,7 +198,7 @@ async function setupMCP(options = {}) {
197
198
  if (mode === 'local') {
198
199
  command = `claude mcp add --transport stdio --scope user ${serverName} -- node ${localPath}`;
199
200
  } else {
200
- command = `claude mcp add --transport stdio --scope user ${serverName} -- npx -y doc-tools-mcp`;
201
+ command = `claude mcp add --transport stdio --scope user ${serverName} -- npx -y --package=${packageName} doc-tools-mcp`;
201
202
  }
202
203
 
203
204
  // Execute the command
@@ -1202,7 +1202,7 @@ claude mcp add --transport stdio --scope user redpanda-docs-tool-assistant -- \
1202
1202
  [,bash]
1203
1203
  ----
1204
1204
  claude mcp add --transport stdio --scope user redpanda-docs-tool-assistant -- \
1205
- npx -y doc-tools-mcp
1205
+ npx -y --package=@redpanda-data/docs-extensions-and-macros doc-tools-mcp
1206
1206
  ----
1207
1207
 
1208
1208
  This adds the MCP server configuration to `~/.claude.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.11.0",
3
+ "version": "5.11.1",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -24,6 +24,7 @@
24
24
  "build": "antora --to-dir docs --fetch local-antora-playbook.yml",
25
25
  "serve": "wds --node-resolve --open preview/test/ --watch --root-dir docs",
26
26
  "test": "jest",
27
+ "test:unit": "jest --config jest.unit.config.js",
27
28
  "test:e2e": "jest __tests__/e2e/",
28
29
  "test:mcp": "jest __tests__/mcp/",
29
30
  "test:mcp:integration": "jest __tests__/mcp/integration.test.js",
@@ -112,23 +112,9 @@ function parseDisableTable (source) {
112
112
  return rows
113
113
  }
114
114
 
115
- /**
116
- * Extract the enterprise-components attribute list from rp-connect-docs
117
- * antora.yml, without a YAML dependency on the full file structure.
118
- *
119
- * @param {object} antoraConfig - Parsed antora.yml object.
120
- * @returns {string[]|undefined} The list, or undefined when absent.
121
- */
122
- function extractAntoraEnterpriseComponents (antoraConfig) {
123
- const attributes = antoraConfig && antoraConfig.asciidoc && antoraConfig.asciidoc.attributes
124
- const list = attributes && attributes['enterprise-components']
125
- return Array.isArray(list) ? list.map(String) : undefined
126
- }
127
-
128
115
  module.exports = {
129
116
  parseCoreEnum,
130
117
  parseEnterpriseProperties,
131
118
  parseConnectEnterprisePlugins,
132
119
  parseDisableTable,
133
- extractAntoraEnterpriseComponents,
134
120
  }
@@ -122,22 +122,36 @@ function checkCoreProperties (features, enterpriseProperties, allPropertyNames)
122
122
  }
123
123
 
124
124
  /**
125
- * Compare connect info.csv enterprise plugins against the stale
126
- * enterprise-components list in rp-connect-docs antora.yml (when given).
125
+ * Validate registry connect-plugin pointers against the enterprise plugins
126
+ * in connect's info.csv.
127
+ *
128
+ * This used to diff info.csv against the hand-maintained
129
+ * enterprise-components list in rp-connect-docs antora.yml, but
130
+ * rp-connect-docs#485 removed that list (it had gone stale — the exact drift
131
+ * this validator exists to catch). Per-plugin enterprise status in the docs
132
+ * now comes from the generated connector catalog, which is built from the
133
+ * same connect metadata as info.csv, so it cannot drift by construction.
134
+ * What can still go stale is the registry itself: any entry that pins a
135
+ * specific plugin via `source: {kind: connect-plugin}` must name a plugin
136
+ * that info.csv actually marks enterprise.
137
+ *
138
+ * The check also reports what it verified, so a run with zero pinned
139
+ * plugins is visibly a shallow pass rather than a silent one.
127
140
  */
128
- function checkConnect (infoCsvPlugins, antoraList) {
141
+ function checkConnect (infoCsvPlugins, features, connectRef) {
129
142
  const findings = []
130
- if (!antoraList) return findings
131
- for (const plugin of infoCsvPlugins) {
132
- if (!antoraList.includes(plugin)) {
133
- findings.push(finding('error', 'connect-list', `Plugin '${plugin}' is enterprise in info.csv but missing from the enterprise-components list in rp-connect-docs antora.yml.`))
134
- }
135
- }
136
- for (const plugin of antoraList) {
137
- if (!infoCsvPlugins.includes(plugin)) {
138
- findings.push(finding('error', 'connect-list', `Plugin '${plugin}' is in the enterprise-components list in rp-connect-docs antora.yml but not enterprise in info.csv.`))
143
+ const pinned = []
144
+ const source = connectRef ? `info.csv@${connectRef}` : 'info.csv'
145
+ for (const feature of features) {
146
+ if (feature.source && feature.source.kind === 'connect-plugin') {
147
+ const value = String(feature.source.value).trim()
148
+ pinned.push(value)
149
+ if (!infoCsvPlugins.includes(value)) {
150
+ findings.push(finding('error', 'connect-list', `'${feature.name}' points at connect plugin '${value}', which is not an enterprise plugin in ${source} (renamed, removed, or no longer enterprise).`))
151
+ }
139
152
  }
140
153
  }
154
+ findings.push(finding('info', 'connect-list', `${source} lists ${infoCsvPlugins.length} enterprise plugin(s); ${pinned.length} pinned by registry connect-plugin entries. Individual plugins are documented by the generated connector catalog; the licensing table covers them through the aggregate connect-scope entries.`))
141
155
  return findings
142
156
  }
143
157
 
@@ -200,7 +214,7 @@ function buildMappingPartial (features, enumValues) {
200
214
  * @param {string} [sources.coreHeader] - enterprise_features.h contents.
201
215
  * @param {string} [sources.configurationHeader] - configuration.h contents.
202
216
  * @param {string} [sources.infoCsv] - connect info.csv contents.
203
- * @param {string[]} [sources.antoraEnterpriseComponents]
217
+ * @param {string} [sources.connectRef] - ref the info.csv was fetched at, for messages.
204
218
  * @param {string} [sources.disablePage] - disable-enterprise-features.adoc contents.
205
219
  * @param {string[]} [sources.allPropertyNames] - full property list for gating checks.
206
220
  * @returns {{findings: object[], features: object[], enumValues: string[]}}
@@ -223,7 +237,7 @@ function runChecks (sources) {
223
237
  }
224
238
  if (sources.infoCsv) {
225
239
  const plugins = parsers.parseConnectEnterprisePlugins(sources.infoCsv)
226
- findings.push(...checkConnect(plugins, sources.antoraEnterpriseComponents))
240
+ findings.push(...checkConnect(plugins, features, sources.connectRef))
227
241
  } else {
228
242
  findings.push(finding('info', 'connect-list', 'Skipped: no connect info.csv provided.'))
229
243
  }