@redpanda-data/docs-extensions-and-macros 5.56.1 → 5.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/doc-tools.js
CHANGED
|
@@ -2132,6 +2132,11 @@ automation
|
|
|
2132
2132
|
* bullets — it is a CANDIDATE for the curation step that rewrites voice, adds
|
|
2133
2133
|
* `Area::` labels, normalizes units, and phrases CVEs.
|
|
2134
2134
|
*
|
|
2135
|
+
* In phase 1, a backticked name that matches a property in the release
|
|
2136
|
+
* series' redpanda-properties-<tag>.json becomes a `prop:<name>[link=true]` link. The
|
|
2137
|
+
* file is found in modules/reference/attachments/ unless --properties-file
|
|
2138
|
+
* names one. With no file, names stay monospace and a warning is logged.
|
|
2139
|
+
*
|
|
2135
2140
|
* @why
|
|
2136
2141
|
* Release notes are the one reference surface whose source of truth is
|
|
2137
2142
|
* engineer-authored prose rather than code, so the deterministic part
|
|
@@ -2164,6 +2169,7 @@ automation
|
|
|
2164
2169
|
.option('--section-file <file>', 'Phase 2: a curated section (from the sm-release-notes skill) to insert (relative to repo root, must stay inside the repository)')
|
|
2165
2170
|
.option('--date <date>', 'Authored release date as YYYY-MM-DD (required with --body; confirmed on the PR)')
|
|
2166
2171
|
.option('--page <file>', 'Target release-notes page (relative to repo root, must stay inside the repository)', 'modules/reference/pages/releases/redpanda.adoc')
|
|
2172
|
+
.option('--properties-file <file>', 'Phase 1: property JSON whose names become prop: links (relative to repo root, must stay inside the repository). Default: the newest redpanda-properties-<tag>.json in modules/reference/attachments/ for the tag\'s release series')
|
|
2167
2173
|
.option('--section-only', 'Phase 1 only: print the candidate section and do not read or modify the page')
|
|
2168
2174
|
.option('--dry-run', 'Print output to stdout instead of writing the page')
|
|
2169
2175
|
.action(async (options) => {
|
|
@@ -2172,6 +2178,11 @@ automation
|
|
|
2172
2178
|
buildReleaseSection,
|
|
2173
2179
|
isGaTag,
|
|
2174
2180
|
} = require('../tools/redpanda-release-notes/generate-release-notes.js')
|
|
2181
|
+
const {
|
|
2182
|
+
ATTACHMENTS_DIR,
|
|
2183
|
+
findPropertiesFile,
|
|
2184
|
+
loadPropertyNames,
|
|
2185
|
+
} = require('../tools/redpanda-release-notes/property-names.js')
|
|
2175
2186
|
try {
|
|
2176
2187
|
const repoRoot = findRepoRoot()
|
|
2177
2188
|
|
|
@@ -2192,6 +2203,19 @@ automation
|
|
|
2192
2203
|
return abs
|
|
2193
2204
|
}
|
|
2194
2205
|
|
|
2206
|
+
// Phase 1 links known property names. An explicit --properties-file must
|
|
2207
|
+
// exist; a missing default only means names stay monospace.
|
|
2208
|
+
const readPropertyNames = () => {
|
|
2209
|
+
const file = options.propertiesFile
|
|
2210
|
+
? readInside(options.propertiesFile, '--properties-file')
|
|
2211
|
+
: findPropertiesFile(path.join(repoRoot, ATTACHMENTS_DIR), options.tag)
|
|
2212
|
+
if (!file) {
|
|
2213
|
+
console.warn(`[release-notes] WARN: no property file for ${options.tag} in ${ATTACHMENTS_DIR}; property names stay monospace.`)
|
|
2214
|
+
return undefined
|
|
2215
|
+
}
|
|
2216
|
+
return loadPropertyNames(file)
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2195
2219
|
// Phase-1 candidate preview: emit the candidate for the curation step. No
|
|
2196
2220
|
// page is read or written — this is the LLM step's input, nothing else.
|
|
2197
2221
|
if (options.sectionOnly) {
|
|
@@ -2203,7 +2227,9 @@ automation
|
|
|
2203
2227
|
return
|
|
2204
2228
|
}
|
|
2205
2229
|
const body = fs.readFileSync(readInside(options.body, '--body'), 'utf8')
|
|
2206
|
-
const section = buildReleaseSection({
|
|
2230
|
+
const section = buildReleaseSection({
|
|
2231
|
+
body, version: options.tag, date: options.date, propertyNames: readPropertyNames(),
|
|
2232
|
+
})
|
|
2207
2233
|
process.stdout.write(section)
|
|
2208
2234
|
return
|
|
2209
2235
|
}
|
|
@@ -2213,6 +2239,7 @@ automation
|
|
|
2213
2239
|
const genArgs = { tag: options.tag, date: options.date, pageContent }
|
|
2214
2240
|
if (hasBody) {
|
|
2215
2241
|
genArgs.body = fs.readFileSync(readInside(options.body, '--body'), 'utf8')
|
|
2242
|
+
genArgs.propertyNames = readPropertyNames()
|
|
2216
2243
|
} else {
|
|
2217
2244
|
genArgs.section = fs.readFileSync(readInside(options.sectionFile, '--section-file'), 'utf8')
|
|
2218
2245
|
}
|
package/package.json
CHANGED
|
@@ -132,48 +132,90 @@ function insertReleaseSection(pageContent, sectionText) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* unchanged — only the order of entries within a category changes. Running it on
|
|
142
|
-
* an already-clustered section is a no-op, so it is safe to apply on every
|
|
143
|
-
* release and on a backfill.
|
|
135
|
+
* Groups the entries within each `=== category` of a curated section by their
|
|
136
|
+
* `Area::` label. Areas are ordered ALPHABETICALLY (case-insensitive), so a given
|
|
137
|
+
* area sits in the same place on every release instead of moving with the
|
|
138
|
+
* source's merge order; entries keep their source order within an area, so the
|
|
139
|
+
* sort is stable. Each area is written ONCE per category, as the definition-list
|
|
140
|
+
* term, with its entries as a bulleted list under it:
|
|
144
141
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
142
|
+
* Cloud Topics::
|
|
143
|
+
* * First entry.
|
|
144
|
+
* * Second entry.
|
|
145
|
+
*
|
|
146
|
+
* An area with one entry still gets a one-item list. Input entries may be in
|
|
147
|
+
* either form: single-line `Area:: description` (what curation writes) or an
|
|
148
|
+
* already-grouped `Area::` paragraph with single-line `* ` items. A category can
|
|
149
|
+
* mix the two. Running it on its own output is a no-op, so it is safe to apply on
|
|
150
|
+
* every release, on a backfill, and on a section grouped by hand.
|
|
151
|
+
*
|
|
152
|
+
* A category with any other kind of entry (a phase-1 candidate's plain `*`
|
|
153
|
+
* bullets, or a multi-line entry) is not grouped, so this is a safe no-op on
|
|
154
|
+
* anything it does not recognize. Its entries keep their order and text. The
|
|
155
|
+
* one change is that single-line entries on adjacent lines come out with a
|
|
156
|
+
* blank line between them, which AsciiDoc renders the same way. Line endings
|
|
157
|
+
* in the output are LF. A curated category (one with at least
|
|
158
|
+
* one `Area::` entry) that is left as it is logs a warning, because it then
|
|
159
|
+
* ships ungrouped. Area keys are trimmed, so `Kafka API` and `Kafka API ` are one
|
|
160
|
+
* area, written as the trimmed label. Headings and any prose outside a category
|
|
161
|
+
* are passed through untouched.
|
|
153
162
|
*
|
|
154
163
|
* @param {string} sectionText - A rendered release section.
|
|
155
|
-
* @return {string} The section with each category's entries
|
|
164
|
+
* @return {string} The section with each category's entries grouped by area,
|
|
156
165
|
* ending in a single newline.
|
|
157
166
|
*/
|
|
158
167
|
function clusterSectionByArea(sectionText) {
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
// that first separator, so a `::` later in the description is
|
|
162
|
-
// leading list marker (`*`, `-`, `.`), at column zero or
|
|
163
|
-
// so a plain bullet that happens to carry a `::`
|
|
164
|
-
// is not mistaken for an area entry
|
|
165
|
-
|
|
166
|
-
|
|
168
|
+
// A single-line entry: the area label is the text up to the first `:: `
|
|
169
|
+
// (double-colon + space), the AsciiDoc definition-list term separator. A lazy
|
|
170
|
+
// match stops at that first separator, so a `::` later in the description is
|
|
171
|
+
// left alone. A leading list marker (`*`, `-`, `.`), at column zero or
|
|
172
|
+
// indented, is rejected so a plain bullet that happens to carry a `::` (a
|
|
173
|
+
// phase-1 candidate line) is not mistaken for an area entry.
|
|
174
|
+
const ENTRY_RE = /^(?!\s*[*.\-]+\s)(.+?)::\s+(\S.*)$/;
|
|
175
|
+
// A grouped area: a term line with nothing after the `::`.
|
|
176
|
+
const TERM_RE = /^(?!\s*[*.\-]+\s)(.+?)::\s*$/;
|
|
177
|
+
// An item of a grouped area: one `* ` bullet on one line.
|
|
178
|
+
const ITEM_RE = /^\* (\S.*)$/;
|
|
179
|
+
|
|
180
|
+
// Parses one paragraph into [area, items], or null if it is not a recognized
|
|
181
|
+
// entry. A grouped paragraph must be a term line followed only by items.
|
|
182
|
+
const parseEntry = (para) => {
|
|
183
|
+
if (para.length === 1) {
|
|
184
|
+
const m = para[0].match(ENTRY_RE);
|
|
185
|
+
if (m) return [m[1].trim(), [m[2]]];
|
|
186
|
+
}
|
|
187
|
+
const t = para[0].match(TERM_RE);
|
|
188
|
+
if (t && para.length > 1) {
|
|
189
|
+
const items = [];
|
|
190
|
+
for (const line of para.slice(1)) {
|
|
191
|
+
const i = line.match(ITEM_RE);
|
|
192
|
+
if (!i) return null;
|
|
193
|
+
items.push(i[1]);
|
|
194
|
+
}
|
|
195
|
+
return [t[1].trim(), items];
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
};
|
|
199
|
+
const looksCurated = (para) => ENTRY_RE.test(para[0]) || TERM_RE.test(para[0]);
|
|
167
200
|
|
|
168
201
|
// Tokenize into paragraphs: maximal runs of non-blank lines. In this format
|
|
169
|
-
// every heading and every
|
|
170
|
-
|
|
202
|
+
// every heading, every single-line entry, and every grouped area is one such
|
|
203
|
+
// paragraph. Single-line entries on adjacent lines, with no blank line
|
|
204
|
+
// between them, are valid AsciiDoc too, so each one starts its own paragraph.
|
|
205
|
+
// A grouped area's term line does not match ENTRY_RE, so its bullets stay in
|
|
206
|
+
// one paragraph with it. CRLF and CR line endings become LF first: `.` in
|
|
207
|
+
// the patterns above does not match `\r`, so a CRLF line would match nothing.
|
|
208
|
+
const lines = String(sectionText).replace(/\r\n?/g, '\n').replace(/\n+$/, '').split('\n');
|
|
171
209
|
const paragraphs = [];
|
|
172
210
|
let current = [];
|
|
173
211
|
for (const line of lines) {
|
|
174
212
|
if (/^\s*$/.test(line)) {
|
|
175
213
|
if (current.length) { paragraphs.push(current); current = []; }
|
|
176
214
|
} else {
|
|
215
|
+
if (current.length === 1 && ENTRY_RE.test(current[0]) && ENTRY_RE.test(line)) {
|
|
216
|
+
paragraphs.push(current);
|
|
217
|
+
current = [];
|
|
218
|
+
}
|
|
177
219
|
current.push(line);
|
|
178
220
|
}
|
|
179
221
|
}
|
|
@@ -181,38 +223,35 @@ function clusterSectionByArea(sectionText) {
|
|
|
181
223
|
|
|
182
224
|
const out = [];
|
|
183
225
|
let categoryEntries = null; // entry paragraphs of the open `=== category`
|
|
184
|
-
let categoryBail = false; // the open category has an unrecognized entry
|
|
185
226
|
let categoryTitle = ''; // the open category's heading text, for warnings
|
|
186
227
|
|
|
187
228
|
const flushCategory = () => {
|
|
188
229
|
if (!categoryEntries) return;
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
230
|
+
const parsed = categoryEntries.map(parseEntry);
|
|
231
|
+
if (parsed.some((e) => e === null)) {
|
|
232
|
+
// A phase-1 bullet category is expected to be left as it is. A curated one
|
|
233
|
+
// (it has at least one `Area::` entry) left as it is means some entry is
|
|
234
|
+
// neither form, so the whole category ships ungrouped. Say so rather than
|
|
235
|
+
// fail quietly.
|
|
236
|
+
if (categoryEntries.some(looksCurated)) {
|
|
237
|
+
console.warn(`${LOG_TAG} WARN: "${categoryTitle}" not grouped by area: an entry is neither a single-line \`Area:: description\` nor an \`Area::\` term with single-line \`* \` items (for example a \`+\` continuation or a code block).`);
|
|
195
238
|
}
|
|
196
239
|
for (const entry of categoryEntries) out.push(entry);
|
|
197
240
|
} else {
|
|
198
241
|
const order = [];
|
|
199
242
|
const groups = new Map();
|
|
200
|
-
for (const
|
|
201
|
-
// Trimmed, so a stray space before `::` does not split one area in two.
|
|
202
|
-
// Only the grouping key is trimmed; the entry text is emitted verbatim.
|
|
203
|
-
const key = entry[0].match(ENTRY_RE)[1].trim();
|
|
243
|
+
for (const [key, items] of parsed) {
|
|
204
244
|
if (!groups.has(key)) { groups.set(key, []); order.push(key); }
|
|
205
|
-
groups.get(key).push(
|
|
245
|
+
groups.get(key).push(...items);
|
|
206
246
|
}
|
|
207
247
|
// Areas alphabetical (case-insensitive), so an area lands in the same
|
|
208
248
|
// place on every release; entries keep their source order within an area.
|
|
209
249
|
order.sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
|
|
210
250
|
for (const key of order) {
|
|
211
|
-
|
|
251
|
+
out.push([`${key}::`, ...groups.get(key).map((item) => `* ${item}`)]);
|
|
212
252
|
}
|
|
213
253
|
}
|
|
214
254
|
categoryEntries = null;
|
|
215
|
-
categoryBail = false;
|
|
216
255
|
};
|
|
217
256
|
|
|
218
257
|
for (const para of paragraphs) {
|
|
@@ -222,7 +261,6 @@ function clusterSectionByArea(sectionText) {
|
|
|
222
261
|
flushCategory();
|
|
223
262
|
out.push(para);
|
|
224
263
|
categoryEntries = [];
|
|
225
|
-
categoryBail = false;
|
|
226
264
|
categoryTitle = first.replace(/^===\s+/, '').trim();
|
|
227
265
|
} else if (/^==\s/.test(first)) {
|
|
228
266
|
// The release heading (or any other top-level heading) closes the open
|
|
@@ -232,7 +270,6 @@ function clusterSectionByArea(sectionText) {
|
|
|
232
270
|
categoryEntries = null;
|
|
233
271
|
} else if (categoryEntries) {
|
|
234
272
|
categoryEntries.push(para);
|
|
235
|
-
if (para.length !== 1 || !ENTRY_RE.test(para[0])) categoryBail = true;
|
|
236
273
|
} else {
|
|
237
274
|
// Prose outside any category (for example an intro paragraph): untouched.
|
|
238
275
|
out.push(para);
|
|
@@ -250,10 +287,11 @@ function clusterSectionByArea(sectionText) {
|
|
|
250
287
|
* @param {string} options.body - The raw rpchangelog release body markdown.
|
|
251
288
|
* @param {string} options.version - The release version (`v26.2.3` or `26.2.3`).
|
|
252
289
|
* @param {string} options.date - The release date as `YYYY-MM-DD`.
|
|
290
|
+
* @param {Set<string>} [options.propertyNames] - Known property names to link as `prop:` macros.
|
|
253
291
|
* @return {string} The rendered AsciiDoc section.
|
|
254
292
|
*/
|
|
255
|
-
function buildReleaseSection({ body, version, date }) {
|
|
256
|
-
const { sections } = parseReleaseBody(body);
|
|
293
|
+
function buildReleaseSection({ body, version, date, propertyNames }) {
|
|
294
|
+
const { sections } = parseReleaseBody(body, { propertyNames });
|
|
257
295
|
return renderReleaseSection({ version, date, sections });
|
|
258
296
|
}
|
|
259
297
|
|
|
@@ -306,9 +344,10 @@ function assertSectionMatchesTag(section, version) {
|
|
|
306
344
|
* @param {string} options.tag - The release tag (`v26.2.3`).
|
|
307
345
|
* @param {string} [options.date] - The authored release date as `YYYY-MM-DD` (phase 1 only).
|
|
308
346
|
* @param {string} options.pageContent - The current target page content.
|
|
347
|
+
* @param {Set<string>} [options.propertyNames] - Known property names to link as `prop:` macros (phase 1 only).
|
|
309
348
|
* @return {{status: 'ok'|'skipped', reason?: string, section?: string, content?: string}}
|
|
310
349
|
*/
|
|
311
|
-
function generateReleaseNotes({ body, section, tag, date, pageContent }) {
|
|
350
|
+
function generateReleaseNotes({ body, section, tag, date, pageContent, propertyNames }) {
|
|
312
351
|
if (body == null && section == null) {
|
|
313
352
|
throw new Error('Provide either body (phase 1) or section (phase 2).');
|
|
314
353
|
}
|
|
@@ -337,7 +376,7 @@ function generateReleaseNotes({ body, section, tag, date, pageContent }) {
|
|
|
337
376
|
// have to, and so a hand-written section is normalized the same way.
|
|
338
377
|
finalSection = clusterSectionByArea(section);
|
|
339
378
|
} else {
|
|
340
|
-
finalSection = buildReleaseSection({ body, version, date });
|
|
379
|
+
finalSection = buildReleaseSection({ body, version, date, propertyNames });
|
|
341
380
|
}
|
|
342
381
|
const content = insertReleaseSection(pageContent, finalSection);
|
|
343
382
|
return { status: 'ok', section: finalSection, content };
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
//
|
|
9
9
|
// This module does ONLY the deterministic, no-judgment work: split into
|
|
10
10
|
// sections, join wrapped bullet lines, strip source noise, unescape backticks,
|
|
11
|
-
// de-duplicate backport repeats, and
|
|
11
|
+
// link known configuration properties, de-duplicate backport repeats, and
|
|
12
|
+
// normalize category order/case. It does NOT
|
|
12
13
|
// rewrite voice, add `Area::` labels, normalize units, phrase CVEs, or drop
|
|
13
14
|
// bodyless entries — those need editorial judgment and belong to the curation
|
|
14
15
|
// step that runs on the generated page.
|
|
@@ -88,18 +89,43 @@ function stripLeadingIssueRef(text) {
|
|
|
88
89
|
* the editorial curation step.
|
|
89
90
|
*
|
|
90
91
|
* @param {string} raw - The joined raw bullet text.
|
|
92
|
+
* @param {Object} [options]
|
|
93
|
+
* @param {Set<string>} [options.propertyNames] - Known property names to link (see linkProperties).
|
|
91
94
|
* @return {string} The cleaned entry text.
|
|
92
95
|
*/
|
|
93
|
-
function cleanEntry(raw) {
|
|
96
|
+
function cleanEntry(raw, { propertyNames } = {}) {
|
|
94
97
|
let text = raw.replace(/\s+/g, ' ').trim();
|
|
95
98
|
text = stripLeadingIssueRef(text);
|
|
96
99
|
text = stripAttribution(text);
|
|
97
100
|
text = text.replace(/\\`/g, '`');
|
|
101
|
+
text = linkProperties(text, propertyNames);
|
|
98
102
|
text = normalizeUnits(text);
|
|
99
103
|
text = escapeAsciiDocBraces(text);
|
|
100
104
|
return text.trim();
|
|
101
105
|
}
|
|
102
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Rewrites a backticked configuration property name as a `prop:` macro with
|
|
109
|
+
* `link=true`, so the page links it to the property reference. Without
|
|
110
|
+
* `link=true`, the macro shows only a hover tooltip. Engineers cannot write `prop:` in
|
|
111
|
+
* the source, because nothing resolves it outside the docs build, so this is
|
|
112
|
+
* where the link is added.
|
|
113
|
+
*
|
|
114
|
+
* Only an exact match for a known name is rewritten: `` `retention.ms` `` is,
|
|
115
|
+
* `` `retention.ms=1000` `` and `` `health_manager` `` are not. A name with no
|
|
116
|
+
* `_` or `.` (such as `admin` or `rack`) is never rewritten, because in prose
|
|
117
|
+
* those words usually mean something other than the property.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} text - The entry text, with backticks already unescaped.
|
|
120
|
+
* @param {Set<string>} [propertyNames] - Known property names. Without it, the text is returned unchanged.
|
|
121
|
+
* @return {string} The text with known property names as `prop:<name>[link=true]`.
|
|
122
|
+
*/
|
|
123
|
+
function linkProperties(text, propertyNames) {
|
|
124
|
+
if (!propertyNames || propertyNames.size === 0) return text;
|
|
125
|
+
return text.replace(/`([\w.-]+)`/g, (match, name) =>
|
|
126
|
+
/[_.]/.test(name) && propertyNames.has(name) ? `prop:${name}[link=true]` : match);
|
|
127
|
+
}
|
|
128
|
+
|
|
103
129
|
/**
|
|
104
130
|
* Rewrites the source's underscore data-size notation to the readable unit the
|
|
105
131
|
* standard requires (`4_MiB` -> `4 MiB`). Only a digit followed by `_` and a
|
|
@@ -131,11 +157,13 @@ function escapeAsciiDocBraces(text) {
|
|
|
131
157
|
* grouped by canonical category in page order.
|
|
132
158
|
*
|
|
133
159
|
* @param {string} body - The raw GitHub Release body markdown.
|
|
160
|
+
* @param {Object} [options]
|
|
161
|
+
* @param {Set<string>} [options.propertyNames] - Known property names to link as `prop:` macros.
|
|
134
162
|
* @return {{sections: Array<{kind: string, title: string, entries: string[]}>}}
|
|
135
163
|
* Sections in page order (Features, Bug fixes, Improvements); a category with
|
|
136
164
|
* no entries is omitted.
|
|
137
165
|
*/
|
|
138
|
-
function parseReleaseBody(body) {
|
|
166
|
+
function parseReleaseBody(body, { propertyNames } = {}) {
|
|
139
167
|
if (typeof body !== 'string' || body.trim() === '') {
|
|
140
168
|
throw new Error('Release body is empty.');
|
|
141
169
|
}
|
|
@@ -155,7 +183,7 @@ function parseReleaseBody(body) {
|
|
|
155
183
|
|
|
156
184
|
const flush = () => {
|
|
157
185
|
if (current !== null && currentKind && buckets[currentKind]) {
|
|
158
|
-
const entry = cleanEntry(current);
|
|
186
|
+
const entry = cleanEntry(current, { propertyNames });
|
|
159
187
|
if (entry) buckets[currentKind].push(entry);
|
|
160
188
|
}
|
|
161
189
|
current = null;
|
|
@@ -231,6 +259,7 @@ module.exports = {
|
|
|
231
259
|
stripAttribution,
|
|
232
260
|
stripLeadingIssueRef,
|
|
233
261
|
cleanEntry,
|
|
262
|
+
linkProperties,
|
|
234
263
|
normalizeUnits,
|
|
235
264
|
escapeAsciiDocBraces,
|
|
236
265
|
PAGE_ORDER,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Load the configuration property names that the release-notes generator links
|
|
2
|
+
// as `prop:` macros. The names come from the same redpanda-properties-<tag>.json
|
|
3
|
+
// attachment that the prop macro validates against, so a name linked here is a
|
|
4
|
+
// name the docs build can resolve.
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
// The attachment directory, relative to the docs repository root.
|
|
10
|
+
const ATTACHMENTS_DIR = 'modules/reference/attachments';
|
|
11
|
+
const PROPERTIES_JSON_RX = /^redpanda-properties-v(\d+)\.(\d+)\.(\d+)\.json$/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the property names in a parsed property JSON file.
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} json - Parsed redpanda-properties-<tag>.json content.
|
|
17
|
+
* @return {Set<string>} The property names.
|
|
18
|
+
* @throws {Error} If the file has no `properties` object.
|
|
19
|
+
*/
|
|
20
|
+
function propertyNamesFromJson(json) {
|
|
21
|
+
if (!json || typeof json.properties !== 'object' || json.properties === null) {
|
|
22
|
+
throw new Error('Property file has no "properties" object.');
|
|
23
|
+
}
|
|
24
|
+
return new Set(Object.keys(json.properties));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Finds the newest GA property file for a tag's release series. A 26.2 tag
|
|
29
|
+
* uses 26.2 data only, the same rule the prop macro applies to a page, so a
|
|
30
|
+
* property from another release series is never linked.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} dir - The directory to search.
|
|
33
|
+
* @param {string} tag - The release tag (`v26.2.5` or `26.2.5`).
|
|
34
|
+
* @return {string|null} The path to the newest matching file, or null if there is none.
|
|
35
|
+
*/
|
|
36
|
+
function findPropertiesFile(dir, tag) {
|
|
37
|
+
const m = String(tag).match(/^v?(\d+)\.(\d+)\./);
|
|
38
|
+
if (!m || !fs.existsSync(dir)) return null;
|
|
39
|
+
const [, major, minor] = m;
|
|
40
|
+
let best = null;
|
|
41
|
+
for (const file of fs.readdirSync(dir)) {
|
|
42
|
+
const f = file.match(PROPERTIES_JSON_RX);
|
|
43
|
+
if (!f || f[1] !== major || f[2] !== minor) continue;
|
|
44
|
+
const patch = Number(f[3]);
|
|
45
|
+
if (!best || patch > best.patch) best = { patch, file };
|
|
46
|
+
}
|
|
47
|
+
return best ? path.join(dir, best.file) : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Reads the property names from a property JSON file.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} file - Path to a redpanda-properties-<tag>.json file.
|
|
54
|
+
* @return {Set<string>} The property names.
|
|
55
|
+
*/
|
|
56
|
+
function loadPropertyNames(file) {
|
|
57
|
+
return propertyNamesFromJson(JSON.parse(fs.readFileSync(file, 'utf8')));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = {
|
|
61
|
+
ATTACHMENTS_DIR,
|
|
62
|
+
propertyNamesFromJson,
|
|
63
|
+
findPropertiesFile,
|
|
64
|
+
loadPropertyNames,
|
|
65
|
+
};
|