@redpanda-data/docs-extensions-and-macros 5.12.3 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli-utils/big-int-json.js +85 -0
- package/extensions/render-property-descriptions.js +339 -0
- package/extensions/resolve-xrefs-in-attachments.js +24 -1
- package/package.json +7 -2
- package/tools/property-extractor/compare-properties.js +16 -5
- package/tools/property-extractor/generate-handlebars-docs.js +6 -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/metadata-utils.js +29 -0
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +163 -71
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSON parse and stringify that do not destroy integers a JS number cannot hold.
|
|
5
|
+
*
|
|
6
|
+
* Redpanda's property data carries the uint64 and int64 limits as the maximum and
|
|
7
|
+
* minimum of numeric properties: 18446744073709551615, 9223372036854775807 and
|
|
8
|
+
* -9223372036854775808. The Python extractor writes them exactly, because Python
|
|
9
|
+
* integers are arbitrary precision. Every stage after that is JavaScript, where
|
|
10
|
+
* the largest exactly-representable integer is 2^53 - 1, so a plain
|
|
11
|
+
* `JSON.parse` silently rounds all three -- 18446744073709551615 becomes
|
|
12
|
+
* 18446744073709552000, which is not the uint64 limit and is not a value the
|
|
13
|
+
* server accepts.
|
|
14
|
+
*
|
|
15
|
+
* That had been happening on every published dataset since April: the reference
|
|
16
|
+
* pages, the tooltip attachment and the Connect catalog all showed rounded
|
|
17
|
+
* values, and none of them agreed with the source data.
|
|
18
|
+
*
|
|
19
|
+
* `parse` returns those integers as BigInt, so they print exactly and can be
|
|
20
|
+
* written back unchanged by `stringify`. BigInt rather than a string because it
|
|
21
|
+
* is unambiguous: a value that was a number in the source can be told apart from
|
|
22
|
+
* a string that was authored as one, which is what makes the round trip safe.
|
|
23
|
+
*
|
|
24
|
+
* Numbers inside the safe range are untouched and stay plain numbers.
|
|
25
|
+
*
|
|
26
|
+
* KNOWN LIMITATION: this is a regex over the raw text, not a real JSON scanner,
|
|
27
|
+
* so it does not track string/escape state. A value that is legitimate JSON
|
|
28
|
+
* text APPEARING INSIDE an escaped string -- for example a Bloblang example
|
|
29
|
+
* embedded as an example value, `"{\"delay_for_ns\":110839937000000000}"` --
|
|
30
|
+
* is indistinguishable from real JSON structure to this regex, which inserts a
|
|
31
|
+
* quoted sentinel at that position and corrupts the document. This is confirmed
|
|
32
|
+
* against the real, published Connect component attachment, not a theoretical
|
|
33
|
+
* risk: parsing it throws "Expected ',' or ']' after array element". Do not
|
|
34
|
+
* apply this module to Connect/Bloblang-shaped JSON, or to any JSON whose string
|
|
35
|
+
* values may themselves contain escaped JSON-looking text. It has been checked
|
|
36
|
+
* against every redpanda-properties-*.json and property-overrides.json this
|
|
37
|
+
* repo could find (~50 files, every published version) with no false match, and
|
|
38
|
+
* is applied only to property-shaped data for that reason -- not because the
|
|
39
|
+
* risk is theoretical elsewhere.
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
// An integer literal too long to be safe, in JSON value position: after a colon,
|
|
43
|
+
// an opening bracket or a comma, and followed by a delimiter. A digit run inside
|
|
44
|
+
// a string cannot match, because a string value starts with a quote.
|
|
45
|
+
const UNSAFE_INT_RX = /([:[,]\s*)(-?\d{16,})(?=\s*[,}\]])/g
|
|
46
|
+
// Printable ASCII: a control character is not legal inside a JSON string, so a
|
|
47
|
+
// sentinel containing one produces JSON that will not parse.
|
|
48
|
+
const SENTINEL = '@@bigint:'
|
|
49
|
+
const SENTINEL_RX = new RegExp(`"${SENTINEL}(-?\\d+)"`, 'g')
|
|
50
|
+
|
|
51
|
+
/** True if the literal needs BigInt to survive a round trip. */
|
|
52
|
+
function isUnsafeInteger (digits) {
|
|
53
|
+
return !Number.isSafeInteger(Number(digits))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse JSON, returning integers outside the safe range as BigInt.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} text - JSON text.
|
|
60
|
+
* @returns {*} The parsed value.
|
|
61
|
+
*/
|
|
62
|
+
function parse (text) {
|
|
63
|
+
const shielded = String(text).replace(UNSAFE_INT_RX, (match, lead, digits) =>
|
|
64
|
+
isUnsafeInteger(digits) ? `${lead}"${SENTINEL}${digits}"` : match
|
|
65
|
+
)
|
|
66
|
+
return JSON.parse(shielded, (key, value) =>
|
|
67
|
+
typeof value === 'string' && value.startsWith(SENTINEL) ? BigInt(value.slice(SENTINEL.length)) : value
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Stringify JSON, writing BigInt values back as bare integer literals.
|
|
73
|
+
*
|
|
74
|
+
* @param {*} value - Value to serialize.
|
|
75
|
+
* @param {number|string} [space] - Indentation, as JSON.stringify takes it.
|
|
76
|
+
* @returns {string} JSON text.
|
|
77
|
+
*/
|
|
78
|
+
function stringify (value, space) {
|
|
79
|
+
const json = JSON.stringify(value, (key, entry) =>
|
|
80
|
+
typeof entry === 'bigint' ? `${SENTINEL}${entry.toString()}` : entry, space
|
|
81
|
+
)
|
|
82
|
+
return json === undefined ? json : json.replace(SENTINEL_RX, '$1')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = { parse, stringify, isUnsafeInteger }
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders the `description` of every property in a
|
|
5
|
+
* redpanda-properties-<tag>.json attachment to HTML at build time, with
|
|
6
|
+
* Antora's own AsciiDoc loader and the site's own AsciiDoc config, and writes
|
|
7
|
+
* the result to `description_html`.
|
|
8
|
+
*
|
|
9
|
+
* Why this exists
|
|
10
|
+
* ---------------
|
|
11
|
+
* Property descriptions are authored in AsciiDoc and shipped as JSON, so
|
|
12
|
+
* something has to render them. The docs UI was doing it in the browser, with a
|
|
13
|
+
* hand-written parser covering seven constructs: backticks, prop:, config_ref:,
|
|
14
|
+
* glossterm:, link:, <<anchor>>, and xrefs pre-resolved by a regex extension.
|
|
15
|
+
*
|
|
16
|
+
* A whitelist of seven fails open. Everything else -- bold, italics, attribute
|
|
17
|
+
* references, character replacements, bare URLs, any macro this repo adds next
|
|
18
|
+
* -- reaches the reader as raw source, and nothing warns. In v26.2.1 that was 31
|
|
19
|
+
* of 689 properties, including seven that displayed a literal
|
|
20
|
+
* `include::reference:partial$internal-use-property.adoc[]` where an
|
|
21
|
+
* internal-use warning or a breaking-change notice should have been.
|
|
22
|
+
*
|
|
23
|
+
* Converting here removes the whole class of defect rather than the instances:
|
|
24
|
+
* Asciidoctor handles all of AsciiDoc, our own macros run as themselves (so
|
|
25
|
+
* glossterm's positional attributes cannot drift from macros/glossary.js), and
|
|
26
|
+
* a macro added next year works with no docs-ui change at all.
|
|
27
|
+
*
|
|
28
|
+
* Both fields ship. `description` stays because consumers read it -- including
|
|
29
|
+
* docs-ui versions older than the one that prefers `description_html`, which
|
|
30
|
+
* deploys separately. The output is minified to pay for the second field.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const loadAsciiDoc = require('@antora/asciidoc-loader')
|
|
34
|
+
const bigIntJson = require('../cli-utils/big-int-json')
|
|
35
|
+
const { buildPageIndex, propertyAnchor } = require('../macros/prop')
|
|
36
|
+
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
37
|
+
|
|
38
|
+
const PROPERTIES_JSON_RX = /^redpanda-properties-(v\d+\.\d+\.\d+(?:-[\w.]+)?)\.json$/
|
|
39
|
+
// xref:target[label] as authored in a description. The .adoc extension is
|
|
40
|
+
// optional: one real description writes xref:develop:transactions#tips[...].
|
|
41
|
+
const XREF_RX = /xref:([^[\]\s#]+?)(?:#([^[\]\s]*))?\[([^\]]*)\]/g
|
|
42
|
+
// <<anchor>> and <<anchor,display text>>, as authored in the description. The
|
|
43
|
+
// anchor must start with a letter or underscore: property anchors always do,
|
|
44
|
+
// and without that a C++ shift expression in a code span (`1<<20>>`) was read as
|
|
45
|
+
// a reference, rewritten, and then reported as a broken anchor.
|
|
46
|
+
const INTERNAL_REF_RX = /<<([A-Za-z_][\w.-]*)(?:,\s*([^<>]*?))?>>/g
|
|
47
|
+
|
|
48
|
+
module.exports.register = function () {
|
|
49
|
+
raiseListenerLimit(this)
|
|
50
|
+
const logger = this.getLogger('render-property-descriptions')
|
|
51
|
+
|
|
52
|
+
// The anchor index has to be built here, while page sources still exist:
|
|
53
|
+
// convert-documents.js deletes page.src.contents once conversion finishes
|
|
54
|
+
// (unless keepSource), and buildPageIndex reads that source to find which page
|
|
55
|
+
// documents which property. Building it at documentsConverted worked only
|
|
56
|
+
// because property-page-index happened to warm the same cache earlier, and
|
|
57
|
+
// without that every <<anchor>> silently degraded to plain text.
|
|
58
|
+
const anchorIndexes = new Map()
|
|
59
|
+
this.once('contentClassified', ({ contentCatalog }) => {
|
|
60
|
+
for (const attachment of propertyAttachments(contentCatalog)) {
|
|
61
|
+
const properties = readProperties(attachment, logger)
|
|
62
|
+
if (!properties) continue
|
|
63
|
+
const key = `${attachment.src.component}@${attachment.src.version || ''}`
|
|
64
|
+
if (anchorIndexes.has(key)) continue
|
|
65
|
+
anchorIndexes.set(key, anchorIndex(contentCatalog, attachment, properties, logger))
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Conversion itself waits for documentsConverted: page aliases and the rest of
|
|
70
|
+
// what page conversion relies on are only fully registered by then. Hooking
|
|
71
|
+
// earlier left an xref to an aliased page rendering as
|
|
72
|
+
// class="xref unresolved" while the same xref resolved fine on a real page.
|
|
73
|
+
// Attachments are still published after this event.
|
|
74
|
+
this.once('documentsConverted', ({ contentCatalog, siteAsciiDocConfig }) => {
|
|
75
|
+
const attachments = propertyAttachments(contentCatalog)
|
|
76
|
+
if (!attachments.length) return
|
|
77
|
+
|
|
78
|
+
for (const attachment of attachments) {
|
|
79
|
+
const where = `${attachment.src.component}@${attachment.src.version || 'unversioned'}`
|
|
80
|
+
const data = readDataset(attachment, logger)
|
|
81
|
+
if (!data) continue
|
|
82
|
+
const properties = data.properties
|
|
83
|
+
|
|
84
|
+
// Convert "as" a real page rather than a fabricated one. A hand-built
|
|
85
|
+
// file.pub is missing fields Antora's resolver needs, and resolving an
|
|
86
|
+
// xref to another module then throws instead of resolving.
|
|
87
|
+
const hostPage = findHostPage(contentCatalog, attachment)
|
|
88
|
+
if (!hostPage) {
|
|
89
|
+
logger.debug(`${where}: no reference page to convert descriptions as, skipped`)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
const anchors = anchorIndexes.get(`${attachment.src.component}@${attachment.src.version || ''}`) || new Map()
|
|
93
|
+
|
|
94
|
+
// Antora converts each page with its component version's own config
|
|
95
|
+
// (convert-documents.js builds one per component version and prefers it
|
|
96
|
+
// over the site config), so attributes declared in antora.yml -- env-cloud
|
|
97
|
+
// among them, which five descriptions branch on -- only exist there.
|
|
98
|
+
const componentVersion = contentCatalog.getComponentVersion
|
|
99
|
+
? contentCatalog.getComponentVersion(attachment.src.component, attachment.src.version)
|
|
100
|
+
: undefined
|
|
101
|
+
const asciidocConfig = (componentVersion && componentVersion.asciidoc) || siteAsciiDocConfig
|
|
102
|
+
|
|
103
|
+
let rendered = 0
|
|
104
|
+
let failed = 0
|
|
105
|
+
const brokenAnchors = new Set()
|
|
106
|
+
const unresolved = new Set()
|
|
107
|
+
const xrefCache = new Map()
|
|
108
|
+
for (const [name, entry] of Object.entries(properties)) {
|
|
109
|
+
const description = entry && entry.description
|
|
110
|
+
if (typeof description !== 'string' || !description.trim()) continue
|
|
111
|
+
try {
|
|
112
|
+
const source = resolveInternalRefs(description, anchors, (anchor) => brokenAnchors.add(anchor))
|
|
113
|
+
const html = render(source, hostPage, contentCatalog, asciidocConfig)
|
|
114
|
+
if (html == null) continue
|
|
115
|
+
entry.description_html = html
|
|
116
|
+
// The attachment is a published download, and `xref:...[]` means
|
|
117
|
+
// nothing to someone reading the JSON on its own -- resolving it is
|
|
118
|
+
// why resolve-xrefs-in-attachments existed for these files. This
|
|
119
|
+
// extension owns that job for property datasets now, so it has to keep
|
|
120
|
+
// doing it, using Antora's resolver rather than rewriting paths by
|
|
121
|
+
// hand. Descriptions stay AsciiDoc apart from the resolved anchors,
|
|
122
|
+
// which is the shape consumers already read.
|
|
123
|
+
entry.description = resolveXrefs(
|
|
124
|
+
description, hostPage, contentCatalog, asciidocConfig, xrefCache, (macro) => unresolved.add(macro)
|
|
125
|
+
)
|
|
126
|
+
rendered++
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// One description that will not convert must not cost the other 680.
|
|
129
|
+
failed++
|
|
130
|
+
logger.warn(`${where} ${name}: description could not be converted, so its tooltip falls back to the raw source (${error.message})`)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (rendered) attachment.contents = Buffer.from(bigIntJson.stringify(data))
|
|
135
|
+
logger.info(`${where}: rendered ${rendered} property descriptions to HTML${failed ? `, ${failed} failed` : ''}`)
|
|
136
|
+
if (unresolved.size) {
|
|
137
|
+
logger.warn(
|
|
138
|
+
`${where}: ${unresolved.size} xref target(s) in property descriptions could not be resolved, so they stay as raw macros in the published attachment: ` +
|
|
139
|
+
`${[...unresolved].sort().join(', ')}.`
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
if (brokenAnchors.size) {
|
|
143
|
+
logger.warn(
|
|
144
|
+
`${where}: ${brokenAnchors.size} <<anchor>> reference(s) in property descriptions name no documented property, so they render as plain text: ` +
|
|
145
|
+
`${[...brokenAnchors].sort().join(', ')}. Property anchors replace dots with hyphens, so redpanda.storage.mode is <<redpanda-storage-mode>>. ` +
|
|
146
|
+
'Fix them in the description or in docs-data/property-overrides.json.'
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function basename (file) {
|
|
154
|
+
return file.src.relative.split('/').pop()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function propertyAttachments (contentCatalog) {
|
|
158
|
+
return (contentCatalog.findBy({ family: 'attachment' }) || []).filter(
|
|
159
|
+
(file) => file.src.module === 'reference' && PROPERTIES_JSON_RX.test(basename(file))
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Parse one dataset, or report why it is unusable and return undefined. Left
|
|
165
|
+
* as-is on failure: the prop macro reports an unusable dataset with the detail,
|
|
166
|
+
* and failing the whole build over one bad attachment helps nobody.
|
|
167
|
+
*/
|
|
168
|
+
function readDataset (attachment, logger) {
|
|
169
|
+
const where = `${attachment.src.component}@${attachment.src.version || 'unversioned'}`
|
|
170
|
+
let data
|
|
171
|
+
try {
|
|
172
|
+
data = bigIntJson.parse(attachment.contents.toString())
|
|
173
|
+
} catch (error) {
|
|
174
|
+
logger.warn(`${where}: ${basename(attachment)} is not valid JSON, descriptions not rendered (${error.message})`)
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
const properties = data && data.properties
|
|
178
|
+
if (!properties || typeof properties !== 'object' || Array.isArray(properties)) {
|
|
179
|
+
logger.warn(`${where}: ${basename(attachment)} has no usable 'properties' object, descriptions not rendered`)
|
|
180
|
+
return undefined
|
|
181
|
+
}
|
|
182
|
+
return data
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readProperties (attachment, logger) {
|
|
186
|
+
const data = readDataset(attachment, logger)
|
|
187
|
+
return data ? data.properties : undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A real page in the attachment's own component and reference module, preferring
|
|
192
|
+
* one under properties/ so includes and relative xrefs resolve from where the
|
|
193
|
+
* property docs actually live.
|
|
194
|
+
*/
|
|
195
|
+
function findHostPage (contentCatalog, attachment) {
|
|
196
|
+
const pages = contentCatalog.findBy({
|
|
197
|
+
component: attachment.src.component,
|
|
198
|
+
version: attachment.src.version,
|
|
199
|
+
family: 'page',
|
|
200
|
+
}) || []
|
|
201
|
+
return (
|
|
202
|
+
pages.find((f) => f.src.module === 'reference' && /^properties\//.test(f.src.relative)) ||
|
|
203
|
+
pages.find((f) => f.src.module === 'reference') ||
|
|
204
|
+
undefined
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* anchor -> xref target, for the <<anchor>> references that point at another
|
|
210
|
+
* documented property. Built from the same page index the prop macro links
|
|
211
|
+
* with, so a description and a prop: call agree about which page documents what.
|
|
212
|
+
*/
|
|
213
|
+
function anchorIndex (contentCatalog, attachment, properties, logger) {
|
|
214
|
+
const where = `${attachment.src.component}@${attachment.src.version || 'unversioned'}`
|
|
215
|
+
const index = new Map()
|
|
216
|
+
let pages
|
|
217
|
+
try {
|
|
218
|
+
pages = buildPageIndex(contentCatalog, attachment.src.component, properties, attachment.src.version)
|
|
219
|
+
} catch (error) {
|
|
220
|
+
// Returning an empty index silently made a thrown index and an empty one
|
|
221
|
+
// indistinguishable, and every <<anchor>> then degraded to plain text with
|
|
222
|
+
// no explanation.
|
|
223
|
+
logger.warn(`${where}: could not index which page documents each property, so <<anchor>> references in descriptions render as plain text (${error.message})`)
|
|
224
|
+
return index
|
|
225
|
+
}
|
|
226
|
+
if (!pages.size) {
|
|
227
|
+
logger.warn(`${where}: no reference page was found to document any property, so <<anchor>> references in descriptions render as plain text.`)
|
|
228
|
+
return index
|
|
229
|
+
}
|
|
230
|
+
for (const [name, entry] of pages) {
|
|
231
|
+
if (!entry || !entry.page) continue
|
|
232
|
+
index.set(propertyAnchor(name), { page: entry.page, name })
|
|
233
|
+
}
|
|
234
|
+
return index
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Rewrite <<anchor>> to an xref before conversion.
|
|
239
|
+
*
|
|
240
|
+
* Asciidoctor renders <<anchor>> as a same-page fragment link, which is right
|
|
241
|
+
* on the reference page and wrong in a tooltip: a tooltip is shown on an
|
|
242
|
+
* arbitrary page, where that fragment does not exist. Pointing it at the page
|
|
243
|
+
* that documents the property keeps the link working wherever the tooltip
|
|
244
|
+
* appears, and lets Antora resolve the URL rather than us building one.
|
|
245
|
+
*/
|
|
246
|
+
function resolveInternalRefs (description, anchors, report) {
|
|
247
|
+
return description.replace(INTERNAL_REF_RX, (match, anchor, display) => {
|
|
248
|
+
const text = (display || '').trim()
|
|
249
|
+
const target = anchors.get(anchor)
|
|
250
|
+
if (target) return `xref:reference:${target.page}.adoc#${anchor}[${attrlistValue(text || `\`${target.name}\``)}]`
|
|
251
|
+
// The anchor names no documented property, so there is nothing to link to.
|
|
252
|
+
// Asciidoctor would render a same-page fragment, and a tooltip is shown on
|
|
253
|
+
// arbitrary pages, so that link would go nowhere and still invite a click --
|
|
254
|
+
// the same reasoning the prop macro applies to a page it cannot verify.
|
|
255
|
+
// Render the text and report the anchor so it gets fixed at the source.
|
|
256
|
+
report(anchor)
|
|
257
|
+
return text || `\`${anchor}\``
|
|
258
|
+
})
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Display text as a single positional attribute. An unescaped `]` ends the
|
|
263
|
+
* attribute list, so `<<prop,the flag [beta] option>>` published a truncated
|
|
264
|
+
* link with ` option]` outside it; and a bare `=` makes Asciidoctor read the
|
|
265
|
+
* value as a named attribute, which dropped the label entirely and applied part
|
|
266
|
+
* of it as a CSS role. Escaping the bracket and quoting when there is an equals
|
|
267
|
+
* sign round-trips every combination of brackets, equals, quotes and commas.
|
|
268
|
+
*/
|
|
269
|
+
function attrlistValue (text) {
|
|
270
|
+
const escaped = text.replace(/]/g, '\\]')
|
|
271
|
+
return /=/.test(escaped) ? `"${escaped.replace(/"/g, '\\"')}"` : escaped
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Rewrite xref macros in a description to resolved links, so the published
|
|
276
|
+
* attachment is useful on its own.
|
|
277
|
+
*
|
|
278
|
+
* The conversion is done by Antora, one macro at a time, rather than by building
|
|
279
|
+
* a URL here: that way the page resolution, the fragment, and any id rules from
|
|
280
|
+
* the site's AsciiDoc config are whatever the site itself would produce, and
|
|
281
|
+
* cannot drift from the pages the links point at. The previous implementation
|
|
282
|
+
* rewrote paths with regexes and converted underscores in the fragment to
|
|
283
|
+
* hyphens -- but property page anchors keep their underscores
|
|
284
|
+
* (`id="log_cleanup_policy"`), so all 25 of its fragment links in the published
|
|
285
|
+
* v26.2.1 data were dead.
|
|
286
|
+
*
|
|
287
|
+
* Conversions are cached per macro, because the same xref appears in many
|
|
288
|
+
* descriptions.
|
|
289
|
+
*/
|
|
290
|
+
function resolveXrefs (description, hostPage, contentCatalog, asciidocConfig, cache, report) {
|
|
291
|
+
return description.replace(XREF_RX, (match) => {
|
|
292
|
+
if (!cache.has(match)) cache.set(match, convertXref(match, hostPage, contentCatalog, asciidocConfig))
|
|
293
|
+
const anchor = cache.get(match)
|
|
294
|
+
if (!anchor) {
|
|
295
|
+
report(match)
|
|
296
|
+
// Leave the macro alone rather than emitting a link to nowhere: a reader
|
|
297
|
+
// of the raw JSON can still see what was meant.
|
|
298
|
+
return match
|
|
299
|
+
}
|
|
300
|
+
return anchor
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** One xref macro, converted by Antora. Returns undefined if it does not resolve. */
|
|
305
|
+
function convertXref (macro, hostPage, contentCatalog, asciidocConfig) {
|
|
306
|
+
let html
|
|
307
|
+
try {
|
|
308
|
+
html = render(macro, hostPage, contentCatalog, asciidocConfig)
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return undefined
|
|
311
|
+
}
|
|
312
|
+
if (!html) return undefined
|
|
313
|
+
const anchor = /<a\b[^>]*>[\s\S]*?<\/a>/.exec(html)
|
|
314
|
+
// Antora marks a target it could not resolve, and that is not a link worth
|
|
315
|
+
// publishing into a file people read on its own.
|
|
316
|
+
if (!anchor || /class="[^"]*\bunresolved\b/.test(anchor[0])) return undefined
|
|
317
|
+
return anchor[0]
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Convert one description with the site's converter, as the host page. */
|
|
321
|
+
function render (source, hostPage, contentCatalog, siteAsciiDocConfig) {
|
|
322
|
+
const file = {
|
|
323
|
+
contents: Buffer.from(source),
|
|
324
|
+
mediaType: 'text/asciidoc',
|
|
325
|
+
src: { ...hostPage.src },
|
|
326
|
+
pub: { ...hostPage.pub },
|
|
327
|
+
}
|
|
328
|
+
// relativizeResourceRefs: false gives site-root-relative URLs. The default
|
|
329
|
+
// computes them relative to the converting file, which is wrong for a tooltip
|
|
330
|
+
// shown on some other page.
|
|
331
|
+
const doc = loadAsciiDoc(file, contentCatalog, { ...siteAsciiDocConfig, relativizeResourceRefs: false })
|
|
332
|
+
const blocks = doc.getBlocks()
|
|
333
|
+
if (!blocks.length) return doc.convert() || null
|
|
334
|
+
// A single paragraph gives inline HTML with no <p> wrapper, which is what a
|
|
335
|
+
// tooltip wants. Anything richer -- an included admonition, a list, several
|
|
336
|
+
// paragraphs -- keeps its block markup, and the UI styles it.
|
|
337
|
+
if (blocks.length === 1 && blocks[0].getContext() === 'paragraph') return blocks[0].getContent()
|
|
338
|
+
return doc.convert()
|
|
339
|
+
}
|
|
@@ -20,6 +20,8 @@ const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
|
20
20
|
* - xref:path.adoc[] (empty display uses page title or path)
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
+
const PROPERTIES_JSON_RX = /^redpanda-properties-(v\d+\.\d+\.\d+(?:-[\w.]+)?)\.json$/
|
|
24
|
+
|
|
23
25
|
module.exports.register = function () {
|
|
24
26
|
raiseListenerLimit(this)
|
|
25
27
|
const logger = this.getLogger('resolve-xrefs-in-attachments')
|
|
@@ -29,7 +31,15 @@ module.exports.register = function () {
|
|
|
29
31
|
const allAttachments = contentCatalog.findBy({ family: 'attachment' })
|
|
30
32
|
const jsonAttachments = allAttachments.filter((att) => {
|
|
31
33
|
const path = att.src?.path || att.out?.path || ''
|
|
32
|
-
|
|
34
|
+
if (!path.endsWith('.json')) return false
|
|
35
|
+
// Property datasets are handled by render-property-descriptions, which
|
|
36
|
+
// converts their descriptions with Asciidoctor instead of rewriting xrefs
|
|
37
|
+
// by hand. Both running double-processed 53 descriptions in v26.2.1: the
|
|
38
|
+
// anchor inserted here was escaped to <a href= by the converter. This
|
|
39
|
+
// extension still serves every other JSON attachment, including the
|
|
40
|
+
// Connect component catalog, which carries far more xrefs than the
|
|
41
|
+
// property files do.
|
|
42
|
+
return !PROPERTIES_JSON_RX.test(path.split('/').pop())
|
|
33
43
|
})
|
|
34
44
|
|
|
35
45
|
if (!jsonAttachments.length) {
|
|
@@ -72,6 +82,19 @@ function processJsonAttachment (attachment, contentCatalog, logger) {
|
|
|
72
82
|
|
|
73
83
|
let data
|
|
74
84
|
try {
|
|
85
|
+
// Deliberately plain JSON.parse, not bigIntJson: this function rewrites
|
|
86
|
+
// whatever JSON attachment it is handed, including the Connect component
|
|
87
|
+
// catalog, which contains example config text as an ESCAPED JSON-shaped
|
|
88
|
+
// string -- "{\"delay_for_ns\":110839937000000000}" -- indistinguishable
|
|
89
|
+
// by regex from real JSON value position. bigIntJson's value-position regex
|
|
90
|
+
// matched inside that escaped string and inserted stray quotes, which is not
|
|
91
|
+
// a rounding error, it is invalid JSON: confirmed to throw
|
|
92
|
+
// "Expected ',' or ']' after array element" on the real, currently-published
|
|
93
|
+
// connect-4.103.1.json attachment. A plain round trip still silently rounds
|
|
94
|
+
// large integers (the pre-existing, understood defect this file has always
|
|
95
|
+
// had), which is the safer failure mode until a real JSON scanner -- one
|
|
96
|
+
// that tracks string/escape state rather than pattern-matching text --
|
|
97
|
+
// replaces the regex here.
|
|
75
98
|
data = JSON.parse(contentStr)
|
|
76
99
|
} catch (err) {
|
|
77
100
|
logger.debug(`Skipping invalid JSON: ${attachment.src?.path}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
"./extensions/convert-llms-to-txt": "./extensions/convert-llms-to-txt.js",
|
|
78
78
|
"./extensions/convert-sitemap-to-markdown": "./extensions/convert-sitemap-to-markdown.js",
|
|
79
79
|
"./extensions/modify-redirects": "./extensions/produce-redirects.js",
|
|
80
|
+
"./extensions/produce-redirects": "./extensions/produce-redirects.js",
|
|
80
81
|
"./extensions/algolia-indexer/index": "./extensions/algolia-indexer/index.js",
|
|
81
82
|
"./extensions/aggregate-terms": "./extensions/aggregate-terms.js",
|
|
82
83
|
"./extensions/resolve-xrefs-in-attachments": "./extensions/resolve-xrefs-in-attachments.js",
|
|
@@ -91,7 +92,8 @@
|
|
|
91
92
|
"./macros/badge": "./macros/badge.js",
|
|
92
93
|
"./macros/enterprise": "./macros/enterprise.js",
|
|
93
94
|
"./macros/prop": "./macros/prop.js",
|
|
94
|
-
"./extensions/property-page-index": "./extensions/property-page-index.js"
|
|
95
|
+
"./extensions/property-page-index": "./extensions/property-page-index.js",
|
|
96
|
+
"./extensions/render-property-descriptions": "./extensions/render-property-descriptions.js"
|
|
95
97
|
},
|
|
96
98
|
"files": [
|
|
97
99
|
"extensions",
|
|
@@ -156,5 +158,8 @@
|
|
|
156
158
|
"@web/dev-server": "^0.2.5",
|
|
157
159
|
"jest": "^29.7.0",
|
|
158
160
|
"jest-junit": "^16.0.0"
|
|
161
|
+
},
|
|
162
|
+
"peerDependencies": {
|
|
163
|
+
"@antora/asciidoc-loader": "^3.1.0"
|
|
159
164
|
}
|
|
160
165
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const fs = require('fs');
|
|
18
|
+
const bigIntJson = require('../../cli-utils/big-int-json');
|
|
18
19
|
const path = require('path');
|
|
19
20
|
const semver = require('semver');
|
|
20
21
|
|
|
@@ -109,7 +110,10 @@ function formatValue(value) {
|
|
|
109
110
|
return `[${value.length} items]`;
|
|
110
111
|
}
|
|
111
112
|
if (typeof value === 'object') {
|
|
112
|
-
|
|
113
|
+
// bigIntJson, not JSON.stringify: a default/minimum/maximum value that's an
|
|
114
|
+
// object or array can itself carry an unsafe-integer BigInt from
|
|
115
|
+
// bigIntJson.parse(), which plain JSON.stringify throws on.
|
|
116
|
+
return bigIntJson.stringify(value);
|
|
113
117
|
}
|
|
114
118
|
if (typeof value === 'string') {
|
|
115
119
|
return `"${value}"`;
|
|
@@ -393,7 +397,11 @@ function generateJsonReport(report, oldVersion, newVersion, outputPath) {
|
|
|
393
397
|
details: report
|
|
394
398
|
};
|
|
395
399
|
|
|
396
|
-
|
|
400
|
+
// bigIntJson, not JSON.stringify: `report.details` carries oldDefault/newDefault
|
|
401
|
+
// values straight from bigIntJson.parse(), so an unsafe-integer default is a
|
|
402
|
+
// real BigInt here. Plain JSON.stringify throws on that ("Do not know how to
|
|
403
|
+
// serialize a BigInt") instead of writing the report.
|
|
404
|
+
fs.writeFileSync(outputPath, bigIntJson.stringify(jsonReport, 2));
|
|
397
405
|
console.log(`📄 Detailed JSON report saved to: ${outputPath}`);
|
|
398
406
|
}
|
|
399
407
|
|
|
@@ -424,8 +432,8 @@ function comparePropertyFiles(oldFilePath, newFilePath, oldVersion, newVersion,
|
|
|
424
432
|
console.log(` Old: ${oldFilePath}`);
|
|
425
433
|
console.log(` New: ${newFilePath}`);
|
|
426
434
|
|
|
427
|
-
const oldData =
|
|
428
|
-
const newData =
|
|
435
|
+
const oldData = bigIntJson.parse(fs.readFileSync(oldFilePath, 'utf8'));
|
|
436
|
+
const newData = bigIntJson.parse(fs.readFileSync(newFilePath, 'utf8'));
|
|
429
437
|
|
|
430
438
|
const report = compareProperties(oldData, newData, oldVersion, newVersion);
|
|
431
439
|
|
|
@@ -447,7 +455,10 @@ function comparePropertyFiles(oldFilePath, newFilePath, oldVersion, newVersion,
|
|
|
447
455
|
});
|
|
448
456
|
|
|
449
457
|
newData.properties = newProps;
|
|
450
|
-
|
|
458
|
+
// bigIntJson: this rewrites the dataset in place, so a plain stringify
|
|
459
|
+
// corrupts the uint64/int64 limits on disk -- ahead of, and defeating, the
|
|
460
|
+
// lossless read in generate-handlebars-docs.js.
|
|
461
|
+
fs.writeFileSync(newFilePath, bigIntJson.stringify(newData, 2), 'utf8');
|
|
451
462
|
console.log(`\nMerged ${report.removedDeprecatedProperties.length} removed deprecated properties back into ${newVersion} JSON`);
|
|
452
463
|
}
|
|
453
464
|
|
|
@@ -2,6 +2,7 @@ const AUTOGEN_NOTICE = '// This content is autogenerated. Do not edit manually.
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const bigIntJson = require('../../cli-utils/big-int-json');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const handlebars = require('handlebars');
|
|
7
8
|
const helpers = require('./helpers');
|
|
@@ -425,7 +426,11 @@ function convertConfigRefsToProp(text) {
|
|
|
425
426
|
* Main generator
|
|
426
427
|
*/
|
|
427
428
|
function generateAllDocs(inputFile, outputDir) {
|
|
428
|
-
|
|
429
|
+
// bigIntJson, not JSON.parse: the uint64 and int64 limits carried as property
|
|
430
|
+
// maxima are beyond what a JS number holds, so a plain parse rounded them and
|
|
431
|
+
// the generated partials -- and therefore the published reference pages --
|
|
432
|
+
// showed 18446744073709552000 where the data says 18446744073709551615.
|
|
433
|
+
const data = bigIntJson.parse(fs.readFileSync(inputFile, 'utf8'));
|
|
429
434
|
// Support both 'properties' (from property_extractor.py) and 'topic_properties' (from topic_property_extractor.py)
|
|
430
435
|
const properties = data.properties || data.topic_properties || {};
|
|
431
436
|
|
|
@@ -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`);
|
|
@@ -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
|
|
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)
|
|
@@ -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
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
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
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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,47 @@ 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
|
+
// Deliberately plain JSON.parse/stringify, not bigIntJson: this is connect
|
|
896
|
+
// data, which carries example config text as escaped JSON-shaped strings
|
|
897
|
+
// (e.g. bloblang examples), indistinguishable by regex from real JSON
|
|
898
|
+
// value position. bigIntJson's value-position regex matches inside such a
|
|
899
|
+
// string and inserts stray quotes -- confirmed to throw on the real,
|
|
900
|
+
// currently-published connect attachment, not merely round a number. A
|
|
901
|
+
// plain round trip still silently rounds int64 defaults past MaxInt64 (the
|
|
902
|
+
// pre-existing defect this file has always had), which is the safer
|
|
903
|
+
// failure mode until a real JSON scanner replaces the regex.
|
|
904
|
+
const parsed = JSON.parse(rawJson)
|
|
905
|
+
fs.writeFileSync(finalFile, JSON.stringify(parsed, null, 2))
|
|
906
|
+
fs.unlinkSync(tmpFile)
|
|
907
|
+
dataFile = finalFile
|
|
908
|
+
needsAugmentation = true
|
|
909
|
+
console.log(`Done: Fetched connector data for version ${newVersion}`)
|
|
803
910
|
}
|
|
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
911
|
|
|
821
912
|
// Fetch info.csv
|
|
822
913
|
try {
|
|
@@ -1458,11 +1549,8 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1458
1549
|
|
|
1459
1550
|
// Strip CGO/cloud-only connectors and metadata from old data
|
|
1460
1551
|
oldIndexForDiff = JSON.parse(JSON.stringify(oldIndex))
|
|
1461
|
-
const connectorTypes = ['inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
1462
|
-
'buffers', 'metrics', 'scanners', 'tracers', 'config']
|
|
1463
|
-
|
|
1464
1552
|
let totalStripped = 0
|
|
1465
|
-
for (const type of
|
|
1553
|
+
for (const type of COMPONENT_TYPE_KEYS_WITH_CONFIG) {
|
|
1466
1554
|
if (Array.isArray(oldIndexForDiff[type])) {
|
|
1467
1555
|
const originalCount = oldIndexForDiff[type].length
|
|
1468
1556
|
|
|
@@ -1649,8 +1737,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1649
1737
|
const dataObj = JSON.parse(rawData)
|
|
1650
1738
|
|
|
1651
1739
|
const validConnectors = []
|
|
1652
|
-
|
|
1653
|
-
types.forEach(type => {
|
|
1740
|
+
COMPONENT_TYPE_KEYS.forEach(type => {
|
|
1654
1741
|
if (Array.isArray(dataObj[type])) {
|
|
1655
1742
|
dataObj[type].forEach(connector => {
|
|
1656
1743
|
if (connector.name) {
|
|
@@ -1721,8 +1808,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1721
1808
|
// Fallback when binary analysis is unavailable:
|
|
1722
1809
|
// Check all connectors that have cloudSupported flag or assume all non-deprecated are cloud-supported
|
|
1723
1810
|
console.log(' ℹ️ Binary analysis unavailable - checking all non-deprecated connectors for cloud-docs')
|
|
1724
|
-
|
|
1725
|
-
types.forEach(type => {
|
|
1811
|
+
COMPONENT_TYPE_KEYS.forEach(type => {
|
|
1726
1812
|
if (Array.isArray(dataObj[type])) {
|
|
1727
1813
|
dataObj[type].forEach(connector => {
|
|
1728
1814
|
// Include if cloudSupported is explicitly true, or if it's null/undefined and not deprecated
|
|
@@ -1738,17 +1824,21 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1738
1824
|
}
|
|
1739
1825
|
|
|
1740
1826
|
// Check for missing connector documentation in rp-connect-docs
|
|
1827
|
+
// `type` came from a data key, and the rate-limit family's data key
|
|
1828
|
+
// ('rate-limits') is not its directory name ('rate_limits'). Resolve the
|
|
1829
|
+
// directory against disk per root, or every existing rate-limit page is
|
|
1830
|
+
// reported missing and drafted again beside itself.
|
|
1741
1831
|
const allMissing = validConnectors.filter(({ name, type, cloudOnly }) => {
|
|
1742
|
-
const
|
|
1832
|
+
const relPathIn = (root) => path.join(resolvePageTypeDir(root, `${type}s`), `${name}.adoc`)
|
|
1743
1833
|
|
|
1744
1834
|
// For cloud-only connectors, ONLY check the cloud-only directory
|
|
1745
1835
|
if (cloudOnly) {
|
|
1746
|
-
return !fs.existsSync(path.join(roots.cloudOnly,
|
|
1836
|
+
return !fs.existsSync(path.join(roots.cloudOnly, relPathIn(roots.cloudOnly)))
|
|
1747
1837
|
}
|
|
1748
1838
|
|
|
1749
1839
|
// For regular connectors, check pages and partials (not cloud-only)
|
|
1750
1840
|
const existsInAny = [roots.pages, roots.partials].some(root =>
|
|
1751
|
-
fs.existsSync(path.join(root,
|
|
1841
|
+
fs.existsSync(path.join(root, relPathIn(root)))
|
|
1752
1842
|
)
|
|
1753
1843
|
return !existsInAny
|
|
1754
1844
|
})
|
|
@@ -2081,6 +2171,8 @@ module.exports = {
|
|
|
2081
2171
|
capToTwoSentences,
|
|
2082
2172
|
augmentConnectorData,
|
|
2083
2173
|
buildCleanOssData,
|
|
2174
|
+
loadConnectorDataForVersion,
|
|
2175
|
+
snapshotReuseBlocker,
|
|
2084
2176
|
fieldAnchor,
|
|
2085
2177
|
buildFieldsTable,
|
|
2086
2178
|
buildChangedDefaultsTable
|