@redpanda-data/docs-extensions-and-macros 5.35.1 → 5.37.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 +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/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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which streaming docs versions Kapa has a source for.
|
|
3
|
+
*
|
|
4
|
+
* Kapa names one crawl per published version, `Documentation (24.2)`,
|
|
5
|
+
* `Documentation (current)`, and so on. The sources endpoint exposes no URL,
|
|
6
|
+
* only the name, so the version is read from the name. That convention is the
|
|
7
|
+
* contract this check depends on: a crawl named anything else is invisible to it
|
|
8
|
+
* and reported as missing, which is the right direction to be wrong in.
|
|
9
|
+
*
|
|
10
|
+
* A source that exists but sits in no group is reported separately. Unassigned
|
|
11
|
+
* sources are GLOBAL in Kapa, returned for every query on every version, so a
|
|
12
|
+
* `Documentation (26.2)` left unassigned would leak 26.2 content into 24.2
|
|
13
|
+
* readers' answers with no error anywhere.
|
|
14
|
+
*
|
|
15
|
+
* The group ids each version's sources sit in are returned too, so the caller
|
|
16
|
+
* can compare them with the committed mapping. Every deployed surface scopes a
|
|
17
|
+
* query by the mapping's group_id, not by Kapa's current state, so a group that
|
|
18
|
+
* was deleted and recreated, or a source moved into another version's group,
|
|
19
|
+
* leaves the mapping pointing at a group that no longer holds that version.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const { KAPA_API_BASE } = require('../../cli-utils/kapa-credentials')
|
|
23
|
+
const { fetchAllPages, toList, idOf } = require('./generate-kapa-source-groups.js')
|
|
24
|
+
|
|
25
|
+
const SOURCE_NAME_RE = /^Documentation \(([^)]+)\)$/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} name - A Kapa source name
|
|
29
|
+
* @returns {string|null} The version segment, or null when the name is not a docs crawl
|
|
30
|
+
*/
|
|
31
|
+
function versionFromSourceName (name) {
|
|
32
|
+
const m = typeof name === 'string' ? name.match(SOURCE_NAME_RE) : null
|
|
33
|
+
if (!m) return null
|
|
34
|
+
const v = m[1].trim()
|
|
35
|
+
// Cloud and RPCN crawls use the same naming but are not streaming versions.
|
|
36
|
+
return /^(\d+\.\d+|current)$/.test(v) ? v : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Classify a list of Kapa source objects.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{name: string, source_groups?: unknown}>} sources
|
|
43
|
+
* @returns {{covered: Set<string>, unassigned: Set<string>, groups: Map<string, Set<string>>}}
|
|
44
|
+
* covered: versions with a source that is in at least one group
|
|
45
|
+
* unassigned: versions with a source that is in no group (global)
|
|
46
|
+
* groups: for each covered version, the ids of every group its sources sit in
|
|
47
|
+
*/
|
|
48
|
+
function classifySources (sources) {
|
|
49
|
+
const covered = new Set()
|
|
50
|
+
const unassigned = new Set()
|
|
51
|
+
const groups = new Map()
|
|
52
|
+
for (const s of sources || []) {
|
|
53
|
+
const v = versionFromSourceName(s && s.name)
|
|
54
|
+
if (!v) continue
|
|
55
|
+
const ids = toList(s.source_groups).map(idOf).filter(Boolean)
|
|
56
|
+
if (ids.length) {
|
|
57
|
+
covered.add(v)
|
|
58
|
+
if (!groups.has(v)) groups.set(v, new Set())
|
|
59
|
+
for (const id of ids) groups.get(v).add(String(id))
|
|
60
|
+
} else {
|
|
61
|
+
unassigned.add(v)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// A version with two sources, one grouped and one not, is covered: the
|
|
65
|
+
// grouped one scopes correctly. The stray global is still worth a report,
|
|
66
|
+
// but it is a generator concern (it lists global_sources), not a gap here.
|
|
67
|
+
for (const v of covered) unassigned.delete(v)
|
|
68
|
+
return { covered, unassigned, groups }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {{apiKey: string, projectId: string, fetchImpl?: typeof fetch}} opts
|
|
73
|
+
* @returns {Promise<{covered: Set<string>, unassigned: Set<string>, groups: Map<string, Set<string>>}>}
|
|
74
|
+
*/
|
|
75
|
+
async function fetchKapaSourceVersions ({ apiKey, projectId, fetchImpl }) {
|
|
76
|
+
if (!apiKey) throw new Error('fetchKapaSourceVersions requires an apiKey')
|
|
77
|
+
if (!projectId) throw new Error('fetchKapaSourceVersions requires a projectId')
|
|
78
|
+
const url = `${KAPA_API_BASE}/ingestion/v1/projects/${encodeURIComponent(projectId)}/sources/`
|
|
79
|
+
const sources = await fetchAllPages(url, apiKey, fetchImpl)
|
|
80
|
+
return classifySources(sources)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = { versionFromSourceName, classifySources, fetchKapaSourceVersions, SOURCE_NAME_RE }
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Reads the docs site's own sitemap to find out which version segments are
|
|
2
|
+
// actually published.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS
|
|
5
|
+
// ---------------
|
|
6
|
+
// `validate kapa-source-groups` compares Kapa's sources and the committed
|
|
7
|
+
// mapping, but both of those are blind to the failure that actually matters: a
|
|
8
|
+
// new docs version being published with no Kapa source group behind it.
|
|
9
|
+
//
|
|
10
|
+
// Cutting a v/X.Y branch in redpanda-data/docs publishes /streaming/X.Y/ because
|
|
11
|
+
// the playbook globs `branches: v/*`, and no file changes in this repo or
|
|
12
|
+
// docs-site. Kapa has no write API, so no source or group appears either. The
|
|
13
|
+
// mapping and Kapa still agree with each other, and every reader on the new
|
|
14
|
+
// version silently falls back to the default segment.
|
|
15
|
+
//
|
|
16
|
+
// So the check needs a third input: what the site publishes. The sitemap is the
|
|
17
|
+
// right source because it reflects what is actually live, rather than what a
|
|
18
|
+
// playbook or a branch list intends.
|
|
19
|
+
|
|
20
|
+
const { fetchWithDeadline } = require('./fetch-with-deadline')
|
|
21
|
+
|
|
22
|
+
const SITEMAP_TIMEOUT_MS = 15000
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract the distinct version segments from a streaming sitemap.
|
|
26
|
+
*
|
|
27
|
+
* Parsed with a regex rather than an XML parser on purpose: the only thing wanted
|
|
28
|
+
* is the path segment after /streaming/, this runs in CI where a dependency is
|
|
29
|
+
* cost, and a malformed sitemap should yield "no segments found" (which the
|
|
30
|
+
* caller treats as an error) rather than a parser exception.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} xml - Sitemap XML
|
|
33
|
+
* @returns {string[]} Sorted, de-duplicated version segments
|
|
34
|
+
*/
|
|
35
|
+
function parsePublishedSegments (xml) {
|
|
36
|
+
const found = new Set()
|
|
37
|
+
for (const m of String(xml || '').matchAll(/\/streaming\/([^/"'<\s]+)\//g)) {
|
|
38
|
+
found.add(m[1])
|
|
39
|
+
}
|
|
40
|
+
return [...found].sort((a, b) => a.localeCompare(b, 'en', { numeric: true }))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fetch the published version segments from a docs site.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} options
|
|
47
|
+
* @param {string} options.siteUrl - Site origin, e.g. https://docs.redpanda.com
|
|
48
|
+
* @param {Function} [options.fetchImpl] - Injectable for tests
|
|
49
|
+
* @returns {Promise<string[]>}
|
|
50
|
+
* @throws {Error} On a non-OK response, a network failure, or an empty result
|
|
51
|
+
*/
|
|
52
|
+
async function fetchPublishedSegments ({ siteUrl, fetchImpl = globalThis.fetch } = {}) {
|
|
53
|
+
if (!siteUrl) throw new Error('fetchPublishedSegments requires a siteUrl')
|
|
54
|
+
const url = `${String(siteUrl).replace(/\/+$/, '')}/sitemap-streaming.xml`
|
|
55
|
+
|
|
56
|
+
let res, body
|
|
57
|
+
try {
|
|
58
|
+
// The deadline covers the body read too; see fetch-with-deadline.js for
|
|
59
|
+
// why clearing the timer after headers alone is not a timeout.
|
|
60
|
+
;({ res, body } = await fetchWithDeadline(
|
|
61
|
+
fetchImpl, url, {}, SITEMAP_TIMEOUT_MS,
|
|
62
|
+
(r) => (r.ok ? r.text() : null)
|
|
63
|
+
))
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// A network failure means "could not find out", which the caller must keep
|
|
66
|
+
// distinct from "a version is missing" so a scheduled run does not file an
|
|
67
|
+
// issue every time the site is briefly unreachable.
|
|
68
|
+
throw new Error(`Could not fetch ${url}: ${err.message}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!res.ok) throw new Error(`Could not fetch ${url}: ${res.status} ${res.statusText}`)
|
|
72
|
+
|
|
73
|
+
const segments = parsePublishedSegments(body)
|
|
74
|
+
if (segments.length === 0) {
|
|
75
|
+
// An empty sitemap and a moved sitemap look identical, and treating either as
|
|
76
|
+
// "nothing is published" would report every mapped segment as stale.
|
|
77
|
+
throw new Error(`No /streaming/<version>/ URLs found in ${url}. The sitemap may have moved or changed shape.`)
|
|
78
|
+
}
|
|
79
|
+
return segments
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* URL segments that publish without a durable version behind them.
|
|
84
|
+
*
|
|
85
|
+
* docs-site sets `latest_prerelease_version_segment: 'beta'`, so a branch with
|
|
86
|
+
* `prerelease: true` publishes at /streaming/beta/ for the whole pre-GA cycle
|
|
87
|
+
* (26.2's ran 2026-06-11 to 2026-07-28). No Kapa group is expected for it, and
|
|
88
|
+
* a check that reported it as missing would file the same false issue every
|
|
89
|
+
* Monday of every beta.
|
|
90
|
+
*/
|
|
91
|
+
const PRERELEASE_SEGMENTS = new Set(['beta'])
|
|
92
|
+
|
|
93
|
+
/** @param {string} segment */
|
|
94
|
+
function isPrereleaseSegment (segment) {
|
|
95
|
+
return PRERELEASE_SEGMENTS.has(segment)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { parsePublishedSegments, fetchPublishedSegments, isPrereleaseSegment, PRERELEASE_SEGMENTS, SITEMAP_TIMEOUT_MS }
|