@redpanda-data/docs-extensions-and-macros 5.35.0 → 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.
@@ -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
+ }
@@ -0,0 +1,180 @@
1
+ /* Publishes the Kapa source-group mapping to the UI as an AsciiDoc attribute.
2
+ *
3
+ * Example use in the playbook:
4
+ * antora:
5
+ * extensions:
6
+ * - require: '@redpanda-data/docs-extensions-and-macros/extensions/kapa-source-groups'
7
+ *
8
+ * WHY THIS EXISTS
9
+ * ---------------
10
+ * docs-ui's get-kapa-source-groups helper needs to turn the page's version
11
+ * segment into a Kapa source group id, so Ask AI retrieval is scoped to the docs
12
+ * version the reader is actually on (DOC-1807, DOC-2450). docs-ui does not depend
13
+ * on this package and must not carry a second copy of the mapping, which would
14
+ * drift silently. So the mapping travels as an attribute: generated here by
15
+ * `doc-tools generate kapa-source-groups`, committed to docs-data, and merged
16
+ * onto every component version at build time.
17
+ *
18
+ * Set on every component version rather than only the versioned ones, because the
19
+ * helper needs the mapping on unversioned pages too. Those resolve to the
20
+ * mapping's default_segment, and sending no filter there is exactly what caused
21
+ * DOC-2450: the reporter was on a page with no version of its own.
22
+ *
23
+ * DEGRADATION
24
+ * -----------
25
+ * Every failure sets nothing and warns. The helper treats a missing attribute as
26
+ * "send no source group", which is the pre-DOC-2450 behaviour of searching every
27
+ * version. That is the right way to fail: a wrong group is worse than no group,
28
+ * because scoping to a group that does not hold the reader's version returns only
29
+ * Kapa's global sources, silently and with no error.
30
+ */
31
+
32
+ const fs = require('fs')
33
+ const path = require('path')
34
+ const { raiseListenerLimit } = require('./util/raise-listener-limit')
35
+
36
+ const ATTRIBUTE_NAME = 'kapa-source-groups'
37
+ // Sits beside assets/widgets/, which proxy-api-docs.js already fetches from the
38
+ // site origin, so the /api/ path needs no new hostname or auth.
39
+ const ASSET_DIR = 'assets/data'
40
+ const ASSET_FILENAME = 'kapa-source-groups.json'
41
+ const DEFAULT_MAPPING_PATH = path.join(__dirname, '..', 'docs-data', 'kapa-source-groups.json')
42
+
43
+ /**
44
+ * Validate the shape the helper depends on, so a malformed mapping is caught at
45
+ * build time with a named reason rather than producing pages that quietly lose
46
+ * version scoping.
47
+ *
48
+ * @param {*} mapping
49
+ * @returns {string|null} Reason it is unusable, or null when it is fine
50
+ */
51
+ function validateMapping (mapping) {
52
+ if (!mapping || typeof mapping !== 'object') return 'not an object'
53
+ if (!mapping.segments || typeof mapping.segments !== 'object') return 'no segments object'
54
+ const segments = Object.keys(mapping.segments)
55
+ if (segments.length === 0) return 'segments is empty'
56
+ if (!mapping.default_segment) return 'no default_segment'
57
+ if (!mapping.segments[mapping.default_segment]) {
58
+ // The helper falls back to default_segment for every unversioned page, so a
59
+ // dangling default silently disables scoping across most of the site.
60
+ return `default_segment "${mapping.default_segment}" is not one of the segments (${segments.join(', ')})`
61
+ }
62
+ const missing = segments.filter((s) => !mapping.segments[s] || !mapping.segments[s].group_id)
63
+ if (missing.length) return `segments missing group_id: ${missing.join(', ')}`
64
+ return null
65
+ }
66
+
67
+ module.exports.register = function ({ config = {} } = {}) {
68
+ const logger = this.getLogger('kapa-source-groups-extension')
69
+
70
+ // Three listeners on a generator context that already carries 30+ extensions,
71
+ // which trips Node's default max-listeners warning. Raised before subscribing,
72
+ // per __tests__/extensions/raise-listener-limit.test.js.
73
+ raiseListenerLimit(this)
74
+ const mappingPath = config.mapping_file ? path.resolve(config.mapping_file) : DEFAULT_MAPPING_PATH
75
+
76
+ // Loaded on first use and reused by all three listeners, so a missing or
77
+ // broken mapping warns once per build rather than once per event.
78
+ let loaded = false
79
+ let cached = null
80
+ function load () {
81
+ if (loaded) return cached
82
+ loaded = true
83
+ cached = readMapping()
84
+ return cached
85
+ }
86
+
87
+ /** Read + validate, or return null having warned with the consequence. */
88
+ function readMapping () {
89
+ let mapping
90
+ try {
91
+ if (!fs.existsSync(mappingPath)) {
92
+ logger.warn(
93
+ `Kapa source-group mapping not found at ${mappingPath}. Ask AI will search every docs version. ` +
94
+ 'Generate it with: doc-tools generate kapa-source-groups'
95
+ )
96
+ return null
97
+ }
98
+ mapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'))
99
+ } catch (err) {
100
+ logger.warn(`Could not read the Kapa source-group mapping at ${mappingPath}: ${err.message}. Ask AI will search every docs version.`)
101
+ return null
102
+ }
103
+ const problem = validateMapping(mapping)
104
+ if (problem) {
105
+ logger.warn(`Kapa source-group mapping at ${mappingPath} is unusable (${problem}). Ask AI will search every docs version.`)
106
+ return null
107
+ }
108
+ return mapping
109
+ }
110
+
111
+ // site.keys reaches EVERY page, including ones with no component: the 404 page
112
+ // renders the Ask AI panel but has no page.component or page.componentVersion,
113
+ // so a component-version attribute alone leaves it unscoped. Its layout pulls
114
+ // in head and header, both of which already read site.keys, so this is the one
115
+ // channel that covers it. Set in playbookBuilt because the site UI model is
116
+ // derived from the playbook.
117
+ this.on('playbookBuilt', ({ playbook }) => {
118
+ const mapping = load()
119
+ if (!mapping) return
120
+ playbook.site = playbook.site || {}
121
+ playbook.site.keys = playbook.site.keys || {}
122
+ if (playbook.site.keys[ATTRIBUTE_NAME] === undefined) {
123
+ playbook.site.keys[ATTRIBUTE_NAME] = JSON.stringify(mapping)
124
+ }
125
+ })
126
+
127
+ // The /api/ reference is Bump.sh HTML assembled by docs-site's proxy-api-docs
128
+ // edge function, which has no Antora page context and cannot import from
129
+ // node_modules. Publishing the mapping as a site asset lets that function read
130
+ // the default group at request time, in parallel with the widget fetches it
131
+ // already does. The alternative -- a UUID pasted into docs-ui's static widget
132
+ // context or a Netlify env var -- is a second copy of the mapping that drifts
133
+ // silently the first time a group is recreated.
134
+ this.on('beforePublish', ({ siteCatalog }) => {
135
+ const mapping = load()
136
+ if (!mapping) return
137
+ siteCatalog.addFile({
138
+ contents: Buffer.from(JSON.stringify(mapping, null, 2) + '\n', 'utf8'),
139
+ out: { path: path.join(ASSET_DIR, ASSET_FILENAME) },
140
+ })
141
+ logger.info(`Kapa source groups: published ${ASSET_DIR}/${ASSET_FILENAME} for the /api/ proxy.`)
142
+ })
143
+
144
+ this.on('contentClassified', async ({ contentCatalog }) => {
145
+ const mapping = load()
146
+ if (!mapping) return
147
+
148
+ // Serialised once. The helper accepts a string or an object, but a string is
149
+ // what survives Antora's attribute handling unchanged, and it keeps the
150
+ // attribute value comparable between builds.
151
+ const serialised = JSON.stringify(mapping)
152
+
153
+ let versions = 0
154
+ const components = await contentCatalog.getComponents()
155
+ for (const component of components) {
156
+ component.versions.forEach(({ asciidoc }) => {
157
+ if (!asciidoc) return
158
+ asciidoc.attributes = asciidoc.attributes || {}
159
+ // Do not clobber an explicit override: a playbook or antora.yml that
160
+ // already sets this wins, matching how add-global-attributes.js lets
161
+ // component attributes beat the shared file.
162
+ if (asciidoc.attributes[ATTRIBUTE_NAME] === undefined) {
163
+ asciidoc.attributes[ATTRIBUTE_NAME] = serialised
164
+ }
165
+ versions += 1
166
+ })
167
+ }
168
+
169
+ logger.info(
170
+ `Kapa source groups: set ${ATTRIBUTE_NAME} on ${versions} component version(s); ` +
171
+ `${Object.keys(mapping.segments).length} version segments, default "${mapping.default_segment}".`
172
+ )
173
+ })
174
+ }
175
+
176
+ module.exports.validateMapping = validateMapping
177
+ module.exports.ATTRIBUTE_NAME = ATTRIBUTE_NAME
178
+ module.exports.DEFAULT_MAPPING_PATH = DEFAULT_MAPPING_PATH
179
+ module.exports.ASSET_DIR = ASSET_DIR
180
+ module.exports.ASSET_FILENAME = ASSET_FILENAME
@@ -10,10 +10,13 @@ module.exports.register = function ({ config }) {
10
10
  const GetLatestConnectVersion = require('./get-latest-connect');
11
11
  const logger = this.getLogger('set-latest-version-extension');
12
12
 
13
- const { getGitHubToken } = require('../../cli-utils/github-token');
13
+ const { getGitHubApiToken } = require('../../cli-utils/github-token');
14
14
  // Shared with tools/bundle-openapi.js so there is one major.minor derivation.
15
15
  const { toShortVersion } = require('../../cli-utils/version');
16
- const token = getGitHubToken();
16
+ // API token, not the git one: GIT_CREDENTIALS (Antora's clone credential)
17
+ // is only a last resort here, because on Netlify it is scoped to the docs
18
+ // content repos and cannot see streaming-enterprise.
19
+ const token = getGitHubApiToken();
17
20
 
18
21
  if (!token) {
19
22
  // Unauthenticated requests still work for the public repos this
@@ -21,7 +24,7 @@ module.exports.register = function ({ config }) {
21
24
  // version now comes from the private streaming-enterprise repo, whose
22
25
  // API 404s without a token -- the latest-redpanda-* attributes are then
23
26
  // left unset and pages fall back to their antora.yml values.
24
- logger.warn('GitHub token not set (GIT_CREDENTIALS, REDPANDA_GITHUB_TOKEN, GITHUB_TOKEN, or GH_TOKEN). The Redpanda version lookup against the private streaming-enterprise repo will fail and leave latest-redpanda-* attributes unset; other lookups proceed unauthenticated.');
27
+ logger.warn('GitHub token not set (REDPANDA_GITHUB_TOKEN, GITHUB_TOKEN, GH_TOKEN, or GIT_CREDENTIALS). The Redpanda version lookup against the private streaming-enterprise repo will fail and leave latest-redpanda-* attributes unset; other lookups proceed unauthenticated.');
25
28
  }
26
29
 
27
30
  this.on('contentClassified', async ({ contentCatalog }) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.35.0",
3
+ "version": "5.37.0",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -92,7 +92,9 @@
92
92
  "./macros/enterprise": "./macros/enterprise.js",
93
93
  "./macros/prop": "./macros/prop.js",
94
94
  "./extensions/property-page-index": "./extensions/property-page-index.js",
95
- "./extensions/render-property-descriptions": "./extensions/render-property-descriptions.js"
95
+ "./extensions/render-property-descriptions": "./extensions/render-property-descriptions.js",
96
+ "./extensions/kapa-source-groups": "./extensions/kapa-source-groups.js",
97
+ "./docs-data/kapa-source-groups.json": "./docs-data/kapa-source-groups.json"
96
98
  },
97
99
  "files": [
98
100
  "extensions",
@@ -104,7 +106,9 @@
104
106
  "tools",
105
107
  "docker-compose",
106
108
  "mcp",
107
- "docs-data/rpk-overrides.schema.json"
109
+ "docs-data/rpk-overrides.schema.json",
110
+ "docs-data/kapa-source-groups.json",
111
+ "docs-data/kapa-source-groups.schema.json"
108
112
  ],
109
113
  "license": "ISC",
110
114
  "engines": {
@@ -138,11 +138,19 @@ function parseJSDocComments(sourceFile) {
138
138
  // was documented with the first command's prose. That is exactly what
139
139
  // happened to `generate migrate-rpcn-descriptions`, which shipped a
140
140
  // regenerated section describing migrate-rpcn-metadata.
141
- const pattern = /\/\*\*\s*((?:(?!\*\/)[\s\S])*?)\s*\*\/\s*(?:programCli|automation|validation)\s*\.command\(['"]([^'"]+)['"]\)/g;
141
+ //
142
+ // The group is captured too, because a command NAME is not unique: the same
143
+ // name can be registered under both `automation` (generate) and `validation`
144
+ // (validate). `kapa-source-groups` is, and keying on the bare name alone made
145
+ // the later registration silently overwrite the earlier, so the
146
+ // `generate kapa-source-groups` section shipped describing the validate
147
+ // command, complete with validate examples under a generate usage line.
148
+ // Same failure as the migrate-rpcn one above, one level up.
149
+ const pattern = /\/\*\*\s*((?:(?!\*\/)[\s\S])*?)\s*\*\/\s*(programCli|automation|validation)\s*\.command\(['"]([^'"]+)['"]\)/g;
142
150
 
143
151
  let match;
144
152
  while ((match = pattern.exec(content)) !== null) {
145
- const [, commentText, commandName] = match;
153
+ const [, commentText, group, commandName] = match;
146
154
 
147
155
  // Parse the comment into sections
148
156
  const parsed = {
@@ -189,7 +197,12 @@ function parseJSDocComments(sourceFile) {
189
197
  .trim();
190
198
  }
191
199
 
192
- comments[commandName] = parsed;
200
+ // Keyed by the CLI path a reader would type, so `generate x` and
201
+ // `validate x` cannot collide. The bare name is kept as well, for the
202
+ // top-level commands registered straight onto programCli.
203
+ const groupPrefix = { automation: 'generate ', validation: 'validate ', programCli: '' }[group]
204
+ comments[`${groupPrefix}${commandName}`] = parsed;
205
+ if (comments[commandName] === undefined) comments[commandName] = parsed;
193
206
  }
194
207
 
195
208
  return comments;
@@ -290,7 +303,9 @@ IMPORTANT: This documentation is auto-generated. Do not edit manually. Run \`npm
290
303
  const label = commandPath.join(' ');
291
304
  console.log(`${' '.repeat(level - 1)}Generating docs for: ${label}`);
292
305
  const data = parseHelp(getHelpOutput(label));
293
- const jsdoc = jsdocs[commandPath[commandPath.length - 1]];
306
+ // Prefer the full command path, falling back to the bare name so that
307
+ // commands registered directly on programCli still resolve.
308
+ const jsdoc = jsdocs[label] || jsdocs[commandPath[commandPath.length - 1]];
294
309
  doc += generateCommandDoc(label, data, jsdoc, level);
295
310
  for (const child of data.commands) {
296
311
  // Commander lists `help [command]` in every group; it is not a command
@@ -2,7 +2,7 @@
2
2
 
3
3
  const GetLatestRedpandaVersion = require('../extensions/version-fetcher/get-latest-redpanda-version.js');
4
4
  const { getPrereleaseFromAntora } = require('../cli-utils/antora-utils.js');
5
- const { getGitHubToken } = require('../cli-utils/github-token');
5
+ const { getGitHubApiToken } = require('../cli-utils/github-token');
6
6
 
7
7
  /**
8
8
  * Fetches and prints the latest Redpanda version and Docker repository.
@@ -20,7 +20,7 @@ module.exports = async function getRedpandaVersion({ beta = false, fromAntora =
20
20
  const owner = 'redpanda-data';
21
21
  const repo = 'streaming-enterprise';
22
22
 
23
- if (!getGitHubToken()) {
23
+ if (!getGitHubApiToken()) {
24
24
  console.error(`❌ redpanda-data/${repo} is a private repository.`);
25
25
  console.error(' Set GH_TOKEN, REDPANDA_GITHUB_TOKEN, or GITHUB_TOKEN to a token with access.');
26
26
  process.exit(1);
@@ -0,0 +1,33 @@
1
+ /**
2
+ * fetch() with a deadline that covers the WHOLE exchange, body included.
3
+ *
4
+ * The obvious shape, `try { res = await fetch(url, { signal }) } finally
5
+ * { clearTimeout(timer) }` and then `await res.text()`, only bounds the time to
6
+ * headers. fetch resolves as soon as headers arrive, the timer is cleared, and a
7
+ * server that then stalls mid-body holds the caller forever. For a scheduled
8
+ * check that is a job that never finishes rather than one that exits 2, so
9
+ * nobody is told and the next run queues behind it.
10
+ *
11
+ * @param {typeof fetch} fetchImpl
12
+ * @param {string} url
13
+ * @param {RequestInit} init - Merged with the abort signal
14
+ * @param {number} timeoutMs
15
+ * @param {(res: Response, signal: AbortSignal) => Promise<*>} read
16
+ * Consumes the body while the timer is still armed. Return null for a
17
+ * response you do not want to read (a non-OK status, say) so the caller can
18
+ * inspect res.status first.
19
+ * @returns {Promise<{res: Response, body: *}>}
20
+ */
21
+ async function fetchWithDeadline (fetchImpl, url, init, timeoutMs, read) {
22
+ const controller = new AbortController()
23
+ const timer = setTimeout(() => controller.abort(), timeoutMs)
24
+ try {
25
+ const res = await fetchImpl(url, { ...init, signal: controller.signal })
26
+ const body = await read(res, controller.signal)
27
+ return { res, body }
28
+ } finally {
29
+ clearTimeout(timer)
30
+ }
31
+ }
32
+
33
+ module.exports = { fetchWithDeadline }