@redpanda-data/docs-extensions-and-macros 5.12.2 → 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/extensions/generate-fields-only-pages.js +6 -20
- package/package.json +1 -1
- package/tools/redpanda-connect/connector-binary-analyzer.js +142 -43
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +2 -6
- package/tools/redpanda-connect/helpers/flattenConnectFields.js +73 -0
- package/tools/redpanda-connect/helpers/renderConnectFields.js +5 -5
- package/tools/redpanda-connect/metadata-utils.js +29 -0
- package/tools/redpanda-connect/report-delta.js +134 -80
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +177 -81
|
@@ -11,6 +11,7 @@ const handlebars = require('handlebars')
|
|
|
11
11
|
|
|
12
12
|
// Import and register Redpanda Connect helpers
|
|
13
13
|
const helpers = require('../tools/redpanda-connect/helpers')
|
|
14
|
+
const { flattenConnectFields } = require('../tools/redpanda-connect/helpers/flattenConnectFields')
|
|
14
15
|
Object.entries(helpers).forEach(([name, fn]) => {
|
|
15
16
|
if (typeof fn === 'function') {
|
|
16
17
|
handlebars.registerHelper(name, fn)
|
|
@@ -25,18 +26,11 @@ handlebars.registerHelper('renderConnectFieldsTable', function (children) {
|
|
|
25
26
|
|
|
26
27
|
const rows = []
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const isArray = field.kind === 'array'
|
|
35
|
-
const nameWithArray = (typeof field.name === 'string' && isArray && !field.name.endsWith('[]'))
|
|
36
|
-
? `${field.name}[]`
|
|
37
|
-
: field.name
|
|
38
|
-
const currentPath = pathPrefix ? `${pathPrefix}.${nameWithArray}` : nameWithArray
|
|
39
|
-
|
|
29
|
+
// The dotted paths, the `[]` array marker and the deprecated-field skipping
|
|
30
|
+
// all come from the shared walker, so this table, the reference-page
|
|
31
|
+
// headings and the connector delta report cannot drift apart.
|
|
32
|
+
flattenConnectFields(children, { arrayMarker: true, skipDeprecated: true })
|
|
33
|
+
.forEach(({ path: currentPath, field }) => {
|
|
40
34
|
// Normalize type
|
|
41
35
|
let displayType
|
|
42
36
|
const isArrayTitle = typeof field.name === 'string' && field.name.endsWith('[]')
|
|
@@ -88,15 +82,7 @@ handlebars.registerHelper('renderConnectFieldsTable', function (children) {
|
|
|
88
82
|
default: defaultValue || '-',
|
|
89
83
|
description: desc || '-'
|
|
90
84
|
})
|
|
91
|
-
|
|
92
|
-
// Recurse for children
|
|
93
|
-
if (field.children && Array.isArray(field.children) && field.children.length > 0) {
|
|
94
|
-
collectFields(field.children, currentPath)
|
|
95
|
-
}
|
|
96
85
|
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
collectFields(children, '')
|
|
100
86
|
|
|
101
87
|
if (rows.length === 0) return 'No configuration fields available.\n\n'
|
|
102
88
|
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const octokit = require('../../cli-utils/octokit-client');
|
|
2
|
-
const {
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
440
|
-
|
|
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
|
-
//
|
|
512
|
-
|
|
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
|
-
|
|
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`);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared walker for Redpanda Connect field trees.
|
|
5
|
+
*
|
|
6
|
+
* This is a plain module, not a Handlebars helper, so it is deliberately not
|
|
7
|
+
* exported from helpers/index.js. It exists because the same recursion was
|
|
8
|
+
* written four times (the reference-page renderer, the fields-only table
|
|
9
|
+
* extension and the connector delta report), and the copies disagreed about
|
|
10
|
+
* what a field path looks like. The renderer is the authority: the path this
|
|
11
|
+
* module produces must match the `=== ` heading renderConnectFields emits,
|
|
12
|
+
* because the what's-new tables link to those headings.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Field name as it appears in the rendered docs: an array-of-object field
|
|
17
|
+
* carries an `[]` marker so the path is a valid config path.
|
|
18
|
+
* @param {object} field - Field definition
|
|
19
|
+
* @returns {string} Display name for the field
|
|
20
|
+
*/
|
|
21
|
+
function connectFieldName (field) {
|
|
22
|
+
const name = field && field.name;
|
|
23
|
+
if (typeof name === 'string' && field.kind === 'array' && !name.endsWith('[]')) {
|
|
24
|
+
return `${name}[]`;
|
|
25
|
+
}
|
|
26
|
+
return name;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Walk a field tree depth-first and return one entry per field, in the order
|
|
31
|
+
* the fields appear in the source data.
|
|
32
|
+
*
|
|
33
|
+
* A field nested under an existing group (for example
|
|
34
|
+
* `sqs.zero_key_warn_interval` under aws_s3's pre-existing `sqs` group) is
|
|
35
|
+
* only visible to a set-based diff if the tree is flattened first: a map of
|
|
36
|
+
* top-level names alone treats that as no change at all, because `sqs` itself
|
|
37
|
+
* is not new.
|
|
38
|
+
*
|
|
39
|
+
* Nameless nodes are skipped along with their subtree, matching
|
|
40
|
+
* renderConnectFields: a node with no name gets no heading, so nothing under
|
|
41
|
+
* it is addressable in the published docs.
|
|
42
|
+
*
|
|
43
|
+
* @param {Array} children - Field definitions (each may itself have `children`)
|
|
44
|
+
* @param {object} [opts] - { arrayMarker, skipDeprecated, prefix }
|
|
45
|
+
* @param {boolean} [opts.arrayMarker=false] - Append `[]` to array-of-object names
|
|
46
|
+
* @param {boolean} [opts.skipDeprecated=false] - Skip deprecated fields and their subtrees
|
|
47
|
+
* @param {string} [opts.prefix=''] - Dotted path of the parent, if any
|
|
48
|
+
* @returns {Array<{path: string, name: string, parentPath: (string|null), field: object}>}
|
|
49
|
+
*/
|
|
50
|
+
function flattenConnectFields (children, opts = {}) {
|
|
51
|
+
const { arrayMarker = false, skipDeprecated = false, prefix = '' } = opts;
|
|
52
|
+
const result = [];
|
|
53
|
+
|
|
54
|
+
const walk = (list, currentPrefix, parentPath) => {
|
|
55
|
+
if (!Array.isArray(list)) return;
|
|
56
|
+
list.forEach(field => {
|
|
57
|
+
if (!field || !field.name) return;
|
|
58
|
+
if (skipDeprecated && field.is_deprecated) return;
|
|
59
|
+
|
|
60
|
+
const name = arrayMarker ? connectFieldName(field) : field.name;
|
|
61
|
+
const path = currentPrefix ? `${currentPrefix}.${name}` : `${name}`;
|
|
62
|
+
|
|
63
|
+
result.push({ path, name, parentPath, field });
|
|
64
|
+
walk(field.children, path, path);
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
walk(children, prefix, prefix || null);
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { flattenConnectFields, connectFieldName };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const yaml = require('yaml');
|
|
2
2
|
const renderYamlList = require('./renderYamlList');
|
|
3
3
|
const handlebars = require('handlebars');
|
|
4
|
+
const { connectFieldName } = require('./flattenConnectFields');
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Renders the children of a configuration object into AsciiDoc.
|
|
@@ -56,11 +57,10 @@ module.exports = function renderConnectFields(children, prefix = '') {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
let block = '';
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
: child.name;
|
|
60
|
+
// Only append [] if kind is array and name does not already end with [].
|
|
61
|
+
// Shared with the delta report and the fields-only table so all three
|
|
62
|
+
// producers agree on what a field path looks like.
|
|
63
|
+
const nameWithArray = connectFieldName(child);
|
|
64
64
|
const currentPath = prefix
|
|
65
65
|
? `${prefix}.${nameWithArray}`
|
|
66
66
|
: `${nameWithArray}`;
|
|
@@ -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,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
|
+
const { flattenConnectFields } = require('./helpers/flattenConnectFields');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Generate a JSON diff report between two connector index objects.
|
|
@@ -54,22 +55,29 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
54
55
|
if (!(cKey in oldMap)) return;
|
|
55
56
|
const oldFields = new Set(oldMap[cKey].fields || []);
|
|
56
57
|
const newFieldsArr = newMap[cKey].fields || [];
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
58
|
+
const added = collapseToTopmost(
|
|
59
|
+
newFieldsArr.filter(fName => !oldFields.has(fName)),
|
|
60
|
+
newMap[cKey].parents
|
|
61
|
+
);
|
|
62
|
+
added.forEach(fName => {
|
|
63
|
+
const rawFieldObj = newMap[cKey].fieldMap.get(fName) || null;
|
|
64
|
+
// A field that arrives already deprecated -- or sits under a
|
|
65
|
+
// deprecated ancestor group -- gets no heading on the reference page,
|
|
66
|
+
// so advertising it under "New field support" would link to a fragment
|
|
67
|
+
// that never exists.
|
|
68
|
+
if (isDeprecatedOrDescendsFromDeprecated(fName, newMap[cKey].fieldMap, newMap[cKey].parents)) return;
|
|
69
|
+
newFields.push({
|
|
70
|
+
component: cKey,
|
|
71
|
+
field: fName,
|
|
72
|
+
// Most fields carry no version of their own, so fall back to the
|
|
73
|
+
// version this diff is comparing against: the field is new as of
|
|
74
|
+
// exactly that release. Some fields do carry `version` (268 entries
|
|
75
|
+
// in connect-4.105.0), and that wins, because it can predate the
|
|
76
|
+
// span being compared: inputs:oracledb_cdc/snapshot_mode reports
|
|
77
|
+
// 4.99.0 in the 4.98.0 to 4.103.1 diff.
|
|
78
|
+
introducedIn: (rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version)) || opts.newVersion || null,
|
|
79
|
+
description: rawFieldObj && rawFieldObj.description
|
|
80
|
+
});
|
|
73
81
|
});
|
|
74
82
|
});
|
|
75
83
|
|
|
@@ -79,13 +87,15 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
79
87
|
if (!(cKey in newMap)) return;
|
|
80
88
|
const newFieldsSet = new Set(newMap[cKey].fields || []);
|
|
81
89
|
const oldFieldsArr = oldMap[cKey].fields || [];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
const removed = collapseToTopmost(
|
|
91
|
+
oldFieldsArr.filter(fName => !newFieldsSet.has(fName)),
|
|
92
|
+
oldMap[cKey].parents
|
|
93
|
+
);
|
|
94
|
+
removed.forEach(fName => {
|
|
95
|
+
removedFields.push({
|
|
96
|
+
component: cKey,
|
|
97
|
+
field: fName
|
|
98
|
+
});
|
|
89
99
|
});
|
|
90
100
|
});
|
|
91
101
|
|
|
@@ -172,26 +182,11 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
172
182
|
// Check fields that exist in both versions
|
|
173
183
|
const commonFields = oldFieldsArr.filter(f => newFieldsArr.includes(f));
|
|
174
184
|
commonFields.forEach(fName => {
|
|
175
|
-
const
|
|
185
|
+
const oldFieldObj = oldMap[cKey].fieldMap.get(fName) || null;
|
|
186
|
+
const newFieldObj = newMap[cKey].fieldMap.get(fName) || null;
|
|
176
187
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (type === 'config') {
|
|
180
|
-
oldFieldObj = (oldMap[cKey].raw.children || []).find(f => f.name === fName);
|
|
181
|
-
} else {
|
|
182
|
-
oldFieldObj = (oldMap[cKey].raw.config?.children || []).find(f => f.name === fName);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Get new field object
|
|
186
|
-
let newFieldObj = null;
|
|
187
|
-
if (type === 'config') {
|
|
188
|
-
newFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
|
|
189
|
-
} else {
|
|
190
|
-
newFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const oldDeprecated = oldFieldObj && (oldFieldObj.is_deprecated === true || oldFieldObj.deprecated === true || (oldFieldObj.status || '').toLowerCase() === 'deprecated');
|
|
194
|
-
const newDeprecated = newFieldObj && (newFieldObj.is_deprecated === true || newFieldObj.deprecated === true || (newFieldObj.status || '').toLowerCase() === 'deprecated');
|
|
188
|
+
const oldDeprecated = isDeprecatedField(oldFieldObj);
|
|
189
|
+
const newDeprecated = isDeprecatedField(newFieldObj);
|
|
195
190
|
|
|
196
191
|
if (!oldDeprecated && newDeprecated) {
|
|
197
192
|
deprecatedFields.push({
|
|
@@ -380,6 +375,67 @@ function discoverComponentKeys(obj) {
|
|
|
380
375
|
return Object.keys(obj).filter(key => Array.isArray(obj[key]));
|
|
381
376
|
}
|
|
382
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Whether a field is marked deprecated.
|
|
380
|
+
*
|
|
381
|
+
* Three markers have been used by the source data over time. Only
|
|
382
|
+
* `is_deprecated` appears in current releases (verified across connect-4.63.0,
|
|
383
|
+
* 4.104.0 and 4.105.0: 85, 75 and 75 fields, zero uses of the other two), and
|
|
384
|
+
* it is the one renderConnectFields keys on when it decides not to render a
|
|
385
|
+
* heading for a field.
|
|
386
|
+
*
|
|
387
|
+
* @param {object} field - Field definition, or null
|
|
388
|
+
* @returns {boolean} True when the field is deprecated
|
|
389
|
+
*/
|
|
390
|
+
function isDeprecatedField(field) {
|
|
391
|
+
if (!field) return false;
|
|
392
|
+
return field.is_deprecated === true ||
|
|
393
|
+
field.deprecated === true ||
|
|
394
|
+
(field.status || '').toLowerCase() === 'deprecated';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* True if `fName` or any of its ancestor fields is deprecated.
|
|
399
|
+
*
|
|
400
|
+
* renderConnectFields never renders a field under a deprecated group (the
|
|
401
|
+
* whole group is hidden), so a leaf-only check reports a field added under an
|
|
402
|
+
* existing-but-deprecated group in "New field support" even though it never
|
|
403
|
+
* appears on the page. Walking the parent chain via `parents` is what
|
|
404
|
+
* catches that.
|
|
405
|
+
*/
|
|
406
|
+
function isDeprecatedOrDescendsFromDeprecated(fName, fieldMap, parents) {
|
|
407
|
+
let current = fName;
|
|
408
|
+
while (current) {
|
|
409
|
+
if (isDeprecatedField(fieldMap.get(current))) return true;
|
|
410
|
+
current = parents && parents.get(current);
|
|
411
|
+
}
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Drop paths whose parent changed in the same way, keeping only the topmost
|
|
417
|
+
* field of each changed subtree.
|
|
418
|
+
*
|
|
419
|
+
* Adding or removing one group adds or removes every field under it, and the
|
|
420
|
+
* flattened field list contains all of them. Reporting each descendant turns
|
|
421
|
+
* a single change into one row per node: removing `batching` from
|
|
422
|
+
* outputs:redpanda_migrator would list `batching` plus batching.count,
|
|
423
|
+
* .byte_size, .period, .check and .processors, and the group row already says
|
|
424
|
+
* all of that. Only the fields added to or removed from a group that still
|
|
425
|
+
* exists are news.
|
|
426
|
+
*
|
|
427
|
+
* @param {Array<string>} changedPaths - Paths that are new (or removed)
|
|
428
|
+
* @param {Map<string, (string|null)>} parents - Path to parent path
|
|
429
|
+
* @returns {Array<string>} The topmost changed path of each subtree, in order
|
|
430
|
+
*/
|
|
431
|
+
function collapseToTopmost(changedPaths, parents) {
|
|
432
|
+
const changed = new Set(changedPaths);
|
|
433
|
+
return changedPaths.filter(path => {
|
|
434
|
+
const parent = parents && parents.get(path);
|
|
435
|
+
return !(parent && changed.has(parent));
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
383
439
|
function buildComponentMap(indexObj) {
|
|
384
440
|
const map = {};
|
|
385
441
|
const types = discoverComponentKeys(indexObj);
|
|
@@ -402,7 +458,14 @@ function buildComponentMap(indexObj) {
|
|
|
402
458
|
}
|
|
403
459
|
}
|
|
404
460
|
|
|
405
|
-
|
|
461
|
+
// arrayMarker keeps the paths identical to the headings
|
|
462
|
+
// renderConnectFields emits, so a field name in the what's-new table is
|
|
463
|
+
// a config path a reader can paste into YAML (`sasl[].aws.tcp`, not the
|
|
464
|
+
// invalid `sasl.aws.tcp`).
|
|
465
|
+
const flattened = flattenConnectFields(childArray, { arrayMarker: true });
|
|
466
|
+
const fieldNames = flattened.map(f => f.path);
|
|
467
|
+
const fieldMap = new Map(flattened.map(f => [f.path, f.field]));
|
|
468
|
+
const parents = new Map(flattened.map(f => [f.path, f.parentPath]));
|
|
406
469
|
|
|
407
470
|
// Preserve platform metadata for accurate diff comparison
|
|
408
471
|
const metadata = {
|
|
@@ -414,6 +477,8 @@ function buildComponentMap(indexObj) {
|
|
|
414
477
|
map[lookupKey] = {
|
|
415
478
|
raw: component,
|
|
416
479
|
fields: fieldNames,
|
|
480
|
+
fieldMap: fieldMap,
|
|
481
|
+
parents: parents,
|
|
417
482
|
metadata: metadata
|
|
418
483
|
};
|
|
419
484
|
});
|
|
@@ -499,8 +564,11 @@ function getRpkConnectVersion() {
|
|
|
499
564
|
* • which connectors/components are brand-new
|
|
500
565
|
* • which new fields appeared under existing connectors (including “config” entries)
|
|
501
566
|
* • for each new component/field, if the raw object contains “version” or “introducedInVersion” or “requiresVersion” metadata, print it
|
|
567
|
+
* @param {object} oldIndex - Previous version connector index
|
|
568
|
+
* @param {object} newIndex - Current version connector index
|
|
569
|
+
* @param {object} [opts] - { newVersion } used as the introducedIn fallback
|
|
502
570
|
*/
|
|
503
|
-
function printDeltaReport(oldIndex, newIndex) {
|
|
571
|
+
function printDeltaReport(oldIndex, newIndex, opts = {}) {
|
|
504
572
|
const oldMap = buildComponentMap(oldIndex);
|
|
505
573
|
const newMap = buildComponentMap(newIndex);
|
|
506
574
|
|
|
@@ -513,27 +581,24 @@ function printDeltaReport(oldIndex, newIndex) {
|
|
|
513
581
|
if (!(cKey in oldMap)) return; // skip brand-new components here
|
|
514
582
|
const oldFields = new Set(oldMap[cKey].fields || []);
|
|
515
583
|
const newFieldsArr = newMap[cKey].fields || [];
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
requiresVersion: requiresVer,
|
|
535
|
-
});
|
|
536
|
-
}
|
|
584
|
+
const added = collapseToTopmost(
|
|
585
|
+
newFieldsArr.filter(fName => !oldFields.has(fName)),
|
|
586
|
+
newMap[cKey].parents
|
|
587
|
+
);
|
|
588
|
+
added.forEach(fName => {
|
|
589
|
+
const rawFieldObj = newMap[cKey].fieldMap.get(fName) || null;
|
|
590
|
+
if (isDeprecatedField(rawFieldObj)) return;
|
|
591
|
+
// Same fallback as generateConnectorDiffJson, so the console report and
|
|
592
|
+
// the published table cannot disagree about the same input.
|
|
593
|
+
const introducedIn = (rawFieldObj && (rawFieldObj.introducedInVersion || rawFieldObj.version)) || opts.newVersion || null;
|
|
594
|
+
const requiresVer = rawFieldObj && rawFieldObj.requiresVersion;
|
|
595
|
+
|
|
596
|
+
newFields.push({
|
|
597
|
+
component: cKey,
|
|
598
|
+
field: fName,
|
|
599
|
+
introducedIn,
|
|
600
|
+
requiresVersion: requiresVer,
|
|
601
|
+
});
|
|
537
602
|
});
|
|
538
603
|
});
|
|
539
604
|
|
|
@@ -560,21 +625,10 @@ function printDeltaReport(oldIndex, newIndex) {
|
|
|
560
625
|
const commonFields = oldFieldsArr.filter(f => newFieldsArr.includes(f));
|
|
561
626
|
|
|
562
627
|
commonFields.forEach(fName => {
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
} else {
|
|
568
|
-
oldFieldObj = (oldMap[cKey].raw.config?.children || []).find(f => f.name === fName);
|
|
569
|
-
}
|
|
570
|
-
let newFieldObj = null;
|
|
571
|
-
if (type === 'config') {
|
|
572
|
-
newFieldObj = (newMap[cKey].raw.children || []).find(f => f.name === fName);
|
|
573
|
-
} else {
|
|
574
|
-
newFieldObj = (newMap[cKey].raw.config?.children || []).find(f => f.name === fName);
|
|
575
|
-
}
|
|
576
|
-
const oldDeprecated = oldFieldObj && (oldFieldObj.is_deprecated === true || oldFieldObj.deprecated === true || (oldFieldObj.status || '').toLowerCase() === 'deprecated');
|
|
577
|
-
const newDeprecated = newFieldObj && (newFieldObj.is_deprecated === true || newFieldObj.deprecated === true || (newFieldObj.status || '').toLowerCase() === 'deprecated');
|
|
628
|
+
const oldFieldObj = oldMap[cKey].fieldMap.get(fName) || null;
|
|
629
|
+
const newFieldObj = newMap[cKey].fieldMap.get(fName) || null;
|
|
630
|
+
const oldDeprecated = isDeprecatedField(oldFieldObj);
|
|
631
|
+
const newDeprecated = isDeprecatedField(newFieldObj);
|
|
578
632
|
if (!oldDeprecated && newDeprecated) {
|
|
579
633
|
deprecatedFieldsList.push({ component: cKey, field: fName });
|
|
580
634
|
}
|
|
@@ -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
|
|
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
|
|
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)
|
|
@@ -413,7 +426,7 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
|
|
|
413
426
|
if (regularFields.length > 0) {
|
|
414
427
|
section += '\n=== New field support\n\n'
|
|
415
428
|
section += 'This release adds support for the following new fields:\n\n'
|
|
416
|
-
section += buildFieldsTable(regularFields, capToTwoSentences)
|
|
429
|
+
section += buildFieldsTable(regularFields, capToTwoSentences, { showIntroducedIn: true })
|
|
417
430
|
}
|
|
418
431
|
}
|
|
419
432
|
|
|
@@ -553,24 +566,35 @@ function fieldAnchor (fieldName) {
|
|
|
553
566
|
* @param {Function} capFn - Caption function
|
|
554
567
|
* @returns {string} AsciiDoc table
|
|
555
568
|
*/
|
|
556
|
-
function buildFieldsTable (fields, capFn) {
|
|
569
|
+
function buildFieldsTable (fields, capFn, opts = {}) {
|
|
570
|
+
const { showIntroducedIn = false } = opts
|
|
557
571
|
const byField = {}
|
|
558
572
|
for (const field of fields) {
|
|
559
573
|
const [type, compName] = field.component.split(':')
|
|
560
|
-
|
|
561
|
-
|
|
574
|
+
// Two components can gain the same field path in different releases
|
|
575
|
+
// (`urls` arrived in 3.58.0 for one and 4.23.0 for another). Merging those
|
|
576
|
+
// into a single row would publish the first version for both, so when the
|
|
577
|
+
// version is on show it is part of the row's identity.
|
|
578
|
+
const key = showIntroducedIn
|
|
579
|
+
? `${field.field}\u0000${field.introducedIn || ''}`
|
|
580
|
+
: field.field
|
|
581
|
+
if (!byField[key]) {
|
|
582
|
+
byField[key] = {
|
|
583
|
+
field: field.field,
|
|
562
584
|
description: field.description,
|
|
585
|
+
introducedIn: field.introducedIn,
|
|
563
586
|
components: []
|
|
564
587
|
}
|
|
565
588
|
}
|
|
566
|
-
byField[
|
|
589
|
+
byField[key].components.push({ type, name: compName })
|
|
567
590
|
}
|
|
568
591
|
|
|
569
|
-
let section = '[cols="1m,3,2a"]\n'
|
|
592
|
+
let section = showIntroducedIn ? '[cols="1m,3,2a,1m"]\n' : '[cols="1m,3,2a"]\n'
|
|
570
593
|
section += '|===\n'
|
|
571
|
-
section += '|Field |Description |Affected components\n\n'
|
|
594
|
+
section += showIntroducedIn ? '|Field |Description |Affected components |Introduced in\n\n' : '|Field |Description |Affected components\n\n'
|
|
572
595
|
|
|
573
|
-
for (const
|
|
596
|
+
for (const info of Object.values(byField)) {
|
|
597
|
+
const fieldName = info.field
|
|
574
598
|
const byType = {}
|
|
575
599
|
for (const comp of info.components) {
|
|
576
600
|
if (!byType[comp.type]) byType[comp.type] = []
|
|
@@ -595,7 +619,9 @@ function buildFieldsTable (fields, capFn) {
|
|
|
595
619
|
|
|
596
620
|
section += `|${fieldName}\n`
|
|
597
621
|
section += `|${desc}\n`
|
|
598
|
-
section += `|${componentList}\n
|
|
622
|
+
section += `|${componentList}\n`
|
|
623
|
+
if (showIntroducedIn) section += `|${info.introducedIn || '-'}\n`
|
|
624
|
+
section += '\n'
|
|
599
625
|
}
|
|
600
626
|
|
|
601
627
|
section += '|===\n\n'
|
|
@@ -690,11 +716,52 @@ function logCollapsed (label, filesArray, maxToShow = 10) {
|
|
|
690
716
|
}
|
|
691
717
|
|
|
692
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
|
+
|
|
693
759
|
/**
|
|
694
760
|
* Load or fetch connector data for a specific version
|
|
695
761
|
* @param {string} version - Version to load (e.g., "4.50.0")
|
|
696
762
|
* @param {string} dataDir - Directory where JSON files are stored
|
|
697
763
|
* @param {Object} options - Options for fetching if needed
|
|
764
|
+
* @param {boolean} [options.forceFresh] - Refetch even when a usable file exists
|
|
698
765
|
* @returns {Promise<Object>} Parsed connector data
|
|
699
766
|
*/
|
|
700
767
|
async function loadConnectorDataForVersion(version, dataDir, options = {}) {
|
|
@@ -702,51 +769,75 @@ async function loadConnectorDataForVersion(version, dataDir, options = {}) {
|
|
|
702
769
|
|
|
703
770
|
// If forceFresh is set, always fetch even if file exists
|
|
704
771
|
// This ensures we have accurate connector lists for diff comparison
|
|
772
|
+
let reason = options.forceFresh ? 'force refresh requested' : 'file not found';
|
|
773
|
+
|
|
705
774
|
if (!options.forceFresh && fs.existsSync(dataFile)) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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;
|
|
709
791
|
}
|
|
710
792
|
|
|
711
793
|
// Fetch fresh data
|
|
712
|
-
const reason = options.forceFresh ? 'force refresh requested' : 'file not found';
|
|
713
794
|
console.log(`📥 Fetching data for ${version} (${reason})...`);
|
|
714
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.
|
|
715
808
|
try {
|
|
716
|
-
|
|
717
|
-
console.log(` Installing Redpanda Connect version ${version}...`);
|
|
718
|
-
const installResult = spawnSync('rpk', ['connect', 'install', '--connect-version', version, '--force'], {
|
|
719
|
-
stdio: 'pipe'
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
if (installResult.status !== 0) {
|
|
723
|
-
throw new Error(`Failed to install Connect version ${version}`);
|
|
724
|
-
}
|
|
809
|
+
const { downloadBinary, getConnectorList, getBinaryCacheDir } = require('./connector-binary-analyzer.js');
|
|
725
810
|
|
|
726
|
-
//
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
const
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (listResult.status !== 0) {
|
|
735
|
-
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');
|
|
736
818
|
}
|
|
737
819
|
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
|
|
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
|
+
}
|
|
741
826
|
|
|
742
|
-
//
|
|
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));
|
|
743
831
|
fs.renameSync(tmpFile, dataFile);
|
|
744
832
|
|
|
745
833
|
console.log(`✓ Successfully fetched data for version ${version}`);
|
|
746
834
|
return parsed;
|
|
747
835
|
} catch (error) {
|
|
748
836
|
console.error(`❌ Failed to fetch data for version ${version}: ${error.message}`);
|
|
749
|
-
|
|
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})`);
|
|
750
841
|
}
|
|
751
842
|
}
|
|
752
843
|
|
|
@@ -776,34 +867,38 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
776
867
|
console.error('Expected format: X.Y.Z (e.g., 4.50.0)')
|
|
777
868
|
process.exit(1)
|
|
778
869
|
}
|
|
779
|
-
console.log(`Installing Redpanda Connect version ${options.connectVersion}...`)
|
|
780
|
-
const installResult = spawnSync('rpk', ['connect', 'install', '--connect-version', options.connectVersion, '--force'], {
|
|
781
|
-
stdio: 'inherit'
|
|
782
|
-
})
|
|
783
|
-
if (installResult.status !== 0) {
|
|
784
|
-
throw new Error(`Failed to install Connect version ${options.connectVersion}`)
|
|
785
|
-
}
|
|
786
|
-
console.log(`Done: Installed Redpanda Connect version ${options.connectVersion}`)
|
|
787
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}`)
|
|
788
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.
|
|
789
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}`)
|
|
790
901
|
}
|
|
791
|
-
console.log(`Fetching connector data from Connect ${newVersion}...`)
|
|
792
|
-
|
|
793
|
-
const tmpFile = path.join(dataDir, `connect-${newVersion}.tmp.json`)
|
|
794
|
-
const finalFile = path.join(dataDir, `connect-${newVersion}.json`)
|
|
795
|
-
|
|
796
|
-
const fd = fs.openSync(tmpFile, 'w')
|
|
797
|
-
const r = spawnSync('rpk', ['connect', 'list', '--format', 'json-full'], { stdio: ['ignore', fd, 'inherit'] })
|
|
798
|
-
fs.closeSync(fd)
|
|
799
|
-
|
|
800
|
-
const rawJson = fs.readFileSync(tmpFile, 'utf8')
|
|
801
|
-
const parsed = JSON.parse(rawJson)
|
|
802
|
-
fs.writeFileSync(finalFile, JSON.stringify(parsed, null, 2))
|
|
803
|
-
fs.unlinkSync(tmpFile)
|
|
804
|
-
dataFile = finalFile
|
|
805
|
-
needsAugmentation = true
|
|
806
|
-
console.log(`Done: Fetched connector data for version ${newVersion}`)
|
|
807
902
|
|
|
808
903
|
// Fetch info.csv
|
|
809
904
|
try {
|
|
@@ -1445,11 +1540,8 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1445
1540
|
|
|
1446
1541
|
// Strip CGO/cloud-only connectors and metadata from old data
|
|
1447
1542
|
oldIndexForDiff = JSON.parse(JSON.stringify(oldIndex))
|
|
1448
|
-
const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
1449
|
-
'buffers', 'metrics', 'scanners', 'tracers', 'config']
|
|
1450
|
-
|
|
1451
1543
|
let totalStripped = 0
|
|
1452
|
-
for (const type of
|
|
1544
|
+
for (const type of COMPONENT_TYPE_KEYS_WITH_CONFIG) {
|
|
1453
1545
|
if (Array.isArray(oldIndexForDiff[type])) {
|
|
1454
1546
|
const originalCount = oldIndexForDiff[type].length
|
|
1455
1547
|
|
|
@@ -1475,7 +1567,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1475
1567
|
}
|
|
1476
1568
|
}
|
|
1477
1569
|
|
|
1478
|
-
printDeltaReport(oldIndexForDiff, newIndex)
|
|
1570
|
+
printDeltaReport(oldIndexForDiff, newIndex, { newVersion })
|
|
1479
1571
|
|
|
1480
1572
|
const { generateConnectorDiffJson } = require('./report-delta.js')
|
|
1481
1573
|
diffJson = generateConnectorDiffJson(
|
|
@@ -1636,8 +1728,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1636
1728
|
const dataObj = JSON.parse(rawData)
|
|
1637
1729
|
|
|
1638
1730
|
const validConnectors = []
|
|
1639
|
-
|
|
1640
|
-
types.forEach(type => {
|
|
1731
|
+
COMPONENT_TYPE_KEYS.forEach(type => {
|
|
1641
1732
|
if (Array.isArray(dataObj[type])) {
|
|
1642
1733
|
dataObj[type].forEach(connector => {
|
|
1643
1734
|
if (connector.name) {
|
|
@@ -1708,8 +1799,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1708
1799
|
// Fallback when binary analysis is unavailable:
|
|
1709
1800
|
// Check all connectors that have cloudSupported flag or assume all non-deprecated are cloud-supported
|
|
1710
1801
|
console.log(' ℹ️ Binary analysis unavailable - checking all non-deprecated connectors for cloud-docs')
|
|
1711
|
-
|
|
1712
|
-
types.forEach(type => {
|
|
1802
|
+
COMPONENT_TYPE_KEYS.forEach(type => {
|
|
1713
1803
|
if (Array.isArray(dataObj[type])) {
|
|
1714
1804
|
dataObj[type].forEach(connector => {
|
|
1715
1805
|
// Include if cloudSupported is explicitly true, or if it's null/undefined and not deprecated
|
|
@@ -1725,17 +1815,21 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1725
1815
|
}
|
|
1726
1816
|
|
|
1727
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.
|
|
1728
1822
|
const allMissing = validConnectors.filter(({ name, type, cloudOnly }) => {
|
|
1729
|
-
const
|
|
1823
|
+
const relPathIn = (root) => path.join(resolvePageTypeDir(root, `${type}s`), `${name}.adoc`)
|
|
1730
1824
|
|
|
1731
1825
|
// For cloud-only connectors, ONLY check the cloud-only directory
|
|
1732
1826
|
if (cloudOnly) {
|
|
1733
|
-
return !fs.existsSync(path.join(roots.cloudOnly,
|
|
1827
|
+
return !fs.existsSync(path.join(roots.cloudOnly, relPathIn(roots.cloudOnly)))
|
|
1734
1828
|
}
|
|
1735
1829
|
|
|
1736
1830
|
// For regular connectors, check pages and partials (not cloud-only)
|
|
1737
1831
|
const existsInAny = [roots.pages, roots.partials].some(root =>
|
|
1738
|
-
fs.existsSync(path.join(root,
|
|
1832
|
+
fs.existsSync(path.join(root, relPathIn(root)))
|
|
1739
1833
|
)
|
|
1740
1834
|
return !existsInAny
|
|
1741
1835
|
})
|
|
@@ -2068,6 +2162,8 @@ module.exports = {
|
|
|
2068
2162
|
capToTwoSentences,
|
|
2069
2163
|
augmentConnectorData,
|
|
2070
2164
|
buildCleanOssData,
|
|
2165
|
+
loadConnectorDataForVersion,
|
|
2166
|
+
snapshotReuseBlocker,
|
|
2071
2167
|
fieldAnchor,
|
|
2072
2168
|
buildFieldsTable,
|
|
2073
2169
|
buildChangedDefaultsTable
|