@redpanda-data/docs-extensions-and-macros 5.38.2 → 5.38.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/macros/PROPERTY_AND_ENTERPRISE_REFERENCES.adoc +3 -1
- package/macros/REFERENCE.adoc +14 -2
- package/macros/enterprise.js +89 -15
- package/macros/prop.js +1 -0
- package/package.json +1 -1
- package/tools/enterprise-features/verify.js +3 -1
- package/tools/lint-strings/rules/common.js +55 -0
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +41 -1
- package/tools/redpanda-connect/pr-summary-formatter.js +32 -2
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +18 -4
|
@@ -98,7 +98,9 @@ Two more things come from the same registry:
|
|
|
98
98
|
* The licensing feature tables on the licensing pages are *generated* from it (`enterprise_features::redpanda[]` and friends), so the tables can never disagree with the registry.
|
|
99
99
|
* `doc-tools validate enterprise-features` compares the registry against the actual license gates in Redpanda's source code and Connect's plugin list. When engineering ships a new license-gated feature, CI fails until someone decides its documented name -- naming stays a human decision, but forgetting is no longer possible.
|
|
100
100
|
|
|
101
|
-
Like properties, feature links adapt to context: registry entries can carry Kubernetes-
|
|
101
|
+
Like properties, feature links adapt to context: registry entries can carry a Kubernetes-specific page, chosen by the page's environment attributes.
|
|
102
|
+
|
|
103
|
+
Cloud is different from Kubernetes here, not just another context to adapt to. Redpanda Cloud has no Enterprise Edition license, so on a page in a Cloud component `enterprise:` renders as plain text -- no styling, tooltip, or link -- the same way `prop:` renders a non-`cloud_supported` property as plain code there, and `enterprise_features::[]` renders nothing at all: no table and no admonition, because a note about a macro belongs to the writer, not the reader. Both macros report the Cloud page to the build log according to `enterprise-validate`, naming the page. That includes the Cloud rendering of single-sourced self-managed prose: the check reads the page actually being converted, regardless of which repo the mention was written in. See link:REFERENCE.adoc#cloud-pages[Cloud pages] in the macros reference.
|
|
102
104
|
|
|
103
105
|
== Where to see all of this working
|
|
104
106
|
|
package/macros/REFERENCE.adoc
CHANGED
|
@@ -85,7 +85,7 @@ The macro target is the feature name, which can contain spaces. Named attributes
|
|
|
85
85
|
xref::
|
|
86
86
|
Optional Antora resource ID of the feature's documentation page. Overrides the registry entry's link. When neither is set, the term links to the licensing page.
|
|
87
87
|
+
|
|
88
|
-
Registry entries can carry
|
|
88
|
+
Registry entries can carry a Kubernetes-specific page: on pages with the `env-kubernetes` attribute the entry's `xref-kubernetes` is used, and otherwise the entry's default `xref` (the Linux or generic self-managed page). This mirrors the `config_ref` macro's environment awareness. There is no Cloud counterpart -- see <<cloud-pages>>.
|
|
89
89
|
|
|
90
90
|
text::
|
|
91
91
|
Optional display text override. Use it when the sentence needs different casing or phrasing than the feature name.
|
|
@@ -113,6 +113,15 @@ Set to `false` to render the term without a link.
|
|
|
113
113
|
enterprise-beta-badge::
|
|
114
114
|
Set to `false` to suppress the beta badge in prose. Generated tables always show it. See <<beta-features>>.
|
|
115
115
|
|
|
116
|
+
[[cloud-pages]]
|
|
117
|
+
=== Cloud pages
|
|
118
|
+
|
|
119
|
+
Redpanda Cloud has no Enterprise Edition license -- customers never apply or manage one -- so a page in a Cloud component (recognized by its `env-cloud` attribute) has nothing true to say about a license, its expiration, or its restrictions. On such a page the macro renders the target as plain text with no styling, tooltip, or link, skipping registry validation and the release-status checks below entirely, and reports the mention according to `enterprise-validate`.
|
|
120
|
+
|
|
121
|
+
This also covers the Cloud rendering of single-sourced self-managed prose: `env-cloud` is read from the document being converted, which reflects the page actually being rendered (the Cloud stub page), regardless of which repo the included `enterprise:` mention was written in. See the `prop` macro's <<prop-availability>> for the same mechanism applied to properties.
|
|
122
|
+
|
|
123
|
+
The `enterprise_features` block macro is affected too: see <<cloud-pages-block>>.
|
|
124
|
+
|
|
116
125
|
[[release-status]]
|
|
117
126
|
=== Release status
|
|
118
127
|
|
|
@@ -195,7 +204,7 @@ enterprise_features::connect[]
|
|
|
195
204
|
enterprise_features::operator[title=Operator features,heading=What stops working]
|
|
196
205
|
----
|
|
197
206
|
|
|
198
|
-
The target is the scope: `redpanda`, `console`, `connect`,
|
|
207
|
+
The target is the scope: `redpanda`, `console`, `connect`, or `operator`. Rows render alphabetically by feature name with three columns: the feature name (linked to its documentation page when the entry has an `xref` or `url`), the description, and the expiration or restriction behavior. Named attributes:
|
|
199
208
|
|
|
200
209
|
title::
|
|
201
210
|
Optional table title override. The default is scope-specific, for example `Enterprise features in Redpanda`.
|
|
@@ -203,6 +212,9 @@ Optional table title override. The default is scope-specific, for example `Enter
|
|
|
203
212
|
heading::
|
|
204
213
|
Optional third-column heading override. The default is `Behavior Upon Expiration` for the `redpanda` scope and `Restrictions Without Valid License` for the other scopes.
|
|
205
214
|
|
|
215
|
+
[[cloud-pages-block]]
|
|
216
|
+
There is no `cloud` scope: Redpanda Cloud has no Enterprise Edition license, so there is no licensing table to generate for it (see <<cloud-pages>>). Calling this macro from a page in a Cloud component -- directly or through single-sourced self-managed content -- renders nothing: no table and no admonition. The diagnostic goes to the build log, reported according to `enterprise-validate` and naming the page, so it reaches the writer and never the reader.
|
|
217
|
+
|
|
206
218
|
=== Registration
|
|
207
219
|
|
|
208
220
|
Registered together with the `enterprise` inline macro.
|
package/macros/enterprise.js
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
* the same way an unreleased feature renders on released docs, because as
|
|
38
38
|
* of that page's version the feature had not shipped yet.
|
|
39
39
|
*
|
|
40
|
+
* Redpanda Cloud has no Enterprise Edition license -- customers never apply
|
|
41
|
+
* or manage one -- so both macros treat a page in a Cloud component
|
|
42
|
+
* (env-cloud) as having no enterprise feature to mark or list at all. The
|
|
43
|
+
* inline macro renders plain text with no styling, tooltip, or link, and the
|
|
44
|
+
* block macro renders nothing instead of a licensing table; both report to
|
|
45
|
+
* the build log according to enterprise-validate. This also covers the Cloud rendering of
|
|
46
|
+
* single-sourced self-managed prose: the document reflects the page being
|
|
47
|
+
* converted, regardless of which repo the included prose was written in.
|
|
48
|
+
*
|
|
40
49
|
* Document or site attributes:
|
|
41
50
|
*
|
|
42
51
|
* enterprise-validate 'warn' (default) to log unknown feature names,
|
|
@@ -62,6 +71,7 @@
|
|
|
62
71
|
const yaml = require('js-yaml')
|
|
63
72
|
const semver = require('semver')
|
|
64
73
|
const { buildBadgeHtml, DEFAULT_TOOLTIPS } = require('./badge')
|
|
74
|
+
const { isAttributeSet } = require('./prop')
|
|
65
75
|
const logger = require('@antora/logger')('enterprise-macro')
|
|
66
76
|
|
|
67
77
|
const $enterpriseRegistry = Symbol('$enterpriseRegistry')
|
|
@@ -181,6 +191,42 @@ function isPrereleasePage (config, document) {
|
|
|
181
191
|
return Boolean(componentVersion && componentVersion.prerelease)
|
|
182
192
|
}
|
|
183
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Whether the page being converted belongs to a Cloud component.
|
|
196
|
+
*
|
|
197
|
+
* Read from the document rather than the component's antora.yml, because the
|
|
198
|
+
* document is what reflects a Cloud stub page's own attributes even when its
|
|
199
|
+
* content came from an included self-managed file -- the same mechanism
|
|
200
|
+
* resolveEntryXref already relied on for env-cloud.
|
|
201
|
+
*
|
|
202
|
+
* The value test is prop.js's isAttributeSet, so `env-cloud: false` reads as
|
|
203
|
+
* unset in both places. The two readers are still not interchangeable:
|
|
204
|
+
* prop.js asks the CATALOG about the page's component, while this asks the
|
|
205
|
+
* DOCUMENT being converted, which is the whole point above.
|
|
206
|
+
* rp-connect-components.js still uses a bare `!== undefined` for env-cloud,
|
|
207
|
+
* so `env-cloud: false` reads as Cloud there. Worth aligning separately, but
|
|
208
|
+
* not from here.
|
|
209
|
+
*/
|
|
210
|
+
function isCloudPage (document) {
|
|
211
|
+
return isAttributeSet(document && document.getAttribute('env-cloud'))
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Report an enterprise: mention or enterprise_features::[] call on a page
|
|
216
|
+
* belonging to a Cloud component. Redpanda Cloud has no Enterprise Edition
|
|
217
|
+
* license, so nothing about a license, its expiration, or its restrictions
|
|
218
|
+
* is true there.
|
|
219
|
+
*/
|
|
220
|
+
function reportNoLicenseOnCloud ({ call, mode, filePath }) {
|
|
221
|
+
if (mode === 'off') return
|
|
222
|
+
const where = filePath ? ` in ${filePath}` : ''
|
|
223
|
+
const message =
|
|
224
|
+
`${call}${where}: this page belongs to a Cloud component, and Redpanda Cloud has no Enterprise Edition license. ` +
|
|
225
|
+
'Reword the content for Cloud instead of relying on this macro, or wrap it in ifndef::env-cloud[] if the same prose is single-sourced to a non-Cloud page too.'
|
|
226
|
+
if (mode === 'error') throw new Error(message)
|
|
227
|
+
logger.warn(message)
|
|
228
|
+
}
|
|
229
|
+
|
|
184
230
|
/**
|
|
185
231
|
* Render the release-status badge for a registry entry.
|
|
186
232
|
*
|
|
@@ -310,14 +356,15 @@ function buildBetaBadgeAsciiDoc (entry) {
|
|
|
310
356
|
return html ? `pass:[${html}]` : ''
|
|
311
357
|
}
|
|
312
358
|
const REGISTRY_FILENAME = 'enterprise-features.yml'
|
|
313
|
-
|
|
359
|
+
// No 'cloud' scope: Redpanda Cloud has no Enterprise Edition license, so
|
|
360
|
+
// there is no licensing table to generate for it. See isCloudPage.
|
|
361
|
+
const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator']
|
|
314
362
|
|
|
315
363
|
const TABLE_TITLES = {
|
|
316
364
|
redpanda: 'Enterprise features in Redpanda',
|
|
317
365
|
console: 'Enterprise features in Redpanda Console',
|
|
318
366
|
connect: 'Enterprise features in Redpanda Connect',
|
|
319
367
|
operator: 'Enterprise features in the Redpanda Operator',
|
|
320
|
-
cloud: 'Enterprise features in Redpanda Cloud',
|
|
321
368
|
}
|
|
322
369
|
|
|
323
370
|
const THIRD_COLUMN_HEADINGS = {
|
|
@@ -438,12 +485,15 @@ function reportUnknownFeature ({ feature, mode, registry, filePath }) {
|
|
|
438
485
|
}
|
|
439
486
|
|
|
440
487
|
/**
|
|
441
|
-
* Pick the environment-appropriate feature page from a registry entry.
|
|
442
|
-
*
|
|
443
|
-
* (self-managed)
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
488
|
+
* Pick the environment-appropriate feature page from a registry entry. Some
|
|
489
|
+
* features have separate documentation for Kubernetes and Linux
|
|
490
|
+
* (self-managed). Mirrors the config_ref macro's environment-awareness: the
|
|
491
|
+
* env-kubernetes page attribute selects the xref-kubernetes registry field,
|
|
492
|
+
* falling back to the default xref.
|
|
493
|
+
*
|
|
494
|
+
* No Cloud counterpart: a page in a Cloud component never reaches this
|
|
495
|
+
* function, because isCloudPage short-circuits the inline macro to plain
|
|
496
|
+
* text first. See isCloudPage.
|
|
447
497
|
*
|
|
448
498
|
* @param {object} entry - Registry entry.
|
|
449
499
|
* @param {object} document - Asciidoctor document (for env attributes).
|
|
@@ -451,7 +501,6 @@ function reportUnknownFeature ({ feature, mode, registry, filePath }) {
|
|
|
451
501
|
*/
|
|
452
502
|
function resolveEntryXref (entry, document) {
|
|
453
503
|
if (!entry) return undefined
|
|
454
|
-
if (document.getAttribute('env-cloud') !== undefined && entry['xref-cloud']) return entry['xref-cloud']
|
|
455
504
|
if (document.getAttribute('env-kubernetes') !== undefined && entry['xref-kubernetes']) return entry['xref-kubernetes']
|
|
456
505
|
return entry.xref
|
|
457
506
|
}
|
|
@@ -577,6 +626,16 @@ function enterpriseInlineMacro (config) {
|
|
|
577
626
|
self.$option('regexp', /enterprise:([^[]+)\[(|.*?[^\\])\]/)
|
|
578
627
|
self.process(function (parent, target, attributes) {
|
|
579
628
|
const document = parent.getDocument()
|
|
629
|
+
const mode = document.getAttribute('enterprise-validate', 'warn')
|
|
630
|
+
const filePath = config && config.file && config.file.src && config.file.src.path
|
|
631
|
+
if (isCloudPage(document)) {
|
|
632
|
+
// Redpanda Cloud has no Enterprise Edition license, so there is
|
|
633
|
+
// nothing to validate against the registry, no status to gate on,
|
|
634
|
+
// and no styling that would be true here. Bail out before any of
|
|
635
|
+
// that runs, the same way an unrecognized name bails out below.
|
|
636
|
+
reportNoLicenseOnCloud({ call: `enterprise:${target}[]`, mode, filePath })
|
|
637
|
+
return self.createInline(parent, 'quoted', attributes.text || target)
|
|
638
|
+
}
|
|
580
639
|
let registry
|
|
581
640
|
if (config && config.contentCatalog) {
|
|
582
641
|
registry = loadRegistry(config)
|
|
@@ -591,14 +650,12 @@ function enterpriseInlineMacro (config) {
|
|
|
591
650
|
} else {
|
|
592
651
|
reportUnknownFeature({
|
|
593
652
|
feature: target,
|
|
594
|
-
mode
|
|
653
|
+
mode,
|
|
595
654
|
registry,
|
|
596
|
-
filePath
|
|
655
|
+
filePath,
|
|
597
656
|
})
|
|
598
657
|
}
|
|
599
658
|
}
|
|
600
|
-
const mode = document.getAttribute('enterprise-validate', 'warn')
|
|
601
|
-
const filePath = config && config.file && config.file.src && config.file.src.path
|
|
602
659
|
const status = entryStatus(entry, { mode, filePath, contentCatalog: config && config.contentCatalog })
|
|
603
660
|
if (status === STATUS_UNRELEASED && !isPrereleasePage(config, document)) {
|
|
604
661
|
// Documented for an upcoming release, referenced from released docs.
|
|
@@ -654,6 +711,23 @@ function enterpriseFeaturesBlockMacro (config) {
|
|
|
654
711
|
if (!VALID_SCOPES.includes(scope)) {
|
|
655
712
|
throw new Error(`enterprise_features::[] needs a scope of ${VALID_SCOPES.join(', ')} as its target, got '${scope}'.`)
|
|
656
713
|
}
|
|
714
|
+
const document = parent.getDocument()
|
|
715
|
+
if (isCloudPage(document)) {
|
|
716
|
+
// Same reasoning as the inline macro: no license, no table.
|
|
717
|
+
reportNoLicenseOnCloud({
|
|
718
|
+
call: `enterprise_features::${scope}[]`,
|
|
719
|
+
mode: document.getAttribute('enterprise-validate', 'warn'),
|
|
720
|
+
filePath: config && config.file && config.file.src && config.file.src.path,
|
|
721
|
+
})
|
|
722
|
+
// Render NOTHING, not an admonition. The message is internal tooling
|
|
723
|
+
// language aimed at a writer, and all three docs-site playbooks run
|
|
724
|
+
// with enterprise-validate: warn, so an admonition here would ship to
|
|
725
|
+
// docs.redpanda.com and tell a reader about a macro. The diagnostic
|
|
726
|
+
// belongs in the build log, which reportNoLicenseOnCloud just wrote,
|
|
727
|
+
// and it names the page. This also makes the inline and block macros
|
|
728
|
+
// behave the same way on Cloud, which the docs claim.
|
|
729
|
+
return undefined
|
|
730
|
+
}
|
|
657
731
|
const registry = config && config.contentCatalog ? loadRegistry(config) : undefined
|
|
658
732
|
if (!registry) {
|
|
659
733
|
warnNoRegistry(config && config.contentCatalog)
|
|
@@ -663,13 +737,13 @@ function enterpriseFeaturesBlockMacro (config) {
|
|
|
663
737
|
title: attributes.title,
|
|
664
738
|
heading: attributes.heading,
|
|
665
739
|
report: {
|
|
666
|
-
mode:
|
|
740
|
+
mode: document.getAttribute('enterprise-validate', 'warn'),
|
|
667
741
|
filePath: config && config.file && config.file.src && config.file.src.path,
|
|
668
742
|
contentCatalog: config && config.contentCatalog,
|
|
669
743
|
},
|
|
670
744
|
// Prerelease docs describe the upcoming release, so they list features
|
|
671
745
|
// that are still only in a release candidate.
|
|
672
|
-
includeUnreleased: isPrereleasePage(config,
|
|
746
|
+
includeUnreleased: isPrereleasePage(config, document),
|
|
673
747
|
config,
|
|
674
748
|
})
|
|
675
749
|
// A block anchor before the macro call ([[my-id]]) is consumed into the
|
package/macros/prop.js
CHANGED
|
@@ -1012,6 +1012,7 @@ module.exports.propertyAnchor = propertyAnchor
|
|
|
1012
1012
|
module.exports.helmValuesPath = helmValuesPath
|
|
1013
1013
|
module.exports.loadPropertiesFor = loadPropertiesFor
|
|
1014
1014
|
module.exports.compareTags = compareTags
|
|
1015
|
+
module.exports.isAttributeSet = isAttributeSet
|
|
1015
1016
|
module.exports.isPrerelease = isPrerelease
|
|
1016
1017
|
module.exports.releaseSeries = releaseSeries
|
|
1017
1018
|
module.exports.extractHeadingsWithTags = extractHeadingsWithTags
|
package/package.json
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
const yaml = require('js-yaml')
|
|
15
15
|
const semver = require('semver')
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Mirrors macros/enterprise.js: no 'cloud' scope, because Redpanda Cloud has
|
|
18
|
+
// no Enterprise Edition license.
|
|
19
|
+
const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator']
|
|
18
20
|
const SOURCE_KINDS = ['core-enum', 'core-property', 'connect-plugin', 'manual']
|
|
19
21
|
|
|
20
22
|
function finding (level, check, message) {
|
|
@@ -127,6 +127,18 @@ function findBareCodeTokens (text, name = null) {
|
|
|
127
127
|
return found
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* A short window of `text` around the first occurrence of `needle`, for use in
|
|
132
|
+
* rule messages so the writer can see the offending phrase in context.
|
|
133
|
+
*/
|
|
134
|
+
function excerpt (text, needle, width = 60) {
|
|
135
|
+
const at = text.toLowerCase().indexOf(needle.toLowerCase())
|
|
136
|
+
if (at === -1) return text.slice(0, width)
|
|
137
|
+
const start = Math.max(0, at - Math.floor(width / 2))
|
|
138
|
+
const slice = text.slice(start, start + width)
|
|
139
|
+
return `${start > 0 ? '...' : ''}${slice}${start + width < text.length ? '...' : ''}`
|
|
140
|
+
}
|
|
141
|
+
|
|
130
142
|
const COMMON_RULES = [
|
|
131
143
|
{
|
|
132
144
|
name: 'empty-description',
|
|
@@ -203,6 +215,49 @@ const COMMON_RULES = [
|
|
|
203
215
|
return issues
|
|
204
216
|
}
|
|
205
217
|
},
|
|
218
|
+
{
|
|
219
|
+
name: 'em-dash',
|
|
220
|
+
description: 'Em dash in prose that ships to docs.redpanda.com',
|
|
221
|
+
severity: 'warning',
|
|
222
|
+
check: (decl) => {
|
|
223
|
+
const text = decl.string || ''
|
|
224
|
+
if (!text.includes('\u2014')) return []
|
|
225
|
+
// The docs style guide takes commas, parentheses or a sentence break
|
|
226
|
+
// instead of em dashes. Reference docs generated from these strings pass
|
|
227
|
+
// prose through unchanged, so an em dash here is published verbatim, and
|
|
228
|
+
// correcting it in the generated partial is undone by the next
|
|
229
|
+
// regeneration.
|
|
230
|
+
const count = (text.match(/\u2014/g) || []).length
|
|
231
|
+
return [{
|
|
232
|
+
message: `Contains ${count} em dash${count === 1 ? '' : 'es'}. Use a comma, parentheses, or a separate sentence: "${excerpt(text, '\u2014')}"`
|
|
233
|
+
}]
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
name: 'latin-abbreviation',
|
|
238
|
+
description: 'Latin abbreviation instead of plain English',
|
|
239
|
+
severity: 'warning',
|
|
240
|
+
check: (decl) => {
|
|
241
|
+
const text = decl.string || ''
|
|
242
|
+
if (!text) return []
|
|
243
|
+
const issues = []
|
|
244
|
+
// "e.g." and "i.e." are both ruled out by the style guide's terminology
|
|
245
|
+
// list. Match the abbreviation only, so "e.g" inside a longer token such
|
|
246
|
+
// as a URL or an identifier is left alone.
|
|
247
|
+
for (const [abbr, replacement] of [['e.g.', '"for example"'], ['i.e.', '"that is"']]) {
|
|
248
|
+
// Boundaries on both sides: the abbreviation must not be part of a
|
|
249
|
+
// longer token, so a host or identifier that happens to contain it
|
|
250
|
+
// (https://e.g.example/path) is left alone.
|
|
251
|
+
const pattern = new RegExp(`(^|[^\\w.])${abbr.replace(/\./g, '\\.')}(?=$|[^\\w])`, 'i')
|
|
252
|
+
if (pattern.test(text)) {
|
|
253
|
+
issues.push({
|
|
254
|
+
message: `Uses "${abbr}". Write ${replacement} instead: "${excerpt(text, abbr)}"`
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return issues
|
|
259
|
+
}
|
|
260
|
+
},
|
|
206
261
|
{
|
|
207
262
|
name: 'unbalanced-backticks',
|
|
208
263
|
description: 'Odd number of backticks ships broken markup',
|
|
@@ -196,6 +196,33 @@ function mergeOverrides(target, overrides) {
|
|
|
196
196
|
return target;
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* House-style regressions that a regeneration would reintroduce.
|
|
201
|
+
*
|
|
202
|
+
* Generated prose passes through unchanged by design (see
|
|
203
|
+
* helpers/renderConnectDescription.js), so when a writer corrects an em dash or
|
|
204
|
+
* an "e.g." in a published partial instead of upstream, the next regeneration
|
|
205
|
+
* silently puts it back. Comparing the published text with the incoming text
|
|
206
|
+
* catches exactly that case: a style issue the published file does not have and
|
|
207
|
+
* the new one does. Same rules the doc-strings lint applies at the source, so
|
|
208
|
+
* the two agree on what counts as a regression.
|
|
209
|
+
*
|
|
210
|
+
* @param {string} published current on-disk partial
|
|
211
|
+
* @param {string} incoming regenerated partial
|
|
212
|
+
* @returns {string[]} rule names whose hit count increases
|
|
213
|
+
*/
|
|
214
|
+
function styleRegressions (published, incoming) {
|
|
215
|
+
return STYLE_REGRESSION_RULES
|
|
216
|
+
.filter(({ count }) => count(incoming) > count(published))
|
|
217
|
+
.map(({ label }) => label);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const STYLE_REGRESSION_RULES = [
|
|
221
|
+
{ label: 'em dash', count: (text) => (text.match(/\u2014/g) || []).length },
|
|
222
|
+
{ label: '"e.g."', count: (text) => (text.match(/(^|[^\w.])e\.g\./gi) || []).length },
|
|
223
|
+
{ label: '"i.e."', count: (text) => (text.match(/(^|[^\w.])i\.e\./gi) || []).length }
|
|
224
|
+
];
|
|
225
|
+
|
|
199
226
|
/**
|
|
200
227
|
* Resolves $ref references in an object by replacing them with their definitions.
|
|
201
228
|
* Supports JSON Pointer style references like "#/definitions/client_certs".
|
|
@@ -448,6 +475,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
448
475
|
let partialsWritten = 0;
|
|
449
476
|
const descriptionReports = [];
|
|
450
477
|
const lostSectionWarnings = [];
|
|
478
|
+
const styleRegressionWarnings = [];
|
|
451
479
|
// typeDir/name keys handled this run; used after the loop to blank
|
|
452
480
|
// description partials orphaned by a connector deleted upstream.
|
|
453
481
|
const visitedDescriptionPartials = new Set();
|
|
@@ -487,6 +515,12 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
487
515
|
if (fieldsOut.trim()) {
|
|
488
516
|
const fPath = path.join(fieldsOutRoot, type, `${name}.adoc`);
|
|
489
517
|
fs.mkdirSync(path.dirname(fPath), { recursive: true });
|
|
518
|
+
if (fs.existsSync(fPath)) {
|
|
519
|
+
const regressions = styleRegressions(fs.readFileSync(fPath, 'utf8'), fieldsOut);
|
|
520
|
+
if (regressions.length) {
|
|
521
|
+
styleRegressionWarnings.push({ partial: path.relative(process.cwd(), fPath), issues: regressions });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
490
524
|
fs.writeFileSync(fPath, fieldsOut);
|
|
491
525
|
if (!writeFullDrafts) {
|
|
492
526
|
partialsWritten++;
|
|
@@ -587,7 +621,12 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
587
621
|
// because it left the upstream description or because a human
|
|
588
622
|
// added it here -- so warn before overwriting instead of
|
|
589
623
|
// dropping it without a trace.
|
|
590
|
-
const
|
|
624
|
+
const publishedDescription = fs.readFileSync(dPath, 'utf8');
|
|
625
|
+
const regressions = styleRegressions(publishedDescription, descriptionOut);
|
|
626
|
+
if (regressions.length) {
|
|
627
|
+
styleRegressionWarnings.push({ partial: path.relative(process.cwd(), dPath), issues: regressions });
|
|
628
|
+
}
|
|
629
|
+
const lost = lostMetadataSections(publishedDescription, descriptionOut);
|
|
591
630
|
if (lost.length) {
|
|
592
631
|
lostSectionWarnings.push({ partial: path.relative(process.cwd(), dPath), sections: lost });
|
|
593
632
|
console.warn(
|
|
@@ -938,6 +977,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
938
977
|
draftFiles,
|
|
939
978
|
descriptionReports,
|
|
940
979
|
lostSectionWarnings,
|
|
980
|
+
styleRegressionWarnings,
|
|
941
981
|
orphanSweepSkipped,
|
|
942
982
|
descriptionBackfill
|
|
943
983
|
};
|
|
@@ -164,6 +164,29 @@ function renderLostSectionWarnings(warnings) {
|
|
|
164
164
|
return lines;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Render the house-style regression block.
|
|
169
|
+
*
|
|
170
|
+
* A regeneration reintroducing an em dash or an "e.g." that the published
|
|
171
|
+
* partial does not have means the correction was made in the generated file
|
|
172
|
+
* instead of upstream, so every release undoes it again. Surfacing it in the
|
|
173
|
+
* summary is what tells the reviewer the diff contains reverted copy, which
|
|
174
|
+
* the schema delta above never shows.
|
|
175
|
+
* @param {Array<{partial: string, issues: string[]}>} warnings
|
|
176
|
+
* @returns {string[]} lines (empty when there is nothing to warn about)
|
|
177
|
+
*/
|
|
178
|
+
function renderStyleRegressionWarnings(warnings) {
|
|
179
|
+
if (!Array.isArray(warnings) || warnings.length === 0) return [];
|
|
180
|
+
const lines = [];
|
|
181
|
+
lines.push('> [!WARNING]');
|
|
182
|
+
lines.push('> **This update reintroduces house-style issues that the published docs do not have.** The fix belongs upstream in the Connect source, or in `docs-data/overrides.json`: correcting the generated partial is undone by the next regeneration.');
|
|
183
|
+
for (const w of warnings) {
|
|
184
|
+
lines.push(`> - \`${w.partial}\` adds: ${w.issues.join(', ')}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push('');
|
|
187
|
+
return lines;
|
|
188
|
+
}
|
|
189
|
+
|
|
167
190
|
function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, draftedConnectors = null) {
|
|
168
191
|
const lines = [];
|
|
169
192
|
|
|
@@ -176,6 +199,7 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
|
|
|
176
199
|
lines.push('<!-- PR_SUMMARY_START -->');
|
|
177
200
|
lines.push('');
|
|
178
201
|
lines.push(...renderLostSectionWarnings(masterDiff?.lostSectionWarnings));
|
|
202
|
+
lines.push(...renderStyleRegressionWarnings(masterDiff?.styleRegressionWarnings));
|
|
179
203
|
lines.push(...renderDescriptionReports(masterDiff?.descriptionReports));
|
|
180
204
|
lines.push('## Redpanda Connect Documentation Update');
|
|
181
205
|
lines.push('');
|
|
@@ -583,6 +607,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
|
|
|
583
607
|
lines.push('<!-- PR_SUMMARY_START -->');
|
|
584
608
|
lines.push('');
|
|
585
609
|
lines.push(...renderLostSectionWarnings(diffData.lostSectionWarnings));
|
|
610
|
+
lines.push(...renderStyleRegressionWarnings(diffData.styleRegressionWarnings));
|
|
586
611
|
lines.push(...renderDescriptionReports(diffData.descriptionReports));
|
|
587
612
|
|
|
588
613
|
// Detect if this is a master diff
|
|
@@ -1364,13 +1389,17 @@ function printPRSummary(diffData, binaryAnalysis = null, draftedConnectors = nul
|
|
|
1364
1389
|
* something each call site has to remember.
|
|
1365
1390
|
*
|
|
1366
1391
|
* @param {object} result generator result
|
|
1367
|
-
* @param {{descriptionReports: Array, lostSectionWarnings: Array}} into accumulators, mutated in place
|
|
1368
|
-
* @returns {{descriptionReports: Array, lostSectionWarnings: Array}} the same accumulators
|
|
1392
|
+
* @param {{descriptionReports: Array, lostSectionWarnings: Array, styleRegressionWarnings?: Array}} into accumulators, mutated in place
|
|
1393
|
+
* @returns {{descriptionReports: Array, lostSectionWarnings: Array, styleRegressionWarnings: Array}} the same accumulators
|
|
1369
1394
|
*/
|
|
1370
1395
|
function collectGeneratorReports (result, into) {
|
|
1371
1396
|
if (!result) return into;
|
|
1372
1397
|
into.descriptionReports.push(...(result.descriptionReports || []));
|
|
1373
1398
|
into.lostSectionWarnings.push(...(result.lostSectionWarnings || []));
|
|
1399
|
+
// Tolerate an accumulator created before this key existed, so an older call
|
|
1400
|
+
// site collects what it knows about instead of throwing.
|
|
1401
|
+
if (!into.styleRegressionWarnings) into.styleRegressionWarnings = [];
|
|
1402
|
+
into.styleRegressionWarnings.push(...(result.styleRegressionWarnings || []));
|
|
1374
1403
|
return into;
|
|
1375
1404
|
}
|
|
1376
1405
|
|
|
@@ -1381,5 +1410,6 @@ module.exports = {
|
|
|
1381
1410
|
printPRSummary,
|
|
1382
1411
|
renderDescriptionReports,
|
|
1383
1412
|
renderLostSectionWarnings,
|
|
1413
|
+
renderStyleRegressionWarnings,
|
|
1384
1414
|
truncateToSentence
|
|
1385
1415
|
};
|
|
@@ -500,7 +500,18 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
|
|
|
500
500
|
const firstMatch = versionHeading.exec(contentWithoutOldSection)
|
|
501
501
|
const insertIdx = firstMatch ? firstMatch.index : contentWithoutOldSection.length
|
|
502
502
|
|
|
503
|
-
|
|
503
|
+
// Entries in this file are separated by two blank lines. Normalize both
|
|
504
|
+
// boundaries instead of appending a newline to whatever is already there:
|
|
505
|
+
// the insertion point already carries the separator that preceded the
|
|
506
|
+
// previous first entry, so adding to it gave every new entry one more blank
|
|
507
|
+
// line than the entries below it, which then had to be corrected by hand
|
|
508
|
+
// after each release. Mirrors the rpk what's-new fix in #285.
|
|
509
|
+
const before = contentWithoutOldSection.slice(0, insertIdx).replace(/\n*$/, '')
|
|
510
|
+
const after = contentWithoutOldSection.slice(insertIdx).replace(/^\n*/, '')
|
|
511
|
+
const entry = section.replace(/^\n*/, '').replace(/\n*$/, '')
|
|
512
|
+
const updated = after
|
|
513
|
+
? `${before}\n\n\n${entry}\n\n\n${after}`
|
|
514
|
+
: `${before}\n\n\n${entry}\n`
|
|
504
515
|
|
|
505
516
|
if (startIdx !== -1) {
|
|
506
517
|
console.log(`♻️ whats-new.adoc: replaced section for Version ${diff.comparison.newVersion}`)
|
|
@@ -1236,13 +1247,14 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1236
1247
|
let partialsWritten, partialFiles
|
|
1237
1248
|
const descriptionReports = []
|
|
1238
1249
|
const lostSectionWarnings = []
|
|
1250
|
+
const styleRegressionWarnings = []
|
|
1239
1251
|
// Both generator call sites (partials and drafts) feed the same PR summary,
|
|
1240
1252
|
// so they collect through one function. Pushed inline, the draft call site
|
|
1241
1253
|
// silently dropped descriptionReports for months: the structure reports for
|
|
1242
1254
|
// newly drafted connectors, the ones most likely to need an upstream fix,
|
|
1243
1255
|
// never reached the summary.
|
|
1244
1256
|
const { collectGeneratorReports } = require('./pr-summary-formatter.js')
|
|
1245
|
-
const collect = (result) => collectGeneratorReports(result, { descriptionReports, lostSectionWarnings })
|
|
1257
|
+
const collect = (result) => collectGeneratorReports(result, { descriptionReports, lostSectionWarnings, styleRegressionWarnings })
|
|
1246
1258
|
|
|
1247
1259
|
try {
|
|
1248
1260
|
const result = await generateRpcnConnectorDocs({
|
|
@@ -2109,22 +2121,24 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
2109
2121
|
|
|
2110
2122
|
// Generate PR summary
|
|
2111
2123
|
try {
|
|
2112
|
-
const { printPRSummary, renderLostSectionWarnings, renderDescriptionReports } = require('./pr-summary-formatter.js')
|
|
2124
|
+
const { printPRSummary, renderLostSectionWarnings, renderStyleRegressionWarnings, renderDescriptionReports } = require('./pr-summary-formatter.js')
|
|
2113
2125
|
// Use master diff if available, otherwise use single diff. Content-loss
|
|
2114
2126
|
// warnings and structure reports ride the diff object so they land in the
|
|
2115
2127
|
// PR summary body instead of dying in the collapsed workflow log.
|
|
2116
2128
|
const summaryDiff = masterDiff || diffJson
|
|
2117
2129
|
if (summaryDiff) {
|
|
2118
2130
|
if (lostSectionWarnings.length) summaryDiff.lostSectionWarnings = lostSectionWarnings
|
|
2131
|
+
if (styleRegressionWarnings.length) summaryDiff.styleRegressionWarnings = styleRegressionWarnings
|
|
2119
2132
|
if (descriptionReports.length) summaryDiff.descriptionReports = descriptionReports
|
|
2120
2133
|
printPRSummary(summaryDiff, binaryAnalysis, draftFiles, masterDiff ? true : false)
|
|
2121
|
-
} else if (lostSectionWarnings.length || descriptionReports.length) {
|
|
2134
|
+
} else if (lostSectionWarnings.length || styleRegressionWarnings.length || descriptionReports.length) {
|
|
2122
2135
|
// No diff to summarize (no prior version, or versions match), but this
|
|
2123
2136
|
// run still produced content-loss warnings or description-structure
|
|
2124
2137
|
// reports that a reviewer needs to see -- print them on their own
|
|
2125
2138
|
// instead of losing them because there was nothing to diff against.
|
|
2126
2139
|
const lines = [
|
|
2127
2140
|
...renderLostSectionWarnings(lostSectionWarnings),
|
|
2141
|
+
...renderStyleRegressionWarnings(styleRegressionWarnings),
|
|
2128
2142
|
...renderDescriptionReports(descriptionReports)
|
|
2129
2143
|
]
|
|
2130
2144
|
console.log('\n' + lines.join('\n') + '\n')
|