@redpanda-data/docs-extensions-and-macros 5.11.0 → 5.12.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 +11 -42
- package/cli-utils/enterprise-sources.js +244 -0
- package/cli-utils/self-managed-docs-branch.js +32 -24
- package/cli-utils/setup-mcp.js +4 -3
- package/docs-data/rpk-overrides.schema.json +744 -0
- package/extensions/set-available-attachment-versions.js +9 -1
- package/macros/DOCUMENTING_UNRELEASED_FEATURES.adoc +188 -0
- package/macros/PROPERTY_AND_ENTERPRISE_REFERENCES.adoc +20 -2
- package/macros/REFERENCE.adoc +92 -3
- package/macros/badge.js +9 -1
- package/macros/enterprise.js +180 -19
- package/macros/prop.js +636 -183
- package/mcp/USER_GUIDE.adoc +1 -1
- package/package.json +6 -4
- package/tools/enterprise-features/parsers.js +0 -14
- package/tools/enterprise-features/verify.js +28 -14
- package/tools/property-extractor/cloud_config.py +16 -3
- package/tools/redpanda-connect/report-delta.js +49 -3
- package/tools/rpk-docs/extract-conditional-content.js +3 -1
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
|
|
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>',
|
|
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
|
|
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
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
const
|
|
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 =
|
|
2279
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
/**
|
package/cli-utils/setup-mcp.js
CHANGED
|
@@ -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}` :
|
|
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
|