@redpanda-data/docs-extensions-and-macros 5.53.0 → 5.55.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/bin/doc-tools-mcp.js +4 -5
- package/bin/doc-tools.js +166 -2
- package/bin/mcp-tools/generated-docs-review.js +26 -9
- package/bin/mcp-tools/overrides-audit.js +1 -1
- package/bin/mcp-tools/rpcn-docs.js +4 -14
- package/cli-utils/format-helm-spec.js +4 -3
- package/cli-utils/helm-commented-values.js +88 -18
- package/cli-utils/self-managed-docs-branch.js +36 -30
- package/extension-utils/html-utils.js +27 -0
- package/extensions/REFERENCE.adoc +64 -12
- package/extensions/add-git-dates.js +13 -1
- package/extensions/aggregate-terms.js +34 -20
- package/extensions/generate-rp-connect-info.js +79 -24
- package/extensions/modify-connect-tag-playbook.js +498 -75
- package/extensions/resolve-xrefs-in-attachments.js +1 -12
- package/extensions/url-to-xref.js +3 -0
- package/extensions/util/connect-catalog.js +10 -5
- package/extensions/util/connect-docs-asset.js +188 -0
- package/extensions/util/connect-github.js +16 -0
- package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
- package/extensions/version-fetcher/list-connect-releases.js +10 -0
- package/extensions/version-fetcher/release-lines.js +64 -0
- package/extensions/version-fetcher/set-latest-version.js +73 -3
- package/macros/README.adoc +1 -0
- package/macros/REFERENCE.adoc +99 -0
- package/macros/badge.js +5 -8
- package/macros/enterprise.js +2 -5
- package/macros/glossary.js +4 -14
- package/macros/iceberg-explorer.js +242 -0
- package/macros/prop.js +3 -13
- package/mcp/USER_GUIDE.adoc +25 -135
- package/package.json +4 -2
- package/tools/connect-docs/check-build-log.js +311 -0
- package/tools/connect-docs/check-rendered-html.js +469 -0
- package/tools/connect-docs/ci-playbook.yml +77 -0
- package/tools/connect-docs/diff-generated.js +382 -0
- package/tools/connect-docs/pr-comment.js +104 -0
- package/tools/doc-impact-recording/index.js +190 -0
- package/tools/overrides-audit/README.adoc +1 -2
- package/tools/overrides-audit/adapters/properties.js +1 -1
- package/tools/overrides-audit/index.js +3 -4
- package/tools/redpanda-connect/AUTOMATION.md +3 -36
- package/tools/redpanda-connect/README.adoc +5 -33
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +3 -3
- package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
- package/tools/redpanda-connect/report-delta.js +6 -3
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +38 -16
- package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
- package/tools/rpk-docs/templates/command.hbs +1 -1
- package/tools/overrides-audit/adapters/connect.js +0 -144
|
@@ -1,38 +1,90 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Sources the Redpanda Connect reference
|
|
3
|
+
// Sources the Redpanda Connect generated reference docs (field reference,
|
|
4
|
+
// examples, metadata, descriptions, config snippets, Bloblang reference, and
|
|
5
|
+
// platforms/catalog.json) for the `connect` component.
|
|
4
6
|
//
|
|
5
|
-
//
|
|
7
|
+
// The default: the playbook has no connect content source. Every connect
|
|
8
|
+
// release publishes the generated tree as a redpanda-connect-docs.tar.gz
|
|
9
|
+
// release asset (util/connect-docs-asset). In contentClassified, the extension
|
|
10
|
+
// resolves the release to use, downloads that asset, and adds its partials and
|
|
11
|
+
// examples to the `connect` component that rp-connect-docs provides:
|
|
12
|
+
//
|
|
13
|
+
// - The latest stable vX.Y.Z release by default, so the docs never show fields
|
|
14
|
+
// from unreleased code on connect's main branch.
|
|
15
|
+
// - The `tag` extension config key picks a specific release instead.
|
|
16
|
+
// - The REDPANDA_CONNECT_DOCS_DIR environment variable points at a local copy
|
|
17
|
+
// of the tree (a directory that contains modules/, such as a connect
|
|
18
|
+
// checkout's docs/ after running its generator, or a local copy of the
|
|
19
|
+
// tarball) to preview unreleased connect changes. It wins over everything,
|
|
20
|
+
// and its files replace any copy another source provides.
|
|
21
|
+
//
|
|
22
|
+
// Connect publishes a release first and attaches the asset later, in a
|
|
23
|
+
// separate job that can also fail. So when the latest release has no asset
|
|
24
|
+
// (404), the extension falls back to the newest older stable release that has
|
|
25
|
+
// one, and logs a warning that names both. It checks the asset lists from the
|
|
26
|
+
// GitHub releases API (at most FALLBACK_RELEASE_LIMIT releases), or, without
|
|
27
|
+
// the API, probes the downloads of the next FALLBACK_PROBE_LIMIT lower stable
|
|
28
|
+
// tags from git ls-remote. A release picked with the `tag` config never falls
|
|
29
|
+
// back: a release picked with `tag` that has no asset stops the build. When
|
|
30
|
+
// no release has the asset, nothing is added, and the guard in
|
|
31
|
+
// generate-rp-connect-info stops the build if connector pages then have no
|
|
32
|
+
// generated partials. Any other download or archive failure stops the build
|
|
33
|
+
// here. A file another source already provides (rp-connect-docs still commits
|
|
34
|
+
// its own copies until it migrates) is skipped.
|
|
35
|
+
//
|
|
36
|
+
// Playbooks that still list the upstream connect repo with `tags: latest` as
|
|
37
|
+
// its only ref (the setup from before the release asset):
|
|
6
38
|
//
|
|
7
39
|
// - url: https://github.com/redpanda-data/connect
|
|
8
|
-
// tags: latest
|
|
40
|
+
// tags: latest
|
|
9
41
|
// start_path: docs
|
|
10
42
|
//
|
|
11
|
-
//
|
|
43
|
+
// That source is removed from the playbook in contextStarted, with a warning,
|
|
44
|
+
// and the build continues with the release asset. Connect releases no longer
|
|
45
|
+
// ship docs/antora.yml, so Antora could not aggregate it anyway.
|
|
12
46
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// after rp-connect-docs stops committing its own generated copies, and it
|
|
21
|
-
// ignores the full pages that older connect tags still carry.
|
|
47
|
+
// A connect source with explicit refs (a branch, a specific tag, a fork, or a
|
|
48
|
+
// local clone named connect) is how people preview a connect branch, so it is
|
|
49
|
+
// kept, and no asset is downloaded. Only the generated
|
|
50
|
+
// modules/components/partials and modules/components/examples trees of the
|
|
51
|
+
// `connect` component are kept from that source, and any file another source
|
|
52
|
+
// already provides is dropped, because Antora fails the build on a duplicate
|
|
53
|
+
// page or partial.
|
|
22
54
|
//
|
|
23
|
-
//
|
|
24
|
-
// generate-rp-connect-info reads info.csv from the same ref
|
|
25
|
-
// content.
|
|
55
|
+
// The release asset tag is shared (util/connect-catalog), so
|
|
56
|
+
// generate-rp-connect-info reads info.csv and catalog.json from the same ref
|
|
57
|
+
// as the reference content.
|
|
26
58
|
|
|
27
59
|
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
28
60
|
const getLatestConnectTag = require('./version-fetcher/get-latest-connect')
|
|
29
|
-
const
|
|
61
|
+
const listConnectReleases = require('./version-fetcher/list-connect-releases')
|
|
62
|
+
const { createGitHub } = require('./util/connect-github')
|
|
30
63
|
const { isConnectSource, isConnectOrigin, githubRepoOf, setResolvedConnectRef } = require('./util/connect-catalog')
|
|
64
|
+
const asset = require('./util/connect-docs-asset')
|
|
65
|
+
const { getGitHubApiToken } = require('../cli-utils/github-token')
|
|
66
|
+
const { gitAuthEnv } = require('../cli-utils/git-credential-env')
|
|
31
67
|
|
|
32
68
|
const OWNER = 'redpanda-data'
|
|
33
69
|
const REPO = 'connect'
|
|
34
70
|
const COMPONENT = 'connect'
|
|
35
71
|
const KEPT_PATHS = ['modules/components/partials/', 'modules/components/examples/']
|
|
72
|
+
const CONNECT_URL = `https://github.com/${OWNER}/${REPO}`
|
|
73
|
+
const LOCAL_DIR_ENV = 'REDPANDA_CONNECT_DOCS_DIR'
|
|
74
|
+
// How many releases the API fallback looks at, and how many lower git tags
|
|
75
|
+
// the probe fallback tries, when the latest release has no asset yet.
|
|
76
|
+
const FALLBACK_RELEASE_LIMIT = 10
|
|
77
|
+
const FALLBACK_PROBE_LIMIT = 3
|
|
78
|
+
// Dating pages by release (only when add-git-dates is in the playbook): the
|
|
79
|
+
// page size of the releases listing, the first release that has the asset
|
|
80
|
+
// (no older release can be compared), and the most older releases the walk
|
|
81
|
+
// downloads per build.
|
|
82
|
+
const DATE_RELEASE_PAGE_SIZE = 100
|
|
83
|
+
const FIRST_ASSET_RELEASE = 'v4.113.0'
|
|
84
|
+
const DATE_RELEASE_LIMIT = 10
|
|
85
|
+
// How deep the walk follows includes inside the asset's own partials, such as
|
|
86
|
+
// bloblang/methods.adoc including every bloblang-methods/* partial.
|
|
87
|
+
const NESTED_INCLUDE_DEPTH = 3
|
|
36
88
|
|
|
37
89
|
// HTTPS, SSH (ssh://), and scp-style (git@host:owner/repo) URLs are all
|
|
38
90
|
// remote content sources in Antora.
|
|
@@ -54,23 +106,43 @@ function redact (text) {
|
|
|
54
106
|
return String(text || '').replace(/([a-z][a-z+.-]*:\/\/)[^@/\s]*@/gi, '$1')
|
|
55
107
|
}
|
|
56
108
|
|
|
109
|
+
// [major, minor, patch] of a stable vX.Y.Z tag, or null for anything else,
|
|
110
|
+
// such as a prerelease.
|
|
111
|
+
function stableVersion (tag) {
|
|
112
|
+
const m = /^v(\d+)\.(\d+)\.(\d+)$/.exec(tag || '')
|
|
113
|
+
return m ? m.slice(1).map(Number) : null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function compareVersions (a, b) {
|
|
117
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Stable vX.Y.Z tags from `git ls-remote` output, highest first.
|
|
121
|
+
function stableTags (lsRemoteOutput) {
|
|
122
|
+
const tags = new Set()
|
|
123
|
+
for (const line of String(lsRemoteOutput || '').split('\n')) {
|
|
124
|
+
const m = line.match(/refs\/tags\/(v\d+\.\d+\.\d+)$/)
|
|
125
|
+
if (m) tags.add(m[1])
|
|
126
|
+
}
|
|
127
|
+
return [...tags].sort((a, b) => compareVersions(stableVersion(b), stableVersion(a)))
|
|
128
|
+
}
|
|
129
|
+
|
|
57
130
|
// Highest stable vX.Y.Z tag from `git ls-remote` output. Used when the GitHub
|
|
58
131
|
// API is unavailable, for example when an unauthenticated build is rate limited.
|
|
59
132
|
function highestStableTag (lsRemoteOutput) {
|
|
60
|
-
|
|
61
|
-
let best = null
|
|
62
|
-
for (const line of String(lsRemoteOutput || '').split('\n')) {
|
|
63
|
-
const m = line.match(/refs\/tags\/(v(\d+)\.(\d+)\.(\d+))$/)
|
|
64
|
-
if (!m) continue
|
|
65
|
-
const v = m.slice(2, 5).map(Number)
|
|
66
|
-
if (!best || newer(v, best.v) > 0) best = { tag: m[1], v }
|
|
67
|
-
}
|
|
68
|
-
return best && best.tag
|
|
133
|
+
return stableTags(lsRemoteOutput)[0] || null
|
|
69
134
|
}
|
|
70
135
|
|
|
71
|
-
|
|
136
|
+
// With a token, git authenticates through the shared credential helper, so a
|
|
137
|
+
// private connect repo still lists its tags.
|
|
138
|
+
function gitTags (url) {
|
|
72
139
|
const { execFileSync } = require('child_process')
|
|
73
|
-
|
|
140
|
+
const env = gitAuthEnv(getGitHubApiToken())
|
|
141
|
+
return execFileSync('git', ['ls-remote', '--tags', '--refs', url], { encoding: 'utf8', timeout: 60000, env })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function latestTagFromGit (url) {
|
|
145
|
+
return highestStableTag(gitTags(url))
|
|
74
146
|
}
|
|
75
147
|
|
|
76
148
|
function toTag (tagName) {
|
|
@@ -78,9 +150,10 @@ function toTag (tagName) {
|
|
|
78
150
|
return tagName.startsWith('v') ? tagName : `v${tagName}`
|
|
79
151
|
}
|
|
80
152
|
|
|
81
|
-
// The upstream connect source
|
|
82
|
-
//
|
|
83
|
-
// to preview a connect change, keeps what the playbook
|
|
153
|
+
// The upstream connect source with `tags: latest` as its only ref: the setup
|
|
154
|
+
// from before the release asset. A source with any other refs, such as a fork
|
|
155
|
+
// or a PR branch used to preview a connect change, keeps what the playbook
|
|
156
|
+
// says.
|
|
84
157
|
function wantsLatest (source) {
|
|
85
158
|
if (!isRemote(source.url) || !isConnectSource(source.url) || !isUpstreamConnect(source.url)) return false
|
|
86
159
|
const tags = Array.isArray(source.tags) ? source.tags : [source.tags]
|
|
@@ -88,20 +161,17 @@ function wantsLatest (source) {
|
|
|
88
161
|
return tags.length === 1 && tags[0] === 'latest' && branches.length === 0
|
|
89
162
|
}
|
|
90
163
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
164
|
+
// Removes the upstream connect sources that ask for the latest release, which
|
|
165
|
+
// the release asset replaces. Mutates the playbook's source list in place and
|
|
166
|
+
// returns the removed sources.
|
|
167
|
+
function removeLatestConnectSources (playbook) {
|
|
94
168
|
const sources = playbook && playbook.content && playbook.content.sources
|
|
95
|
-
if (!Array.isArray(sources)
|
|
96
|
-
|
|
97
|
-
for (
|
|
98
|
-
if (
|
|
99
|
-
source.tags = [tag]
|
|
100
|
-
// Without this, Antora's default branch patterns also pull in main.
|
|
101
|
-
source.branches = []
|
|
102
|
-
updated = true
|
|
169
|
+
if (!Array.isArray(sources)) return []
|
|
170
|
+
const removed = []
|
|
171
|
+
for (let i = sources.length - 1; i >= 0; i--) {
|
|
172
|
+
if (wantsLatest(sources[i])) removed.unshift(...sources.splice(i, 1))
|
|
103
173
|
}
|
|
104
|
-
return
|
|
174
|
+
return removed
|
|
105
175
|
}
|
|
106
176
|
|
|
107
177
|
// Removes connect-sourced files that must not be published. Mutates the
|
|
@@ -143,53 +213,406 @@ function filterConnectContent (contentAggregate) {
|
|
|
143
213
|
return report
|
|
144
214
|
}
|
|
145
215
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
216
|
+
// True when the playbook lists the connect repo (or a local clone named
|
|
217
|
+
// connect) as a content source, which keeps the git source behavior. Run it
|
|
218
|
+
// after removeLatestConnectSources.
|
|
219
|
+
function hasConnectSource (playbook) {
|
|
220
|
+
const sources = (playbook && playbook.content && playbook.content.sources) || []
|
|
221
|
+
return sources.some((source) => isConnectSource(source.url))
|
|
222
|
+
}
|
|
149
223
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
224
|
+
// The latest stable connect release tag: the GitHub API first, then the
|
|
225
|
+
// highest stable vX.Y.Z tag from git ls-remote. Null when both fail.
|
|
226
|
+
async function resolveLatestTag (url, logger) {
|
|
227
|
+
let tag = null
|
|
228
|
+
try {
|
|
229
|
+
tag = toTag(await getLatestConnectTag(await createGitHub(), OWNER, REPO, logger))
|
|
230
|
+
} catch (error) {
|
|
231
|
+
logger.warn(`GitHub API lookup of the latest Redpanda Connect release failed: ${error.message}`)
|
|
232
|
+
}
|
|
233
|
+
if (!tag) {
|
|
157
234
|
try {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
const token = getGitHubApiToken()
|
|
161
|
-
const github = new (Octokit.plugin(retry))({ userAgent: 'Redpanda Docs', auth: token || undefined, retry: { doNotRetry: [403, 404, 429] } })
|
|
162
|
-
tag = toTag(await getLatestConnectTag(github, OWNER, REPO, logger))
|
|
235
|
+
tag = latestTagFromGit(url)
|
|
236
|
+
if (tag) logger.info(`Resolved the latest Redpanda Connect release from git tags: ${tag}`)
|
|
163
237
|
} catch (error) {
|
|
164
|
-
logger.warn(`
|
|
238
|
+
logger.warn(redact(`git ls-remote of ${url} failed: ${error.message}`))
|
|
165
239
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
240
|
+
}
|
|
241
|
+
return tag
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Downloads the asset of a release. Null on a 404; throws on any other failure.
|
|
245
|
+
async function downloadRelease (tag, logger) {
|
|
246
|
+
try {
|
|
247
|
+
return await asset.downloadAsset(tag, { logger, token: getGitHubApiToken() })
|
|
248
|
+
} catch (error) {
|
|
249
|
+
throw new Error(`Could not download the Redpanda Connect reference docs for ${tag}: ${error.message}`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// The newest stable release older than `latestTag` that has the asset, as
|
|
254
|
+
// { tag, archive }, or null when none has it. The GitHub releases API lists
|
|
255
|
+
// the most recent releases with their assets, so only the chosen release is
|
|
256
|
+
// downloaded. Without the API, it probes the downloads of the next few lower
|
|
257
|
+
// stable tags from git ls-remote instead.
|
|
258
|
+
async function findOlderReleaseWithAsset (latestTag, url, logger) {
|
|
259
|
+
const latest = stableVersion(latestTag)
|
|
260
|
+
const isOlder = (tag) => {
|
|
261
|
+
const v = stableVersion(tag)
|
|
262
|
+
return !!v && (!latest || compareVersions(v, latest) < 0)
|
|
263
|
+
}
|
|
264
|
+
let releases = null
|
|
265
|
+
try {
|
|
266
|
+
releases = await listConnectReleases(await createGitHub(), OWNER, REPO, FALLBACK_RELEASE_LIMIT)
|
|
267
|
+
} catch (error) {
|
|
268
|
+
logger.warn(`GitHub API listing of Redpanda Connect releases failed: ${error.message}; probing the next ${FALLBACK_PROBE_LIMIT} lower release tags from git instead`)
|
|
269
|
+
}
|
|
270
|
+
if (releases) {
|
|
271
|
+
const found = releases
|
|
272
|
+
.filter((r) => r && !r.draft && !r.prerelease && isOlder(r.tag_name))
|
|
273
|
+
.sort((a, b) => compareVersions(stableVersion(b.tag_name), stableVersion(a.tag_name)))
|
|
274
|
+
.find((r) => (r.assets || []).some((a) => a && a.name === asset.ASSET_NAME))
|
|
275
|
+
if (!found) {
|
|
276
|
+
logger.info(`None of the ${releases.length} most recent Redpanda Connect releases has a ${asset.ASSET_NAME} release asset`)
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
const archive = await downloadRelease(found.tag_name, logger)
|
|
280
|
+
return archive ? { tag: found.tag_name, archive } : null
|
|
281
|
+
}
|
|
282
|
+
let tags
|
|
283
|
+
try {
|
|
284
|
+
tags = stableTags(gitTags(url)).filter(isOlder).slice(0, FALLBACK_PROBE_LIMIT)
|
|
285
|
+
} catch (error) {
|
|
286
|
+
logger.warn(redact(`git ls-remote of ${url} failed: ${error.message}`))
|
|
287
|
+
return null
|
|
288
|
+
}
|
|
289
|
+
for (const tag of tags) {
|
|
290
|
+
const archive = await downloadRelease(tag, logger)
|
|
291
|
+
if (archive) return { tag, archive }
|
|
292
|
+
}
|
|
293
|
+
if (tags.length) logger.info(`None of the next ${tags.length} lower Redpanda Connect release tags (${tags.join(', ')}) has a ${asset.ASSET_NAME} release asset`)
|
|
294
|
+
return null
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The resource keys (`partial$<relative>`, `example$<relative>`) of generated
|
|
298
|
+
// files that differ between two releases' assets, or are new in `files`.
|
|
299
|
+
function changedReferenceKeys (files, previousFiles) {
|
|
300
|
+
const before = new Map()
|
|
301
|
+
for (const f of previousFiles) {
|
|
302
|
+
const r = asset.toResource(f.path)
|
|
303
|
+
if (r && !before.has(`${r.family}$${r.relative}`)) before.set(`${r.family}$${r.relative}`, f.contents)
|
|
304
|
+
}
|
|
305
|
+
const changed = new Set()
|
|
306
|
+
const seen = new Set()
|
|
307
|
+
for (const f of files) {
|
|
308
|
+
const r = asset.toResource(f.path)
|
|
309
|
+
if (!r || r.family === 'attachment') continue
|
|
310
|
+
const key = `${r.family}$${r.relative}`
|
|
311
|
+
// addConnectDocs retains the first copy of a resource in an asset.
|
|
312
|
+
if (seen.has(key)) continue
|
|
313
|
+
seen.add(key)
|
|
314
|
+
const old = before.get(key)
|
|
315
|
+
if (old === undefined || Buffer.compare(Buffer.from(old), Buffer.from(f.contents)) !== 0) changed.add(key)
|
|
316
|
+
}
|
|
317
|
+
return changed
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Includes of the connect components module in a page: `partial$x` and
|
|
321
|
+
// `example$x` keys. In the connect component's own components module the
|
|
322
|
+
// module may be implicit; elsewhere only fully qualified includes count. With
|
|
323
|
+
// a content catalog, only files this asset added count, and includes inside
|
|
324
|
+
// those files are followed too (up to NESTED_INCLUDE_DEPTH levels), so a page
|
|
325
|
+
// that includes an aggregate partial is dated by the partials it pulls in.
|
|
326
|
+
const INCLUDE_RX = /^include::([^[\s]+)\[/gm
|
|
327
|
+
function includeKey (target, src) {
|
|
328
|
+
const own = src.component === COMPONENT && src.module === asset.MODULE
|
|
329
|
+
const q = target.match(/^connect:components:((?:partial|example)\$.+)$/) ||
|
|
330
|
+
(own ? target.match(/^(?:components:)?((?:partial|example)\$.+)$/) : null)
|
|
331
|
+
return q ? q[1] : null
|
|
332
|
+
}
|
|
333
|
+
function connectIncludes (page, contentCatalog, origin) {
|
|
334
|
+
const keys = []
|
|
335
|
+
const seen = new Set()
|
|
336
|
+
const visit = (file, depth) => {
|
|
337
|
+
for (const m of file.contents.toString('utf8').matchAll(INCLUDE_RX)) {
|
|
338
|
+
const key = includeKey(m[1], file.src)
|
|
339
|
+
if (!key) continue
|
|
340
|
+
if (!contentCatalog) {
|
|
341
|
+
keys.push(key)
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
const included = contentCatalog.resolveResource(m[1], file.src)
|
|
345
|
+
// Only files inserted from this asset can acquire a release date.
|
|
346
|
+
if (!included || included.src.origin !== origin || seen.has(included)) continue
|
|
347
|
+
seen.add(included)
|
|
348
|
+
keys.push(key)
|
|
349
|
+
if (depth < NESTED_INCLUDE_DEPTH) visit(included, depth + 1)
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
visit(page, 1)
|
|
353
|
+
return keys
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Marks pages with the newest change date of their included asset references.
|
|
357
|
+
// Catalog resolution excludes retained duplicates and respects each version.
|
|
358
|
+
// Cloud stubs inherit the date of the connect page they actually include.
|
|
359
|
+
function markReferenceChanges (contentCatalog, dates, origin) {
|
|
360
|
+
if (!dates.size) return 0
|
|
361
|
+
const marked = new Set()
|
|
362
|
+
const pages = contentCatalog.getPages()
|
|
363
|
+
const mark = (page, date) => {
|
|
364
|
+
if (!date) return
|
|
365
|
+
if (!page.connectReferenceModified || date > page.connectReferenceModified) page.connectReferenceModified = date
|
|
366
|
+
marked.add(page)
|
|
367
|
+
}
|
|
368
|
+
for (const page of pages) {
|
|
369
|
+
for (const key of connectIncludes(page, contentCatalog, origin)) mark(page, dates.get(key))
|
|
370
|
+
}
|
|
371
|
+
for (const page of pages) {
|
|
372
|
+
for (const m of page.contents.toString('utf8').matchAll(INCLUDE_RX)) {
|
|
373
|
+
if (!/^connect:([^:$]+):([^$]+\.adoc)$/.test(m[1])) continue
|
|
374
|
+
const included = contentCatalog.resolvePage(m[1], page.src)
|
|
375
|
+
if (marked.has(included)) mark(page, included.connectReferenceModified)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return marked.size
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// The date of the newest release that changed each of `keys`, as a Map of
|
|
382
|
+
// key to YYYY-MM-DD. It compares the release with older releases that have
|
|
383
|
+
// the asset, newest first, so a file left unchanged by the current release
|
|
384
|
+
// keeps the date of the release that last changed it. Bounded so the cost
|
|
385
|
+
// does not grow with every connect release: the listing stops paging at the
|
|
386
|
+
// first release older than FIRST_ASSET_RELEASE, and the walk downloads at
|
|
387
|
+
// most DATE_RELEASE_LIMIT older releases. A file still unchanged at the end
|
|
388
|
+
// of the walk gets no date. A failed listing, download, or read only costs
|
|
389
|
+
// dates: it logs a warning, and every file not dated yet keeps its git date.
|
|
390
|
+
async function referenceReleaseDates (tag, files, keys, logger) {
|
|
391
|
+
const dates = new Map()
|
|
392
|
+
if (!keys.size) return dates
|
|
393
|
+
const floor = stableVersion(FIRST_ASSET_RELEASE)
|
|
394
|
+
const releases = []
|
|
395
|
+
try {
|
|
396
|
+
const github = await createGitHub()
|
|
397
|
+
for (let page = 1; ; page++) {
|
|
398
|
+
const batch = await listConnectReleases(github, OWNER, REPO, DATE_RELEASE_PAGE_SIZE, page)
|
|
399
|
+
releases.push(...batch)
|
|
400
|
+
if (batch.length < DATE_RELEASE_PAGE_SIZE) break
|
|
401
|
+
// Newest first: past the first release older than the asset, no
|
|
402
|
+
// release can have it.
|
|
403
|
+
if (batch.some((r) => r && stableVersion(r.tag_name) && compareVersions(stableVersion(r.tag_name), floor) < 0)) break
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
logger.warn(`Couldn't list Redpanda Connect releases (${error.message}), so Connect pages keep their git modified dates`)
|
|
407
|
+
return dates
|
|
408
|
+
}
|
|
409
|
+
let newer = releases.find((r) => r && r.tag_name === tag)
|
|
410
|
+
if (!newer || !newer.published_at) return dates
|
|
411
|
+
const latest = stableVersion(tag)
|
|
412
|
+
const older = releases
|
|
413
|
+
.filter((r) => r && !r.draft && !r.prerelease && stableVersion(r.tag_name) && latest && compareVersions(stableVersion(r.tag_name), latest) < 0)
|
|
414
|
+
.filter((r) => compareVersions(stableVersion(r.tag_name), floor) >= 0)
|
|
415
|
+
.filter((r) => (r.assets || []).some((a) => a && a.name === asset.ASSET_NAME))
|
|
416
|
+
.sort((a, b) => compareVersions(stableVersion(b.tag_name), stableVersion(a.tag_name)))
|
|
417
|
+
.slice(0, DATE_RELEASE_LIMIT)
|
|
418
|
+
const pending = new Set(keys)
|
|
419
|
+
for (const release of older) {
|
|
420
|
+
let previousFiles
|
|
421
|
+
try {
|
|
422
|
+
const archive = await downloadRelease(release.tag_name, logger)
|
|
423
|
+
if (!archive) throw new Error(`the release lists a ${asset.ASSET_NAME} asset but its download returns 404`)
|
|
424
|
+
previousFiles = await asset.readTarGz(archive)
|
|
425
|
+
} catch (error) {
|
|
426
|
+
// Skipping the release would credit its changes to the newer release,
|
|
427
|
+
// so stop here: dates found so far are exact, the rest keep git dates.
|
|
428
|
+
logger.warn(`Couldn't read the Redpanda Connect ${release.tag_name} reference docs to date pages (${error.message}), so ${pending.size} generated files keep their pages' git modified dates`)
|
|
429
|
+
break
|
|
430
|
+
}
|
|
431
|
+
const changed = changedReferenceKeys(files, previousFiles)
|
|
432
|
+
for (const key of pending) {
|
|
433
|
+
if (!changed.has(key)) continue
|
|
434
|
+
if (newer.published_at) dates.set(key, String(newer.published_at).slice(0, 10))
|
|
435
|
+
pending.delete(key)
|
|
436
|
+
}
|
|
437
|
+
if (!pending.size) break
|
|
438
|
+
files = previousFiles
|
|
439
|
+
newer = release
|
|
440
|
+
}
|
|
441
|
+
return dates
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// True when the playbook registers add-git-dates, the only reader of the
|
|
445
|
+
// release dates. Without it, the dating step is skipped.
|
|
446
|
+
function usesGitDates (playbook) {
|
|
447
|
+
const extensions = (playbook && playbook.antora && playbook.antora.extensions) || []
|
|
448
|
+
return [].concat(extensions).some((ext) => {
|
|
449
|
+
const id = typeof ext === 'string' ? ext : ext && ext.enabled !== false ? ext.require || ext.id : null
|
|
450
|
+
return /(^|[/\\])add-git-dates(\.js)?$/.test(String(id || ''))
|
|
451
|
+
})
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Adds the generated files to every version of the connect component.
|
|
455
|
+
// Mutates the content catalog and returns what it added, replaced, and
|
|
456
|
+
// skipped. A file another source already provides is skipped, unless
|
|
457
|
+
// `replace` is set (a local directory, which previews unreleased changes and
|
|
458
|
+
// so wins over committed copies).
|
|
459
|
+
function addConnectDocs (contentCatalog, files, origin, { replace = false } = {}) {
|
|
460
|
+
const report = { added: 0, replaced: 0, providedElsewhere: 0, outsideGenerated: 0, versions: [] }
|
|
461
|
+
const component = contentCatalog.getComponent(COMPONENT)
|
|
462
|
+
if (!component) return report
|
|
463
|
+
report.versions = component.versions.map((v) => v.version)
|
|
464
|
+
for (const entry of files) {
|
|
465
|
+
const resource = asset.toResource(entry.path)
|
|
466
|
+
if (!resource) {
|
|
467
|
+
report.outsideGenerated++
|
|
468
|
+
continue
|
|
469
|
+
}
|
|
470
|
+
for (const version of report.versions) {
|
|
471
|
+
const src = { component: COMPONENT, version, module: asset.MODULE, family: resource.family, relative: resource.relative }
|
|
472
|
+
const existing = contentCatalog.getById(src)
|
|
473
|
+
if (existing && !replace) {
|
|
474
|
+
report.providedElsewhere++
|
|
475
|
+
continue
|
|
172
476
|
}
|
|
477
|
+
if (existing) contentCatalog.removeFile(existing)
|
|
478
|
+
contentCatalog.addFile({
|
|
479
|
+
path: resource.path,
|
|
480
|
+
contents: Buffer.from(entry.contents),
|
|
481
|
+
src: { ...src, path: resource.path, origin },
|
|
482
|
+
})
|
|
483
|
+
if (existing) report.replaced++
|
|
484
|
+
else report.added++
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return report
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
module.exports.register = function ({ config }) {
|
|
491
|
+
raiseListenerLimit(this)
|
|
492
|
+
const logger = this.getLogger('modify-connect-tag-playbook-extension')
|
|
493
|
+
const rawTag = config && config.tag != null ? String(config.tag).trim() : ''
|
|
494
|
+
// `latest` is the default; as a tag it would become the nonexistent vlatest.
|
|
495
|
+
if (rawTag.toLowerCase() === 'latest') {
|
|
496
|
+
throw new Error('The `tag` config of modify-connect-tag-playbook is `latest`. The latest stable Redpanda Connect release is the default, so remove `tag`, or set it to a release tag such as v4.113.0.')
|
|
497
|
+
}
|
|
498
|
+
const configuredTag = toTag(rawTag || null)
|
|
499
|
+
// Set in contextStarted: true when the playbook still lists a connect
|
|
500
|
+
// content source with explicit refs.
|
|
501
|
+
let gitSourceMode = false
|
|
502
|
+
// Set in contextStarted: true when add-git-dates is in the playbook.
|
|
503
|
+
let dateReferencePages = false
|
|
504
|
+
|
|
505
|
+
this.on('contextStarted', ({ playbook }) => {
|
|
506
|
+
// Clear a ref left over from an earlier build in the same process.
|
|
507
|
+
setResolvedConnectRef(null)
|
|
508
|
+
const removed = removeLatestConnectSources(playbook)
|
|
509
|
+
if (removed.length) {
|
|
510
|
+
this.updateVariables({ playbook })
|
|
511
|
+
logger.warn(
|
|
512
|
+
`The Redpanda Connect content source (${removed.map((s) => redact(s.url)).join(', ')} with tags: latest) is no longer needed and was removed from the playbook, ` +
|
|
513
|
+
`so the reference docs come from the ${asset.ASSET_NAME} release asset. Remove that source from the playbook.`
|
|
514
|
+
)
|
|
173
515
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
516
|
+
gitSourceMode = hasConnectSource(playbook)
|
|
517
|
+
dateReferencePages = usesGitDates(playbook)
|
|
518
|
+
if (gitSourceMode && (process.env[LOCAL_DIR_ENV] || '').trim()) {
|
|
519
|
+
logger.warn(`${LOCAL_DIR_ENV} is ignored because the playbook lists the connect repo as a content source`)
|
|
178
520
|
}
|
|
179
|
-
pinConnectSource(playbook, tag)
|
|
180
|
-
setResolvedConnectRef(tag)
|
|
181
|
-
this.updateVariables({ playbook })
|
|
182
|
-
logger.info(`Sourcing Redpanda Connect reference content from ${tag}`)
|
|
183
521
|
})
|
|
184
522
|
|
|
185
523
|
this.on('contentAggregated', ({ contentAggregate }) => {
|
|
524
|
+
if (!gitSourceMode) return
|
|
186
525
|
const r = filterConnectContent(contentAggregate)
|
|
187
526
|
if (r.kept + r.outsideGenerated + r.providedElsewhere + r.otherComponents === 0) return
|
|
188
527
|
logger.info(
|
|
189
528
|
`Redpanda Connect content: kept ${r.kept} generated files; skipped ${r.providedElsewhere} already provided by another source, ` +
|
|
190
|
-
`${r.outsideGenerated} outside the generated partials and
|
|
529
|
+
`${r.outsideGenerated} outside the generated partials, examples, and attachments, and ${r.otherComponents} from other components`
|
|
191
530
|
)
|
|
192
531
|
})
|
|
532
|
+
|
|
533
|
+
// Antora runs listeners of an event one at a time, in registration order,
|
|
534
|
+
// which follows the order of the playbook's extension list. Prepending puts
|
|
535
|
+
// this listener first wherever the extension is listed, so the files exist
|
|
536
|
+
// before generate-rp-connect-info's guard and any other contentClassified
|
|
537
|
+
// listener reads the connect partials.
|
|
538
|
+
const subscribe = typeof this.prependListener === 'function' ? this.prependListener : this.on
|
|
539
|
+
subscribe.call(this, 'contentClassified', async ({ contentCatalog }) => {
|
|
540
|
+
if (gitSourceMode) return
|
|
541
|
+
if (!contentCatalog.getComponent(COMPONENT)) return
|
|
542
|
+
const localDir = (process.env[LOCAL_DIR_ENV] || '').trim()
|
|
543
|
+
let files
|
|
544
|
+
let origin
|
|
545
|
+
let from
|
|
546
|
+
if (localDir) {
|
|
547
|
+
setResolvedConnectRef(configuredTag)
|
|
548
|
+
try {
|
|
549
|
+
files = await asset.readLocal(localDir)
|
|
550
|
+
} catch (error) {
|
|
551
|
+
throw new Error(`Could not read the Redpanda Connect reference docs from ${LOCAL_DIR_ENV} (${localDir}): ${error.message}`)
|
|
552
|
+
}
|
|
553
|
+
origin = { type: 'local', url: CONNECT_URL, startPath: 'docs', localDir }
|
|
554
|
+
from = `${LOCAL_DIR_ENV} (${localDir})`
|
|
555
|
+
} else {
|
|
556
|
+
let tag = configuredTag || (await resolveLatestTag(`${CONNECT_URL}.git`, logger))
|
|
557
|
+
if (!tag) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
'Could not resolve the latest Redpanda Connect release, so the connect reference docs cannot be downloaded. ' +
|
|
560
|
+
`Set a GitHub token, check network access, or set the \`tag\` config of this extension or ${LOCAL_DIR_ENV}.`
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
setResolvedConnectRef(tag)
|
|
564
|
+
let archive = await downloadRelease(tag, logger)
|
|
565
|
+
// The latest release can be published before its asset is attached, or
|
|
566
|
+
// its asset job can fail. An explicit `tag` means that release only.
|
|
567
|
+
if (!archive && !configuredTag) {
|
|
568
|
+
const older = await findOlderReleaseWithAsset(tag, `${CONNECT_URL}.git`, logger)
|
|
569
|
+
if (older) {
|
|
570
|
+
logger.warn(`Redpanda Connect ${tag} has no ${asset.ASSET_NAME} asset yet; using ${older.tag}`)
|
|
571
|
+
tag = older.tag
|
|
572
|
+
archive = older.archive
|
|
573
|
+
setResolvedConnectRef(tag)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (!archive && configuredTag) {
|
|
577
|
+
throw new Error(
|
|
578
|
+
`Redpanda Connect release ${tag} has no ${asset.ASSET_NAME} asset (404). ` +
|
|
579
|
+
`Set the \`tag\` config of modify-connect-tag-playbook to a release that has it, or remove \`tag\` to use the latest release.`
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
if (!archive) {
|
|
583
|
+
logger.info(`Redpanda Connect ${tag} has no ${asset.ASSET_NAME} release asset (404), so no generated reference docs were added from it`)
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
try {
|
|
587
|
+
files = await asset.readTarGz(archive)
|
|
588
|
+
} catch (error) {
|
|
589
|
+
throw new Error(`The ${asset.ASSET_NAME} release asset of Redpanda Connect ${tag} is corrupt: ${error.message}`)
|
|
590
|
+
}
|
|
591
|
+
origin = { type: 'release-asset', url: CONNECT_URL, startPath: 'docs', reftype: 'tag', refname: tag, tag, asset: asset.assetUrl(tag) }
|
|
592
|
+
from = `the ${tag} ${asset.ASSET_NAME} release asset`
|
|
593
|
+
}
|
|
594
|
+
if (!files.some((f) => asset.toResource(f.path))) {
|
|
595
|
+
throw new Error(`${from} has no files under modules/components/partials or modules/components/examples`)
|
|
596
|
+
}
|
|
597
|
+
const r = addConnectDocs(contentCatalog, files, origin, { replace: !!localDir })
|
|
598
|
+
const versions = r.versions.filter(Boolean)
|
|
599
|
+
logger.info(
|
|
600
|
+
`Redpanda Connect reference docs from ${from}: added ${r.added} files to the ${COMPONENT} component` +
|
|
601
|
+
`${versions.length ? ` (${versions.join(', ')})` : ''}; ` +
|
|
602
|
+
(localDir
|
|
603
|
+
? `replaced ${r.replaced} provided by another source; skipped ${r.outsideGenerated}`
|
|
604
|
+
: `skipped ${r.providedElsewhere} already provided by another source and ${r.outsideGenerated}`) +
|
|
605
|
+
' outside the generated partials, examples, and attachments'
|
|
606
|
+
)
|
|
607
|
+
// Resolve only included files supplied by this asset. Retained copies
|
|
608
|
+
// from another source keep their git dates, even if the asset differs.
|
|
609
|
+
if (!localDir && dateReferencePages) {
|
|
610
|
+
const keys = new Set(contentCatalog.getPages().flatMap((page) => connectIncludes(page, contentCatalog, origin)))
|
|
611
|
+
const dates = await referenceReleaseDates(origin.tag, files, keys, logger)
|
|
612
|
+
const count = markReferenceChanges(contentCatalog, dates, origin)
|
|
613
|
+
logger.info(`Redpanda Connect ${origin.tag}: resolved last change dates for ${dates.size} included generated files; ${count} pages use those release dates when later than their last commit`)
|
|
614
|
+
}
|
|
615
|
+
})
|
|
193
616
|
}
|
|
194
617
|
|
|
195
|
-
module.exports._internal = { isRemote, isUpstreamConnect, redact, isConnectSource, isConnectOrigin, toTag, highestStableTag, wantsLatest,
|
|
618
|
+
module.exports._internal = { isRemote, isUpstreamConnect, redact, isConnectSource, isConnectOrigin, toTag, highestStableTag, stableTags, findOlderReleaseWithAsset, resolveLatestTag, FALLBACK_RELEASE_LIMIT, FALLBACK_PROBE_LIMIT, wantsLatest, removeLatestConnectSources, filterConnectContent, hasConnectSource, addConnectDocs, LOCAL_DIR_ENV, changedReferenceKeys, connectIncludes, markReferenceChanges, usesGitDates, FIRST_ASSET_RELEASE, DATE_RELEASE_LIMIT }
|