@redpanda-data/docs-extensions-and-macros 5.46.0 → 5.47.1
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.js +15 -2
- package/cli-utils/sync-schemas.js +66 -12
- package/docs-data/property-overrides.schema.json +202 -29
- package/package.json +1 -1
- package/tools/overrides-audit/adapters/properties.js +14 -3
- package/tools/overrides-audit/classify.js +175 -3
- package/tools/property-extractor/README.adoc +141 -2
- package/tools/property-extractor/compare-properties.js +12 -2
- package/tools/property-extractor/generate-handlebars-docs.js +262 -11
- package/tools/property-extractor/helpers/applyPropertyLinks.js +586 -0
- package/tools/property-extractor/helpers/audienceScope.js +172 -0
- package/tools/property-extractor/helpers/findAudienceScopeProblems.js +121 -0
- package/tools/property-extractor/helpers/index.js +3 -0
- package/tools/property-extractor/helpers/seeAlsoView.js +10 -24
- package/tools/property-extractor/property_extractor.py +30 -2
- package/tools/property-extractor/templates/property.hbs +32 -2
- package/tools/property-extractor/templates/topic-property.hbs +32 -2
- package/tools/redpanda-connect/helpers/renderConnectDescription.js +7 -1
- package/tools/redpanda-connect/helpers/renderConnectFields.js +4 -1
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// Points at the overrides file and its schema, both of which live in the repo a
|
|
2
|
+
// writer already has open. The previous pointer was a Confluence page, and
|
|
3
|
+
// Confluence is retired, so it sent every writer who read this line to a dead
|
|
4
|
+
// end. A repo-relative path cannot rot the same way.
|
|
5
|
+
const AUTOGEN_NOTICE = '// This content is autogenerated. Do not edit manually. To change a description, add a link, or scope text to one docs build, edit docs-data/property-overrides.json (fields are documented in docs-data/property-overrides.schema.json and docs-data/README.adoc) and regenerate.\n';
|
|
2
6
|
'use strict';
|
|
3
7
|
|
|
4
8
|
const fs = require('fs');
|
|
@@ -49,6 +53,35 @@ function isObjectStorageProperty(prop) {
|
|
|
49
53
|
);
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Which reference page a property is written to.
|
|
58
|
+
*
|
|
59
|
+
* The one place that decides, so a link's target page cannot disagree with the
|
|
60
|
+
* page the property actually lands on. generatePropertyPartials uses it to pick
|
|
61
|
+
* the partial to append to; applyPropertyLinks uses it to tell a same-page
|
|
62
|
+
* reference from a cross-page one.
|
|
63
|
+
*
|
|
64
|
+
* Returns null for a property with no scope, which is not rendered at all.
|
|
65
|
+
*
|
|
66
|
+
* @param {Object} prop - The property object.
|
|
67
|
+
* @returns {('cluster'|'topic'|'broker'|'object-storage'|null)}
|
|
68
|
+
*/
|
|
69
|
+
function propertyPageFor(prop) {
|
|
70
|
+
if (!prop || !prop.config_scope) return null;
|
|
71
|
+
switch (prop.config_scope) {
|
|
72
|
+
case 'topic':
|
|
73
|
+
return 'topic';
|
|
74
|
+
case 'broker':
|
|
75
|
+
return 'broker';
|
|
76
|
+
case 'cluster':
|
|
77
|
+
return isObjectStorageProperty(prop) ? 'object-storage' : 'cluster';
|
|
78
|
+
case 'object-storage':
|
|
79
|
+
return 'object-storage';
|
|
80
|
+
default:
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
52
85
|
/**
|
|
53
86
|
* Gets template path, checking environment variables for custom paths first
|
|
54
87
|
*/
|
|
@@ -249,20 +282,16 @@ function generatePropertyPartials(properties, partialsDir, onRender) {
|
|
|
249
282
|
// This fixes issues where key != name field due to bugs in the source code
|
|
250
283
|
prop.name = key;
|
|
251
284
|
|
|
252
|
-
|
|
285
|
+
const page = propertyPageFor(prop);
|
|
286
|
+
switch (page) {
|
|
253
287
|
case 'topic':
|
|
254
288
|
annotateCorrespondingClusterSupport(prop, properties);
|
|
255
289
|
propertyGroups.topic.push(prop);
|
|
256
290
|
break;
|
|
257
291
|
case 'broker':
|
|
258
|
-
propertyGroups.broker.push(prop);
|
|
259
|
-
break;
|
|
260
292
|
case 'cluster':
|
|
261
|
-
if (isObjectStorageProperty(prop)) propertyGroups['object-storage'].push(prop);
|
|
262
|
-
else propertyGroups.cluster.push(prop);
|
|
263
|
-
break;
|
|
264
293
|
case 'object-storage':
|
|
265
|
-
propertyGroups[
|
|
294
|
+
propertyGroups[page].push(prop);
|
|
266
295
|
break;
|
|
267
296
|
default:
|
|
268
297
|
console.warn(`Warning: Unknown config_scope: ${prop.config_scope} for ${prop.name}`);
|
|
@@ -410,6 +439,13 @@ function generateErrorReports(properties, documentedProperties = []) {
|
|
|
410
439
|
* backticked property name is redundant; anything else becomes text=.
|
|
411
440
|
*/
|
|
412
441
|
function convertConfigRefsToProp(text) {
|
|
442
|
+
// An array is the audience-scoped paragraph form of a description, and it
|
|
443
|
+
// reaches here BEFORE applyPropertyLinks flattens it. Without this the
|
|
444
|
+
// `typeof text !== 'string'` guard below returned the array untouched, so a
|
|
445
|
+
// config_ref inside a scoped paragraph survived into the published page as
|
|
446
|
+
// literal text -- contradicting this pass's own contract of emitting the
|
|
447
|
+
// prop macro regardless of what the input or the overrides carry.
|
|
448
|
+
if (Array.isArray(text)) return text.map(convertConfigRefsToProp);
|
|
413
449
|
if (!text || typeof text !== 'string') return text;
|
|
414
450
|
return text.replace(/config_ref:([^[,]+)(?:,([^[,]*))?(?:,([^[,]*))?\[([^\]]*)\]/g, (match, name, isLink, _path, payload) => {
|
|
415
451
|
const trimmed = name.trim();
|
|
@@ -440,6 +476,193 @@ function generateAllDocs(inputFile, outputDir) {
|
|
|
440
476
|
if (prop && prop.description) prop.description = convertConfigRefsToProp(prop.description);
|
|
441
477
|
});
|
|
442
478
|
|
|
479
|
+
// The published attachment (modules/reference/attachments/redpanda-properties-
|
|
480
|
+
// <tag>.json) is a DIFFERENT consumer of this same file than the .adoc
|
|
481
|
+
// partials generated below, and it was never taught the array-form
|
|
482
|
+
// description this feature introduces. property_extractor.py writes
|
|
483
|
+
// inputFile with whatever shape the override declared -- an array, for an
|
|
484
|
+
// audience-scoped description -- and the Makefile then copies inputFile
|
|
485
|
+
// to that attachment VERBATIM, before this function ever runs. Two real
|
|
486
|
+
// readers of that attachment assume `description` is always a string:
|
|
487
|
+
// extensions/render-property-descriptions.js skips anything else with a
|
|
488
|
+
// bare `continue` (no warning), so the browser-side fallback in docs-ui's
|
|
489
|
+
// tooltip formatter receives the raw array and does `String([...])`,
|
|
490
|
+
// which comma-joins it -- publishing the literal token "cloud-only:" and
|
|
491
|
+
// the Cloud-only sentence to Self-Managed readers, live on the two
|
|
492
|
+
// properties in today's corpus that use this shape.
|
|
493
|
+
//
|
|
494
|
+
// Fixed by snapshotting a flattened, link-free rendering of every
|
|
495
|
+
// array-form description (and every includes-bearing one, for the
|
|
496
|
+
// parallel case below) and writing it back into inputFile once partial
|
|
497
|
+
// generation finishes, so the Makefile's copy picks up a plain string. No
|
|
498
|
+
// links: the attachment has never carried them (see the comment on
|
|
499
|
+
// `enhancedOutputPath` near the end of this function), and Antora's own
|
|
500
|
+
// AsciiDoc loader resolves the ifdef/ifndef pair per SITE when
|
|
501
|
+
// render-property-descriptions.js converts it -- cloud-docs sets
|
|
502
|
+
// env-cloud in its antora.yml, so the existing per-site machinery is what
|
|
503
|
+
// makes this correct rather than anything new here.
|
|
504
|
+
//
|
|
505
|
+
// Includes are folded in for the same reason `includes` regressed a
|
|
506
|
+
// second surface: on origin/main the include:: directive was written
|
|
507
|
+
// inline into the description string, so the tooltip's Antora-loader
|
|
508
|
+
// conversion resolved it into a real admonition (an internal-use warning,
|
|
509
|
+
// or the HTTP Proxy ephemeral-credentials breaking-change notice on
|
|
510
|
+
// scram_password, scram_username and sasl_mechanism). Moving it to its
|
|
511
|
+
// own `includes` field fixed the .adoc partial but orphaned the
|
|
512
|
+
// attachment, since render-property-descriptions.js never reads
|
|
513
|
+
// `includes`. Appending it back as an inline directive, exactly the shape
|
|
514
|
+
// it used to be, restores that without teaching the extension a new
|
|
515
|
+
// field.
|
|
516
|
+
const attachmentDescriptions = {};
|
|
517
|
+
for (const [attachName, attachProp] of Object.entries(properties)) {
|
|
518
|
+
if (!attachProp) continue;
|
|
519
|
+
const hasArrayDescription = Array.isArray(attachProp.description);
|
|
520
|
+
const hasIncludes = attachProp.includes !== undefined;
|
|
521
|
+
if (!hasArrayDescription && !hasIncludes) continue;
|
|
522
|
+
|
|
523
|
+
let text = helpers.applyPropertyLinks.flattenDescription(attachProp.description);
|
|
524
|
+
if (hasIncludes) {
|
|
525
|
+
// A scratch warnings array: the real applyPropertyLinks call below
|
|
526
|
+
// normalizes and warns on the same includes once more, and that is
|
|
527
|
+
// the run's single copy of each warning. Duplicating the call here
|
|
528
|
+
// only to read the normalized shape must not double them.
|
|
529
|
+
const items = helpers.applyPropertyLinks.normalizeIncludes(attachName, attachProp.includes, []);
|
|
530
|
+
for (const item of items) {
|
|
531
|
+
const directive = `include::${item.target}`;
|
|
532
|
+
// Idempotency guard: in the real Makefile flow inputFile is fresh
|
|
533
|
+
// from property_extractor.py every run, so `text` never already
|
|
534
|
+
// carries this. It CAN already carry it if generateAllDocs runs
|
|
535
|
+
// twice against the same file without Python re-writing it in
|
|
536
|
+
// between (a repeated call in a test, or a re-run of just this
|
|
537
|
+
// step), because the append below is what makes it a string in
|
|
538
|
+
// the first place, and `includes` itself is never cleared. Without
|
|
539
|
+
// this check a second run appended the directive a second time.
|
|
540
|
+
if (text.includes(directive)) continue;
|
|
541
|
+
if (item.cloud_only) {
|
|
542
|
+
text += `\n\nifdef::env-cloud[]\n${directive}\nendif::[]`;
|
|
543
|
+
} else if (item.self_managed_only) {
|
|
544
|
+
text += `\n\nifndef::env-cloud[]\n${directive}\nendif::[]`;
|
|
545
|
+
} else {
|
|
546
|
+
text += `\n\n${directive}`;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
attachmentDescriptions[attachName] = text;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Links and audience-scoped text come out of the overrides as data -- a `links`
|
|
554
|
+
// map and, for a description, an array of optionally scoped paragraphs -- and
|
|
555
|
+
// become AsciiDoc here. Running before the three renderers means the property
|
|
556
|
+
// partials, the deprecated partial and the topic mappings partial all see the
|
|
557
|
+
// same text.
|
|
558
|
+
//
|
|
559
|
+
// This is what lets a description stay plain prose. Markup mixed into the
|
|
560
|
+
// sentence is what the overrides audit cannot compare against the source
|
|
561
|
+
// string, so an enriched description could never be retired or upstreamed;
|
|
562
|
+
// keeping the enrichment in its own fields keeps the prose comparable.
|
|
563
|
+
// Audience scopes that will not take effect, checked BEFORE the link pass:
|
|
564
|
+
// applyPropertyLinks flattens an array description into one string, after
|
|
565
|
+
// which a per-paragraph prefix can no longer be attributed to its paragraph.
|
|
566
|
+
//
|
|
567
|
+
// This is a louder failure than an unmatched link. An unmatched link key is
|
|
568
|
+
// simply not applied, so the page reads correctly minus a link. An
|
|
569
|
+
// unrecognised audience prefix is PUBLISHED: `cloud_only: Cloud clusters
|
|
570
|
+
// require...` renders as that literal string, prefix and all, in both
|
|
571
|
+
// builds. A misspelled audience boolean is silent the other way round, with
|
|
572
|
+
// the value rendering in both builds instead of the one it was scoped to.
|
|
573
|
+
const scopeProblems = helpers.findAudienceScopeProblems(properties);
|
|
574
|
+
if (scopeProblems.length > 0) {
|
|
575
|
+
console.warn(
|
|
576
|
+
`Warning: ${scopeProblems.length} audience scope(s) will not take effect:`
|
|
577
|
+
);
|
|
578
|
+
for (const p of scopeProblems) {
|
|
579
|
+
console.warn(
|
|
580
|
+
p.problem === 'unrecognized-prefix'
|
|
581
|
+
? ` ${p.property}: ${p.field} starts with "${p.detail}:", which is not a recognized audience prefix, so it will publish as literal text. Use "cloud-only:" or "self-managed-only:".`
|
|
582
|
+
: ` ${p.property}: ${p.field} carries "${p.detail}", which is not a recognized audience flag, so the value will render in both builds. Use cloud_only or self_managed_only.`
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const linkResult = helpers.applyPropertyLinks(properties);
|
|
588
|
+
if (linkResult.applied > 0) {
|
|
589
|
+
console.log(`Applied ${linkResult.applied} declared link(s) from the overrides file.`);
|
|
590
|
+
}
|
|
591
|
+
for (const warning of linkResult.warnings) console.warn(`Warning: ${warning}`);
|
|
592
|
+
if (linkResult.unmatched.length > 0) {
|
|
593
|
+
// A link key that matches nothing is silent link rot: the override looks
|
|
594
|
+
// maintained and the page has no link. Name every one.
|
|
595
|
+
console.warn(
|
|
596
|
+
`Warning: ${linkResult.unmatched.length} declared link(s) matched no text and were not applied:`
|
|
597
|
+
);
|
|
598
|
+
for (const { property, key } of linkResult.unmatched) {
|
|
599
|
+
console.warn(` ${property}: links["${key}"]`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Count the declarative enrichment so the caller can report what this run
|
|
604
|
+
// did, rather than a writer having to read a diff of generated AsciiDoc to
|
|
605
|
+
// find out. Counted here, after the link pass, while the override-derived
|
|
606
|
+
// fields are still on the property objects.
|
|
607
|
+
const enrichment = {
|
|
608
|
+
declaredLinks: 0,
|
|
609
|
+
appliedLinks: linkResult.applied,
|
|
610
|
+
unmatchedLinks: linkResult.unmatched,
|
|
611
|
+
// Everything else the link pass rejected: an empty key, a missing
|
|
612
|
+
// target, a #target naming no property, a glossterm with no term.
|
|
613
|
+
// Console-only until now, so it never reached a reviewer.
|
|
614
|
+
linkWarnings: linkResult.warnings,
|
|
615
|
+
audienceScopeProblems: scopeProblems,
|
|
616
|
+
includes: 0,
|
|
617
|
+
audienceScopedParagraphs: 0
|
|
618
|
+
};
|
|
619
|
+
for (const prop of Object.values(properties)) {
|
|
620
|
+
if (!prop) continue;
|
|
621
|
+
if (prop.links) enrichment.declaredLinks += Object.keys(prop.links).length;
|
|
622
|
+
if (Array.isArray(prop.includes)) enrichment.includes += prop.includes.length;
|
|
623
|
+
// Counted from the emitted directives, not from the paragraph array:
|
|
624
|
+
// applyPropertyLinks has already flattened an array description into a
|
|
625
|
+
// string by this point, so the array is gone. Counting the output also
|
|
626
|
+
// catches a legacy description that carries a hand-written conditional.
|
|
627
|
+
if (typeof prop.description === 'string') {
|
|
628
|
+
const conditionals = prop.description.match(/^ifn?def::env-cloud\[\]$/gm);
|
|
629
|
+
if (conditionals) enrichment.audienceScopedParagraphs += conditionals.length;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Write the flattened attachment text back into inputFile, so the
|
|
634
|
+
// Makefile's later `cp` of this same file to
|
|
635
|
+
// modules/reference/attachments/ carries a plain string rather than the
|
|
636
|
+
// array/includes shape that broke the tooltip. Best-effort and re-reads
|
|
637
|
+
// inputFile fresh rather than serializing the already-mutated
|
|
638
|
+
// `properties` object, so only the specific properties snapshotted above
|
|
639
|
+
// change on disk -- every other property, and every field on these two,
|
|
640
|
+
// is byte-for-byte what property_extractor.py wrote.
|
|
641
|
+
if (Object.keys(attachmentDescriptions).length > 0) {
|
|
642
|
+
try {
|
|
643
|
+
const onDisk = bigIntJson.parse(fs.readFileSync(inputFile, 'utf8'));
|
|
644
|
+
const onDiskProperties = onDisk.properties || onDisk.topic_properties || {};
|
|
645
|
+
let flattenedCount = 0;
|
|
646
|
+
for (const [name, text] of Object.entries(attachmentDescriptions)) {
|
|
647
|
+
if (onDiskProperties[name]) {
|
|
648
|
+
onDiskProperties[name].description = text;
|
|
649
|
+
flattenedCount += 1;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (flattenedCount > 0) {
|
|
653
|
+
// indent 4, matching property_extractor.py's json.dumps(indent=4), so
|
|
654
|
+
// a real regeneration's diff is the description fields that changed,
|
|
655
|
+
// not a whole-file reindent.
|
|
656
|
+
fs.writeFileSync(inputFile, bigIntJson.stringify(onDisk, 4) + '\n');
|
|
657
|
+
console.log(
|
|
658
|
+
`Flattened ${flattenedCount} audience-scoped/include-bearing description(s) back into ${inputFile} for the published attachment.`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
console.warn(`Warning: could not write flattened descriptions back into ${inputFile}: ${err.message}`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
443
666
|
registerPartials();
|
|
444
667
|
|
|
445
668
|
let partialsCount = 0;
|
|
@@ -479,13 +702,40 @@ function generateAllDocs(inputFile, outputDir) {
|
|
|
479
702
|
console.log('Ignored:\n ' + errors.undocumented_properties.join('\n '));
|
|
480
703
|
}
|
|
481
704
|
|
|
482
|
-
|
|
705
|
+
const summary = {
|
|
483
706
|
totalProperties,
|
|
484
707
|
generatedPartials: partialsCount,
|
|
485
708
|
undocumentedProperties: errors.undocumented_properties,
|
|
486
709
|
deprecatedProperties: deprecatedCount,
|
|
487
|
-
percentageRendered: pctRendered
|
|
710
|
+
percentageRendered: pctRendered,
|
|
711
|
+
enrichment
|
|
488
712
|
};
|
|
713
|
+
|
|
714
|
+
// Opt-in machine-readable summary. The re-render workflows set this to a
|
|
715
|
+
// path outside the checkout and turn the file into the PR body, so a writer
|
|
716
|
+
// sees what the run did without reading a diff of generated AsciiDoc.
|
|
717
|
+
//
|
|
718
|
+
// Opt-in, and never defaulted to outputDir: that directory is inside the
|
|
719
|
+
// docs checkout, and the re-render workflow commits every change in the
|
|
720
|
+
// tree, so a default would commit a generated artefact into the repo on
|
|
721
|
+
// every run. Best-effort, because a summary that cannot be written must not
|
|
722
|
+
// fail a generation that otherwise succeeded.
|
|
723
|
+
const summaryPath = process.env.PROPERTY_DOCS_SUMMARY_PATH;
|
|
724
|
+
if (summaryPath) {
|
|
725
|
+
try {
|
|
726
|
+
// bigIntJson.stringify, not JSON.stringify: this file parses property
|
|
727
|
+
// data with bigIntJson.parse, so a uint64 limit arrives as a real
|
|
728
|
+
// BigInt. Plain JSON.stringify throws on one, and the throw lands in
|
|
729
|
+
// the catch below -- a warning and no summary file, which is exactly
|
|
730
|
+
// the silent degradation this summary exists to remove.
|
|
731
|
+
fs.writeFileSync(summaryPath, bigIntJson.stringify(summary, 2) + '\n');
|
|
732
|
+
console.log(`Wrote run summary to ${summaryPath}`);
|
|
733
|
+
} catch (err) {
|
|
734
|
+
console.warn(`Warning: could not write ${summaryPath}: ${err.message}`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
return summary;
|
|
489
739
|
}
|
|
490
740
|
|
|
491
741
|
|
|
@@ -493,7 +743,8 @@ module.exports = {
|
|
|
493
743
|
generateAllDocs,
|
|
494
744
|
generateDeprecatedDocs,
|
|
495
745
|
generatePropertyPartials,
|
|
496
|
-
convertConfigRefsToProp
|
|
746
|
+
convertConfigRefsToProp,
|
|
747
|
+
propertyPageFor
|
|
497
748
|
};
|
|
498
749
|
|
|
499
750
|
// CLI
|