@redpanda-data/docs-extensions-and-macros 4.13.4 → 4.13.6

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.
@@ -15,6 +15,9 @@
15
15
  * - Telemetry: Usage tracking for adoption metrics
16
16
  */
17
17
 
18
+ // Load environment variables from .env file if it exists
19
+ require('dotenv').config();
20
+
18
21
  const fs = require('fs');
19
22
  const path = require('path');
20
23
  const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
package/bin/doc-tools.js CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  'use strict'
4
4
 
5
+ // Load environment variables from .env file if it exists
6
+ require('dotenv').config()
7
+
5
8
  const { spawnSync } = require('child_process')
6
9
  const os = require('os')
7
10
  const { Command, Option } = require('commander')
@@ -9,14 +9,18 @@
9
9
  * Get GitHub token from environment variables
10
10
  * Checks multiple common variable names in priority order:
11
11
  * 1. REDPANDA_GITHUB_TOKEN - Custom Redpanda token
12
- * 2. GITHUB_TOKEN - GitHub Actions default
13
- * 3. GH_TOKEN - GitHub CLI default
12
+ * 2. ACTIONS_BOT_TOKEN - GitHub Actions bot token
13
+ * 3. GITHUB_TOKEN - GitHub Actions default
14
+ * 4. VBOT_GITHUB_API_TOKEN - Legacy bot token
15
+ * 5. GH_TOKEN - GitHub CLI default
14
16
  *
15
17
  * @returns {string|null} GitHub token or null if not found
16
18
  */
17
19
  function getGitHubToken() {
18
20
  return process.env.REDPANDA_GITHUB_TOKEN ||
21
+ process.env.ACTIONS_BOT_TOKEN ||
19
22
  process.env.GITHUB_TOKEN ||
23
+ process.env.VBOT_GITHUB_API_TOKEN ||
20
24
  process.env.GH_TOKEN ||
21
25
  null;
22
26
  }
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+
3
+ const { Octokit } = require('@octokit/rest')
4
+ const { getGitHubToken } = require('./github-token')
5
+
6
+ /**
7
+ * Shared Octokit client instance for GitHub API access
8
+ * Configured with optional authentication and retry logic
9
+ *
10
+ * This singleton instance is shared across all doc-tools modules to:
11
+ * - Avoid redundant initialization
12
+ * - Share rate limit tracking
13
+ * - Centralize GitHub API configuration
14
+ */
15
+
16
+ // Get authentication token from environment
17
+ const token = getGitHubToken()
18
+
19
+ // Configure Octokit options
20
+ const octokitOptions = {
21
+ userAgent: 'redpanda-docs-tools',
22
+ retry: {
23
+ enabled: true,
24
+ retries: 3
25
+ }
26
+ }
27
+
28
+ // Only add auth if token is available
29
+ if (token) {
30
+ octokitOptions.auth = token
31
+ }
32
+
33
+ // Create singleton instance
34
+ const octokit = new Octokit(octokitOptions)
35
+
36
+ module.exports = octokit
@@ -25,11 +25,9 @@ module.exports.register = function ({ config }) {
25
25
  // Use csvpath (legacy) or csvPath
26
26
  const localCsvPath = csvpath || null
27
27
 
28
- async function loadOctokit () {
29
- const { Octokit } = await import('@octokit/rest')
30
- const { getGitHubToken } = require('../cli-utils/github-token')
31
- const token = getGitHubToken()
32
- return token ? new Octokit({ auth: token }) : new Octokit()
28
+ function loadOctokit () {
29
+ // Use shared Octokit client
30
+ return require('../cli-utils/octokit-client')
33
31
  }
34
32
 
35
33
  // Use 'on' and return the promise so Antora waits for async completion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "4.13.4",
3
+ "version": "4.13.6",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -103,6 +103,7 @@
103
103
  "chalk": "4.1.2",
104
104
  "cheerio": "^1.1.2",
105
105
  "commander": "^14.0.0",
106
+ "dotenv": "^16.6.1",
106
107
  "glob": "^11.0.0",
107
108
  "gulp": "^4.0.2",
108
109
  "gulp-connect": "^5.7.0",
@@ -70,8 +70,8 @@ function displayClusterType(ct) {
70
70
  */
71
71
  async function fetchYaml({ owner, repo, path, ref = 'main', token }) {
72
72
  try {
73
- const { Octokit } = await import('@octokit/rest');
74
- const octokit = new Octokit(token ? { auth: token } : {});
73
+ // Use shared Octokit client
74
+ const octokit = require('../../cli-utils/octokit-client');
75
75
 
76
76
  console.log(`[cloud-regions] INFO: Fetching ${owner}/${repo}/${path}@${ref} via GitHub API`);
77
77
 
@@ -1,23 +1,10 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
- let octokitInstance = null;
5
- async function loadOctokit() {
6
- if (!octokitInstance) {
7
- const { Octokit } = await import('@octokit/rest');
8
- octokitInstance = process.env.VBOT_GITHUB_API_TOKEN
9
- ? new Octokit({
10
- auth: process.env.VBOT_GITHUB_API_TOKEN,
11
- })
12
- : new Octokit();
13
-
14
- if (!process.env.VBOT_GITHUB_API_TOKEN) {
15
- console.info(
16
- 'No GitHub token found (VBOT_GITHUB_API_TOKEN).'
17
- );
18
- }
19
- }
20
- return octokitInstance;
4
+ // Use shared Octokit client
5
+ function loadOctokit() {
6
+ const octokit = require('../cli-utils/octokit-client');
7
+ return octokit;
21
8
  }
22
9
 
23
10
  async function saveFile(content, saveDir, filename) {
@@ -23,13 +23,8 @@ module.exports = async function getConsoleVersion({ beta = false, fromAntora = f
23
23
  useBeta = getPrereleaseFromAntora();
24
24
  }
25
25
 
26
- // Initialize GitHub client
27
- const { getGitHubToken } = require('../cli-utils/github-token');
28
- const { Octokit } = await import('@octokit/rest');
29
- const token = getGitHubToken();
30
- const octokit = token
31
- ? new Octokit({ auth: token })
32
- : new Octokit();
26
+ // Use shared Octokit client
27
+ const octokit = require('../cli-utils/octokit-client');
33
28
 
34
29
  // Fetch latest release info
35
30
  let data;
@@ -19,13 +19,8 @@ module.exports = async function getRedpandaVersion({ beta = false, fromAntora =
19
19
  useBeta = getPrereleaseFromAntora();
20
20
  }
21
21
 
22
- // Load Octokit
23
- const { getGitHubToken } = require('../cli-utils/github-token');
24
- const { Octokit } = await import('@octokit/rest');
25
- const token = getGitHubToken();
26
- const octokit = token
27
- ? new Octokit({ auth: token })
28
- : new Octokit();
22
+ // Use shared Octokit client
23
+ const octokit = require('../cli-utils/octokit-client');
29
24
 
30
25
  // Fetch version data
31
26
  let data;
@@ -1,4 +1,4 @@
1
- const { Octokit } = require('@octokit/rest');
1
+ const octokit = require('../../cli-utils/octokit-client');
2
2
  const { execSync, spawnSync } = require('child_process');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
@@ -13,16 +13,6 @@ const https = require('https');
13
13
  * - Which connectors are self-hosted only
14
14
  */
15
15
 
16
- // Initialize Octokit with optional authentication
17
- const octokit = new Octokit({
18
- auth: process.env.GITHUB_TOKEN,
19
- userAgent: 'redpanda-docs-tools',
20
- retry: {
21
- enabled: true,
22
- retries: 3
23
- }
24
- });
25
-
26
16
  const REPO_OWNER = 'redpanda-data';
27
17
  const REPO_NAME = 'connect';
28
18
 
@@ -36,11 +36,16 @@ module.exports = function buildConfigYaml(type, connectorName, children, include
36
36
  return; // skip deprecated fields
37
37
  }
38
38
  if (!includeAdvanced && field.is_advanced) {
39
- return; // skip advanced fields in common mode
39
+ return; // skip advanced fields in "common" mode
40
40
  }
41
41
 
42
- if (field.type === 'object' && Array.isArray(field.children)) {
43
- // Render nested object
42
+ // Check if this is an array-of-objects (e.g., client_certs[])
43
+ // These should render as empty arrays, not expanded object structures
44
+ if (field.kind === 'array' && field.type === 'object' && Array.isArray(field.children)) {
45
+ // Render as array leaf (e.g., "client_certs: []")
46
+ lines.push(renderLeafField(field, baseIndent));
47
+ } else if (field.type === 'object' && Array.isArray(field.children)) {
48
+ // Render nested object (plain object, not array)
44
49
  const nestedLines = renderObjectField(field, baseIndent);
45
50
  lines.push(...nestedLines);
46
51
  } else {
@@ -28,7 +28,12 @@ module.exports = function renderObjectField(field, indentLevel) {
28
28
  if (child.is_deprecated) {
29
29
  return; // skip entirely
30
30
  }
31
- if (Array.isArray(child.children) && child.children.length > 0) {
31
+ // Check if this is an array-of-objects (e.g., client_certs[])
32
+ // These should render as empty arrays, not expanded object structures
33
+ if (child.kind === 'array' && child.type === 'object' && Array.isArray(child.children)) {
34
+ // Render as array leaf (e.g., "client_certs: []")
35
+ lines.push(renderLeafField(child, childIndent));
36
+ } else if (Array.isArray(child.children) && child.children.length > 0) {
32
37
  // Nested object → recurse
33
38
  lines.push(...renderObjectField(child, childIndent));
34
39
  } else {
@@ -1072,7 +1072,9 @@ async function handleRpcnConnectorDocs (options) {
1072
1072
  validConnectors.push({
1073
1073
  name: connector.name,
1074
1074
  type: type.replace(/s$/, ''),
1075
- status: connector.status || connector.type || 'stable'
1075
+ status: connector.status || connector.type || 'stable',
1076
+ cloudOnly: connector.cloudOnly === true,
1077
+ requiresCgo: connector.requiresCgo === true
1076
1078
  })
1077
1079
  }
1078
1080
  })
@@ -1119,14 +1121,175 @@ async function handleRpcnConnectorDocs (options) {
1119
1121
  cloudOnly: path.resolve(process.cwd(), 'modules/components/partials/components/cloud-only')
1120
1122
  }
1121
1123
 
1122
- const allMissing = validConnectors.filter(({ name, type }) => {
1124
+ // Build a set of cloud-supported connectors (inCloud + cloudOnly, excluding self-hosted-only)
1125
+ const cloudSupportedSet = new Set()
1126
+ if (binaryAnalysis?.comparison) {
1127
+ // inCloud = available in both OSS and Cloud
1128
+ binaryAnalysis.comparison.inCloud?.forEach(c => {
1129
+ cloudSupportedSet.add(`${c.type}:${c.name}`)
1130
+ })
1131
+ // cloudOnly = only available in Cloud (not in OSS)
1132
+ binaryAnalysis.comparison.cloudOnly?.forEach(c => {
1133
+ cloudSupportedSet.add(`${c.type}:${c.name}`)
1134
+ })
1135
+ } else {
1136
+ // Fallback when binary analysis is unavailable:
1137
+ // Check all connectors that have cloudSupported flag or assume all non-deprecated are cloud-supported
1138
+ console.log(' ℹ️ Binary analysis unavailable - checking all non-deprecated connectors for cloud-docs')
1139
+ const types = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits', 'buffers', 'metrics', 'scanners', 'tracers']
1140
+ types.forEach(type => {
1141
+ if (Array.isArray(dataObj[type])) {
1142
+ dataObj[type].forEach(connector => {
1143
+ // Include if cloudSupported is explicitly true, or if it's null/undefined and not deprecated
1144
+ const isCloudSupported = connector.cloudSupported === true ||
1145
+ (connector.cloudSupported == null && connector.status !== 'deprecated')
1146
+ if (isCloudSupported && connector.name) {
1147
+ // Store type as plural to match binary analysis format
1148
+ cloudSupportedSet.add(`${type}:${connector.name}`)
1149
+ }
1150
+ })
1151
+ }
1152
+ })
1153
+ }
1154
+
1155
+ // Check for missing connector documentation in rp-connect-docs
1156
+ const allMissing = validConnectors.filter(({ name, type, cloudOnly }) => {
1123
1157
  const relPath = path.join(`${type}s`, `${name}.adoc`)
1124
- const existsInAny = Object.values(roots).some(root =>
1158
+
1159
+ // For cloud-only connectors, ONLY check the cloud-only directory
1160
+ if (cloudOnly) {
1161
+ return !fs.existsSync(path.join(roots.cloudOnly, relPath))
1162
+ }
1163
+
1164
+ // For regular connectors, check pages and partials (not cloud-only)
1165
+ const existsInAny = [roots.pages, roots.partials].some(root =>
1125
1166
  fs.existsSync(path.join(root, relPath))
1126
1167
  )
1127
1168
  return !existsInAny
1128
1169
  })
1129
1170
 
1171
+ // Check for cloud-supported connectors missing from cloud-docs repo (via GitHub API)
1172
+ const missingFromCloudDocs = []
1173
+ const cloudDocsErrors = []
1174
+ if (cloudSupportedSet.size > 0 && options.checkCloudDocs !== false) {
1175
+ console.log('\n INFO: Checking cloud-docs repository for missing connector pages...')
1176
+
1177
+ // Use shared Octokit instance
1178
+ const octokit = require('../../cli-utils/octokit-client')
1179
+
1180
+ try {
1181
+ // Optimization: Fetch entire directory tree in 1 API call instead of 471 individual calls
1182
+ console.log(' Fetching cloud-docs directory tree (1 API call)...')
1183
+
1184
+ let existingFiles = new Set()
1185
+
1186
+ try {
1187
+ // Get the tree for the components directory
1188
+ const { data: tree } = await octokit.git.getTree({
1189
+ owner: 'redpanda-data',
1190
+ repo: 'cloud-docs',
1191
+ tree_sha: 'main:modules/develop/pages/connect/components',
1192
+ recursive: true
1193
+ })
1194
+
1195
+ // Build a set of existing file paths for O(1) lookup
1196
+ tree.tree.forEach(item => {
1197
+ if (item.type === 'blob' && item.path.endsWith('.adoc')) {
1198
+ existingFiles.add(item.path)
1199
+ }
1200
+ })
1201
+
1202
+ console.log(` Loaded ${existingFiles.size} existing connector pages from cloud-docs`)
1203
+ } catch (treeError) {
1204
+ console.log(` WARNING: Could not fetch tree (${treeError.status}), falling back to individual checks`)
1205
+ // If tree API fails, fall back to individual checks (old behavior)
1206
+ existingFiles = null
1207
+ }
1208
+
1209
+ // Check each cloud-supported connector
1210
+ // Filter to only check actual connector/component types that need individual pages
1211
+ const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'buffers', 'scanners', 'metrics', 'tracers']
1212
+
1213
+ for (const connectorKey of cloudSupportedSet) {
1214
+ const [type, name] = connectorKey.split(':')
1215
+
1216
+ // Skip non-connector types (config, bloblang-functions, bloblang-methods, rate-limits)
1217
+ if (!connectorTypes.includes(type)) {
1218
+ continue
1219
+ }
1220
+
1221
+ // Skip deprecated connectors - they don't need cloud-docs pages
1222
+ if (Array.isArray(dataObj[type])) {
1223
+ const connector = dataObj[type].find(c => c.name === name)
1224
+ if (connector && connector.status === 'deprecated') {
1225
+ continue
1226
+ }
1227
+ }
1228
+
1229
+ const relativePath = `${type}/${name}.adoc`
1230
+ const fullPath = `modules/develop/pages/connect/components/${relativePath}`
1231
+
1232
+ // Fast path: Check against tree if we have it
1233
+ if (existingFiles !== null) {
1234
+ if (!existingFiles.has(relativePath)) {
1235
+ missingFromCloudDocs.push({ type, name, path: fullPath })
1236
+ }
1237
+ continue
1238
+ }
1239
+
1240
+ // Fallback path: Individual API calls (only if tree fetch failed)
1241
+ try {
1242
+ await octokit.repos.getContent({
1243
+ owner: 'redpanda-data',
1244
+ repo: 'cloud-docs',
1245
+ path: fullPath,
1246
+ ref: 'main'
1247
+ })
1248
+ // File exists, no action needed
1249
+ } catch (error) {
1250
+ if (error.status === 404) {
1251
+ // File doesn't exist in cloud-docs
1252
+ missingFromCloudDocs.push({ type, name, path: fullPath })
1253
+ } else {
1254
+ // Non-404 error - record as error
1255
+ cloudDocsErrors.push({
1256
+ type,
1257
+ name,
1258
+ path: fullPath,
1259
+ status: error.status || 'unknown',
1260
+ message: error.message
1261
+ })
1262
+ }
1263
+ }
1264
+ }
1265
+
1266
+ // Report results
1267
+ if (cloudDocsErrors.length > 0) {
1268
+ console.log(` WARNING: Encountered ${cloudDocsErrors.length} error(s) while checking cloud-docs (check inconclusive):`)
1269
+ cloudDocsErrors.forEach(({ type, name, status, message }) => {
1270
+ console.log(` - ${type}/${name} - Status ${status}: ${message}`)
1271
+ })
1272
+ console.log(` INFO: Please resolve these errors (e.g., check GITHUB_TOKEN or VBOT_GITHUB_API_TOKEN, API rate limits, network connectivity)`)
1273
+ if (missingFromCloudDocs.length > 0) {
1274
+ console.log(` INFO: Additionally, ${missingFromCloudDocs.length} connector(s) confirmed missing from cloud-docs:`)
1275
+ missingFromCloudDocs.forEach(({ type, name }) => {
1276
+ console.log(` - ${type}/${name}`)
1277
+ })
1278
+ }
1279
+ } else if (missingFromCloudDocs.length > 0) {
1280
+ console.log(` WARNING: Found ${missingFromCloudDocs.length} cloud-supported connector(s) missing from cloud-docs:`)
1281
+ missingFromCloudDocs.forEach(({ type, name }) => {
1282
+ console.log(` - ${type}/${name}`)
1283
+ })
1284
+ console.log(` INFO: These connectors need pages added to https://github.com/redpanda-data/cloud-docs`)
1285
+ } else {
1286
+ console.log(` All cloud-supported connectors have pages in cloud-docs`)
1287
+ }
1288
+ } catch (error) {
1289
+ console.log(` WARNING: Could not check cloud-docs: ${error.message}`)
1290
+ }
1291
+ }
1292
+
1130
1293
  const missingConnectors = allMissing.filter(c =>
1131
1294
  !c.name.includes('sql_driver') &&
1132
1295
  c.status !== 'deprecated'