@redpanda-data/docs-extensions-and-macros 5.12.3 → 5.12.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.12.3",
3
+ "version": "5.12.4",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -1,5 +1,5 @@
1
1
  const octokit = require('../../cli-utils/octokit-client');
2
- const { execSync, spawnSync } = require('child_process');
2
+ const { execFileSync, spawnSync } = require('child_process');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
@@ -16,6 +16,31 @@ const https = require('https');
16
16
  const REPO_OWNER = 'redpanda-data';
17
17
  const REPO_NAME = 'connect';
18
18
 
19
+ // Release asset prefixes per binary flavour. The plain OSS build is the one
20
+ // `rpk connect install` puts on PATH, so fetching that asset directly yields
21
+ // the same connector schema without passing a version through rpk's
22
+ // --connect-version validation.
23
+ const BINARY_ASSET_PREFIXES = {
24
+ oss: 'redpanda-connect',
25
+ cloud: 'redpanda-connect-cloud',
26
+ cgo: 'redpanda-connect-cgo'
27
+ };
28
+
29
+ /**
30
+ * Resolve a binary flavour to its release asset prefix.
31
+ * @param {string} binaryType - 'oss', 'cloud' or 'cgo'
32
+ * @returns {string} The asset name prefix
33
+ */
34
+ function resolveAssetPrefix (binaryType) {
35
+ const prefix = BINARY_ASSET_PREFIXES[binaryType];
36
+ if (!prefix) {
37
+ throw new Error(
38
+ `Unknown binary type "${binaryType}" (expected one of ${Object.keys(BINARY_ASSET_PREFIXES).join(', ')})`
39
+ );
40
+ }
41
+ return prefix;
42
+ }
43
+
19
44
  /**
20
45
  * Get platform-specific binary name
21
46
  * @returns {Object} Platform and arch info
@@ -46,10 +71,15 @@ function getPlatformInfo() {
46
71
 
47
72
  /**
48
73
  * Get the latest release version from GitHub
49
- * @param {string} binaryType - Type of binary ('cloud' or 'cgo')
74
+ * @param {string} binaryType - Type of binary ('oss', 'cloud' or 'cgo')
50
75
  * @returns {Promise<string>} Version string (e.g., "4.76.0")
51
76
  */
52
77
  async function getLatestVersion(binaryType = 'cloud') {
78
+ // Resolved outside the try so an unknown flavour surfaces its own message
79
+ // instead of being wrapped as a failure to reach the GitHub API. tools/ is
80
+ // published, so callers outside this repo see this error.
81
+ const prefix = resolveAssetPrefix(binaryType);
82
+
53
83
  try {
54
84
  const { data: releases } = await octokit.repos.listReleases({
55
85
  owner: REPO_OWNER,
@@ -57,12 +87,12 @@ async function getLatestVersion(binaryType = 'cloud') {
57
87
  per_page: 20
58
88
  });
59
89
 
60
- const prefix = binaryType === 'cloud' ? 'redpanda-connect-cloud' : 'redpanda-connect-cgo';
61
-
62
- // Find the latest release with the specified binary type
90
+ // Find the latest release with the specified binary type. Match the
91
+ // separator too: every prefix starts with 'redpanda-connect', so a bare
92
+ // startsWith on the OSS prefix would also match the cloud and cgo assets.
63
93
  for (const release of releases) {
64
94
  const asset = release.assets.find(a =>
65
- a.name.startsWith(prefix) &&
95
+ a.name.startsWith(`${prefix}_`) &&
66
96
  a.name.includes('linux') &&
67
97
  a.name.includes('amd64')
68
98
  );
@@ -124,9 +154,99 @@ function downloadFile(url, destPath) {
124
154
  });
125
155
  }
126
156
 
157
+ /**
158
+ * Directory that holds downloaded Connect binaries for the life of the process.
159
+ *
160
+ * Binaries are ~320 MB extracted, so they never belong in docs-data: the
161
+ * consuming repo commits that directory wholesale, and a run killed before its
162
+ * cleanup would leave a blob over GitHub's 100 MB file limit staged for the
163
+ * auto-docs PR. One directory for the whole run also lets downloadBinary's
164
+ * skip-if-present check serve repeat requests: the multi-release loop asks for
165
+ * every interior version twice (as the new side of one diff pair and the old
166
+ * side of the next), and a per-call directory made that a second download.
167
+ *
168
+ * @returns {string} Path to the process-wide binary cache directory
169
+ */
170
+ let binaryCacheDir = null;
171
+ function getBinaryCacheDir () {
172
+ if (binaryCacheDir && fs.existsSync(binaryCacheDir)) return binaryCacheDir;
173
+
174
+ binaryCacheDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpcn-binaries-'));
175
+ const created = binaryCacheDir;
176
+ process.on('exit', () => {
177
+ try {
178
+ fs.rmSync(created, { recursive: true, force: true });
179
+ } catch (err) {
180
+ // Nothing useful to do while exiting; the OS reclaims tmpdir anyway.
181
+ }
182
+ });
183
+ return binaryCacheDir;
184
+ }
185
+
186
+ // Cap the cache: a backfill can touch dozens of versions, but no more than a
187
+ // handful of binaries are live at once (the two OSS versions of the current
188
+ // diff pair plus the cloud and cgo binaries analysed alongside them). Without
189
+ // a cap, sharing one directory would trade repeat downloads for unbounded disk.
190
+ const BINARY_CACHE_LIMIT = 6;
191
+ const binaryCacheUse = [];
192
+
193
+ /**
194
+ * Record a binary as most recently used and evict the oldest beyond the cap.
195
+ * @param {string} destPath - Path to the binary just downloaded or reused
196
+ */
197
+ function touchBinaryCache (destPath) {
198
+ const existing = binaryCacheUse.indexOf(destPath);
199
+ if (existing !== -1) binaryCacheUse.splice(existing, 1);
200
+ binaryCacheUse.push(destPath);
201
+
202
+ while (binaryCacheUse.length > BINARY_CACHE_LIMIT) {
203
+ const evicted = binaryCacheUse.shift();
204
+ try {
205
+ fs.rmSync(evicted, { force: true });
206
+ } catch (err) {
207
+ console.warn(`Warning: could not evict cached binary ${evicted}: ${err.message}`);
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Extract the Connect binary out of a release tarball.
214
+ *
215
+ * Extraction goes into an empty directory of its own because the archive's
216
+ * inner file name is not the destination name, and the destination directory
217
+ * holds other flavours and versions whose names also contain
218
+ * 'redpanda-connect'. Scanning the shared directory instead would pick a
219
+ * neighbour in readdir order and analyse, say, the cloud binary as the cgo one.
220
+ *
221
+ * @param {string} tarballPath - Path to the downloaded .tar.gz
222
+ * @param {string} destPath - Path the extracted binary should end up at
223
+ * @returns {string} destPath
224
+ */
225
+ function extractBinaryFromTarball (tarballPath, destPath) {
226
+ const extractDir = fs.mkdtempSync(`${destPath}.extract-`);
227
+ try {
228
+ execFileSync('tar', ['-xzf', tarballPath, '-C', extractDir], { stdio: 'ignore' });
229
+
230
+ const candidates = fs.readdirSync(extractDir)
231
+ .filter(name => name.includes('redpanda-connect') && !name.endsWith('.tar.gz'))
232
+ .filter(name => fs.statSync(path.join(extractDir, name)).isFile())
233
+ .sort();
234
+
235
+ if (candidates.length === 0) {
236
+ throw new Error('Binary not found after extraction');
237
+ }
238
+
239
+ fs.renameSync(path.join(extractDir, candidates[0]), destPath);
240
+ fs.chmodSync(destPath, 0o755);
241
+ return destPath;
242
+ } finally {
243
+ fs.rmSync(extractDir, { recursive: true, force: true });
244
+ }
245
+ }
246
+
127
247
  /**
128
248
  * Download a binary from GitHub releases
129
- * @param {string} binaryType - 'cloud' or 'cgo'
249
+ * @param {string} binaryType - 'oss', 'cloud' or 'cgo'
130
250
  * @param {string} version - Version to download (e.g., "4.76.0")
131
251
  * @param {string} destDir - Destination directory
132
252
  * @returns {Promise<string>} Path to downloaded binary
@@ -136,13 +256,14 @@ async function downloadBinary(binaryType, version, destDir) {
136
256
  const platformInfo = binaryType === 'cgo'
137
257
  ? { platform: 'linux', arch: 'amd64' }
138
258
  : getPlatformInfo();
139
- const prefix = binaryType === 'cloud' ? 'redpanda-connect-cloud' : 'redpanda-connect-cgo';
259
+ const prefix = resolveAssetPrefix(binaryType);
140
260
  const binaryName = `${prefix}-${version}-${platformInfo.platform}-${platformInfo.arch}`;
141
261
  const destPath = path.join(destDir, binaryName);
142
262
 
143
263
  // Skip download if already exists
144
264
  if (fs.existsSync(destPath)) {
145
265
  console.log(`✓ ${binaryType.toUpperCase()} binary already downloaded: ${binaryName}`);
266
+ touchBinaryCache(destPath);
146
267
  return destPath;
147
268
  }
148
269
 
@@ -170,35 +291,12 @@ async function downloadBinary(binaryType, version, destDir) {
170
291
 
171
292
  // Extract the binary
172
293
  console.log(`Extracting ${binaryType.toUpperCase()} binary...`);
173
- execSync(`tar -xzf "${tarballPath}" -C "${destDir}"`, { stdio: 'ignore' });
174
-
175
- // Find the extracted binary
176
- const extractedFiles = fs.readdirSync(destDir);
177
- let binaryPath = null;
178
-
179
- for (const file of extractedFiles) {
180
- const fullPath = path.join(destDir, file);
181
- if (fs.statSync(fullPath).isFile() && file.includes('redpanda-connect') && !file.endsWith('.tar.gz')) {
182
- binaryPath = fullPath;
183
- break;
184
- }
185
- }
186
-
187
- if (!binaryPath) {
188
- throw new Error('Binary not found after extraction');
189
- }
190
-
191
- // Rename to standard name if needed
192
- if (binaryPath !== destPath) {
193
- fs.renameSync(binaryPath, destPath);
194
- }
195
-
196
- // Make executable
197
- fs.chmodSync(destPath, 0o755);
294
+ extractBinaryFromTarball(tarballPath, destPath);
198
295
 
199
296
  // Clean up tarball
200
297
  fs.unlinkSync(tarballPath);
201
298
 
299
+ touchBinaryCache(destPath);
202
300
  console.log(`Done: Downloaded ${binaryType.toUpperCase()} binary: ${binaryName}`);
203
301
  return destPath;
204
302
  } catch (error) {
@@ -435,9 +533,10 @@ async function analyzeAllBinaries(ossVersion, cloudVersion = null, dataDir = nul
435
533
 
436
534
  const docsDataDir = dataDir || path.resolve(process.cwd(), 'docs-data');
437
535
 
438
- // Use temp directory for binaries (not docs-data)
439
- const os = require('os');
440
- const binaryDir = fs.mkdtempSync(path.join(os.tmpdir(), 'rpcn-binaries-'));
536
+ // Use temp directory for binaries (not docs-data). Shared with every other
537
+ // download in this process, so a multi-release run fetches each cloud and cgo
538
+ // binary once instead of once per diff pair.
539
+ const binaryDir = getBinaryCacheDir();
441
540
 
442
541
  console.log(`📁 Using temp directory for binaries: ${binaryDir}`);
443
542
 
@@ -508,13 +607,8 @@ async function analyzeAllBinaries(ossVersion, cloudVersion = null, dataDir = nul
508
607
  console.log('⏩ Skipping cgo binary analysis');
509
608
  }
510
609
 
511
- // Clean up temp binaries
512
- try {
513
- fs.rmSync(binaryDir, { recursive: true, force: true });
514
- console.log(`🧹 Cleaned up temp binaries`);
515
- } catch (err) {
516
- console.warn(`Warning: Could not clean up temp directory: ${err.message}`);
517
- }
610
+ // Binaries stay in the shared cache so the next diff pair can reuse them; the
611
+ // directory is removed when the process exits (see getBinaryCacheDir).
518
612
 
519
613
  // IMPORTANT: cgoOnly contains connectors that exist ONLY in the cgo binary
520
614
  // and NOT in the regular OSS binary (rpk connect list). These are the
@@ -566,6 +660,11 @@ module.exports = {
566
660
  analyzeAllBinaries,
567
661
  findCgoOnlyConnectors,
568
662
  getLatestVersion,
663
+ getPlatformInfo,
664
+ resolveAssetPrefix,
665
+ getBinaryCacheDir,
666
+ extractBinaryFromTarball,
667
+ downloadBinary,
569
668
  downloadCloudBinary,
570
669
  downloadCgoBinary,
571
670
  getConnectorList,
@@ -5,7 +5,7 @@ const path = require('path');
5
5
  const handlebars = require('handlebars');
6
6
  const yaml = require('yaml');
7
7
  const helpers = require('./helpers');
8
- const { lostMetadataSections } = require('./metadata-utils.js');
8
+ const { lostMetadataSections, resolvePageTypeDir } = require('./metadata-utils.js');
9
9
 
10
10
  // Register each helper under handlebars, verifying that it’s a function
11
11
  Object.entries(helpers).forEach(([name, fn]) => {
@@ -730,11 +730,7 @@ function backfillPageDescriptions (connectorData, { pagesRoot, dryRun = false }
730
730
 
731
731
  for (const [dataKey, items] of Object.entries(connectorData)) {
732
732
  if (!PAGE_TYPE_DIRS.has(dataKey) || !Array.isArray(items)) continue;
733
- // The data uses 'rate-limits' while the pages directory is
734
- // 'rate_limits'. Resolve whichever spelling exists on disk.
735
- const typeDir = fs.existsSync(path.join(root, dataKey))
736
- ? dataKey
737
- : dataKey.replace(/-/g, '_');
733
+ const typeDir = resolvePageTypeDir(root, dataKey);
738
734
  for (const item of items) {
739
735
  if (!item || !item.name) continue;
740
736
  const pagePath = path.join(root, typeDir, `${item.name}.adoc`);
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
3
6
  /**
4
7
  * Utilities for extracting the "== Metadata" section out of a connector's
5
8
  * `description` prose so it can be emitted as a regenerated partial (like the
@@ -87,6 +90,31 @@ function typeDirFor (item) {
87
90
  return type.endsWith('s') ? type : `${type}s`;
88
91
  }
89
92
 
93
+ /**
94
+ * Resolve the on-disk page/partial directory for a connector data key.
95
+ *
96
+ * The Connect binary emits the rate-limit family as `rate-limits` while the
97
+ * directory in rp-connect-docs is `rate_limits`, so the data key and the
98
+ * directory name disagree for exactly one family. Resolve against what is
99
+ * actually on disk rather than encoding either spelling: a caller that guesses
100
+ * `rate-limits` reports every existing rate-limit page as missing and then
101
+ * drafts a duplicate beside it, in a directory update-nav.js's regex does not
102
+ * match, re-added on every run.
103
+ *
104
+ * @param {string} root directory the type directories live under
105
+ * @param {string} dataKey the data key, for example `inputs` or `rate-limits`
106
+ * @returns {string} the directory name that exists, preferring the data key
107
+ */
108
+ function resolvePageTypeDir (root, dataKey) {
109
+ if (!root || !dataKey) return dataKey || '';
110
+ if (fs.existsSync(path.join(root, dataKey))) return dataKey;
111
+ const underscored = dataKey.replace(/-/g, '_');
112
+ if (underscored !== dataKey && fs.existsSync(path.join(root, underscored))) return underscored;
113
+ const hyphenated = dataKey.replace(/_/g, '-');
114
+ if (hyphenated !== dataKey && fs.existsSync(path.join(root, hyphenated))) return hyphenated;
115
+ return dataKey;
116
+ }
117
+
90
118
  /**
91
119
  * Build the Antora include directive for a connector's metadata partial.
92
120
  * @param {object} item connector data with `type`/`typeDir` and `name`
@@ -170,6 +198,7 @@ function lostMetadataSections (oldContent, newContent) {
170
198
 
171
199
  module.exports = {
172
200
  locateMetadata,
201
+ resolvePageTypeDir,
173
202
  extractMetadata,
174
203
  typeDirFor,
175
204
  metadataIncludeLine,
@@ -11,6 +11,7 @@ const { getRpkConnectVersion, printDeltaReport } = require('./report-delta')
11
11
  const { discoverIntermediateReleases, findCloudVersionForDate } = require('./github-release-utils')
12
12
  const { analyzeAllBinaries } = require('./connector-binary-analyzer.js')
13
13
  const parseCSVConnectors = require('./parse-csv-connectors.js')
14
+ const { resolvePageTypeDir } = require('./metadata-utils.js')
14
15
  const semver = require('semver')
15
16
 
16
17
  /**
@@ -106,6 +107,23 @@ function capToTwoSentences (description) {
106
107
  return result.trim()
107
108
  }
108
109
 
110
+ // Component families in a connector index, in the spelling the Connect binary
111
+ // emits. Rate limits are 'rate-limits' there; 'rate_limits' is the docs page
112
+ // directory spelling, which older snapshots also carry as an (always empty)
113
+ // data key, so it is walked when present but never created. Every copy of this
114
+ // list used to be the underscore form only, which meant rate-limit components
115
+ // were never given cloudSupported or requiresCgo flags and any cgo-only or
116
+ // cloud-only rate limit was dropped from the published snapshot.
117
+ const COMPONENT_TYPE_KEYS = ['inputs', 'outputs', 'processors', 'caches', 'rate-limits',
118
+ 'rate_limits', 'buffers', 'metrics', 'scanners', 'tracers']
119
+
120
+ // 'config' components live in the same index but have no config/fields wrapper.
121
+ const COMPONENT_TYPE_KEYS_WITH_CONFIG = [...COMPONENT_TYPE_KEYS, 'config']
122
+
123
+ // Keys kept only for snapshots written before the spelling was fixed. Walked
124
+ // if a snapshot has them, never added to one.
125
+ const LEGACY_COMPONENT_TYPE_KEYS = new Set(['rate_limits'])
126
+
109
127
  /**
110
128
  * Remove platform metadata fields from connectors
111
129
  * @param {Array} connectors - Array of connector objects
@@ -146,11 +164,9 @@ function augmentConnectorData (connectorData, binaryAnalysis) {
146
164
  let addedCgoCount = 0
147
165
  let addedCloudOnlyCount = 0
148
166
 
149
- const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
150
- 'buffers', 'metrics', 'scanners', 'tracers', 'config']
151
-
152
- for (const type of connectorTypes) {
167
+ for (const type of COMPONENT_TYPE_KEYS_WITH_CONFIG) {
153
168
  if (!Array.isArray(augmentedData[type])) {
169
+ if (LEGACY_COMPONENT_TYPE_KEYS.has(type)) continue
154
170
  augmentedData[type] = []
155
171
  }
156
172
 
@@ -219,10 +235,7 @@ function augmentConnectorData (connectorData, binaryAnalysis) {
219
235
  */
220
236
  function buildCleanOssData (connectorIndex) {
221
237
  const cleanData = JSON.parse(JSON.stringify(connectorIndex))
222
- const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
223
- 'buffers', 'metrics', 'scanners', 'tracers']
224
-
225
- for (const type of connectorTypes) {
238
+ for (const type of COMPONENT_TYPE_KEYS) {
226
239
  if (Array.isArray(cleanData[type])) {
227
240
  // Keep only connectors from OSS rpk (have config/fields)
228
241
  // Remove augmentation-only connectors (added by previous binary analysis)
@@ -703,11 +716,52 @@ function logCollapsed (label, filesArray, maxToShow = 10) {
703
716
  }
704
717
 
705
718
 
719
+ /**
720
+ * Why a stored connect-<version>.json cannot stand in for a fresh fetch.
721
+ *
722
+ * The handler writes augmentConnectorData() output back over the same file, so
723
+ * a stored snapshot is not necessarily raw binary output. Reusing an augmented
724
+ * one would re-augment already-augmented data: the cloud-only and cgo-only
725
+ * entries injected by the previous run are not in the new analysis, so they
726
+ * survive with their platform markers reset and a cgo-only connector ends up
727
+ * published as an ordinary OSS one. The version field is the only provenance
728
+ * the downstream templates get, so a file that disagrees with its own name
729
+ * cannot be trusted either.
730
+ *
731
+ * @param {Object} data - Parsed contents of connect-<version>.json
732
+ * @param {string} version - Version the file is supposed to describe
733
+ * @returns {string|null} Reason it cannot be reused, or null if it can
734
+ */
735
+ function snapshotReuseBlocker (data, version) {
736
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
737
+ return 'file is not a connector index'
738
+ }
739
+
740
+ if (typeof data.version === 'string' && data.version !== version) {
741
+ return `file reports version ${data.version}`
742
+ }
743
+
744
+ const augmented = Object.values(data)
745
+ .filter(Array.isArray)
746
+ .some(components => components.some(c => c && typeof c === 'object' && (
747
+ c.cloudSupported !== undefined ||
748
+ c.requiresCgo !== undefined ||
749
+ c.cloudOnly !== undefined
750
+ )))
751
+
752
+ if (augmented) {
753
+ return 'file already carries cloud/cgo augmentation from an earlier run'
754
+ }
755
+
756
+ return null
757
+ }
758
+
706
759
  /**
707
760
  * Load or fetch connector data for a specific version
708
761
  * @param {string} version - Version to load (e.g., "4.50.0")
709
762
  * @param {string} dataDir - Directory where JSON files are stored
710
763
  * @param {Object} options - Options for fetching if needed
764
+ * @param {boolean} [options.forceFresh] - Refetch even when a usable file exists
711
765
  * @returns {Promise<Object>} Parsed connector data
712
766
  */
713
767
  async function loadConnectorDataForVersion(version, dataDir, options = {}) {
@@ -715,51 +769,75 @@ async function loadConnectorDataForVersion(version, dataDir, options = {}) {
715
769
 
716
770
  // If forceFresh is set, always fetch even if file exists
717
771
  // This ensures we have accurate connector lists for diff comparison
772
+ let reason = options.forceFresh ? 'force refresh requested' : 'file not found';
773
+
718
774
  if (!options.forceFresh && fs.existsSync(dataFile)) {
719
- console.log(`✓ Using existing data file: connect-${version}.json`);
720
- const data = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
721
- return data;
775
+ let stored = null;
776
+ let blocker = null;
777
+ try {
778
+ stored = JSON.parse(fs.readFileSync(dataFile, 'utf8'));
779
+ blocker = snapshotReuseBlocker(stored, version);
780
+ } catch (err) {
781
+ blocker = `file could not be parsed (${err.message})`;
782
+ }
783
+
784
+ if (!blocker) {
785
+ console.log(`✓ Using existing data file: connect-${version}.json`);
786
+ return stored;
787
+ }
788
+
789
+ console.warn(`↻ Ignoring existing connect-${version}.json: ${blocker}`);
790
+ reason = blocker;
722
791
  }
723
792
 
724
793
  // Fetch fresh data
725
- const reason = options.forceFresh ? 'force refresh requested' : 'file not found';
726
794
  console.log(`📥 Fetching data for ${version} (${reason})...`);
727
795
 
796
+ // Fetch the OSS release asset for this exact version instead of asking rpk to
797
+ // install it. rpk's validateVersion (rpk/pkg/cli/connect/install.go) matches
798
+ // --connect-version against a regex that caps each segment at two digits, so
799
+ // it rejects every version >= 4.100.0, which is every version worth asking
800
+ // for. The cap is lifted in rpk v26.1.15 and on dev, but v26.1.14 and the
801
+ // whole v26.2.x line still carry it, so which versions can be installed
802
+ // depends on which rpk happens to be on PATH. Fetching the asset removes that
803
+ // dependency instead of working around one rpk release, so this is the
804
+ // permanent mechanism rather than something to revert once rpk ships a fix:
805
+ // the plain OSS asset is the same build rpk would have installed, so the
806
+ // schema is identical, and this leaves the caller's own Connect installation
807
+ // untouched instead of forcing a managed plugin over the top of it.
728
808
  try {
729
- // Try installing that specific version and fetching data
730
- console.log(` Installing Redpanda Connect version ${version}...`);
731
- const installResult = spawnSync('rpk', ['connect', 'install', '--connect-version', version, '--force'], {
732
- stdio: 'pipe'
733
- });
734
-
735
- if (installResult.status !== 0) {
736
- throw new Error(`Failed to install Connect version ${version}`);
737
- }
809
+ const { downloadBinary, getConnectorList, getBinaryCacheDir } = require('./connector-binary-analyzer.js');
738
810
 
739
- // Fetch connector list
740
- const tmpFile = path.join(dataDir, `connect-${version}.tmp.json`);
741
- const fd = fs.openSync(tmpFile, 'w');
742
- const listResult = spawnSync('rpk', ['connect', 'list', '--format', 'json-full'], {
743
- stdio: ['ignore', fd, 'pipe']
744
- });
745
- fs.closeSync(fd);
746
-
747
- if (listResult.status !== 0) {
748
- throw new Error(`Failed to fetch connector list for version ${version}`);
811
+ // Binaries go to the process-wide temp cache, never to dataDir: the
812
+ // consuming repo commits dataDir, and the extracted binary is ~320 MB.
813
+ const binaryPath = await downloadBinary('oss', version, getBinaryCacheDir());
814
+ const parsed = getConnectorList(binaryPath);
815
+
816
+ if (!parsed || typeof parsed !== 'object') {
817
+ throw new Error('connector list was empty or not an object');
749
818
  }
750
819
 
751
- // Parse and validate
752
- const rawJson = fs.readFileSync(tmpFile, 'utf8');
753
- const parsed = JSON.parse(rawJson);
820
+ // The asset name embeds the version and the release was fetched by tag, so
821
+ // a binary that reports a different version means the wrong asset was
822
+ // downloaded. Fail rather than publish a snapshot under the wrong name.
823
+ if (typeof parsed.version === 'string' && parsed.version !== version) {
824
+ throw new Error(`asset reports version ${parsed.version}, not ${version}`);
825
+ }
754
826
 
755
- // Move to final location
827
+ // Write via a temp file so an interrupted run cannot leave a half-written
828
+ // snapshot that the next run would happily reuse.
829
+ const tmpFile = path.join(dataDir, `connect-${version}.tmp.json`);
830
+ fs.writeFileSync(tmpFile, JSON.stringify(parsed, null, 2));
756
831
  fs.renameSync(tmpFile, dataFile);
757
832
 
758
833
  console.log(`✓ Successfully fetched data for version ${version}`);
759
834
  return parsed;
760
835
  } catch (error) {
761
836
  console.error(`❌ Failed to fetch data for version ${version}: ${error.message}`);
762
- throw new Error(`Cannot process version ${version} - data unavailable`);
837
+ // Carry the reason into the thrown error: this message is what reaches the
838
+ // failure summary and the PR body, where a bare "data unavailable" sends
839
+ // the reader back to the raw workflow log.
840
+ throw new Error(`Cannot process version ${version} - data unavailable (${error.message})`);
763
841
  }
764
842
  }
765
843
 
@@ -789,34 +867,38 @@ async function handleRpcnConnectorDocs (options) {
789
867
  console.error('Expected format: X.Y.Z (e.g., 4.50.0)')
790
868
  process.exit(1)
791
869
  }
792
- console.log(`Installing Redpanda Connect version ${options.connectVersion}...`)
793
- const installResult = spawnSync('rpk', ['connect', 'install', '--connect-version', options.connectVersion, '--force'], {
794
- stdio: 'inherit'
795
- })
796
- if (installResult.status !== 0) {
797
- throw new Error(`Failed to install Connect version ${options.connectVersion}`)
798
- }
799
- console.log(`Done: Installed Redpanda Connect version ${options.connectVersion}`)
800
870
  newVersion = options.connectVersion
871
+ // An explicit version comes from the release asset rather than from
872
+ // rpk, which rejects anything >= 4.100.0 on the rpk releases this
873
+ // pipeline runs. See loadConnectorDataForVersion. Reuses an existing
874
+ // snapshot when it is raw, unaugmented output for this same version,
875
+ // and leaves the caller's own Connect install alone either way.
876
+ console.log(`Fetching connector data for Connect ${newVersion}...`)
877
+ await loadConnectorDataForVersion(newVersion, dataDir)
878
+ dataFile = path.join(dataDir, `connect-${newVersion}.json`)
879
+ needsAugmentation = true
880
+ console.log(`Done: Fetched connector data for version ${newVersion}`)
801
881
  } else {
882
+ // No version asked for, so read whatever rpk has: this is install's
883
+ // unvalidated "latest" path, which the version regex never sees.
802
884
  newVersion = getRpkConnectVersion()
885
+ console.log(`Fetching connector data from Connect ${newVersion}...`)
886
+
887
+ const tmpFile = path.join(dataDir, `connect-${newVersion}.tmp.json`)
888
+ const finalFile = path.join(dataDir, `connect-${newVersion}.json`)
889
+
890
+ const fd = fs.openSync(tmpFile, 'w')
891
+ spawnSync('rpk', ['connect', 'list', '--format', 'json-full'], { stdio: ['ignore', fd, 'inherit'] })
892
+ fs.closeSync(fd)
893
+
894
+ const rawJson = fs.readFileSync(tmpFile, 'utf8')
895
+ const parsed = JSON.parse(rawJson)
896
+ fs.writeFileSync(finalFile, JSON.stringify(parsed, null, 2))
897
+ fs.unlinkSync(tmpFile)
898
+ dataFile = finalFile
899
+ needsAugmentation = true
900
+ console.log(`Done: Fetched connector data for version ${newVersion}`)
803
901
  }
804
- console.log(`Fetching connector data from Connect ${newVersion}...`)
805
-
806
- const tmpFile = path.join(dataDir, `connect-${newVersion}.tmp.json`)
807
- const finalFile = path.join(dataDir, `connect-${newVersion}.json`)
808
-
809
- const fd = fs.openSync(tmpFile, 'w')
810
- const r = spawnSync('rpk', ['connect', 'list', '--format', 'json-full'], { stdio: ['ignore', fd, 'inherit'] })
811
- fs.closeSync(fd)
812
-
813
- const rawJson = fs.readFileSync(tmpFile, 'utf8')
814
- const parsed = JSON.parse(rawJson)
815
- fs.writeFileSync(finalFile, JSON.stringify(parsed, null, 2))
816
- fs.unlinkSync(tmpFile)
817
- dataFile = finalFile
818
- needsAugmentation = true
819
- console.log(`Done: Fetched connector data for version ${newVersion}`)
820
902
 
821
903
  // Fetch info.csv
822
904
  try {
@@ -1458,11 +1540,8 @@ async function handleRpcnConnectorDocs (options) {
1458
1540
 
1459
1541
  // Strip CGO/cloud-only connectors and metadata from old data
1460
1542
  oldIndexForDiff = JSON.parse(JSON.stringify(oldIndex))
1461
- const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
1462
- 'buffers', 'metrics', 'scanners', 'tracers', 'config']
1463
-
1464
1543
  let totalStripped = 0
1465
- for (const type of connectorTypes) {
1544
+ for (const type of COMPONENT_TYPE_KEYS_WITH_CONFIG) {
1466
1545
  if (Array.isArray(oldIndexForDiff[type])) {
1467
1546
  const originalCount = oldIndexForDiff[type].length
1468
1547
 
@@ -1649,8 +1728,7 @@ async function handleRpcnConnectorDocs (options) {
1649
1728
  const dataObj = JSON.parse(rawData)
1650
1729
 
1651
1730
  const validConnectors = []
1652
- const types = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits', 'buffers', 'metrics', 'scanners', 'tracers']
1653
- types.forEach(type => {
1731
+ COMPONENT_TYPE_KEYS.forEach(type => {
1654
1732
  if (Array.isArray(dataObj[type])) {
1655
1733
  dataObj[type].forEach(connector => {
1656
1734
  if (connector.name) {
@@ -1721,8 +1799,7 @@ async function handleRpcnConnectorDocs (options) {
1721
1799
  // Fallback when binary analysis is unavailable:
1722
1800
  // Check all connectors that have cloudSupported flag or assume all non-deprecated are cloud-supported
1723
1801
  console.log(' ℹ️ Binary analysis unavailable - checking all non-deprecated connectors for cloud-docs')
1724
- const types = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits', 'buffers', 'metrics', 'scanners', 'tracers']
1725
- types.forEach(type => {
1802
+ COMPONENT_TYPE_KEYS.forEach(type => {
1726
1803
  if (Array.isArray(dataObj[type])) {
1727
1804
  dataObj[type].forEach(connector => {
1728
1805
  // Include if cloudSupported is explicitly true, or if it's null/undefined and not deprecated
@@ -1738,17 +1815,21 @@ async function handleRpcnConnectorDocs (options) {
1738
1815
  }
1739
1816
 
1740
1817
  // Check for missing connector documentation in rp-connect-docs
1818
+ // `type` came from a data key, and the rate-limit family's data key
1819
+ // ('rate-limits') is not its directory name ('rate_limits'). Resolve the
1820
+ // directory against disk per root, or every existing rate-limit page is
1821
+ // reported missing and drafted again beside itself.
1741
1822
  const allMissing = validConnectors.filter(({ name, type, cloudOnly }) => {
1742
- const relPath = path.join(`${type}s`, `${name}.adoc`)
1823
+ const relPathIn = (root) => path.join(resolvePageTypeDir(root, `${type}s`), `${name}.adoc`)
1743
1824
 
1744
1825
  // For cloud-only connectors, ONLY check the cloud-only directory
1745
1826
  if (cloudOnly) {
1746
- return !fs.existsSync(path.join(roots.cloudOnly, relPath))
1827
+ return !fs.existsSync(path.join(roots.cloudOnly, relPathIn(roots.cloudOnly)))
1747
1828
  }
1748
1829
 
1749
1830
  // For regular connectors, check pages and partials (not cloud-only)
1750
1831
  const existsInAny = [roots.pages, roots.partials].some(root =>
1751
- fs.existsSync(path.join(root, relPath))
1832
+ fs.existsSync(path.join(root, relPathIn(root)))
1752
1833
  )
1753
1834
  return !existsInAny
1754
1835
  })
@@ -2081,6 +2162,8 @@ module.exports = {
2081
2162
  capToTwoSentences,
2082
2163
  augmentConnectorData,
2083
2164
  buildCleanOssData,
2165
+ loadConnectorDataForVersion,
2166
+ snapshotReuseBlocker,
2084
2167
  fieldAnchor,
2085
2168
  buildFieldsTable,
2086
2169
  buildChangedDefaultsTable