@redpanda-data/docs-extensions-and-macros 5.53.0 → 5.55.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-mcp.js +4 -5
- package/bin/doc-tools.js +166 -2
- package/bin/mcp-tools/generated-docs-review.js +26 -9
- package/bin/mcp-tools/overrides-audit.js +1 -1
- package/bin/mcp-tools/rpcn-docs.js +4 -14
- package/cli-utils/format-helm-spec.js +4 -3
- package/cli-utils/helm-commented-values.js +88 -18
- package/cli-utils/self-managed-docs-branch.js +36 -30
- package/extension-utils/html-utils.js +27 -0
- package/extensions/REFERENCE.adoc +64 -12
- package/extensions/add-git-dates.js +13 -1
- package/extensions/aggregate-terms.js +34 -20
- package/extensions/generate-rp-connect-info.js +79 -24
- package/extensions/modify-connect-tag-playbook.js +498 -75
- package/extensions/resolve-xrefs-in-attachments.js +1 -12
- package/extensions/url-to-xref.js +3 -0
- package/extensions/util/connect-catalog.js +10 -5
- package/extensions/util/connect-docs-asset.js +188 -0
- package/extensions/util/connect-github.js +16 -0
- package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
- package/extensions/version-fetcher/list-connect-releases.js +10 -0
- package/extensions/version-fetcher/release-lines.js +64 -0
- package/extensions/version-fetcher/set-latest-version.js +73 -3
- package/macros/README.adoc +1 -0
- package/macros/REFERENCE.adoc +99 -0
- package/macros/badge.js +5 -8
- package/macros/enterprise.js +2 -5
- package/macros/glossary.js +4 -14
- package/macros/iceberg-explorer.js +242 -0
- package/macros/prop.js +3 -13
- package/mcp/USER_GUIDE.adoc +25 -135
- package/package.json +4 -2
- package/tools/connect-docs/check-build-log.js +311 -0
- package/tools/connect-docs/check-rendered-html.js +469 -0
- package/tools/connect-docs/ci-playbook.yml +77 -0
- package/tools/connect-docs/diff-generated.js +382 -0
- package/tools/connect-docs/pr-comment.js +104 -0
- package/tools/doc-impact-recording/index.js +190 -0
- package/tools/overrides-audit/README.adoc +1 -2
- package/tools/overrides-audit/adapters/properties.js +1 -1
- package/tools/overrides-audit/index.js +3 -4
- package/tools/redpanda-connect/AUTOMATION.md +3 -36
- package/tools/redpanda-connect/README.adoc +5 -33
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +3 -3
- package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
- package/tools/redpanda-connect/report-delta.js +6 -3
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +38 -16
- package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
- package/tools/rpk-docs/templates/command.hbs +1 -1
- package/tools/overrides-audit/adapters/connect.js +0 -144
package/mcp/USER_GUIDE.adoc
CHANGED
|
@@ -200,24 +200,6 @@ The MCP server provides *writer-friendly tools* designed for natural language in
|
|
|
200
200
|
* Requires knowledge of doc-tools CLI syntax
|
|
201
201
|
* Only works in repos that have doc-tools installed
|
|
202
202
|
|
|
203
|
-
=== Contextual guides (prompts)
|
|
204
|
-
|
|
205
|
-
*property-docs-guide*:: Comprehensive guide for updating Redpanda property documentation
|
|
206
|
-
+
|
|
207
|
-
* Explains the auto-generation system and override workflow
|
|
208
|
-
* Provides rules for property descriptions (no cloud conditionals, no enterprise includes)
|
|
209
|
-
* Lists common scenarios and validation steps
|
|
210
|
-
* Claude automatically retrieves this when working with property documentation
|
|
211
|
-
* Ensures correct workflow: update property-overrides.json, not generated .adoc files
|
|
212
|
-
|
|
213
|
-
*rpcn-connector-docs-guide*:: Comprehensive guide for updating Redpanda Connect connector reference documentation
|
|
214
|
-
+
|
|
215
|
-
* Explains the override system with `$ref` syntax for DRY (Don't Repeat Yourself) principles
|
|
216
|
-
* Describes repository structure and how autogenerated content works with overrides
|
|
217
|
-
* Provides workflow for improving connector documentation
|
|
218
|
-
* Claude automatically retrieves this when working with connector documentation
|
|
219
|
-
* Ensures correct workflow: update docs-data/overrides.json, use $ref for deduplication
|
|
220
|
-
|
|
221
203
|
=== Background job management
|
|
222
204
|
|
|
223
205
|
Long-running documentation generation tasks support background execution with progress updates:
|
|
@@ -265,130 +247,38 @@ Background jobs send real-time progress notifications via MCP protocol, so Claud
|
|
|
265
247
|
|
|
266
248
|
All commands have a 10-minute timeout (increased from 5 minutes) to handle large documentation generation tasks.
|
|
267
249
|
|
|
268
|
-
==
|
|
269
|
-
|
|
270
|
-
The MCP server provides *AI consistency prompts* that help Claude follow your team's documentation standards automatically. These prompts are pre-configured instructions that guide Claude's responses for common documentation tasks.
|
|
271
|
-
|
|
272
|
-
=== Documentation review prompts
|
|
273
|
-
|
|
274
|
-
*review-for-style*:: Review documentation content for style guide compliance
|
|
275
|
-
+
|
|
276
|
-
* Checks: Style guide compliance, terminology consistency, voice/tone, AsciiDoc formatting
|
|
277
|
-
* Input: Content to review (can be file path or raw text)
|
|
278
|
-
* Output: Detailed feedback with critical issues, suggestions, and actionable recommendations
|
|
279
|
-
* Use when: Reviewing any documentation before publishing
|
|
280
|
-
|
|
281
|
-
*check-terminology*:: Validate terminology usage against approved team terminology
|
|
282
|
-
+
|
|
283
|
-
* Checks: Incorrect terms, deprecated terms (whitelist/blacklist, master/slave), inconsistent usage
|
|
284
|
-
* Input: Content to check
|
|
285
|
-
* Output: Critical issues with corrections, inconsistencies found, missing glossary links
|
|
286
|
-
* Use when: Ensuring consistent terminology across documentation
|
|
287
|
-
|
|
288
|
-
*improve-clarity*:: Refactor existing documentation for clarity and readability
|
|
289
|
-
+
|
|
290
|
-
* Applies: Style guide principles, simplifies complex explanations, improves structure
|
|
291
|
-
* Input: Content to improve
|
|
292
|
-
* Output: Refactored content with explanations of changes made
|
|
293
|
-
* Use when: Content is technically correct but difficult to understand
|
|
294
|
-
|
|
295
|
-
=== Content creation prompts
|
|
296
|
-
|
|
297
|
-
*write-new-guide*:: Generate a new tutorial or guide following team standards
|
|
298
|
-
+
|
|
299
|
-
* Applies: Team templates, approved terminology, voice/tone guidelines, AsciiDoc formatting
|
|
300
|
-
* Input: Topic (what to write about), optional audience (beginners, experienced devs, operators)
|
|
301
|
-
* Output: Complete guide following team standards
|
|
302
|
-
* Use when: Starting a new tutorial, how-to, or getting started guide
|
|
303
|
-
|
|
304
|
-
=== Workflow guide prompts
|
|
305
|
-
|
|
306
|
-
*property-docs-guide*:: Comprehensive guide for updating Redpanda property documentation
|
|
307
|
-
+
|
|
308
|
-
* Explains: Auto-generation system, override workflow, validation steps
|
|
309
|
-
* Rules: Never edit generated `.adoc` files, always update `property-overrides.json`
|
|
310
|
-
* Use when: Claude is helping with property documentation
|
|
311
|
-
* Claude automatically retrieves this prompt when working with properties
|
|
312
|
-
|
|
313
|
-
*rpcn-connector-docs-guide*:: Guide for updating Redpanda Connect connector documentation
|
|
314
|
-
+
|
|
315
|
-
* Explains: Override system with `$ref` syntax for DRY principles, repository structure
|
|
316
|
-
* Rules: Update `docs-data/overrides.json`, use `$ref` for deduplication
|
|
317
|
-
* Use when: Claude is helping with connector documentation
|
|
318
|
-
* Claude automatically retrieves this prompt when working with connectors
|
|
319
|
-
|
|
320
|
-
=== How to use prompts
|
|
250
|
+
== Guidance from docs-team-standards
|
|
321
251
|
|
|
322
|
-
|
|
252
|
+
This server provides tools and one resource. It has no MCP prompts. Writing, review, and workflow guidance lives in the https://github.com/redpanda-data/docs-team-standards[docs-team-standards^] Claude Code plugin:
|
|
323
253
|
|
|
324
|
-
|
|
254
|
+
[cols="1,2"]
|
|
255
|
+
|===
|
|
256
|
+
|Task |docs-team-standards
|
|
325
257
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
Claude: *retrieves and applies the review-for-style prompt*
|
|
329
|
-
*provides structured feedback following the prompt instructions*
|
|
330
|
-
----
|
|
331
|
-
|
|
332
|
-
*Implicit prompt usage:*
|
|
333
|
-
|
|
334
|
-
----
|
|
335
|
-
You: "Review this tutorial for our style guide"
|
|
336
|
-
Claude: *automatically uses review-for-style prompt*
|
|
337
|
-
*provides feedback based on your team standards*
|
|
338
|
-
----
|
|
339
|
-
|
|
340
|
-
*Listing available prompts:*
|
|
341
|
-
|
|
342
|
-
----
|
|
343
|
-
You: "List available prompts"
|
|
344
|
-
Claude: *shows all prompts with descriptions*
|
|
345
|
-
----
|
|
346
|
-
|
|
347
|
-
You can also preview prompts using the CLI:
|
|
258
|
+
|Review content for style, terminology, structure, and clarity
|
|
259
|
+
|The `review` skill, the `/review` command, or the `content-reviewer` agent
|
|
348
260
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
# Preview a review prompt
|
|
352
|
-
npx doc-tools preview-prompt review-for-style --content "Test content"
|
|
353
|
-
|
|
354
|
-
# Preview a write prompt
|
|
355
|
-
npx doc-tools preview-prompt write-new-guide --topic "Deploy cluster"
|
|
261
|
+
|Write a new page or guide
|
|
262
|
+
|The `page-templates` skill and the `/plan` command
|
|
356
263
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
264
|
+
|Update property documentation
|
|
265
|
+
|The `property-overrides` skill
|
|
266
|
+
|===
|
|
360
267
|
|
|
361
268
|
== Available resources
|
|
362
269
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
=== Style guide
|
|
366
|
-
|
|
367
|
-
*redpanda://style-guide*:: Redpanda Documentation Style Guide (v1.0.0)
|
|
270
|
+
*redpanda://personas*:: Target audience personas for the current repository
|
|
368
271
|
+
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* Contains: Terminology quick reference, formatting examples, common patterns
|
|
372
|
-
* Claude automatically references this when reviewing or writing documentation
|
|
272
|
+
* Loaded from `docs-data/personas.yaml` in your current working directory
|
|
273
|
+
* Defines reader types, goals, pain points, and content preferences
|
|
373
274
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
Resources are automatically loaded by prompts, but you can also reference them explicitly:
|
|
377
|
-
|
|
378
|
-
*Explicit reference:*
|
|
275
|
+
To read it, ask Claude directly:
|
|
379
276
|
|
|
380
277
|
----
|
|
381
|
-
You: "Read the redpanda://
|
|
382
|
-
Claude: *reads the style guide resource*
|
|
383
|
-
*explains the code block formatting rules*
|
|
278
|
+
You: "Read the redpanda://personas resource and summarize the target readers"
|
|
384
279
|
----
|
|
385
280
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
----
|
|
389
|
-
You: "List available resources"
|
|
390
|
-
Claude: *shows all resources with descriptions and versions*
|
|
391
|
-
----
|
|
281
|
+
The style guide lives in docs-team-standards, not in this server.
|
|
392
282
|
|
|
393
283
|
=== Design philosophy
|
|
394
284
|
|
|
@@ -972,12 +862,12 @@ Suggestion: Check that the version exists in the Redpanda repository. You can fi
|
|
|
972
862
|
https://github.com/redpanda-data/streaming-enterprise/releases
|
|
973
863
|
----
|
|
974
864
|
|
|
975
|
-
=== Example 7: Property documentation updates
|
|
865
|
+
=== Example 7: Property documentation updates
|
|
976
866
|
|
|
977
867
|
----
|
|
978
868
|
You: Improve the description for cleanup.policy property to explain what each value does
|
|
979
869
|
|
|
980
|
-
Claude: *
|
|
870
|
+
Claude: *uses the property-overrides skill from docs-team-standards*
|
|
981
871
|
*reads property-overrides.json*
|
|
982
872
|
*updates the description field with enhanced explanation*
|
|
983
873
|
*uses generate_property_docs MCP tool with generate_partials: true*
|
|
@@ -995,7 +885,7 @@ The description now explains:
|
|
|
995
885
|
- `compact,delete` - Compaction + time-based deletion
|
|
996
886
|
----
|
|
997
887
|
|
|
998
|
-
|
|
888
|
+
The skill provides the property documentation workflow, so Claude updates the overrides file, not the generated files.
|
|
999
889
|
|
|
1000
890
|
=== Example 8: Review documentation for style compliance
|
|
1001
891
|
|
|
@@ -1004,7 +894,7 @@ You: Review this tutorial for style guide compliance:
|
|
|
1004
894
|
|
|
1005
895
|
[Content of tutorial pasted here]
|
|
1006
896
|
|
|
1007
|
-
Claude: *uses review-
|
|
897
|
+
Claude: *uses the review skill from docs-team-standards*
|
|
1008
898
|
*analyzes content against team style guide*
|
|
1009
899
|
|
|
1010
900
|
Style Review for Tutorial
|
|
@@ -1042,7 +932,7 @@ Overall: Good structure, clear explanations. Fix critical issues before publishi
|
|
|
1042
932
|
----
|
|
1043
933
|
You: Check the terminology in this doc for any deprecated or incorrect terms
|
|
1044
934
|
|
|
1045
|
-
Claude: *uses
|
|
935
|
+
Claude: *uses the review skill from docs-team-standards*
|
|
1046
936
|
|
|
1047
937
|
Terminology Review
|
|
1048
938
|
==========================================
|
|
@@ -1070,7 +960,7 @@ Summary:
|
|
|
1070
960
|
----
|
|
1071
961
|
You: Write a getting started guide for deploying a 3-node Redpanda cluster
|
|
1072
962
|
|
|
1073
|
-
Claude: *uses
|
|
963
|
+
Claude: *uses the page-templates skill from docs-team-standards*
|
|
1074
964
|
*applies team templates and standards*
|
|
1075
965
|
|
|
1076
966
|
= Deploy a Three-Node Redpanda Cluster
|
|
@@ -1103,7 +993,7 @@ You: This explanation of compaction is too complex. Can you simplify it?
|
|
|
1103
993
|
|
|
1104
994
|
[Paste complex explanation]
|
|
1105
995
|
|
|
1106
|
-
Claude: *uses
|
|
996
|
+
Claude: *uses the review skill from docs-team-standards*
|
|
1107
997
|
|
|
1108
998
|
Here's a clearer version:
|
|
1109
999
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.55.0",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -94,7 +94,9 @@
|
|
|
94
94
|
"./extensions/property-page-index": "./extensions/property-page-index.js",
|
|
95
95
|
"./extensions/render-property-descriptions": "./extensions/render-property-descriptions.js",
|
|
96
96
|
"./extensions/kapa-source-groups": "./extensions/kapa-source-groups.js",
|
|
97
|
-
"./docs-data/kapa-source-groups.json": "./docs-data/kapa-source-groups.json"
|
|
97
|
+
"./docs-data/kapa-source-groups.json": "./docs-data/kapa-source-groups.json",
|
|
98
|
+
"./macros/iceberg-explorer": "./macros/iceberg-explorer.js",
|
|
99
|
+
"./macros/iceberg-explorer.js": "./macros/iceberg-explorer.js"
|
|
98
100
|
},
|
|
99
101
|
"files": [
|
|
100
102
|
"extensions",
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* doc-tools check-build-log: the blocking gate of the Connect docs PR check.
|
|
8
|
+
*
|
|
9
|
+
* Antora's own --log-failure-level makes Antora exit non-zero, but it says
|
|
10
|
+
* nothing about WHAT failed in a form a job summary can show, and it cannot
|
|
11
|
+
* catch the build that exits 0 having written no pages at all (Antora on an
|
|
12
|
+
* unsupported Node version does exactly that). So the workflow runs Antora
|
|
13
|
+
* with --log-failure-level=fatal and --log-format json, and this command
|
|
14
|
+
* decides:
|
|
15
|
+
*
|
|
16
|
+
* - any record at level error or fatal fails the check. With
|
|
17
|
+
* --blocking-sources, an error fails it only when it comes from one of
|
|
18
|
+
* those repositories (its own source or any page in its include chain),
|
|
19
|
+
* or has no source at all (an extension error); errors from the other
|
|
20
|
+
* sources are listed but do not block. Fatal records always block,
|
|
21
|
+
* - with --min-pages and --site-dir, fewer HTML pages than expected fails it,
|
|
22
|
+
* - warnings never fail it; they are listed in the summary.
|
|
23
|
+
*
|
|
24
|
+
* The summary is Markdown, written to stdout, for $GITHUB_STEP_SUMMARY.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const LEVELS = ['fatal', 'error', 'warn', 'info', 'debug', 'trace']
|
|
28
|
+
const BLOCKING_LEVELS = new Set(['fatal', 'error'])
|
|
29
|
+
// pino's numeric levels, in case a log was written without level labels.
|
|
30
|
+
const NUMERIC_LEVELS = { 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal' }
|
|
31
|
+
|
|
32
|
+
const DEFAULTS = Object.freeze({
|
|
33
|
+
maxGroups: 50,
|
|
34
|
+
maxLocations: 5
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
function normalizeLevel (level) {
|
|
38
|
+
if (typeof level === 'number') return NUMERIC_LEVELS[level] || String(level)
|
|
39
|
+
return String(level || 'info').toLowerCase()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A human-readable location for one log record: the source repository and
|
|
44
|
+
* ref, then the file and line. Local worktree paths are made relative to the
|
|
45
|
+
* worktree so the same file reads the same on every runner.
|
|
46
|
+
*/
|
|
47
|
+
function relativePath (file, source) {
|
|
48
|
+
let filePath = (file && file.path) || ''
|
|
49
|
+
if (filePath && source && source.worktree && path.isAbsolute(filePath)) {
|
|
50
|
+
const rel = path.relative(source.worktree, filePath)
|
|
51
|
+
if (!rel.startsWith('..')) filePath = rel
|
|
52
|
+
}
|
|
53
|
+
return filePath
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** owner/name of a source URL, without the host or .git. */
|
|
57
|
+
function repoSlug (url) {
|
|
58
|
+
return String(url || '').replace(/\.git$/, '').replace(/^[a-z]+:\/\/[^/]+\//i, '').replace(/^git@[^:]+:/, '')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function locationOf (record) {
|
|
62
|
+
const file = record.file || {}
|
|
63
|
+
const source = record.source || {}
|
|
64
|
+
if (!file.path && !source.url) return null
|
|
65
|
+
const filePath = relativePath(file, source)
|
|
66
|
+
let where = ''
|
|
67
|
+
if (source.url) {
|
|
68
|
+
const repo = repoSlug(source.url)
|
|
69
|
+
where = source.refname ? `${repo}@${source.refname}` : repo
|
|
70
|
+
if (source.startPath) where += `/${source.startPath}`
|
|
71
|
+
}
|
|
72
|
+
let loc = filePath
|
|
73
|
+
if (loc && file.line) loc += `:${file.line}`
|
|
74
|
+
// The include chain: the page that pulled the file in, which is the useful
|
|
75
|
+
// location when the problem is in a shared partial.
|
|
76
|
+
const via = Array.isArray(record.stack) && record.stack.length
|
|
77
|
+
? record.stack.map((s) => {
|
|
78
|
+
const p = relativePath(s.file, s.source)
|
|
79
|
+
return p ? `${p}${s.file.line ? `:${s.file.line}` : ''}` : null
|
|
80
|
+
}).filter(Boolean)
|
|
81
|
+
: []
|
|
82
|
+
let out = [where, loc].filter(Boolean).join(': ')
|
|
83
|
+
if (via.length) out += ` (included from ${via.join(' < ')})`
|
|
84
|
+
return out || null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function messageOf (record) {
|
|
88
|
+
if (record.msg) return String(record.msg)
|
|
89
|
+
if (record.err && record.err.message) return String(record.err.message)
|
|
90
|
+
if (record.message) return String(record.message)
|
|
91
|
+
return '(no message)'
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse an Antora JSON (NDJSON) log. Lines that are not JSON objects (stray
|
|
96
|
+
* stderr, a progress bar) are counted, not fatal: a log is still checkable
|
|
97
|
+
* when something else wrote to the same file.
|
|
98
|
+
*/
|
|
99
|
+
function parseLog (text) {
|
|
100
|
+
const records = []
|
|
101
|
+
let unparsed = 0
|
|
102
|
+
for (const line of String(text).split('\n')) {
|
|
103
|
+
const trimmed = line.trim()
|
|
104
|
+
if (!trimmed) continue
|
|
105
|
+
let obj
|
|
106
|
+
try {
|
|
107
|
+
obj = JSON.parse(trimmed)
|
|
108
|
+
} catch {
|
|
109
|
+
unparsed++
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
113
|
+
unparsed++
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
records.push(obj)
|
|
117
|
+
}
|
|
118
|
+
return { records, unparsed }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function countHtmlPages (dir) {
|
|
122
|
+
let count = 0
|
|
123
|
+
const stack = [dir]
|
|
124
|
+
while (stack.length) {
|
|
125
|
+
const current = stack.pop()
|
|
126
|
+
let entries
|
|
127
|
+
try {
|
|
128
|
+
entries = fs.readdirSync(current, { withFileTypes: true })
|
|
129
|
+
} catch {
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const full = path.join(current, entry.name)
|
|
134
|
+
if (entry.isDirectory()) stack.push(full)
|
|
135
|
+
else if (entry.isFile() && entry.name.endsWith('.html')) count++
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return count
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Check a parsed log. Returns the counts, the grouped messages, and whether
|
|
143
|
+
* the check passed.
|
|
144
|
+
*/
|
|
145
|
+
/**
|
|
146
|
+
* Whether an error-level record blocks. Without a source list, every one
|
|
147
|
+
* does. With one, a record blocks when it has no source at all (an
|
|
148
|
+
* extension's own error), or when its source or any source in its include
|
|
149
|
+
* chain is one of the listed repositories.
|
|
150
|
+
*/
|
|
151
|
+
function isBlocking (record, level, blockingSources) {
|
|
152
|
+
if (level === 'fatal') return true
|
|
153
|
+
if (!blockingSources || !blockingSources.length) return true
|
|
154
|
+
const sources = [record.source, ...(Array.isArray(record.stack) ? record.stack.map((s) => s && s.source) : [])]
|
|
155
|
+
.filter((s) => s && s.url)
|
|
156
|
+
if (!sources.length) return true
|
|
157
|
+
const wanted = new Set(blockingSources.map((s) => repoSlug(s).toLowerCase()))
|
|
158
|
+
return sources.some((s) => wanted.has(repoSlug(s.url).toLowerCase()))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function checkBuildLog ({ text, siteDir, minPages, blockingSources } = {}) {
|
|
162
|
+
const { records, unparsed } = parseLog(text)
|
|
163
|
+
const counts = Object.fromEntries(LEVELS.map((l) => [l, 0]))
|
|
164
|
+
const groups = new Map()
|
|
165
|
+
let blocking = 0
|
|
166
|
+
for (const record of records) {
|
|
167
|
+
let level = normalizeLevel(record.level)
|
|
168
|
+
counts[level] = (counts[level] || 0) + 1
|
|
169
|
+
if (!BLOCKING_LEVELS.has(level) && level !== 'warn') continue
|
|
170
|
+
if (BLOCKING_LEVELS.has(level)) {
|
|
171
|
+
if (isBlocking(record, level, blockingSources)) blocking++
|
|
172
|
+
else level = 'error-elsewhere'
|
|
173
|
+
}
|
|
174
|
+
const msg = messageOf(record)
|
|
175
|
+
const key = `${level}\u0000${msg}`
|
|
176
|
+
let group = groups.get(key)
|
|
177
|
+
if (!group) {
|
|
178
|
+
group = { level, msg, count: 0, locations: [] }
|
|
179
|
+
groups.set(key, group)
|
|
180
|
+
}
|
|
181
|
+
group.count++
|
|
182
|
+
const loc = locationOf(record)
|
|
183
|
+
if (loc && !group.locations.includes(loc)) group.locations.push(loc)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const failures = []
|
|
187
|
+
if (blocking > 0) {
|
|
188
|
+
const scope = blockingSources && blockingSources.length ? ` from ${blockingSources.join(', ')}` : ''
|
|
189
|
+
failures.push(`${blocking} log ${blocking === 1 ? 'record' : 'records'} at level error or fatal${scope}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let pages = null
|
|
193
|
+
if (minPages != null) {
|
|
194
|
+
if (!siteDir) throw new Error('--min-pages needs --site-dir')
|
|
195
|
+
pages = fs.existsSync(siteDir) ? countHtmlPages(siteDir) : 0
|
|
196
|
+
if (pages < minPages) {
|
|
197
|
+
failures.push(`${pages} HTML ${pages === 1 ? 'page' : 'pages'} written under ${siteDir}, fewer than the required ${minPages}`)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const byLevel = (level) => [...groups.values()]
|
|
202
|
+
.filter((g) => g.level === level)
|
|
203
|
+
.sort((a, b) => b.count - a.count || a.msg.localeCompare(b.msg))
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
ok: failures.length === 0,
|
|
207
|
+
failures,
|
|
208
|
+
counts,
|
|
209
|
+
total: records.length,
|
|
210
|
+
unparsed,
|
|
211
|
+
pages,
|
|
212
|
+
minPages: minPages == null ? null : minPages,
|
|
213
|
+
blocking,
|
|
214
|
+
blockingSources: blockingSources && blockingSources.length ? blockingSources : null,
|
|
215
|
+
groups: {
|
|
216
|
+
fatal: byLevel('fatal'),
|
|
217
|
+
error: byLevel('error'),
|
|
218
|
+
errorElsewhere: byLevel('error-elsewhere'),
|
|
219
|
+
warn: byLevel('warn')
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Markdown table cells and inline code cannot hold a raw pipe or backtick
|
|
225
|
+
// run, and a message can contain both.
|
|
226
|
+
function mdInline (text) {
|
|
227
|
+
return String(text).replace(/\r?\n/g, ' ').replace(/\|/g, '\\|').replace(/</g, '<')
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function formatGroups (title, groups, { maxGroups, maxLocations }) {
|
|
231
|
+
if (!groups.length) return []
|
|
232
|
+
const lines = [`### ${title}`, '']
|
|
233
|
+
for (const g of groups.slice(0, maxGroups)) {
|
|
234
|
+
lines.push(`- ${g.count > 1 ? `(${g.count}x) ` : ''}${mdInline(g.msg)}`)
|
|
235
|
+
for (const loc of g.locations.slice(0, maxLocations)) lines.push(` - ${mdInline(loc)}`)
|
|
236
|
+
if (g.locations.length > maxLocations) lines.push(` - and ${g.locations.length - maxLocations} more locations`)
|
|
237
|
+
}
|
|
238
|
+
if (groups.length > maxGroups) lines.push(`- and ${groups.length - maxGroups} more distinct messages`)
|
|
239
|
+
lines.push('')
|
|
240
|
+
return lines
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function formatMarkdown (result, opts = {}) {
|
|
244
|
+
const { maxGroups, maxLocations } = { ...DEFAULTS, ...opts }
|
|
245
|
+
const lines = []
|
|
246
|
+
lines.push(`## Antora build log: ${result.ok ? 'passed' : 'failed'}`, '')
|
|
247
|
+
if (!result.ok) {
|
|
248
|
+
for (const f of result.failures) lines.push(`- **Blocking:** ${f}`)
|
|
249
|
+
lines.push('')
|
|
250
|
+
}
|
|
251
|
+
lines.push('| Level | Count |', '|---|---|')
|
|
252
|
+
for (const level of LEVELS) {
|
|
253
|
+
if (level === 'debug' || level === 'trace') {
|
|
254
|
+
if (!result.counts[level]) continue
|
|
255
|
+
}
|
|
256
|
+
lines.push(`| ${level} | ${result.counts[level] || 0} |`)
|
|
257
|
+
}
|
|
258
|
+
if (result.pages != null) lines.push(`| HTML pages | ${result.pages} (minimum ${result.minPages}) |`)
|
|
259
|
+
if (result.unparsed) lines.push(`| non-JSON lines | ${result.unparsed} |`)
|
|
260
|
+
lines.push('')
|
|
261
|
+
lines.push(...formatGroups('Fatal', result.groups.fatal, { maxGroups, maxLocations }))
|
|
262
|
+
lines.push(...formatGroups('Errors', result.groups.error, { maxGroups, maxLocations }))
|
|
263
|
+
if (result.groups.errorElsewhere.length) {
|
|
264
|
+
lines.push(...formatGroups(`Errors from other sources (do not block; only ${result.blockingSources.join(', ')} block)`, result.groups.errorElsewhere, { maxGroups, maxLocations }))
|
|
265
|
+
}
|
|
266
|
+
lines.push(...formatGroups('Warnings (do not block)', result.groups.warn, { maxGroups, maxLocations }))
|
|
267
|
+
return lines.join('\n')
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function intOption (value, flag) {
|
|
271
|
+
if (value == null) return undefined
|
|
272
|
+
const n = Number(value)
|
|
273
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`${flag} must be a non-negative integer, got ${value}`)
|
|
274
|
+
return n
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function runCli (logFile, options = {}) {
|
|
278
|
+
let result
|
|
279
|
+
try {
|
|
280
|
+
if (!logFile || !fs.existsSync(logFile)) throw new Error(`log file not found: ${logFile}`)
|
|
281
|
+
result = checkBuildLog({
|
|
282
|
+
text: fs.readFileSync(logFile, 'utf8'),
|
|
283
|
+
siteDir: options.siteDir,
|
|
284
|
+
minPages: intOption(options.minPages, '--min-pages'),
|
|
285
|
+
blockingSources: options.blockingSources
|
|
286
|
+
? String(options.blockingSources).split(',').map((x) => x.trim()).filter(Boolean)
|
|
287
|
+
: undefined
|
|
288
|
+
})
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error(`Error: ${err.message}`)
|
|
291
|
+
process.exit(2)
|
|
292
|
+
}
|
|
293
|
+
const out = options.format === 'json' ? JSON.stringify(result, null, 2) : formatMarkdown(result)
|
|
294
|
+
console.log(out)
|
|
295
|
+
if (options.output) {
|
|
296
|
+
fs.mkdirSync(path.dirname(path.resolve(options.output)), { recursive: true })
|
|
297
|
+
fs.writeFileSync(path.resolve(options.output), out + '\n')
|
|
298
|
+
}
|
|
299
|
+
process.exit(result.ok ? 0 : 1)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
DEFAULTS,
|
|
304
|
+
parseLog,
|
|
305
|
+
checkBuildLog,
|
|
306
|
+
countHtmlPages,
|
|
307
|
+
formatMarkdown,
|
|
308
|
+
locationOf,
|
|
309
|
+
isBlocking,
|
|
310
|
+
runCli
|
|
311
|
+
}
|