@redpanda-data/docs-extensions-and-macros 5.35.1 → 5.37.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 +262 -0
- package/cli-utils/kapa-credentials.js +109 -0
- package/docs-data/kapa-source-groups.json +119 -0
- package/docs-data/kapa-source-groups.schema.json +73 -0
- package/extensions/kapa-source-groups.js +180 -0
- package/extensions/util/scan-content-urls.js +48 -3
- package/package.json +7 -3
- package/tools/generate-cli-docs.js +19 -4
- package/tools/kapa-source-groups/fetch-with-deadline.js +33 -0
- package/tools/kapa-source-groups/generate-kapa-source-groups.js +319 -0
- package/tools/kapa-source-groups/kapa-source-versions.js +83 -0
- package/tools/kapa-source-groups/published-segments.js +98 -0
package/bin/doc-tools.js
CHANGED
|
@@ -2312,6 +2312,107 @@ automation
|
|
|
2312
2312
|
}
|
|
2313
2313
|
})
|
|
2314
2314
|
|
|
2315
|
+
/**
|
|
2316
|
+
* generate kapa-source-groups
|
|
2317
|
+
*
|
|
2318
|
+
* @description
|
|
2319
|
+
* Reads the source-group tree and the per-source group assignments from Kapa's ingestion API
|
|
2320
|
+
* and writes a mapping from each docs URL version segment to the Kapa source group that scopes
|
|
2321
|
+
* retrieval to that version. Consumers pass the group id as `sourceGroupIdsInclude` (Agent SDK),
|
|
2322
|
+
* `sourceGroupIDsInclude` (Chat SDK, note the capital ID) or `_meta.source_group_ids_include`
|
|
2323
|
+
* (hosted MCP server).
|
|
2324
|
+
*
|
|
2325
|
+
* The mapping is keyed on the URL segment, not Antora's page.version, because the two disagree
|
|
2326
|
+
* for the latest release: latest_version_segment: 'current' publishes 26.2 at
|
|
2327
|
+
* /streaming/current/ while page.version reads 26.2.
|
|
2328
|
+
*
|
|
2329
|
+
* This command only ever reads from Kapa. Kapa publishes no write API ("we do not provide an API
|
|
2330
|
+
* endpoint for uploading or managing sources"), so the groups and their source assignments are
|
|
2331
|
+
* created by hand in the Kapa dashboard under Sources > Manage groups. Run this afterwards to
|
|
2332
|
+
* record what is there.
|
|
2333
|
+
*
|
|
2334
|
+
* Refuses rather than writing a misleading mapping when: no groups exist, no group has children,
|
|
2335
|
+
* any version group has no sources assigned, or --default-segment is not one of the groups. An
|
|
2336
|
+
* empty version group is worse than no group, because scoping a query to it returns only the
|
|
2337
|
+
* global sources and the reader silently gets no version-specific content.
|
|
2338
|
+
*
|
|
2339
|
+
* @why
|
|
2340
|
+
* Kapa indexes one separately crawled source per published docs version. Without retrieval
|
|
2341
|
+
* scoping a reader gets answers drawn from any of them: a measured call for "What is a Redpanda
|
|
2342
|
+
* topic partition?" returned 12 sections, six of which were the same get-started/architecture
|
|
2343
|
+
* page at six different versions. Source groups fix that at retrieval time, but the group UUIDs
|
|
2344
|
+
* live in Kapa while the version list lives in Antora, so something has to join the two. Because
|
|
2345
|
+
* a new docs version appears with no file change in docs-site (the playbook globs branches: v/*),
|
|
2346
|
+
* a hand-maintained mapping goes stale silently; generating it and diff-checking the result in CI
|
|
2347
|
+
* is what makes drift loud.
|
|
2348
|
+
*
|
|
2349
|
+
* @example
|
|
2350
|
+
* # Write the mapping to its default location
|
|
2351
|
+
* export KAPA_API_KEY=... KAPA_PROJECT_ID=...
|
|
2352
|
+
* npx doc-tools generate kapa-source-groups
|
|
2353
|
+
*
|
|
2354
|
+
* # Preview without writing
|
|
2355
|
+
* npx doc-tools generate kapa-source-groups --dry-run
|
|
2356
|
+
*
|
|
2357
|
+
* # Scope unversioned pages to something other than "current"
|
|
2358
|
+
* npx doc-tools generate kapa-source-groups --default-segment 26.1
|
|
2359
|
+
*
|
|
2360
|
+
* # Disambiguate when more than one group has children
|
|
2361
|
+
* npx doc-tools generate kapa-source-groups --parent-group Streaming
|
|
2362
|
+
*
|
|
2363
|
+
* @requirements
|
|
2364
|
+
* - Kapa API key and project ID, set via KAPA_API_KEY and KAPA_PROJECT_ID
|
|
2365
|
+
* - Create the key in the Kapa platform under Configuration > API Keys; the project ID is the
|
|
2366
|
+
* UUID in the dashboard URL (https://app.kapa.ai/<project-id>)
|
|
2367
|
+
* - The parent group and its version sub groups must already exist in Kapa, with each version
|
|
2368
|
+
* group holding at least one source
|
|
2369
|
+
* - Internet connection to reach https://api.kapa.ai
|
|
2370
|
+
*/
|
|
2371
|
+
automation
|
|
2372
|
+
.command('kapa-source-groups')
|
|
2373
|
+
.description('Generate the docs version segment to Kapa source group mapping from Kapa\'s ingestion API')
|
|
2374
|
+
.option('--output <file>', 'Output file (relative to repo root, must stay inside the repository)', 'docs-data/kapa-source-groups.json')
|
|
2375
|
+
.option('--default-segment <segment>', 'Segment used for pages with no version of their own', 'current')
|
|
2376
|
+
.option('--parent-group <name>', 'Restrict to one parent group by name (default: the only group that has children)')
|
|
2377
|
+
.option('--dry-run', 'Print output to stdout instead of writing file')
|
|
2378
|
+
.action(async (options) => {
|
|
2379
|
+
const { generateKapaSourceGroups } = require('../tools/kapa-source-groups/generate-kapa-source-groups.js')
|
|
2380
|
+
const { requireKapaCredentials } = require('../cli-utils/kapa-credentials')
|
|
2381
|
+
|
|
2382
|
+
try {
|
|
2383
|
+
const repoRoot = findRepoRoot()
|
|
2384
|
+
// resolveInsideRepo for the same reason every other generator uses it: this
|
|
2385
|
+
// command is reachable from the MCP server, where a write option that escapes
|
|
2386
|
+
// the repo is a write-anywhere primitive.
|
|
2387
|
+
const absOutput = options.dryRun ? undefined : resolveInsideRepo(repoRoot, options.output, '--output')
|
|
2388
|
+
|
|
2389
|
+
// Fail loud on a missing credential rather than emitting an empty mapping:
|
|
2390
|
+
// unauthenticated would look identical to "Kapa has no source groups", and the
|
|
2391
|
+
// drift check would then report false drift forever.
|
|
2392
|
+
const { apiKey, projectId } = requireKapaCredentials()
|
|
2393
|
+
|
|
2394
|
+
const out = await generateKapaSourceGroups({
|
|
2395
|
+
apiKey,
|
|
2396
|
+
projectId,
|
|
2397
|
+
defaultSegment: options.defaultSegment,
|
|
2398
|
+
parentGroupName: options.parentGroup,
|
|
2399
|
+
})
|
|
2400
|
+
|
|
2401
|
+
if (options.dryRun) {
|
|
2402
|
+
process.stdout.write(out)
|
|
2403
|
+
return
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
fs.mkdirSync(path.dirname(absOutput), { recursive: true })
|
|
2407
|
+
fs.writeFileSync(absOutput, out, 'utf8')
|
|
2408
|
+
const segments = Object.keys(JSON.parse(out).segments)
|
|
2409
|
+
console.log(`Done: Wrote ${absOutput} (${segments.length} version segments: ${segments.join(', ')})`)
|
|
2410
|
+
} catch (err) {
|
|
2411
|
+
console.error(`Error: Failed to generate Kapa source groups: ${err.message}`)
|
|
2412
|
+
process.exit(1)
|
|
2413
|
+
}
|
|
2414
|
+
})
|
|
2415
|
+
|
|
2315
2416
|
const validation = new Command('validate').description('Validate docs data against internal sources of truth')
|
|
2316
2417
|
|
|
2317
2418
|
/**
|
|
@@ -2734,6 +2835,167 @@ overridesGroup
|
|
|
2734
2835
|
})
|
|
2735
2836
|
|
|
2736
2837
|
programCli.addCommand(automation)
|
|
2838
|
+
/**
|
|
2839
|
+
* validate kapa-source-groups
|
|
2840
|
+
*
|
|
2841
|
+
* @description
|
|
2842
|
+
* Checks that every streaming docs version currently published has a Kapa source, so Ask AI
|
|
2843
|
+
* can scope answers to it. Exit status is meaningful: 0 every version is covered, 1 a version
|
|
2844
|
+
* is missing, 2 the command itself failed (Kapa or the sitemap unreachable, bad credentials).
|
|
2845
|
+
*
|
|
2846
|
+
* A version counts as covered when Kapa has a source named `Documentation (X)` for it, that
|
|
2847
|
+
* source is assigned to a source group, AND the committed mapping (docs-data/kapa-source-groups.json)
|
|
2848
|
+
* has a segment for it whose group_id is a group that source actually sits in. Four gaps are
|
|
2849
|
+
* reported, each with its own line:
|
|
2850
|
+
*
|
|
2851
|
+
* - missing: the version is published but Kapa has no `Documentation (X)` source.
|
|
2852
|
+
* - unassigned: the source exists but is in no group, so it is global in Kapa and returned for
|
|
2853
|
+
* EVERY query on every version.
|
|
2854
|
+
* - unmapped: Kapa has the grouped source but the committed mapping has no segment for it. Every
|
|
2855
|
+
* deployed surface reads the mapping, not Kapa, so readers on that version silently get the
|
|
2856
|
+
* default segment until the mapping is regenerated and released.
|
|
2857
|
+
* - mismatched: the mapping has the segment but its group_id is not a group the version's source
|
|
2858
|
+
* sits in, which is what a group deleted and recreated in the dashboard, or a source moved into
|
|
2859
|
+
* another version's group, looks like. Every Ask AI query on that version is then scoped to a
|
|
2860
|
+
* group that no longer holds its docs, until the mapping is regenerated and released.
|
|
2861
|
+
*
|
|
2862
|
+
* The prerelease segment (/streaming/beta/) is ignored: a beta publishes there for the whole
|
|
2863
|
+
* pre-GA cycle and is not expected to have a Kapa group of its own.
|
|
2864
|
+
*
|
|
2865
|
+
* @why
|
|
2866
|
+
* The latest release always publishes at /streaming/current/, and the `Documentation (current)`
|
|
2867
|
+
* crawl follows it, so latest is never the version that goes missing. The one that goes missing
|
|
2868
|
+
* is the version that just got archived: when 26.3 ships, 26.2 moves to /streaming/26.2/ and
|
|
2869
|
+
* needs a crawl of its own that nobody has made yet. Kapa has no write API, so that crawl is
|
|
2870
|
+
* created by hand in the dashboard, and nothing else notices when it is forgotten. Readers on the
|
|
2871
|
+
* archived version then silently fall back to the default segment.
|
|
2872
|
+
*
|
|
2873
|
+
* Run it on a schedule so the gap opens an issue rather than waiting for a bug report.
|
|
2874
|
+
*
|
|
2875
|
+
* @example
|
|
2876
|
+
* export KAPA_API_KEY=... KAPA_PROJECT_ID=...
|
|
2877
|
+
* npx doc-tools validate kapa-source-groups
|
|
2878
|
+
*
|
|
2879
|
+
* # Check a preview or staging site instead of production
|
|
2880
|
+
* npx doc-tools validate kapa-source-groups --site-url https://deploy-preview-123--redpanda-documentation.netlify.app
|
|
2881
|
+
*
|
|
2882
|
+
* @requirements
|
|
2883
|
+
* - Kapa API key and project ID, set via KAPA_API_KEY and KAPA_PROJECT_ID
|
|
2884
|
+
* - Internet connection to reach https://api.kapa.ai and the docs site's sitemap
|
|
2885
|
+
*/
|
|
2886
|
+
validation
|
|
2887
|
+
.command('kapa-source-groups')
|
|
2888
|
+
.description('Check every published streaming version has a grouped Kapa source and the committed mapping points at that group (exit 0 in sync, 1 gap, 2 error)')
|
|
2889
|
+
.option('--site-url <url>', 'Docs site whose sitemap says which versions are published', 'https://docs.redpanda.com')
|
|
2890
|
+
.option('--mapping <file>', 'Committed mapping every surface reads', 'docs-data/kapa-source-groups.json')
|
|
2891
|
+
.action(async (options) => {
|
|
2892
|
+
const { fetchKapaSourceVersions } = require('../tools/kapa-source-groups/kapa-source-versions.js')
|
|
2893
|
+
const { fetchPublishedSegments, isPrereleaseSegment } = require('../tools/kapa-source-groups/published-segments.js')
|
|
2894
|
+
const { requireKapaCredentials } = require('../cli-utils/kapa-credentials')
|
|
2895
|
+
|
|
2896
|
+
// Printed only where a gap has actually been established, and required by
|
|
2897
|
+
// kapa-source-groups-drift.sh before it files an issue. Exit 1 alone is not
|
|
2898
|
+
// trustworthy: Commander exits 1 on a usage error and an uncaught throw
|
|
2899
|
+
// exits 1 too, so without this a typo'd flag would file an issue quoting a
|
|
2900
|
+
// stack trace as the report.
|
|
2901
|
+
const SENTINEL = 'KAPA_DRIFT_CONFIRMED'
|
|
2902
|
+
|
|
2903
|
+
// Exit 2 is reserved for "could not find out", so the scheduled job can tell
|
|
2904
|
+
// "a version is missing" apart from "Kapa was down" and not file a false issue.
|
|
2905
|
+
let published, kapa
|
|
2906
|
+
try {
|
|
2907
|
+
const { apiKey, projectId } = requireKapaCredentials()
|
|
2908
|
+
;[published, kapa] = await Promise.all([
|
|
2909
|
+
fetchPublishedSegments({ siteUrl: options.siteUrl }),
|
|
2910
|
+
fetchKapaSourceVersions({ apiKey, projectId }),
|
|
2911
|
+
])
|
|
2912
|
+
} catch (err) {
|
|
2913
|
+
console.error(`Error: Could not check Kapa sources: ${err.message}`)
|
|
2914
|
+
process.exit(2)
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
// The committed mapping is what every surface actually reads (the Antora
|
|
2918
|
+
// extension, docs-ui, the /api/ proxy, the MCP server). Kapa having a
|
|
2919
|
+
// grouped source is necessary but not sufficient: if nobody regenerated and
|
|
2920
|
+
// released the mapping, readers on that version still get the default
|
|
2921
|
+
// segment and nothing else would ever say so.
|
|
2922
|
+
let mappedSegments, mappedGroupIds
|
|
2923
|
+
try {
|
|
2924
|
+
const mappingPath = path.resolve(options.mapping)
|
|
2925
|
+
const mapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
|
|
2926
|
+
if (!mapping || typeof mapping.segments !== 'object' || mapping.segments === null) {
|
|
2927
|
+
throw new Error('no segments object')
|
|
2928
|
+
}
|
|
2929
|
+
mappedSegments = new Set(Object.keys(mapping.segments))
|
|
2930
|
+
mappedGroupIds = new Map(Object.entries(mapping.segments)
|
|
2931
|
+
.map(([seg, entry]) => [seg, entry && entry.group_id != null ? String(entry.group_id) : null]))
|
|
2932
|
+
} catch (err) {
|
|
2933
|
+
console.error(`Error: Could not read the committed mapping at ${options.mapping}: ${err.message}`)
|
|
2934
|
+
process.exit(2)
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
// A beta publishes at /streaming/beta/ for the whole pre-GA cycle and has no
|
|
2938
|
+
// group of its own by design. Reporting it would file a false issue weekly.
|
|
2939
|
+
const prerelease = published.filter(isPrereleaseSegment)
|
|
2940
|
+
const versions = published.filter((v) => !isPrereleaseSegment(v))
|
|
2941
|
+
|
|
2942
|
+
const missing = versions.filter((v) => !kapa.covered.has(v) && !kapa.unassigned.has(v))
|
|
2943
|
+
const unassigned = versions.filter((v) => kapa.unassigned.has(v))
|
|
2944
|
+
const unmapped = versions.filter((v) => kapa.covered.has(v) && !mappedSegments.has(v))
|
|
2945
|
+
// The mapping is what gets sent with every query, so its group_id has to be a
|
|
2946
|
+
// group the version's source is in right now. A group deleted and recreated
|
|
2947
|
+
// keeps its name and gets a new id; a source moved to another version's
|
|
2948
|
+
// group leaves the old id pointing at a group without these docs. Kapa
|
|
2949
|
+
// reports neither, and the three checks above pass in both cases.
|
|
2950
|
+
const mismatched = versions.filter((v) => {
|
|
2951
|
+
if (!kapa.covered.has(v) || !mappedSegments.has(v)) return false
|
|
2952
|
+
const want = mappedGroupIds.get(v)
|
|
2953
|
+
const have = kapa.groups.get(v) || new Set()
|
|
2954
|
+
return !want || !have.has(want)
|
|
2955
|
+
})
|
|
2956
|
+
|
|
2957
|
+
if (prerelease.length) {
|
|
2958
|
+
console.log(` (ignoring prerelease segment${prerelease.length > 1 ? 's' : ''} ${prerelease.join(', ')}, ` +
|
|
2959
|
+
'which publishes for the pre-GA cycle and is not expected to have a Kapa group)')
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
if (missing.length === 0 && unassigned.length === 0 && unmapped.length === 0 && mismatched.length === 0) {
|
|
2963
|
+
console.log(`✓ Every published streaming version (${versions.join(', ')}) has a Kapa source in a group, ` +
|
|
2964
|
+
`and ${options.mapping} points each one at that group.`)
|
|
2965
|
+
process.exit(0)
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
console.log('✗ Kapa and the committed mapping are out of sync for published streaming docs versions.\n')
|
|
2969
|
+
for (const v of missing) {
|
|
2970
|
+
console.log(` - ${v}: published at /streaming/${v}/ but Kapa has no "Documentation (${v})" source. ` +
|
|
2971
|
+
'Readers on this version fall back to the default segment.')
|
|
2972
|
+
}
|
|
2973
|
+
for (const v of unassigned) {
|
|
2974
|
+
console.log(` - ${v}: "Documentation (${v})" exists but is not in a source group, so it is global ` +
|
|
2975
|
+
'and returned for every query on every version.')
|
|
2976
|
+
}
|
|
2977
|
+
for (const v of unmapped) {
|
|
2978
|
+
console.log(` - ${v}: Kapa has a grouped "Documentation (${v})" source, but ${options.mapping} has no ` +
|
|
2979
|
+
`"${v}" segment. Readers on this version fall back to the default segment until the mapping is ` +
|
|
2980
|
+
'regenerated and released.')
|
|
2981
|
+
}
|
|
2982
|
+
for (const v of mismatched) {
|
|
2983
|
+
const want = mappedGroupIds.get(v)
|
|
2984
|
+
const have = [...(kapa.groups.get(v) || [])]
|
|
2985
|
+
console.log(` - ${v}: ${options.mapping} scopes this version to group ${want || '(no group_id)'}, but Kapa has ` +
|
|
2986
|
+
`"Documentation (${v})" in ${have.length === 1 ? 'group' : 'groups'} ${have.join(', ')}. Every Ask AI query on ` +
|
|
2987
|
+
'this version is sent with a group that no longer holds its docs until the mapping is regenerated and released.')
|
|
2988
|
+
}
|
|
2989
|
+
if (missing.length || unassigned.length) {
|
|
2990
|
+
console.log('\nIn the Kapa dashboard: Sources > Add source for the crawl, then assign it to the version ' +
|
|
2991
|
+
'group under Sources > Manage groups. Then regenerate the mapping:\n doc-tools generate kapa-source-groups')
|
|
2992
|
+
} else {
|
|
2993
|
+
console.log('\nRegenerate the mapping and release docs-extensions-and-macros:\n doc-tools generate kapa-source-groups')
|
|
2994
|
+
}
|
|
2995
|
+
console.log(`\n${SENTINEL}`)
|
|
2996
|
+
process.exit(1)
|
|
2997
|
+
})
|
|
2998
|
+
|
|
2737
2999
|
programCli.addCommand(validation)
|
|
2738
3000
|
programCli.addCommand(overridesGroup)
|
|
2739
3001
|
programCli.parse(process.argv)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kapa API Credentials Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent way to retrieve Kapa API credentials from environment
|
|
5
|
+
* variables, so no generator reads process.env.KAPA_* directly. Mirrors
|
|
6
|
+
* cli-utils/github-token.js: resolve here, return null when absent, and let the
|
|
7
|
+
* calling command decide whether a missing credential is fatal.
|
|
8
|
+
*
|
|
9
|
+
* The variable names are not new. They are already the established pair in
|
|
10
|
+
* redpanda-data/docs-site (netlify/functions/kapa-session.mjs) and in
|
|
11
|
+
* docs-team-standards/scripts/docs_analytics/kapa_probe.py, so a third spelling
|
|
12
|
+
* would just be a fourth place to get it wrong.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// Kapa's public API. Both the ingestion and query endpoints live here, and both
|
|
16
|
+
// authenticate with the same project API key -- there is no separate ingestion
|
|
17
|
+
// key and no scope model.
|
|
18
|
+
const KAPA_API_BASE = 'https://api.kapa.ai';
|
|
19
|
+
|
|
20
|
+
// Kapa's OpenAPI schema spells the header X-API-KEY; their prose pages also use
|
|
21
|
+
// X-API-Key in places. HTTP header names are case-insensitive so either works,
|
|
22
|
+
// but pin the canonical spelling from the schema.
|
|
23
|
+
const KAPA_API_KEY_HEADER = 'X-API-KEY';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get the Kapa API key.
|
|
27
|
+
*
|
|
28
|
+
* @param {object} [env] - Environment to read (defaults to process.env)
|
|
29
|
+
* @returns {string|null} The API key, or null when unset/blank
|
|
30
|
+
*/
|
|
31
|
+
function getKapaApiKey (env = process.env) {
|
|
32
|
+
const key = (env.KAPA_API_KEY || '').trim()
|
|
33
|
+
return key || null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Get the Kapa project ID.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} [env] - Environment to read (defaults to process.env)
|
|
40
|
+
* @returns {string|null} The project ID, or null when unset/blank
|
|
41
|
+
*/
|
|
42
|
+
function getKapaProjectId (env = process.env) {
|
|
43
|
+
const id = (env.KAPA_PROJECT_ID || '').trim()
|
|
44
|
+
return id || null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when both credentials are present.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} [env] - Environment to read (defaults to process.env)
|
|
51
|
+
* @returns {boolean}
|
|
52
|
+
*/
|
|
53
|
+
function hasKapaCredentials (env = process.env) {
|
|
54
|
+
return Boolean(getKapaApiKey(env) && getKapaProjectId(env))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve both credentials, or throw with a remediation hint naming exactly
|
|
59
|
+
* what is missing.
|
|
60
|
+
*
|
|
61
|
+
* Generators that write published content call this and let it throw: an
|
|
62
|
+
* unauthenticated run would otherwise produce an empty mapping that looks
|
|
63
|
+
* identical to "Kapa has no source groups", and the drift check would then
|
|
64
|
+
* report false drift forever. Fail loud, like cloud-regions does for its
|
|
65
|
+
* GitHub token.
|
|
66
|
+
*
|
|
67
|
+
* @param {object} [env] - Environment to read (defaults to process.env)
|
|
68
|
+
* @returns {{apiKey: string, projectId: string, apiBase: string, headerName: string}}
|
|
69
|
+
* @throws {Error} When either credential is missing
|
|
70
|
+
*/
|
|
71
|
+
function requireKapaCredentials (env = process.env) {
|
|
72
|
+
const apiKey = getKapaApiKey(env)
|
|
73
|
+
const projectId = getKapaProjectId(env)
|
|
74
|
+
|
|
75
|
+
const missing = []
|
|
76
|
+
if (!apiKey) missing.push('KAPA_API_KEY')
|
|
77
|
+
if (!projectId) missing.push('KAPA_PROJECT_ID')
|
|
78
|
+
|
|
79
|
+
if (missing.length) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Kapa credentials are required: ${missing.join(' and ')} ${missing.length > 1 ? 'are' : 'is'} not set. ` +
|
|
82
|
+
'Create an API key in the Kapa platform under Configuration > API Keys, and find the project ID ' +
|
|
83
|
+
'in the dashboard URL (https://app.kapa.ai/<project-id>). ' +
|
|
84
|
+
'In CI these come from AWS Secrets Manager via OIDC, not from GitHub secrets directly.'
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { apiKey, projectId, apiBase: KAPA_API_BASE, headerName: KAPA_API_KEY_HEADER }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the auth headers for a Kapa API request.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} apiKey
|
|
95
|
+
* @returns {object} Headers suitable for fetch()
|
|
96
|
+
*/
|
|
97
|
+
function kapaAuthHeaders (apiKey) {
|
|
98
|
+
return { [KAPA_API_KEY_HEADER]: apiKey, Accept: 'application/json' }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
module.exports = {
|
|
102
|
+
KAPA_API_BASE,
|
|
103
|
+
KAPA_API_KEY_HEADER,
|
|
104
|
+
getKapaApiKey,
|
|
105
|
+
getKapaProjectId,
|
|
106
|
+
hasKapaCredentials,
|
|
107
|
+
requireKapaCredentials,
|
|
108
|
+
kapaAuthHeaders,
|
|
109
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"project_id": "97f44223-f930-4fb9-ae1e-ecd436a4d85c",
|
|
3
|
+
"parent_group": {
|
|
4
|
+
"id": "238b3c08-5cf6-4615-937c-fa2f5a2e83a4",
|
|
5
|
+
"name": "Streaming",
|
|
6
|
+
"type": "product"
|
|
7
|
+
},
|
|
8
|
+
"default_segment": "current",
|
|
9
|
+
"segments": {
|
|
10
|
+
"23.3": {
|
|
11
|
+
"group_id": "625e5538-aa73-4969-a5c1-4c35a0108a94",
|
|
12
|
+
"group_name": "23.3",
|
|
13
|
+
"source_ids": [
|
|
14
|
+
"21672ac9-cc94-4529-8835-55334ede0a18"
|
|
15
|
+
],
|
|
16
|
+
"source_names": [
|
|
17
|
+
"Documentation (23.3)"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"24.1": {
|
|
21
|
+
"group_id": "29357fc1-30b1-46d4-a9de-e30f9b696b2f",
|
|
22
|
+
"group_name": "24.1",
|
|
23
|
+
"source_ids": [
|
|
24
|
+
"0d68df8c-1f9b-45f6-baa3-25d8bb46b164"
|
|
25
|
+
],
|
|
26
|
+
"source_names": [
|
|
27
|
+
"Documentation (24.1)"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"24.2": {
|
|
31
|
+
"group_id": "3442b9cb-67db-4800-a320-9def40551b93",
|
|
32
|
+
"group_name": "24.2",
|
|
33
|
+
"source_ids": [
|
|
34
|
+
"11659ad5-b7df-4a06-9e8b-4e9913795889"
|
|
35
|
+
],
|
|
36
|
+
"source_names": [
|
|
37
|
+
"Documentation (24.2)"
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
"24.3": {
|
|
41
|
+
"group_id": "11288265-8a9e-4a49-8c63-2f9fbe459b76",
|
|
42
|
+
"group_name": "24.3",
|
|
43
|
+
"source_ids": [
|
|
44
|
+
"f1377953-8bcc-48c9-a0e7-acb8efae701e"
|
|
45
|
+
],
|
|
46
|
+
"source_names": [
|
|
47
|
+
"Documentation (24.3)"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"25.1": {
|
|
51
|
+
"group_id": "317edba8-6c78-4cca-bad5-1dfe47c9d3dc",
|
|
52
|
+
"group_name": "25.1",
|
|
53
|
+
"source_ids": [
|
|
54
|
+
"6cfe84a2-4d5b-4123-b302-f372a5500d02"
|
|
55
|
+
],
|
|
56
|
+
"source_names": [
|
|
57
|
+
"Documentation (25.1)"
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
"25.2": {
|
|
61
|
+
"group_id": "639427c4-06fb-4b8f-8df3-d6a444b85101",
|
|
62
|
+
"group_name": "25.2",
|
|
63
|
+
"source_ids": [
|
|
64
|
+
"4d4d1a66-08e7-46aa-ac72-fea301f96b5f"
|
|
65
|
+
],
|
|
66
|
+
"source_names": [
|
|
67
|
+
"Documentation (25.2)"
|
|
68
|
+
]
|
|
69
|
+
},
|
|
70
|
+
"25.3": {
|
|
71
|
+
"group_id": "65723bc1-941c-4b3b-b5ba-2c601f7b3ba9",
|
|
72
|
+
"group_name": "25.3",
|
|
73
|
+
"source_ids": [
|
|
74
|
+
"d1213d06-6132-47e2-abca-43fe0a85dde6"
|
|
75
|
+
],
|
|
76
|
+
"source_names": [
|
|
77
|
+
"Documentation (25.3)"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"26.1": {
|
|
81
|
+
"group_id": "f0751d2d-de07-4a9c-8d75-7960121178b7",
|
|
82
|
+
"group_name": "26.1",
|
|
83
|
+
"source_ids": [
|
|
84
|
+
"ec6810ab-6208-4e6b-acef-83da55d45762"
|
|
85
|
+
],
|
|
86
|
+
"source_names": [
|
|
87
|
+
"Documentation (26.1)"
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
"current": {
|
|
91
|
+
"group_id": "35567e94-30c7-4608-ab5d-1ff982339d05",
|
|
92
|
+
"group_name": "current",
|
|
93
|
+
"source_ids": [
|
|
94
|
+
"f5c1e522-be3a-45f6-8463-11abc1b35ea9"
|
|
95
|
+
],
|
|
96
|
+
"source_names": [
|
|
97
|
+
"Documentation (current)"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"global_sources": [
|
|
102
|
+
"Admin API",
|
|
103
|
+
"Agentic Data Plane",
|
|
104
|
+
"Cloud Controlplane API",
|
|
105
|
+
"Cloud Dataplane API",
|
|
106
|
+
"Corrected Responses",
|
|
107
|
+
"Documentation (Cloud)",
|
|
108
|
+
"Documentation (RPCN)",
|
|
109
|
+
"GitHub Discussions",
|
|
110
|
+
"HTTP Proxy API",
|
|
111
|
+
"pylon",
|
|
112
|
+
"Recruitment Scam Page",
|
|
113
|
+
"Redpanda Connect Release Notes",
|
|
114
|
+
"Redpanda Core Release Notes",
|
|
115
|
+
"Schema Registry API",
|
|
116
|
+
"Terraform provider",
|
|
117
|
+
"YouTube Tutorials"
|
|
118
|
+
]
|
|
119
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://redpanda.com/schemas/kapa-source-groups.schema.json",
|
|
4
|
+
"title": "Kapa source groups",
|
|
5
|
+
"description": "Generated mapping from a docs URL version segment to the Kapa source group that scopes retrieval to that version. Produced by `doc-tools generate kapa-source-groups` from Kapa's ingestion API; do not hand-edit. Kapa publishes no write API, so the groups and their source assignments are created in the Kapa dashboard and this file only records what is there. See tools/kapa-source-groups/generate-kapa-source-groups.js.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["project_id", "parent_group", "default_segment", "segments", "global_sources"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"project_id": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"format": "uuid",
|
|
13
|
+
"description": "Kapa project the groups belong to. Matches KAPA_PROJECT_ID. Recorded so a mapping generated against the wrong project is obvious rather than silently wrong."
|
|
14
|
+
},
|
|
15
|
+
"parent_group": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"additionalProperties": false,
|
|
18
|
+
"required": ["id", "name", "type"],
|
|
19
|
+
"description": "The product-level group the version groups hang off. Kapa's hierarchy is two levels and is expressed top-down via sub_groups; there is no parent_id field anywhere in its schema, so the generator finds this by 'the group that has children' rather than by name. Renaming it in the dashboard is therefore safe.",
|
|
20
|
+
"properties": {
|
|
21
|
+
"id": { "type": "string", "format": "uuid" },
|
|
22
|
+
"name": { "type": "string" },
|
|
23
|
+
"type": {
|
|
24
|
+
"type": ["string", "null"],
|
|
25
|
+
"enum": ["product", "version", null],
|
|
26
|
+
"description": "Kapa requires every group to be typed. The enum appears only in Kapa's API schema, not in any prose doc. Nothing in Kapa's documented retrieval semantics reads it, so it is recorded for humans rather than relied on."
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"default_segment": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Segment used for any page with no version of its own: Cloud, Connect, Agentic Data Plane, labs, home, search, the 404 page, and the Bump-rendered /api/ pages. Must be a key of `segments`. Deliberately set rather than left unset: sending no group filter makes Kapa search every version at once, which is the DOC-2450 failure (a reader on an unversioned page was answered from 25.2 instead of current)."
|
|
33
|
+
},
|
|
34
|
+
"segments": {
|
|
35
|
+
"type": "object",
|
|
36
|
+
"minProperties": 1,
|
|
37
|
+
"description": "Keyed by the docs URL version segment, not by the Antora `page.version` attribute. The two disagree for the latest release: `latest_version_segment: 'current'` publishes 26.2 at /streaming/current/ while page.version reads 26.2. Kapa source_url values and the URL prefixes in docs-site's lib/platform-filter.mjs both use the segment, so that is what this keys on.",
|
|
38
|
+
"additionalProperties": { "$ref": "#/$defs/segment" }
|
|
39
|
+
},
|
|
40
|
+
"global_sources": {
|
|
41
|
+
"type": "array",
|
|
42
|
+
"items": { "type": "string" },
|
|
43
|
+
"description": "Names of the Kapa sources deliberately assigned to no group. These are the 'global' set: always returned alongside whichever group a query scopes to. Recorded because the design depends on it: Agentic Data Plane, Cloud and Connect content must stay reachable from every page, whatever version the reader is on. Note 'Global' is NOT a real Kapa group despite the dashboard showing a Global row: the API returns no such object and an unassigned source reports source_groups: []."
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"$defs": {
|
|
47
|
+
"segment": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"additionalProperties": false,
|
|
50
|
+
"required": ["group_id", "group_name", "source_ids", "source_names"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"group_id": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"format": "uuid",
|
|
55
|
+
"description": "The value to pass as sourceGroupIdsInclude (Agent SDK), sourceGroupIDsInclude (Chat SDK, note the capital ID), or _meta.source_group_ids_include (hosted MCP server). Kapa group IDs are stable across renames."
|
|
56
|
+
},
|
|
57
|
+
"group_name": { "type": "string" },
|
|
58
|
+
"source_ids": {
|
|
59
|
+
"type": "array",
|
|
60
|
+
"minItems": 1,
|
|
61
|
+
"items": { "type": "string", "format": "uuid" },
|
|
62
|
+
"description": "Sources assigned to this group. minItems is 1 on purpose: a version group with no sources is worse than no group at all, because scoping to it returns only the global set and the reader silently gets nothing version-specific. The generator refuses to emit such a mapping."
|
|
63
|
+
},
|
|
64
|
+
"source_names": {
|
|
65
|
+
"type": "array",
|
|
66
|
+
"minItems": 1,
|
|
67
|
+
"items": { "type": "string" },
|
|
68
|
+
"description": "Human-readable counterpart to source_ids, so a reviewer can sanity-check the mapping without opening the Kapa dashboard."
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|