@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.56.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 +354 -9
- 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/docs-links-to-xrefs.js +174 -0
- package/cli-utils/format-helm-spec.js +4 -3
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +125 -21
- package/cli-utils/self-managed-docs-branch.js +36 -30
- package/extension-utils/html-utils.js +27 -0
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +110 -8
- package/extensions/add-git-dates.js +13 -1
- package/extensions/aggregate-terms.js +34 -20
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +181 -3
- package/extensions/resolve-xrefs-in-attachments.js +1 -12
- package/extensions/url-to-xref.js +92 -22
- package/extensions/util/connect-docs-asset.js +6 -5
- package/extensions/validate-attributes.js +10 -1
- 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 +3 -3
- 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 +5 -2
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/doc-impact-recording/index.js +190 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -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/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- 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 +32 -11
- 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 +42 -25
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +2 -2
- package/tools/overrides-audit/adapters/connect.js +0 -144
|
@@ -8,6 +8,98 @@ const TurndownService = require('turndown')
|
|
|
8
8
|
const turndownPluginGfm = require('turndown-plugin-gfm')
|
|
9
9
|
const { gfm } = turndownPluginGfm
|
|
10
10
|
|
|
11
|
+
// Every doc-tools generator writes one comment line near the top of each file
|
|
12
|
+
// it produces, in this shape:
|
|
13
|
+
//
|
|
14
|
+
// // This content is autogenerated. Do not edit manually. Generated by doc-tools (<generator>) from <source>. <how to change it>
|
|
15
|
+
//
|
|
16
|
+
// (cli-utils/generated-marker.js builds it.) Some files written before the
|
|
17
|
+
// version was dropped carry one after "doc-tools". Older files carry only the
|
|
18
|
+
// first two sentences. The twin's front matter reports what the marker says, so a
|
|
19
|
+
// reader (or an agent) can tell generated text from hand-written text on the
|
|
20
|
+
// same page and knows where the fix goes. The comment must open with the
|
|
21
|
+
// generated phrase, which every marker in the docs repos does ("This content
|
|
22
|
+
// is autogenerated", "AUTO-GENERATED FILE", "Generated by `doc-tools ...`"),
|
|
23
|
+
// so a prose comment such as "// Table generated by the build" is not one.
|
|
24
|
+
// "Generated by" counts only when doc-tools follows, so a hand-written
|
|
25
|
+
// "// Generated by running rpk cluster info, then edited" is not one either.
|
|
26
|
+
const GENERATED_MARKER_SCAN_LINES = 15
|
|
27
|
+
const GENERATED_MARKER_RE = /^\/\/\s*((?:this content is auto-?generated\b|auto-?generated\b|generated by\s+`?doc-tools\b).*)$/i
|
|
28
|
+
// "doc-tools (property-docs)", or with a version token: "doc-tools 5.55.0
|
|
29
|
+
// (rpk-docs)". A token that is not a version (the Python helpers once wrote
|
|
30
|
+
// "unknown") still yields the generator.
|
|
31
|
+
const MARKER_TOOL_RE = /doc-tools(?:\s+([^\s()]+))?\s*\(([^)]+)\)/i
|
|
32
|
+
// The source is named only right after the tool, or after "autogenerated"
|
|
33
|
+
// (connect's own banner), so a "from" later in a legacy banner's prose is not
|
|
34
|
+
// read as one.
|
|
35
|
+
const MARKER_SOURCE_RE = /(?:doc-tools(?:\s+[^\s()]+)?\s*\([^)]+\)|auto-?generated)\s+from\s+(.+?)(?:\.(?=\s|$)|$)/i
|
|
36
|
+
const INCLUDE_TARGET_RE = /^include::([^[\s{]+)\[/gm
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Read the generated-content marker from the first lines of an AsciiDoc file.
|
|
40
|
+
* @param {Buffer|string} contents - The file's AsciiDoc source
|
|
41
|
+
* @returns {{note: string, generator: (string|null), version: (string|null), source: (string|null)}|null}
|
|
42
|
+
*/
|
|
43
|
+
function readGeneratedMarker (contents) {
|
|
44
|
+
if (!contents) return null
|
|
45
|
+
const text = Buffer.isBuffer(contents) ? contents.toString('utf8') : String(contents)
|
|
46
|
+
const lines = text.split(/\r?\n/, GENERATED_MARKER_SCAN_LINES)
|
|
47
|
+
for (const line of lines) {
|
|
48
|
+
const m = line.match(GENERATED_MARKER_RE)
|
|
49
|
+
if (!m) continue
|
|
50
|
+
const note = m[1].trim()
|
|
51
|
+
const tool = note.match(MARKER_TOOL_RE)
|
|
52
|
+
const generator = tool ? tool[2].trim() : null
|
|
53
|
+
const token = tool && tool[1] ? tool[1].replace(/^v(?=\d)/i, '') : null
|
|
54
|
+
const version = token && /^\d/.test(token) ? token : null
|
|
55
|
+
const source = (note.match(MARKER_SOURCE_RE) || [])[1] || null
|
|
56
|
+
return { note, generator, version, source: source ? source.trim() : null }
|
|
57
|
+
}
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* List the include targets in an AsciiDoc source. Targets that carry an
|
|
63
|
+
* attribute reference cannot be resolved here and are skipped.
|
|
64
|
+
* @param {Buffer|string} contents - The file's AsciiDoc source
|
|
65
|
+
* @returns {string[]} Unique include targets, in order of first appearance
|
|
66
|
+
*/
|
|
67
|
+
function findIncludeTargets (contents) {
|
|
68
|
+
if (!contents) return []
|
|
69
|
+
const text = Buffer.isBuffer(contents) ? contents.toString('utf8') : String(contents)
|
|
70
|
+
const targets = []
|
|
71
|
+
for (const m of text.matchAll(INCLUDE_TARGET_RE)) {
|
|
72
|
+
if (!targets.includes(m[1])) targets.push(m[1])
|
|
73
|
+
}
|
|
74
|
+
return targets
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Work out which text on a page is generated: the page itself, and any
|
|
79
|
+
* include whose file carries the marker. Runs while the catalog still holds
|
|
80
|
+
* AsciiDoc, before conversion.
|
|
81
|
+
* @param {Object} page - Antora page file (src, contents)
|
|
82
|
+
* @param {Object} contentCatalog - Antora content catalog
|
|
83
|
+
* @returns {{page: (Object|null), includes: Object[]}}
|
|
84
|
+
*/
|
|
85
|
+
function collectGeneratedProvenance (page, contentCatalog) {
|
|
86
|
+
const own = readGeneratedMarker(page.contents)
|
|
87
|
+
const includes = []
|
|
88
|
+
for (const target of findIncludeTargets(page.contents)) {
|
|
89
|
+
let resource
|
|
90
|
+
try {
|
|
91
|
+
resource = contentCatalog.resolveResource(target, page.src, 'partial', ['partial', 'page', 'example'])
|
|
92
|
+
} catch (err) {
|
|
93
|
+
resource = null
|
|
94
|
+
}
|
|
95
|
+
if (!resource || !resource.contents) continue
|
|
96
|
+
const marker = readGeneratedMarker(resource.contents)
|
|
97
|
+
if (!marker) continue
|
|
98
|
+
includes.push({ include: target, generator: marker.generator, version: marker.version, source: marker.source, note: marker.note })
|
|
99
|
+
}
|
|
100
|
+
return { page: own, includes }
|
|
101
|
+
}
|
|
102
|
+
|
|
11
103
|
/**
|
|
12
104
|
* Decode HTML entities in a string
|
|
13
105
|
* @param {string} str - String with potential HTML entities
|
|
@@ -70,6 +162,17 @@ function generateFrontmatter(page) {
|
|
|
70
162
|
'page-eol-date',
|
|
71
163
|
'page-git-created-date',
|
|
72
164
|
'page-git-modified-date',
|
|
165
|
+
// Generated-content provenance, from the markers generators write
|
|
166
|
+
'page-generated-content',
|
|
167
|
+
'page-autogenerated',
|
|
168
|
+
'page-generated-by',
|
|
169
|
+
'page-generated-from',
|
|
170
|
+
'page-generated-sources',
|
|
171
|
+
// Release that introduced the page's feature, and the product that
|
|
172
|
+
// release belongs to. Generated Connect reference pages set them on
|
|
173
|
+
// self-managed pages; the UI shows them as a "Since" badge.
|
|
174
|
+
'page-introduced-in',
|
|
175
|
+
'page-introduced-in-product',
|
|
73
176
|
// Component-specific version attributes (from antora.yml)
|
|
74
177
|
'latest-redpanda-tag', // Redpanda docker tag (e.g., v25.3.5)
|
|
75
178
|
'latest-console-tag',
|
|
@@ -107,6 +210,29 @@ function generateFrontmatter(page) {
|
|
|
107
210
|
return
|
|
108
211
|
}
|
|
109
212
|
|
|
213
|
+
// Provenance travels as a JSON string in the attribute; the twin wants a list
|
|
214
|
+
if (key === 'page-generated-sources') {
|
|
215
|
+
try {
|
|
216
|
+
frontmatter[key] = JSON.parse(value)
|
|
217
|
+
} catch (err) {
|
|
218
|
+
frontmatter[key] = value
|
|
219
|
+
}
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
if (key === 'page-autogenerated') {
|
|
223
|
+
frontmatter[key] = value === '' || value === true || value === 'true'
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (key === 'page-introduced-in' || key === 'page-introduced-in-product') {
|
|
227
|
+
// Not boolean flags: an empty value (an unset attribute in a template)
|
|
228
|
+
// means there is nothing to record.
|
|
229
|
+
const text = String(value).trim()
|
|
230
|
+
if (!text) return
|
|
231
|
+
// A bare X.Y.Z, as the UI badge shows it.
|
|
232
|
+
frontmatter[key] = key === 'page-introduced-in' ? text.replace(/^v/i, '').replace(/\.+$/, '') : text
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
110
236
|
// Skip empty attributes (AsciiDoc boolean flags)
|
|
111
237
|
if (value === '') {
|
|
112
238
|
// Special handling for version fields - use actual version from page source
|
|
@@ -294,8 +420,12 @@ function createEnterpriseFeatureRules () {
|
|
|
294
420
|
}
|
|
295
421
|
|
|
296
422
|
module.exports.createEnterpriseFeatureRules = createEnterpriseFeatureRules
|
|
423
|
+
module.exports.generateFrontmatter = generateFrontmatter
|
|
297
424
|
module.exports.formatStatusMarker = formatStatusMarker
|
|
298
425
|
module.exports.ENTERPRISE_MARKER = ENTERPRISE_MARKER
|
|
426
|
+
module.exports.readGeneratedMarker = readGeneratedMarker
|
|
427
|
+
module.exports.findIncludeTargets = findIncludeTargets
|
|
428
|
+
module.exports.collectGeneratedProvenance = collectGeneratedProvenance
|
|
299
429
|
|
|
300
430
|
module.exports.register = function () {
|
|
301
431
|
raiseListenerLimit(this)
|
|
@@ -556,6 +686,27 @@ module.exports.register = function () {
|
|
|
556
686
|
return outerTurndown
|
|
557
687
|
}
|
|
558
688
|
|
|
689
|
+
// Record which text on each page is generated, while the catalog still
|
|
690
|
+
// holds AsciiDoc: the page's own marker, and the marker on every include it
|
|
691
|
+
// resolves to. An extension that generates pages at build time sets
|
|
692
|
+
// page.generatedProvenance itself, and that wins.
|
|
693
|
+
this.on('contentClassified', ({ contentCatalog }) => {
|
|
694
|
+
const pages = contentCatalog.findBy({ family: 'page' })
|
|
695
|
+
let marked = 0
|
|
696
|
+
for (const page of pages) {
|
|
697
|
+
if (page.generatedProvenance) {
|
|
698
|
+
marked++
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
const provenance = collectGeneratedProvenance(page, contentCatalog)
|
|
702
|
+
if (provenance.page || provenance.includes.length) {
|
|
703
|
+
page.generatedProvenance = provenance
|
|
704
|
+
marked++
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
logger.info(`Recorded generated-content provenance on ${marked} of ${pages.length} pages`)
|
|
708
|
+
})
|
|
709
|
+
|
|
559
710
|
// Add marker attribute before UI rendering so templates can detect markdown availability
|
|
560
711
|
this.on('documentsConverted', ({ contentCatalog }) => {
|
|
561
712
|
const pages = contentCatalog.findBy({ family: 'page' })
|
|
@@ -568,6 +719,31 @@ module.exports.register = function () {
|
|
|
568
719
|
|
|
569
720
|
// Add marker that UI templates can check
|
|
570
721
|
page.asciidoc.attributes['page-has-markdown'] = ''
|
|
722
|
+
|
|
723
|
+
// Provenance as page attributes, so the twin's front matter and the UI
|
|
724
|
+
// templates can both read it. page-generated-content is set on every
|
|
725
|
+
// page, so "none" is a statement and not an absence.
|
|
726
|
+
const provenance = page.generatedProvenance
|
|
727
|
+
let generatedContent = 'none'
|
|
728
|
+
if (provenance?.page) {
|
|
729
|
+
generatedContent = 'all'
|
|
730
|
+
page.asciidoc.attributes['page-autogenerated'] = 'true'
|
|
731
|
+
const { generator, version, source } = provenance.page
|
|
732
|
+
if (generator) {
|
|
733
|
+
page.asciidoc.attributes['page-generated-by'] = `doc-tools${version ? ` ${version}` : ''} (${generator})`
|
|
734
|
+
}
|
|
735
|
+
// Only a named source: a legacy banner has none, and its prose is
|
|
736
|
+
// not one.
|
|
737
|
+
if (source) page.asciidoc.attributes['page-generated-from'] = source
|
|
738
|
+
} else if (provenance?.includes?.length) {
|
|
739
|
+
generatedContent = 'partial'
|
|
740
|
+
}
|
|
741
|
+
if (provenance?.includes?.length) {
|
|
742
|
+
page.asciidoc.attributes['page-generated-sources'] = JSON.stringify(
|
|
743
|
+
provenance.includes.map(({ include, generator, version, source }) => ({ include, generator, version, source }))
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
page.asciidoc.attributes['page-generated-content'] = generatedContent
|
|
571
747
|
})
|
|
572
748
|
})
|
|
573
749
|
|
|
@@ -8,6 +8,7 @@ const os = require('os')
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
const handlebars = require('handlebars')
|
|
11
|
+
const { generatedMarker, DOC_TOOLS_VERSION } = require('../cli-utils/generated-marker')
|
|
11
12
|
|
|
12
13
|
// Import and register Redpanda Connect helpers
|
|
13
14
|
const helpers = require('../tools/redpanda-connect/helpers')
|
|
@@ -269,6 +270,19 @@ module.exports.register = function ({ config }) {
|
|
|
269
270
|
// Mark as field-only page (used by convert-to-markdown and add-llms-directive to skip directive)
|
|
270
271
|
file.isFieldOnlyPage = true
|
|
271
272
|
|
|
273
|
+
// The whole page is generated here at build time, so say so the way
|
|
274
|
+
// a file marker would; convert-to-markdown exports it as
|
|
275
|
+
// page-autogenerated and page-generated-from in the twin.
|
|
276
|
+
file.generatedProvenance = {
|
|
277
|
+
page: {
|
|
278
|
+
generator: 'fields-only-pages',
|
|
279
|
+
version: DOC_TOOLS_VERSION,
|
|
280
|
+
source: 'the component spec in redpanda-data/connect',
|
|
281
|
+
note: generatedMarker('fields-only-pages', 'the component spec in redpanda-data/connect').replace(/^\/\/\s*/, ''),
|
|
282
|
+
},
|
|
283
|
+
includes: [],
|
|
284
|
+
}
|
|
285
|
+
|
|
272
286
|
pagesGenerated++
|
|
273
287
|
} catch (err) {
|
|
274
288
|
logger.error(`Failed to generate field-only page for ${type}/${item.name}: ${err.message}`)
|
|
@@ -75,6 +75,16 @@ const LOCAL_DIR_ENV = 'REDPANDA_CONNECT_DOCS_DIR'
|
|
|
75
75
|
// the probe fallback tries, when the latest release has no asset yet.
|
|
76
76
|
const FALLBACK_RELEASE_LIMIT = 10
|
|
77
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
|
|
78
88
|
|
|
79
89
|
// HTTPS, SSH (ssh://), and scp-style (git@host:owner/repo) URLs are all
|
|
80
90
|
// remote content sources in Antora.
|
|
@@ -284,6 +294,163 @@ async function findOlderReleaseWithAsset (latestTag, url, logger) {
|
|
|
284
294
|
return null
|
|
285
295
|
}
|
|
286
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
|
+
|
|
287
454
|
// Adds the generated files to every version of the connect component.
|
|
288
455
|
// Mutates the content catalog and returns what it added, replaced, and
|
|
289
456
|
// skipped. A file another source already provides is skipped, unless
|
|
@@ -332,6 +499,8 @@ module.exports.register = function ({ config }) {
|
|
|
332
499
|
// Set in contextStarted: true when the playbook still lists a connect
|
|
333
500
|
// content source with explicit refs.
|
|
334
501
|
let gitSourceMode = false
|
|
502
|
+
// Set in contextStarted: true when add-git-dates is in the playbook.
|
|
503
|
+
let dateReferencePages = false
|
|
335
504
|
|
|
336
505
|
this.on('contextStarted', ({ playbook }) => {
|
|
337
506
|
// Clear a ref left over from an earlier build in the same process.
|
|
@@ -345,6 +514,7 @@ module.exports.register = function ({ config }) {
|
|
|
345
514
|
)
|
|
346
515
|
}
|
|
347
516
|
gitSourceMode = hasConnectSource(playbook)
|
|
517
|
+
dateReferencePages = usesGitDates(playbook)
|
|
348
518
|
if (gitSourceMode && (process.env[LOCAL_DIR_ENV] || '').trim()) {
|
|
349
519
|
logger.warn(`${LOCAL_DIR_ENV} is ignored because the playbook lists the connect repo as a content source`)
|
|
350
520
|
}
|
|
@@ -422,7 +592,7 @@ module.exports.register = function ({ config }) {
|
|
|
422
592
|
from = `the ${tag} ${asset.ASSET_NAME} release asset`
|
|
423
593
|
}
|
|
424
594
|
if (!files.some((f) => asset.toResource(f.path))) {
|
|
425
|
-
throw new Error(`${from} has no files under modules/components/partials or modules/components/
|
|
595
|
+
throw new Error(`${from} has no files under modules/components/partials, modules/components/examples, or modules/components/attachments`)
|
|
426
596
|
}
|
|
427
597
|
const r = addConnectDocs(contentCatalog, files, origin, { replace: !!localDir })
|
|
428
598
|
const versions = r.versions.filter(Boolean)
|
|
@@ -432,9 +602,17 @@ module.exports.register = function ({ config }) {
|
|
|
432
602
|
(localDir
|
|
433
603
|
? `replaced ${r.replaced} provided by another source; skipped ${r.outsideGenerated}`
|
|
434
604
|
: `skipped ${r.providedElsewhere} already provided by another source and ${r.outsideGenerated}`) +
|
|
435
|
-
' outside the generated partials and
|
|
605
|
+
' outside the generated partials, examples, and attachments'
|
|
436
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
|
+
}
|
|
437
615
|
})
|
|
438
616
|
}
|
|
439
617
|
|
|
440
|
-
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 }
|
|
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 }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
4
|
+
const { escapeHtml } = require('../extension-utils/html-utils')
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Resolves AsciiDoc xrefs in JSON attachment files to HTML links.
|
|
@@ -240,15 +241,3 @@ function resolveXrefsInString (text, contentCatalog, context, logger) {
|
|
|
240
241
|
}
|
|
241
242
|
})
|
|
242
243
|
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Escape HTML special characters to prevent XSS.
|
|
246
|
-
*/
|
|
247
|
-
function escapeHtml (text) {
|
|
248
|
-
if (!text) return ''
|
|
249
|
-
return String(text)
|
|
250
|
-
.replace(/&/g, '&')
|
|
251
|
-
.replace(/</g, '<')
|
|
252
|
-
.replace(/>/g, '>')
|
|
253
|
-
.replace(/"/g, '"')
|
|
254
|
-
}
|