Skip to content

Commit 5a19d68

Browse files
authored
Extract developer guide report summaries to script (#4058)
* Extract developer guide report summaries to script * Fix Vale package reference for developer guide linting * Use available Vale style packages * Handle non-zero Vale exit status * Allow summarize_reports output flag after subcommand * Improve developer guide lint reporting * Fix developer guide lint parsing and Java setup * Switch developer guide linting to asciidoctor-lint * Use Asciidoctor diagnostics for developer guide linting * Handle multiline GitHub outputs in report summarizer
1 parent 5afc04f commit 5a19d68

File tree

7 files changed

+751
-9
lines changed

7 files changed

+751
-9
lines changed

.github/workflows/developer-guide-docs.yml

Lines changed: 186 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,30 @@ jobs:
8484

8585
- name: Install Asciidoctor tooling
8686
run: |
87-
gem install --no-document asciidoctor asciidoctor-pdf
87+
gem install --no-document asciidoctor asciidoctor-pdf rouge
88+
89+
- name: Run Asciidoctor lint
90+
run: |
91+
set -euo pipefail
92+
REPORT_DIR="build/developer-guide/reports"
93+
REPORT_FILE="${REPORT_DIR}/asciidoc-lint-report.txt"
94+
mkdir -p "$REPORT_DIR"
95+
set +e
96+
asciidoctor \
97+
--require rouge \
98+
--failure-level WARN \
99+
--verbose \
100+
--trace \
101+
-o /dev/null \
102+
docs/developer-guide/developer-guide.asciidoc \
103+
2>&1 | tee "$REPORT_FILE"
104+
STATUS=${PIPESTATUS[0]}
105+
set -e
106+
echo "ASCII_DOC_LINT_REPORT=$REPORT_FILE" >> "$GITHUB_ENV"
107+
echo "ASCII_DOC_LINT_STATUS=$STATUS" >> "$GITHUB_ENV"
108+
if [ "$STATUS" -ne 0 ]; then
109+
echo "Asciidoctor exited with status $STATUS" >&2
110+
fi
88111
89112
- name: Build Developer Guide HTML and PDF
90113
run: |
@@ -135,6 +158,76 @@ jobs:
135158
rm -f "$GENERATED_COVER_SVG"
136159
fi
137160
161+
- name: Install Vale
162+
run: |
163+
set -euo pipefail
164+
VALE_VERSION="3.13.0"
165+
VALE_ARCHIVE="vale_${VALE_VERSION}_Linux_64-bit.tar.gz"
166+
curl -fsSL -o "$VALE_ARCHIVE" "https://github.com/errata-ai/vale/releases/download/v${VALE_VERSION}/${VALE_ARCHIVE}"
167+
tar -xzf "$VALE_ARCHIVE"
168+
sudo mv vale /usr/local/bin/vale
169+
rm -f "$VALE_ARCHIVE"
170+
171+
- name: Sync Vale styles
172+
run: |
173+
set -euo pipefail
174+
vale sync --config docs/developer-guide/.vale.ini
175+
176+
- name: Run Vale style linter
177+
run: |
178+
set -euo pipefail
179+
REPORT_DIR="build/developer-guide/reports"
180+
REPORT_FILE="${REPORT_DIR}/vale-report.json"
181+
HTML_REPORT="${REPORT_DIR}/vale-report.html"
182+
mkdir -p "$REPORT_DIR"
183+
set +e
184+
vale --config docs/developer-guide/.vale.ini --output=JSON docs/developer-guide > "$REPORT_FILE"
185+
STATUS=$?
186+
set -e
187+
python3 scripts/developer-guide/vale_report_to_html.py --input "$REPORT_FILE" --output "$HTML_REPORT"
188+
echo "VALE_REPORT=$REPORT_FILE" >> "$GITHUB_ENV"
189+
echo "VALE_HTML_REPORT=$HTML_REPORT" >> "$GITHUB_ENV"
190+
echo "VALE_STATUS=$STATUS" >> "$GITHUB_ENV"
191+
if [ "$STATUS" -ne 0 ]; then
192+
echo "Vale exited with status $STATUS" >&2
193+
fi
194+
195+
- name: Check for unused developer guide images
196+
run: |
197+
set -euo pipefail
198+
REPORT_DIR="build/developer-guide/reports"
199+
JSON_REPORT="${REPORT_DIR}/unused-images.json"
200+
TEXT_REPORT="${REPORT_DIR}/unused-images.txt"
201+
mkdir -p "$REPORT_DIR"
202+
python3 scripts/developer-guide/find_unused_images.py docs/developer-guide --output "$JSON_REPORT" | tee "$TEXT_REPORT"
203+
echo "UNUSED_IMAGES_JSON=$JSON_REPORT" >> "$GITHUB_ENV"
204+
echo "UNUSED_IMAGES_TEXT=$TEXT_REPORT" >> "$GITHUB_ENV"
205+
206+
- name: Summarize AsciiDoc linter findings
207+
id: summarize_asciidoc_lint
208+
run: |
209+
python3 scripts/developer-guide/summarize_reports.py ascii \
210+
--report "${ASCII_DOC_LINT_REPORT}" \
211+
--status "${ASCII_DOC_LINT_STATUS:-0}" \
212+
--output "${GITHUB_OUTPUT}"
213+
214+
- name: Summarize Vale findings
215+
id: summarize_vale
216+
run: |
217+
python3 scripts/developer-guide/summarize_reports.py vale \
218+
--report "${VALE_REPORT}" \
219+
--status "${VALE_STATUS:-0}" \
220+
--output "${GITHUB_OUTPUT}"
221+
222+
- name: Summarize unused image findings
223+
id: summarize_unused_images
224+
run: |
225+
python3 scripts/developer-guide/summarize_reports.py unused-images \
226+
--report "${UNUSED_IMAGES_JSON}" \
227+
--output "${GITHUB_OUTPUT}" \
228+
--details-key details \
229+
--preview-limit 10
230+
138231
- name: Upload HTML artifact
139232
uses: actions/upload-artifact@v4
140233
with:
@@ -149,9 +242,39 @@ jobs:
149242
path: build/developer-guide/pdf/developer-guide.pdf
150243
if-no-files-found: error
151244

245+
- name: Upload AsciiDoc linter report
246+
uses: actions/upload-artifact@v4
247+
with:
248+
name: developer-guide-asciidoc-lint
249+
path: ${{ env.ASCII_DOC_LINT_REPORT }}
250+
if-no-files-found: warn
251+
252+
- name: Upload Vale report
253+
uses: actions/upload-artifact@v4
254+
with:
255+
name: developer-guide-vale-report
256+
path: |
257+
${{ env.VALE_REPORT }}
258+
${{ env.VALE_HTML_REPORT }}
259+
if-no-files-found: warn
260+
261+
- name: Upload unused image report
262+
uses: actions/upload-artifact@v4
263+
with:
264+
name: developer-guide-unused-images
265+
path: |
266+
${{ env.UNUSED_IMAGES_JSON }}
267+
${{ env.UNUSED_IMAGES_TEXT }}
268+
if-no-files-found: warn
269+
152270
- name: Comment with artifact download links
153271
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
154272
uses: actions/github-script@v7
273+
env:
274+
ASCII_SUMMARY: ${{ steps.summarize_asciidoc_lint.outputs.summary }}
275+
VALE_SUMMARY: ${{ steps.summarize_vale.outputs.summary }}
276+
UNUSED_SUMMARY: ${{ steps.summarize_unused_images.outputs.summary }}
277+
UNUSED_DETAILS: ${{ steps.summarize_unused_images.outputs.details }}
155278
with:
156279
github-token: ${{ secrets.GITHUB_TOKEN }}
157280
script: |
@@ -183,22 +306,76 @@ jobs:
183306
per_page: 100
184307
});
185308
186-
const links = [];
309+
const artifactLinks = new Map();
187310
for (const artifact of artifacts.data.artifacts) {
188-
if (artifact.name === 'developer-guide-html') {
189-
links.push(`- [Developer Guide HTML package](https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id})`);
190-
}
191-
if (artifact.name === 'developer-guide-pdf') {
192-
links.push(`- [Developer Guide PDF](https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id})`);
193-
}
311+
artifactLinks.set(
312+
artifact.name,
313+
`https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${artifact.id}`
314+
);
315+
}
316+
317+
const links = [];
318+
if (artifactLinks.has('developer-guide-html')) {
319+
links.push(`- [Developer Guide HTML package](${artifactLinks.get('developer-guide-html')})`);
320+
}
321+
if (artifactLinks.has('developer-guide-pdf')) {
322+
links.push(`- [Developer Guide PDF](${artifactLinks.get('developer-guide-pdf')})`);
323+
}
324+
if (artifactLinks.has('developer-guide-asciidoc-lint')) {
325+
links.push(`- [AsciiDoc linter report](${artifactLinks.get('developer-guide-asciidoc-lint')})`);
326+
}
327+
if (artifactLinks.has('developer-guide-vale-report')) {
328+
links.push(`- [Vale report](${artifactLinks.get('developer-guide-vale-report')})`);
329+
}
330+
if (artifactLinks.has('developer-guide-unused-images')) {
331+
links.push(`- [Unused image report](${artifactLinks.get('developer-guide-unused-images')})`);
194332
}
195333
196334
if (!links.length) {
197335
console.log('No artifacts found to report.');
198336
return;
199337
}
200338
201-
const body = `${marker}\nDeveloper Guide build artifacts are available for download from this workflow run:\n\n${links.join('\n')}\n`;
339+
const qualityLines = [];
340+
const asciiSummary = process.env.ASCII_SUMMARY?.trim();
341+
const valeSummary = process.env.VALE_SUMMARY?.trim();
342+
const unusedSummary = process.env.UNUSED_SUMMARY?.trim();
343+
const asciiLink = artifactLinks.get('developer-guide-asciidoc-lint');
344+
const valeLink = artifactLinks.get('developer-guide-vale-report');
345+
const unusedLink = artifactLinks.get('developer-guide-unused-images');
346+
347+
if (asciiSummary) {
348+
qualityLines.push(`- AsciiDoc linter: ${asciiSummary}${asciiLink ? ` ([report](${asciiLink}))` : ''}`);
349+
}
350+
if (valeSummary) {
351+
qualityLines.push(`- Vale: ${valeSummary}${valeLink ? ` ([report](${valeLink}))` : ''}`);
352+
}
353+
if (unusedSummary) {
354+
qualityLines.push(`- Image references: ${unusedSummary}${unusedLink ? ` ([report](${unusedLink}))` : ''}`);
355+
}
356+
357+
let unusedDetails = process.env.UNUSED_DETAILS ? process.env.UNUSED_DETAILS.split('\n') : [];
358+
unusedDetails = unusedDetails.filter(Boolean);
359+
const detailsSection = unusedDetails.length
360+
? `\nUnused image preview:\n\n${unusedDetails.map(line => ` ${line}`).join('\n')}\n`
361+
: '';
362+
363+
const sections = [
364+
`${marker}`,
365+
'Developer Guide build artifacts are available for download from this workflow run:',
366+
'',
367+
links.join('\n')
368+
];
369+
370+
if (qualityLines.length) {
371+
sections.push('', 'Developer Guide quality checks:', '', qualityLines.join('\n'));
372+
}
373+
374+
if (detailsSection) {
375+
sections.push(detailsSection.trimEnd());
376+
}
377+
378+
const body = sections.join('\n') + '\n';
202379
const comments = await github.rest.issues.listComments({
203380
owner,
204381
repo,

docs/developer-guide/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
book-cover.generated.svg
22
book-cover.generated.png
3+
styles/

docs/developer-guide/.vale.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
StylesPath = styles
2+
MinAlertLevel = suggestion
3+
Packages = https://github.com/errata-ai/packages/releases/download/v0.2.0/Microsoft.zip, https://github.com/errata-ai/packages/releases/download/v0.2.0/proselint.zip, https://github.com/errata-ai/packages/releases/download/v0.2.0/write-good.zip
4+
5+
[*.{adoc,asciidoc}]
6+
BasedOnStyles = Microsoft, proselint, write-good

docs/developer-guide/Config.groovy

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
outputPath = 'build'
2+
3+
inputPath = '.'
4+
5+
inputFiles = [
6+
[file: 'developer-guide.asciidoc', formats: ['html']]
7+
]
8+
9+
imageDirs = [
10+
'img'
11+
]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
"""Identify unreferenced images in the developer guide."""
3+
from __future__ import annotations
4+
5+
import argparse
6+
import json
7+
from pathlib import Path
8+
from typing import Iterable, List
9+
10+
ASCIIDOC_EXTENSIONS = {".adoc", ".asciidoc"}
11+
12+
13+
def iter_text_files(root: Path) -> Iterable[Path]:
14+
for path in root.rglob("*"):
15+
if path.is_file() and path.suffix.lower() in ASCIIDOC_EXTENSIONS:
16+
yield path
17+
18+
19+
def main() -> None:
20+
parser = argparse.ArgumentParser(description=__doc__)
21+
parser.add_argument("doc_root", type=Path, help="Path to the developer guide root directory")
22+
parser.add_argument(
23+
"--image-dir",
24+
type=Path,
25+
default=None,
26+
help="Directory containing images (defaults to <doc_root>/img)",
27+
)
28+
parser.add_argument(
29+
"--output",
30+
type=Path,
31+
default=None,
32+
help="Optional path to write a JSON report",
33+
)
34+
args = parser.parse_args()
35+
36+
doc_root = args.doc_root.resolve()
37+
image_dir = (args.image_dir or (doc_root / "img")).resolve()
38+
39+
if not image_dir.exists():
40+
raise SystemExit(f"Image directory '{image_dir}' does not exist")
41+
42+
adoc_files = list(iter_text_files(doc_root))
43+
contents = [path.read_text(encoding="utf-8", errors="ignore") for path in adoc_files]
44+
45+
unused: List[str] = []
46+
for image_path in sorted(image_dir.rglob("*")):
47+
if not image_path.is_file():
48+
continue
49+
rel_path = image_path.relative_to(doc_root).as_posix()
50+
if any(rel_path in text for text in contents):
51+
continue
52+
# Also fall back to checking just the file name to catch references that rely on imagesdir.
53+
filename = image_path.name
54+
if any(filename in text for text in contents):
55+
continue
56+
unused.append(rel_path)
57+
58+
report = {"unused_images": unused}
59+
60+
if args.output:
61+
args.output.parent.mkdir(parents=True, exist_ok=True)
62+
args.output.write_text(json.dumps(report, indent=2), encoding="utf-8")
63+
64+
if unused:
65+
print("Unused images detected:")
66+
for rel_path in unused:
67+
print(f" - {rel_path}")
68+
else:
69+
print("No unused images found.")
70+
71+
72+
if __name__ == "__main__":
73+
main()

0 commit comments

Comments
 (0)