Skip to content

Commit 878b28b

Browse files
eandreeva-twrclaude
andcommitted
ci(develop): mirror prod MDX validation checks
Adds the lint-mdx and check-mdx-parse jobs to the development deploy workflow so MDX regressions (missing blank line after imports, missing client:load imports, parse errors) fail fast before the Astro build. Both scripts copied verbatim from main; build-en and build-locale now depend on both checks. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 217f656 commit 878b28b

3 files changed

Lines changed: 413 additions & 0 deletions

File tree

.github/workflows/s3-deploy-development.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,38 @@ on:
44
branches:
55
- develop
66
jobs:
7+
# Fast-fail static checks for translator-introduced regressions
8+
# (missing blank line after imports, missing client:load imports).
9+
lint-mdx:
10+
name: Lint MDX
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: 20
17+
- run: node scripts/lint-mdx.mjs
18+
19+
# MDX parse sweep: compiles every .mdx file with the same remark plugins
20+
# Astro uses, surfaces every parse error in one pass. Astro's locale
21+
# builds halt at the first error, so without this gate broken files
22+
# stay hidden behind whichever one CI happens to hit first.
23+
check-mdx-parse:
24+
name: Check MDX parse
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- uses: actions/setup-node@v4
29+
with:
30+
node-version: 20
31+
cache: 'npm'
32+
- run: npm ci
33+
env:
34+
PUPPETEER_SKIP_DOWNLOAD: '1'
35+
- run: node scripts/check-mdx-parse.mjs
36+
737
build-en:
38+
needs: [lint-mdx, check-mdx-parse]
839
runs-on: ubuntu-latest
940
concurrency:
1041
group: build-en-${{ github.workflow }}-${{ github.ref }}
@@ -45,6 +76,7 @@ jobs:
4576
retention-days: 1
4677

4778
build-locale:
79+
needs: [lint-mdx, check-mdx-parse]
4880
runs-on: ubuntu-latest
4981
strategy:
5082
matrix:

scripts/check-mdx-parse.mjs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
#!/usr/bin/env node
2+
/**
3+
* MDX parse sweep — compiles every .mdx in the content tree using Astro's
4+
* MDX-relevant remark plugins (remark-directive, remark-aside) and reports
5+
* every parse error in one pass instead of bailing at the first.
6+
*
7+
* Why: Astro's locale builds halt at the first MDX parse error and surface
8+
* a single failure per CI run. When several broken files exist (typical
9+
* after a buggy translator pass), only one is visible; the rest stay
10+
* hidden until each preceding error is fixed and the deploy retried. This
11+
* check surfaces the whole set up front.
12+
*
13+
* Run: `node scripts/check-mdx-parse.mjs`
14+
* Exits non-zero if any file fails to compile.
15+
*/
16+
17+
import fs from 'node:fs/promises';
18+
import path from 'node:path';
19+
import { compile } from '@mdx-js/mdx';
20+
import remarkDirective from 'remark-directive';
21+
import { remarkAside } from '../src/plugins/remark-aside.mjs';
22+
23+
const ROOT = process.cwd();
24+
const SCAN_DIRS = ['src/content/docs', 'src/locales', 'src/components/reusable'];
25+
26+
// Compile in chunks to avoid spinning up too many parsers in parallel on
27+
// constrained CI runners.
28+
const CONCURRENCY = 8;
29+
30+
async function* walk(dir) {
31+
let entries;
32+
try {
33+
entries = await fs.readdir(dir, { withFileTypes: true });
34+
} catch {
35+
return;
36+
}
37+
for (const e of entries) {
38+
if (e.name.startsWith('.') || e.name === 'node_modules') continue;
39+
const full = path.join(dir, e.name);
40+
if (e.isDirectory()) yield* walk(full);
41+
else if (e.isFile() && full.endsWith('.mdx')) yield full;
42+
}
43+
}
44+
45+
async function checkFile(file) {
46+
try {
47+
await compile(await fs.readFile(file, 'utf-8'), {
48+
jsx: true,
49+
remarkPlugins: [remarkDirective, remarkAside],
50+
});
51+
return null;
52+
} catch (err) {
53+
return {
54+
file: path.relative(ROOT, file),
55+
message: err.message.split('\n')[0],
56+
line: err.place?.start?.line ?? null,
57+
column: err.place?.start?.column ?? null,
58+
};
59+
}
60+
}
61+
62+
const files = [];
63+
for (const dir of SCAN_DIRS) {
64+
for await (const f of walk(path.join(ROOT, dir))) files.push(f);
65+
}
66+
67+
const issues = [];
68+
for (let i = 0; i < files.length; i += CONCURRENCY) {
69+
const chunk = files.slice(i, i + CONCURRENCY);
70+
const results = await Promise.all(chunk.map(checkFile));
71+
for (const r of results) if (r) issues.push(r);
72+
}
73+
74+
issues.sort((a, b) => a.file.localeCompare(b.file));
75+
76+
if (issues.length === 0) {
77+
console.log(`check-mdx-parse: ${files.length} file(s) parsed cleanly`);
78+
process.exit(0);
79+
}
80+
81+
console.error(`check-mdx-parse: ${files.length} scanned, ${issues.length} parse error(s):\n`);
82+
for (const i of issues) {
83+
const loc = i.line ? `${i.file}:${i.line}${i.column ? ':' + i.column : ''}` : i.file;
84+
console.error(` ${loc}`);
85+
console.error(` ${i.message}\n`);
86+
}
87+
process.exit(1);

0 commit comments

Comments
 (0)