@redpanda-data/docs-extensions-and-macros 5.3.0 → 5.3.2

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 CHANGED
@@ -27,7 +27,8 @@ const {
27
27
  generatePropertyComparisonReport,
28
28
  updatePropertyOverridesWithVersion,
29
29
  updatePropertiesJsonWithVersion,
30
- cleanupOldDiffs
30
+ cleanupOldDiffs,
31
+ resolveDiffBaseline
31
32
  } = require('../cli-utils/diff-utils')
32
33
 
33
34
  // Import other utilities
@@ -890,6 +891,7 @@ automation
890
891
  .option('-t, --tag <tag>', 'Git tag for released content (GA/beta)')
891
892
  .option('-b, --branch <branch>', 'Branch name for in-progress content')
892
893
  .option('--diff <oldTag>', 'Diff properties against <oldTag> and restore removed deprecated properties. Recommended for accurate output; falls back to latest-redpanda-tag from antora.yml if not specified')
894
+ .option('--regenerate-old-baseline', 'Re-extract the --diff tag from source instead of using the committed attachments/redpanda-properties-<oldTag>.json baseline')
893
895
  .option('--overrides <path>', 'Optional JSON file with property description overrides', 'docs-data/property-overrides.json')
894
896
  .option('--output-dir <dir>', 'Where to write all generated files', 'modules/reference')
895
897
  .option('--cloud-support', 'Add AsciiDoc tags to generated property docs to indicate which ones are supported in Redpanda Cloud. This data is fetched from the cloudv2 repository so requires a GitHub token with repo permissions. Set the token as an environment variable using GITHUB_TOKEN, GH_TOKEN, or REDPANDA_GITHUB_TOKEN', true)
@@ -982,7 +984,13 @@ automation
982
984
  // When a diff is needed, skip AsciiDoc generation during extraction so we
983
985
  // can merge removed deprecated properties first and generate only once.
984
986
  if (needsDiff) {
985
- make(oldTag, overridesPath, templates, outputDir, { skipPartials: true })
987
+ const { useCommitted, baselinePath } = resolveDiffBaseline(outputDir, oldTag, options.regenerateOldBaseline)
988
+ if (useCommitted) {
989
+ console.log(`Using committed baseline for ${oldTag}: ${baselinePath}`)
990
+ console.log(' Pass --regenerate-old-baseline to rebuild it from source instead.')
991
+ } else {
992
+ make(oldTag, overridesPath, templates, outputDir, { skipPartials: true })
993
+ }
986
994
  make(newTag, overridesPath, templates, outputDir, { skipPartials: true })
987
995
  } else {
988
996
  make(newTag, overridesPath, templates, outputDir)
@@ -992,7 +1000,16 @@ automation
992
1000
  // the new JSON so they appear in the generated documentation.
993
1001
  if (needsDiff) {
994
1002
  const diffOutputDir = overridesPath ? path.dirname(path.resolve(overridesPath)) : outputDir
995
- generatePropertyComparisonReport(oldTag, newTag, diffOutputDir)
1003
+ try {
1004
+ generatePropertyComparisonReport(oldTag, newTag, diffOutputDir)
1005
+ } catch (err) {
1006
+ // A missing baseline only warns inside generatePropertyComparisonReport.
1007
+ // Reaching this catch means both inputs existed and the comparison
1008
+ // itself failed, so fail the run: continuing would silently drop
1009
+ // removed-deprecated restoration and "Introduced in" version stamping.
1010
+ console.error(`Error: Property comparison failed: ${err.message}`)
1011
+ process.exit(1)
1012
+ }
996
1013
 
997
1014
  try {
998
1015
  const diffReportPath = path.join(diffOutputDir, `redpanda-property-changes-${oldTag}-to-${newTag}.json`)
@@ -1012,7 +1029,11 @@ automation
1012
1029
  }
1013
1030
  }
1014
1031
  } catch (err) {
1015
- console.warn(`Warning: Failed to generate PR summary: ${err.message}`)
1032
+ // The diff report exists but could not be read or applied. Fail the
1033
+ // run rather than shipping docs that are missing removed-deprecated
1034
+ // restoration or version stamps.
1035
+ console.error(`Error: Failed to process the property diff report: ${err.message}`)
1036
+ process.exit(1)
1016
1037
  }
1017
1038
 
1018
1039
  cleanupOldDiffs(diffOutputDir)
@@ -1040,15 +1061,32 @@ automation
1040
1061
 
1041
1062
  try {
1042
1063
  const jsonDir = path.resolve(outputDir, 'attachments')
1064
+ // Invariant: always retain the comparison pair this run actually used
1065
+ // (the current tag's JSON and the diff baseline's JSON) in addition to
1066
+ // the 2 newest versioned JSONs. A backport run can compare tags that
1067
+ // are not the 2 newest, and the next run's comparison needs its
1068
+ // baseline JSON to survive. Mirrors the retention in
1069
+ // tools/property-extractor/Makefile (generate-docs cleanup).
1070
+ const parseVersion = f => f.match(/^redpanda-properties-v([\d.]+)\.json$/)[1].split('.').map(Number)
1071
+ const byVersionDesc = (a, b) => {
1072
+ const [va, vb] = [parseVersion(a), parseVersion(b)]
1073
+ for (let i = 0; i < Math.max(va.length, vb.length); i++) {
1074
+ if ((vb[i] || 0) !== (va[i] || 0)) return (vb[i] || 0) - (va[i] || 0)
1075
+ }
1076
+ return 0
1077
+ }
1043
1078
  const propertyFiles = fs.readdirSync(jsonDir)
1044
1079
  .filter(f => /^redpanda-properties-v[\d.]+\.json$/.test(f))
1045
- .sort()
1080
+ .sort(byVersionDesc)
1046
1081
 
1047
- const keepFile = `redpanda-properties-${newTag}.json`
1048
- const filesToDelete = propertyFiles.filter(f => f !== keepFile)
1082
+ const filesToKeep = new Set(propertyFiles.slice(0, 2))
1083
+ for (const tag of [newTag, oldTag]) {
1084
+ if (tag) filesToKeep.add(`redpanda-properties-${tag}.json`)
1085
+ }
1086
+ const filesToDelete = propertyFiles.filter(f => !filesToKeep.has(f))
1049
1087
 
1050
1088
  if (filesToDelete.length > 0) {
1051
- console.log('🧹 Cleaning up old property JSON files...')
1089
+ console.log('🧹 Cleaning up old property JSON files (keeping the 2 newest plus the comparison pair)...')
1052
1090
  filesToDelete.forEach(file => {
1053
1091
  fs.unlinkSync(path.join(jsonDir, file))
1054
1092
  console.log(` Deleted: ${file}`)
@@ -65,9 +65,16 @@ function cleanupOldDiffs (diffDir) {
65
65
  /**
66
66
  * Generate a detailed JSON report describing property changes between two releases.
67
67
  *
68
+ * When either input JSON is missing the comparison is skipped with a warning
69
+ * (a genuinely new oldest tag legitimately has no baseline). When both inputs
70
+ * exist but the comparison itself fails, this throws so callers fail loudly
71
+ * instead of silently dropping removed-deprecated restoration and version
72
+ * stamping.
73
+ *
68
74
  * @param {string} oldTag - Release tag or identifier for the "old" properties set.
69
75
  * @param {string} newTag - Release tag or identifier for the "new" properties set.
70
76
  * @param {string} outputDir - Directory where the comparison report will be written.
77
+ * @throws {Error} When both input JSONs exist but the comparison fails to run or exits non-zero.
71
78
  */
72
79
  function generatePropertyComparisonReport (oldTag, newTag, outputDir) {
73
80
  try {
@@ -81,13 +88,14 @@ function generatePropertyComparisonReport (oldTag, newTag, outputDir) {
81
88
 
82
89
  if (!fs.existsSync(oldJsonPath)) {
83
90
  console.log(`Warning: Old properties JSON not found at: ${oldJsonPath}`)
84
- console.log(' Skipping detailed property comparison.')
91
+ console.log(' Skipping detailed property comparison: no diff report will be generated, so removed deprecated properties are not restored and new properties are not stamped with "Introduced in" versions.')
92
+ console.log(` This is expected only when ${oldTag} has never been extracted (for example, the oldest supported release).`)
85
93
  return
86
94
  }
87
95
 
88
96
  if (!fs.existsSync(newJsonPath)) {
89
97
  console.log(`Warning: New properties JSON not found at: ${newJsonPath}`)
90
- console.log(' Skipping detailed property comparison.')
98
+ console.log(' Skipping detailed property comparison: no diff report will be generated, so removed deprecated properties are not restored and new properties are not stamped with "Introduced in" versions.')
91
99
  return
92
100
  }
93
101
 
@@ -107,15 +115,19 @@ function generatePropertyComparisonReport (oldTag, newTag, outputDir) {
107
115
  cwd: propertyExtractorDir
108
116
  })
109
117
 
118
+ // Both inputs exist, so a failed comparison is a hard error: continuing
119
+ // would silently skip removed-deprecated restoration and version stamping
110
120
  if (result.error) {
111
- console.error(`Error: Property comparison failed: ${result.error.message}`)
112
- } else if (result.status !== 0) {
113
- console.error(`Error: Property comparison exited with code: ${result.status}`)
114
- } else {
115
- console.log(`Done: Property comparison report saved to: ${reportPath}`)
121
+ throw new Error(`Property comparison failed to run: ${result.error.message}`)
122
+ }
123
+ if (result.status !== 0) {
124
+ throw new Error(`Property comparison exited with code ${result.status}`)
116
125
  }
126
+ console.log(`Done: Property comparison report saved to: ${reportPath}`)
117
127
  } catch (error) {
118
128
  console.error(`Error: Error generating property comparison: ${error.message}`)
129
+ // Propagate so callers fail loudly instead of shipping incomplete docs
130
+ throw error
119
131
  }
120
132
  }
121
133
 
@@ -346,7 +358,30 @@ function diffDirs (kind, oldTag, newTag, oldTempDir, newTempDir) {
346
358
  }
347
359
  }
348
360
 
361
+ /**
362
+ * Decide whether a committed baseline attachment can serve as the old side of
363
+ * a property diff. Rebuilding the old tag in place overwrites the committed
364
+ * attachment with a fresh extraction, so any contamination in that extraction
365
+ * silently becomes both the stored baseline and the diff input (the diff then
366
+ * compares the run against its own output and reports no new properties).
367
+ * @param {string} outputDir - Docs output dir that contains attachments/
368
+ * @param {string} oldTag - Old version tag (for example, v26.1.14)
369
+ * @param {boolean} [regenerate=false] - Force re-extraction even if a baseline exists
370
+ * @returns {{useCommitted: boolean, baselinePath: string}}
371
+ */
372
+ function resolveDiffBaseline (outputDir, oldTag, regenerate = false) {
373
+ const attachmentsDir = path.resolve(outputDir, 'attachments')
374
+ const baselinePath = path.resolve(attachmentsDir, `redpanda-properties-${oldTag}.json`)
375
+ const relativePath = path.relative(attachmentsDir, baselinePath)
376
+ if (relativePath.startsWith('..') || path.isAbsolute(relativePath) || relativePath.includes(path.sep)) {
377
+ throw new Error(`Invalid old tag "${oldTag}": baseline must resolve within the attachments directory`)
378
+ }
379
+ return { useCommitted: !regenerate && fs.existsSync(baselinePath), baselinePath }
380
+ }
381
+
349
382
  module.exports = {
383
+ resolveDiffBaseline,
384
+
350
385
  runClusterDocs,
351
386
  cleanupOldDiffs,
352
387
  generatePropertyComparisonReport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.3.0",
3
+ "version": "5.3.2",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -72,19 +72,30 @@ clean:
72
72
  @rm -rf $(VENV) $(TMP_ROOT) $(REPO_ROOT)/autogenerated
73
73
 
74
74
  # --- Clone or update Redpanda, checkout TAG or default branch ---
75
+ # Clones are shallow: the extractor only reads source files at a single tag,
76
+ # so full history is wasted time and disk.
77
+ # Auth is passed as a per-invocation HTTP header (git -c before the clone
78
+ # subcommand), never embedded in the remote URL, so no token is persisted in
79
+ # the clone's .git/config. The clone lives inside the installed package dir,
80
+ # so a persisted token would be readable by anything that can list it.
75
81
  redpanda-git:
76
82
  @mkdir -p "$(TMP_ROOT)"
77
83
  @echo "🔄 Cloning/updating Redpanda into $(REDPANDA_SRC)…"
78
84
  @if [ -d "$(REDPANDA_SRC)" ]; then \
79
- git -C "$(REDPANDA_SRC)" fetch --all --tags -q; \
85
+ git -C "$(REDPANDA_SRC)" fetch --depth 1 -q origin "+refs/tags/$(TAG):refs/tags/$(TAG)" 2>/dev/null || \
86
+ git -C "$(REDPANDA_SRC)" fetch --depth 1 -q origin "+refs/heads/$(TAG):refs/remotes/origin/$(TAG)" 2>/dev/null || \
87
+ git -C "$(REDPANDA_SRC)" fetch --all --tags -q; \
80
88
  else \
81
89
  GH_TOKEN=$${REDPANDA_GITHUB_TOKEN:-$${GITHUB_TOKEN:-$${GH_TOKEN}}}; \
90
+ REPO_URL="https://github.com/redpanda-data/redpanda.git"; \
82
91
  if [ -n "$$GH_TOKEN" ]; then \
83
92
  echo "🔑 Using authenticated clone (token provided)"; \
84
- git clone -q https://$$GH_TOKEN@github.com/redpanda-data/redpanda.git "$(REDPANDA_SRC)"; \
93
+ AUTH_HEADER="AUTHORIZATION: basic $$(printf '%s' "x-access-token:$$GH_TOKEN" | base64 | tr -d '\n')"; \
85
94
  else \
86
- git clone -q https://github.com/redpanda-data/redpanda.git "$(REDPANDA_SRC)"; \
95
+ AUTH_HEADER=""; \
87
96
  fi; \
97
+ git -c http.https://github.com/.extraheader="$$AUTH_HEADER" clone -q --depth 1 --branch "$(TAG)" "$$REPO_URL" "$(REDPANDA_SRC)" 2>/dev/null || \
98
+ git -c http.https://github.com/.extraheader="$$AUTH_HEADER" clone -q --depth 1 "$$REPO_URL" "$(REDPANDA_SRC)"; \
88
99
  fi; \
89
100
  if git -C "$(REDPANDA_SRC)" rev-parse --verify -q "origin/$(TAG)" >/dev/null 2>&1; then \
90
101
  echo "🔖 Checking out remote branch 'origin/$(TAG)'"; \
@@ -94,24 +105,44 @@ redpanda-git:
94
105
  echo "🔖 Checking out '$(TAG)'"; \
95
106
  git -C "$(REDPANDA_SRC)" checkout -q "$(TAG)"; \
96
107
  else \
97
- DEFAULT_BRANCH=$$(git -C "$(REDPANDA_SRC)" symbolic-ref --short refs/remotes/origin/HEAD); \
98
- echo "⚠️ Tag '$(TAG)' not found; falling back to '$${DEFAULT_BRANCH}'"; \
99
- git -C "$(REDPANDA_SRC)" checkout -q "$${DEFAULT_BRANCH}"; \
108
+ DEFAULT_BRANCH=$$(git -C "$(REDPANDA_SRC)" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true); \
109
+ if [ -n "$$DEFAULT_BRANCH" ]; then \
110
+ echo "⚠️ Tag '$(TAG)' not found; falling back to '$${DEFAULT_BRANCH}'"; \
111
+ git -C "$(REDPANDA_SRC)" checkout -q "$${DEFAULT_BRANCH}"; \
112
+ else \
113
+ DEFAULT_BRANCH=$$(git -C "$(REDPANDA_SRC)" ls-remote --symref origin HEAD | awk '/^ref:/ {sub(/^refs\/heads\//, "", $$2); print $$2; exit}'); \
114
+ if [ -z "$$DEFAULT_BRANCH" ]; then \
115
+ echo "❌ Tag '$(TAG)' not found and the default branch could not be determined"; \
116
+ exit 1; \
117
+ fi; \
118
+ echo "⚠️ Tag '$(TAG)' not found; falling back to '$${DEFAULT_BRANCH}'"; \
119
+ git -C "$(REDPANDA_SRC)" fetch --depth 1 -q origin "$$DEFAULT_BRANCH"; \
120
+ git -C "$(REDPANDA_SRC)" checkout -q FETCH_HEAD; \
121
+ fi; \
100
122
  fi
101
123
 
102
124
  # --- Clone Tree-sitter grammar & generate parser ---
125
+ # Skips npm install and parser generation when the generated output already
126
+ # exists for the pinned grammar version, so repeat builds stay fast
103
127
  treesitter:
104
128
  @echo "🌲 Ensuring tree-sitter-cpp grammar…"
105
129
  @if [ ! -d "$(TREESITTER_DIR)/.git" ]; then \
106
130
  rm -rf "$(TREESITTER_DIR)"; \
107
131
  git clone https://github.com/tree-sitter/tree-sitter-cpp.git "$(TREESITTER_DIR)"; \
108
132
  fi
109
- @echo "🔄 Ensuring tree-sitter-cpp is at compatible version v0.20.5…"
110
- @cd "$(TREESITTER_DIR)" && \
111
- git fetch --tags -q && \
112
- git checkout -q v0.20.5
113
- @echo "🔧 Generating parser in $(TREESITTER_DIR)…"
114
- @cd "$(TREESITTER_DIR)" && export CFLAGS="-Wno-unused-but-set-variable" && npm install --silent && export CFLAGS="-Wno-unused-but-set-variable" && $(TREE_SITTER) generate
133
+ @CURRENT_TAG=$$(git -C "$(TREESITTER_DIR)" describe --tags --exact-match 2>/dev/null || true); \
134
+ if [ "$$CURRENT_TAG" = "v0.20.5" ] && [ -f "$(TREESITTER_DIR)/src/parser.c" ]; then \
135
+ echo "✅ tree-sitter-cpp parser already generated at v0.20.5; skipping regeneration"; \
136
+ else \
137
+ echo "🔄 Ensuring tree-sitter-cpp is at compatible version v0.20.5…"; \
138
+ cd "$(TREESITTER_DIR)" && \
139
+ git fetch --tags -q && \
140
+ git checkout -q v0.20.5 && \
141
+ echo "🔧 Generating parser in $(TREESITTER_DIR)…" && \
142
+ export CFLAGS="-Wno-unused-but-set-variable" && \
143
+ npm install --silent && \
144
+ $(TREE_SITTER) generate; \
145
+ fi
115
146
 
116
147
  # --- Install Node.js dependencies for Handlebars ---
117
148
  node-deps:
@@ -146,6 +177,9 @@ generate-docs: node-deps
146
177
  else \
147
178
  echo " ⚠️ Source file $(TOOL_ROOT)/gen/$(TAG)-properties.json not found, skipping copy"; \
148
179
  fi
180
+ @# Invariant: keep at least 2 versioned JSONs, the current release plus the
181
+ @# diff baseline that the next run's comparison depends on. Mirrors the
182
+ @# retention in bin/doc-tools.js (post-run property JSON cleanup).
149
183
  @echo "🧹 Cleaning up old versioned JSON files (keeping only 2 most recent)…"
150
184
  @cd "$(OUTPUT_JSON_DIR)" && \
151
185
  ls -t redpanda-properties-*.json 2>/dev/null | tail -n +3 | while read file; do \
@@ -434,6 +434,22 @@ def fetch_cloud_config(github_token: Optional[str] = None) -> CloudConfig:
434
434
  raise CloudConfigError(error_msg) from e
435
435
 
436
436
 
437
+ def _match_name_or_alias(prop_name: str, prop_data: Dict, cloud_set: Set[str]) -> Optional[str]:
438
+ """
439
+ Return the name under which a property appears in a cloud property set.
440
+
441
+ Checks the property's own name first, then each entry in its `aliases`
442
+ array. Returns the matching name, or None when neither the name nor any
443
+ alias is in the set.
444
+ """
445
+ if prop_name in cloud_set:
446
+ return prop_name
447
+ for alias in prop_data.get('aliases') or []:
448
+ if alias in cloud_set:
449
+ return alias
450
+ return None
451
+
452
+
437
453
  def add_cloud_support_metadata(properties: Dict, cloud_config: CloudConfig) -> Dict:
438
454
  """
439
455
  Annotate property definitions with cloud-support metadata derived from a CloudConfig.
@@ -492,29 +508,39 @@ def add_cloud_support_metadata(properties: Dict, cloud_config: CloudConfig) -> D
492
508
  continue
493
509
 
494
510
  processed_count += 1
495
-
511
+
496
512
  # Initialize cloud metadata with defaults
497
513
  prop_data['cloud_editable'] = False
498
514
  prop_data['cloud_readonly'] = False
499
515
  prop_data['cloud_supported'] = False
500
516
  prop_data['cloud_byoc_only'] = False
501
-
502
- # Determine cloud support status
503
- if prop_name in editable_props:
517
+
518
+ # Determine cloud support status. Match on the property's current
519
+ # name or any of its aliases, so a property renamed in Redpanda
520
+ # still picks up cloud metadata when the cloud configuration
521
+ # references the old name.
522
+ editable_match = _match_name_or_alias(prop_name, prop_data, editable_props)
523
+ readonly_match = None if editable_match else _match_name_or_alias(prop_name, prop_data, readonly_props)
524
+
525
+ if editable_match:
526
+ if editable_match != prop_name:
527
+ logger.info(f"Property '{prop_name}' matched cloud editable configuration via alias '{editable_match}'")
504
528
  prop_data['cloud_editable'] = True
505
529
  prop_data['cloud_readonly'] = False
506
530
  prop_data['cloud_supported'] = True
507
531
  cloud_supported_count += 1
508
532
  editable_count += 1
509
-
510
- # Check if BYOC only
511
- if cloud_config.is_byoc_only(prop_name):
533
+
534
+ # Check if BYOC only, using the name the cloud configuration knows
535
+ if cloud_config.is_byoc_only(editable_match):
512
536
  prop_data['cloud_byoc_only'] = True
513
537
  byoc_only_count += 1
514
538
  else:
515
539
  prop_data['cloud_byoc_only'] = False
516
-
517
- elif prop_name in readonly_props:
540
+
541
+ elif readonly_match:
542
+ if readonly_match != prop_name:
543
+ logger.info(f"Property '{prop_name}' matched cloud readonly configuration via alias '{readonly_match}'")
518
544
  prop_data['cloud_editable'] = False
519
545
  prop_data['cloud_readonly'] = True
520
546
  prop_data['cloud_supported'] = True
@@ -565,11 +591,14 @@ def add_cloud_support_metadata(properties: Dict, cloud_config: CloudConfig) -> D
565
591
  logger.warning(" 2. Property names don't match between sources")
566
592
  logger.warning(" 3. All properties are self-managed only")
567
593
 
568
- # Check for potential mismatches
569
- unmatched_cloud_props = (editable_props | readonly_props) - {
570
- name for name, data in properties.items()
571
- if isinstance(data, dict) and data.get('config_scope') in ['cluster', 'broker', 'topic']
572
- }
594
+ # Check for potential mismatches, counting aliases as known names so a
595
+ # renamed property does not show up as missing
596
+ extracted_cloud_names = set()
597
+ for name, data in properties.items():
598
+ if isinstance(data, dict) and data.get('config_scope') in ['cluster', 'broker', 'topic']:
599
+ extracted_cloud_names.add(name)
600
+ extracted_cloud_names.update(data.get('aliases') or [])
601
+ unmatched_cloud_props = (editable_props | readonly_props) - extracted_cloud_names
573
602
 
574
603
  if unmatched_cloud_props:
575
604
  logger.info(f"Cloud configuration contains {len(unmatched_cloud_props)} properties not found in extracted properties:")
@@ -364,8 +364,10 @@ function generateConsoleReport(report, oldVersion, newVersion) {
364
364
  /**
365
365
  * Write a structured JSON comparison report to disk.
366
366
  *
367
- * Produces a JSON file containing a comparison header (old/new versions and timestamp),
367
+ * Produces a JSON file containing a comparison header (old/new versions),
368
368
  * a summary with counts for each change category, and the full details object passed as `report`.
369
+ * The report is deterministic: rerunning the comparison on the same inputs
370
+ * produces byte-identical output, so committed reports do not churn.
369
371
  *
370
372
  * @param {Object} report - Comparison details object produced by compareProperties; expected to contain arrays: `newProperties`, `changedDefaults`, `changedDescriptions`, `changedTypes`, `deprecatedProperties`, `removedProperties`, and `emptyDescriptions`.
371
373
  * @param {string} oldVersion - The previous version identifier included in the comparison header.
@@ -376,8 +378,7 @@ function generateJsonReport(report, oldVersion, newVersion, outputPath) {
376
378
  const jsonReport = {
377
379
  comparison: {
378
380
  oldVersion,
379
- newVersion,
380
- timestamp: new Date().toISOString()
381
+ newVersion
381
382
  },
382
383
  summary: {
383
384
  newProperties: report.newProperties.length,
@@ -20,7 +20,7 @@
20
20
  *
21
21
  * Input format (diff data from compare-properties.js):
22
22
  * {
23
- * comparison: { oldVersion, newVersion, timestamp },
23
+ * comparison: { oldVersion, newVersion },
24
24
  * summary: { newProperties, changedDefaults, ... },
25
25
  * details: { newProperties: [...], changedDefaults: [...], ... }
26
26
  * }
@@ -278,11 +278,14 @@ function generatePRSummary(diffData) {
278
278
  lines.push('</details>');
279
279
  lines.push('');
280
280
 
281
- // Footer
281
+ // Footer. The timestamp only appears in reports generated before the
282
+ // field was removed to keep committed diff reports deterministic.
282
283
  lines.push('---');
283
284
  lines.push('');
284
- lines.push(`*Generated: ${diffData.comparison.timestamp}*`);
285
- lines.push('');
285
+ if (diffData.comparison.timestamp) {
286
+ lines.push(`*Generated: ${diffData.comparison.timestamp}*`);
287
+ lines.push('');
288
+ }
286
289
  lines.push('<!-- PR_SUMMARY_END -->');
287
290
 
288
291
  return lines.join('\n');
@@ -79,7 +79,7 @@ ENUM_PATTERN = re.compile(r'^[a-zA-Z0-9_:]+::([a-zA-Z0-9_]+)$') # Match full qu
79
79
  CONSTRUCTOR_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\((.*)\)')
80
80
  BRACED_CONSTRUCTOR_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\{(.*)\}')
81
81
  DIGIT_SEPARATOR_PATTERN = re.compile(r"(?<=\d)'(?=\d)")
82
- FUNCTION_CALL_PATTERN = re.compile(r'([a-zA-Z0-9_:]+)\(\)')
82
+ FUNCTION_CALL_PATTERN = re.compile(r'([a-zA-Z0-9_:.]+)\(\)$')
83
83
  CHRONO_PATTERN = re.compile(r'std::chrono::([a-zA-Z]+)\s*\{\s*(\d+)\s*\}')
84
84
  CHRONO_PAREN_PATTERN = re.compile(r'(?:std::)?chrono::([a-zA-Z]+)\s*\(\s*([^)]+)\s*\)')
85
85
  TIME_UNIT_PATTERN = re.compile(r'(\d+)\s*(min|s|ms|h)')
@@ -159,6 +159,15 @@ class ConstexprCache:
159
159
  re.compile(r'const\s+model::ns\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*"([^"]+)"\s*\)'),
160
160
  ]
161
161
 
162
+ # Member accessors on inline constants used in property defaults,
163
+ # for example model::schema_registry_internal_tp.topic() -> "_schemas".
164
+ # Each entry maps a variable declaration to the member name whose call
165
+ # returns the captured string, cached under "<variable>.<member>".
166
+ member_accessor_patterns = [
167
+ # Pattern: inline const model::topic_partition name{model::topic{"value"}, ...}
168
+ (re.compile(r'inline\s+const\s+(?:model::)?topic_partition\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*\{\s*(?:model::)?topic\{\s*"([^"]+)"\s*\}'), 'topic'),
169
+ ]
170
+
162
171
  # Legacy specific patterns (kept for compatibility, but general patterns should cover these)
163
172
  function_patterns = {}
164
173
 
@@ -206,6 +215,17 @@ class ConstexprCache:
206
215
  qualified_name = f"{namespace}::{func_name}"
207
216
  self.function_cache[qualified_name] = func_value
208
217
 
218
+ # Extract member accessors on inline constants
219
+ for pattern, member in member_accessor_patterns:
220
+ for match in pattern.finditer(content):
221
+ var_name = match.group(1)
222
+ value = match.group(2)
223
+ key = f"{var_name}.{member}"
224
+ namespace = self._extract_namespace_for_function(content, match.start())
225
+ self.function_cache[key] = value
226
+ if namespace:
227
+ self.function_cache[f"{namespace}::{key}"] = value
228
+
209
229
  # Legacy: Extract function definitions from hardcoded patterns (if any)
210
230
  for func_name, patterns in function_patterns.items():
211
231
  for pattern in patterns:
@@ -301,16 +321,35 @@ _constexpr_cache = ConstexprCache()
301
321
  _type_definitions_cache = {}
302
322
 
303
323
  # Import topic property extractor
324
+ # Set from --path in main() so every consumer of the Redpanda source uses the
325
+ # tree the caller pointed at, regardless of the current working directory. The
326
+ # hardcoded search below is only a fallback for direct module use.
327
+ _redpanda_source_override = None
328
+
329
+
330
+ def set_redpanda_source(path):
331
+ """Record the Redpanda source directory passed via --path."""
332
+ global _redpanda_source_override
333
+ _redpanda_source_override = path
334
+
335
+
304
336
  def find_redpanda_source():
305
337
  """
306
- Locate the Redpanda source directory by searching standard locations.
307
-
308
- The property extractor looks for the Redpanda source code in multiple
309
- locations to handle different execution contexts (project root, tools directory, etc.).
310
-
338
+ Locate the Redpanda source directory.
339
+
340
+ Prefers the path passed via --path (recorded by set_redpanda_source);
341
+ falls back to searching standard locations relative to the current
342
+ working directory. Before this preference existed, running the extractor
343
+ from any directory that did not match a hardcoded guess silently
344
+ disabled the ConstantResolver: no validator enums, no enterprise enum
345
+ metadata, no constexpr resolution, with only a debug-level log line.
346
+
311
347
  Returns:
312
348
  str or None: Path to the Redpanda source directory if found, None otherwise.
313
349
  """
350
+ if _redpanda_source_override and os.path.exists(_redpanda_source_override):
351
+ return _redpanda_source_override
352
+
314
353
  redpanda_source_paths = [
315
354
  'tmp/redpanda', # Current directory
316
355
  '../tmp/redpanda', # Parent directory
@@ -705,6 +744,13 @@ def transform_files_with_properties(files_with_properties):
705
744
  if src_v_path.exists():
706
745
  constant_resolver = ConstantResolver(src_v_path)
707
746
  logger.debug(f"Initialized ConstantResolver with path: {src_v_path}")
747
+ if constant_resolver is None:
748
+ logger.warning(
749
+ "Redpanda source directory not found: validator enum extraction "
750
+ "and enterprise enum metadata are DISABLED for this run. "
751
+ "Properties like sasl_mechanisms will be missing items.enum and "
752
+ "x-enum-metadata. Pass --path to the Redpanda checkout to fix this."
753
+ )
708
754
 
709
755
  transformers = [
710
756
  EnterpriseTransformer(), ## this must be the first, as it modifies current data
@@ -922,6 +968,32 @@ def merge_properties_and_definitions(properties, definitions):
922
968
  return dict(properties=properties, definitions=definitions)
923
969
 
924
970
 
971
+ # Phantom stub entries created by apply_property_overrides during the current
972
+ # run. An entry is recorded whenever an override key matches no extracted
973
+ # property and a placeholder doc entry is fabricated from the override alone.
974
+ # Tracked at module level so the run summary in main() can report them all in
975
+ # one place at the end of the run.
976
+ phantom_stub_entries = []
977
+
978
+
979
+ def report_phantom_stubs():
980
+ """Log one prominent warning block listing every phantom stub created from overrides."""
981
+ if not phantom_stub_entries:
982
+ return
983
+ logger.warning("=" * 70)
984
+ logger.warning(
985
+ f"⚠️ {len(phantom_stub_entries)} override "
986
+ f"{'entry' if len(phantom_stub_entries) == 1 else 'entries'} matched no extracted property"
987
+ )
988
+ for entry in phantom_stub_entries:
989
+ logger.warning(
990
+ f"Override key '{entry['name']}' matched no extracted property — created a stub entry "
991
+ f"with config_scope '{entry['config_scope']}'. If the property was renamed or removed, "
992
+ f"update docs-data/property-overrides.json."
993
+ )
994
+ logger.warning("=" * 70)
995
+
996
+
925
997
  def apply_property_overrides(properties, overrides, overrides_file_path=None):
926
998
  """
927
999
  Apply overrides from an overrides mapping to the extracted properties, mutating and returning the properties dictionary.
@@ -941,6 +1013,7 @@ def apply_property_overrides(properties, overrides, overrides_file_path=None):
941
1013
  Returns:
942
1014
  dict: The same properties mapping with overrides applied and any new properties created.
943
1015
  """
1016
+ phantom_stub_entries.clear()
944
1017
  if overrides and "properties" in overrides:
945
1018
  for prop, override in overrides["properties"].items():
946
1019
  # First check if property exists by key
@@ -962,7 +1035,13 @@ def apply_property_overrides(properties, overrides, overrides_file_path=None):
962
1035
  else:
963
1036
  # Create new property from override
964
1037
  logger.info(f"Creating new property from override: {prop}")
965
- properties[prop] = _create_property_from_override(prop, override, overrides_file_path)
1038
+ new_property = _create_property_from_override(prop, override, overrides_file_path)
1039
+ properties[prop] = new_property
1040
+ # Record the phantom stub so the run summary can flag it loudly
1041
+ phantom_stub_entries.append({
1042
+ "name": prop,
1043
+ "config_scope": new_property.get("config_scope"),
1044
+ })
966
1045
  return properties
967
1046
 
968
1047
 
@@ -2856,6 +2935,11 @@ def main():
2856
2935
  treesitter_dir, destination_path
2857
2936
  )
2858
2937
 
2938
+ # Every consumer of the Redpanda source (ConstantResolver, constexpr
2939
+ # fallback search) must use the tree the caller pointed at, not a
2940
+ # cwd-relative guess.
2941
+ set_redpanda_source(options.path)
2942
+
2859
2943
  # Pre-build constexpr cache for performance
2860
2944
  # This avoids repeated filesystem walks when resolving C++ identifiers and function calls
2861
2945
  logger.info("🔧 Building constexpr identifier cache...")
@@ -3053,5 +3137,16 @@ def main():
3053
3137
  logging.error(f"Failed to write enhanced output file: {e}")
3054
3138
  sys.exit(1)
3055
3139
 
3140
+ # Surface phantom stubs prominently at the end of the run. These are
3141
+ # override keys that matched no extracted property, so a placeholder doc
3142
+ # entry was fabricated from the override alone.
3143
+ if phantom_stub_entries:
3144
+ report_phantom_stubs()
3145
+ phantom_names = ", ".join(entry["name"] for entry in phantom_stub_entries)
3146
+ print(
3147
+ f"⚠️ Phantom stub entries created from overrides: "
3148
+ f"{len(phantom_stub_entries)} ({phantom_names})"
3149
+ )
3150
+
3056
3151
  if __name__ == "__main__":
3057
3152
  main()