@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.
@@ -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- and Cloud-specific pages, chosen by the page's environment attributes.
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
 
@@ -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 environment-specific pages: on pages with the `env-cloud` attribute the entry's `xref-cloud` is used, on pages with `env-kubernetes` 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.
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`, `operator`, or `cloud`. 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:
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.
@@ -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
- const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator', 'cloud']
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
- * Some features have separate documentation for Kubernetes, Linux
443
- * (self-managed), and Redpanda Cloud. Mirrors the config_ref macro's
444
- * environment-awareness: the env-cloud and env-kubernetes page attributes
445
- * select the xref-cloud and xref-kubernetes registry fields, falling back
446
- * to the default xref.
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: document.getAttribute('enterprise-validate', 'warn'),
653
+ mode,
595
654
  registry,
596
- filePath: config && config.file && config.file.src && config.file.src.path,
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: parent.getDocument().getAttribute('enterprise-validate', 'warn'),
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, parent.getDocument()),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.38.2",
3
+ "version": "5.38.4",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -14,7 +14,9 @@
14
14
  const yaml = require('js-yaml')
15
15
  const semver = require('semver')
16
16
 
17
- const VALID_SCOPES = ['redpanda', 'console', 'connect', 'operator', 'cloud']
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 lost = lostMetadataSections(fs.readFileSync(dPath, 'utf8'), descriptionOut);
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
- const updated = contentWithoutOldSection.slice(0, insertIdx) + section + '\n' + contentWithoutOldSection.slice(insertIdx)
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')