-
Notifications
You must be signed in to change notification settings - Fork 2
418 lines (401 loc) · 20.8 KB
/
Copy pathci.yml
File metadata and controls
418 lines (401 loc) · 20.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
# Cancel superseded runs on the same ref to save CI minutes.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
rust:
name: Rust — fmt, clippy, test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
with:
components: rustfmt, clippy
# mds-python (pyo3, abi3-py311) is compiled by `clippy --all-targets` and
# `test --workspace`; a 3.11+ interpreter makes the pyo3 build deterministic.
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- uses: Swatinem/rust-cache@v2
- name: Format check
run: cargo fmt --all --check
- name: Clippy (warnings are errors)
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
run: cargo test --workspace
# Intra-doc link gate (AC-209-11 / OD-209-G): broken intra-doc links in
# mds-core degrade silently to literal text without this check.
# --no-deps excludes dependency crates; -p mds-core scopes the check to mds-core.
- name: Rustdoc (warnings are errors)
run: RUSTDOCFLAGS="-D warnings" cargo doc -p mds-core --no-deps
msrv:
name: MSRV (Rust 1.88)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# rust-version = 1.88 is declared workspace-wide; verify the published
# crates still compile on it. napi/wasm bindings are checked on stable
# in their own jobs since their toolchain needs can differ.
- uses: dtolnay/rust-toolchain@2eae45db285e407f22119950686d47e1101e071b # 1.88
# mds-python is added to the MSRV gate: its pyo3/abi3 build must compile on
# the workspace floor (Rust 1.88 ≥ pyo3 0.28 MSRV 1.83). setup-python gives
# the pyo3 build script a 3.11+ interpreter.
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- uses: Swatinem/rust-cache@v2
- run: cargo check -p mds-core -p mds-cli -p mds-python
wasm:
name: WASM — build & test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Pinned to 1.96.0 (released 2026-05-28, >30-day soak per project convention).
# RATIONALE: unpinned `stable` lets compiler drift silently change the wasm-opt
# output size independently of any source change, making the 850 KB guard measure
# a different thing each week. Previous guard raises were partly caused by this.
# Scoped to this job only (not repo-wide rust-toolchain.toml) to limit blast radius.
# WHEN BUMPING: rebuild, re-measure with the new compiler, and update the
# budget-history ledger comment above with the new baseline before changing the pin.
- uses: dtolnay/rust-toolchain@01ba1edad32c6f80dbcce879d3e0fa5a00b2a84e # 1.96.0
with:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- uses: ./.github/actions/setup-wasm
- name: Build (nodejs + web targets)
run: |
wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg
wasm-pack build crates/mds-wasm --target web --out-dir pkg-web
- name: Smoke-test built artifact
run: |
node -e "
const m = require('./crates/mds-wasm/pkg/mds_wasm.js');
const r = m.compile('Hello!\n', null);
console.log('smoke ok:', r.output);
"
- name: Test (node)
run: wasm-pack test --node crates/mds-wasm
- name: Test (node, release — verifies wasm-opt)
run: wasm-pack test --node --release crates/mds-wasm
- name: Report WASM binary sizes
if: always()
run: |
for f in crates/mds-wasm/pkg/mds_wasm_bg.wasm crates/mds-wasm/pkg-web/mds_wasm_bg.wasm; do
if [ ! -f "$f" ]; then
echo "::error::WASM file missing: $f"
exit 1
fi
raw=$(wc -c < "$f" | tr -d ' ')
gz=$(gzip -c "$f" | wc -c | tr -d ' ')
label="${f#crates/mds-wasm/}"
echo "::notice::WASM ${label}: ${raw} bytes raw, ${gz} bytes gzipped"
# Soft bloat guard. Budget history:
# 600K initial; raised 600K->650K (2026-06): unpinned CI `stable` toolchain
# drifted past 600K with no source change (local emitted ~592K).
# raised 650K->700K (2026-07, PR #161 / fix/v0-4-0-149-154): toolchain
# drift + new v0.4.0 code (cross-type errors, extended help strings,
# @extends frontmatter merging) pushed CI binary to 650,745 bytes.
# raised 700K->750K (2026-07, #61 / feat/mds-lint-61 S2): 9-rule lint
# engine (AnalysisContext, rule dispatch, fix planner) pushed local
# optimized binary to 712,419 bytes; +50K headroom for CI toolchain drift.
# raised 750K->800K (2026-07-11, #61): post-S4 full lint surface (4 bindings + parity) measured 749,801 raw locally with bundled wasm-opt; 750K left ~200B margin vs CI Binaryen variance; raised to 800K (pre-authorized, #61).
# raised 800K->850K (2026-07-19, fix/v040-dogfood-remediation): v0.4.0
# remediation block-span fix planner, span-bearing parser errors, arity
# signature help pushed optimized binary to ~808K locally (wasm-opt v117
# bundled + release profile); +headroom for CI toolchain variance (CI uses
# Binaryen v129 which may differ).
# PR #294 (2026-08-14, ticket/pr1-lint-json-wire-contract): sort + stdin-label +
# name-span-anchor added +11,840 bytes; wave/v0.4.0-wave1 baseline 821,662,
# post-change 833,502 (wasm-pack 0.15.0 bundled wasm-opt, measured at HEAD
# after removing redundant to_canonical_json re-sort in 56424f7).
# Guard NOT raised: 16,498 bytes (1.94%) headroom. CI uses
# Binaryen v129 (distinct toolchain from local). (AC-P1-23)
# ticket/pr2-unknown-rule-names (2026-08-14, #224): unknown-rule-name warning
# engine (find_unknown_rule_names, format_unknown_rule_names_warning,
# attach_lint_warnings; three duplicated binding copies deleted) plus CLI
# warning body unification (ba117a6) added net +10,670 bytes from PR1
# baseline; PR1 baseline 833,502, post-change 844,172 (wasm-pack 0.15.0
# bundled wasm-opt, nodejs target, measured locally at HEAD ba117a6).
# Guard NOT raised: 5,828 bytes (0.69%) headroom. CI uses Binaryen v129
# (distinct toolchain from local). Three more wave PRs still to land. (AC-224-18)
# ticket/wasm-size-optimization (2026-08-16): wasm-opt flag tuning
# (-Oz --flatten --rereloop -Oz --converge --strip-producers) plus
# sort_unstable at four tie-free sites (value.rs, evaluator.rs,
# builtins.rs x2). PR2 baseline 844,172. CI-measured post-change:
# 836,126 bytes (Binaryen v129, CI run 31946611856). Local wasm-opt
# v117 (bundled by wasm-pack) reads roughly 2-3 KB higher (841,937
# was the local pre-commit estimate; CI number is authoritative).
# Net CI savings vs PR2 baseline: -8,046 bytes.
# Guard NOT raised: 13,874 bytes (1.63%) headroom.
# Toolchain now pinned to 1.96.0 in the wasm job (see companion commit).
# chore/deps-cargo-sweep-2026-09 (2026-09-06, PR #361): wasm-bindgen
# 0.2.121 -> 0.2.126, js-sys 0.3.98 -> 0.3.103 (wasm-bindgen-futures
# 0.4.76, wasm-bindgen-test 0.3.76); no Rust source change.
# Prior CI baseline: 836,126 bytes (ticket/wasm-size-optimization).
# CI-measured post-change: 839,456 bytes raw, 337,231 bytes gzipped
# (Binaryen v129, CI run 33995227961). Delta: +3,330 bytes (+0.40%).
# Guard NOT raised: 10,544 bytes (1.24%) headroom.
if [ "$raw" -gt 850000 ]; then
echo "::error::WASM binary exceeds 850,000 byte threshold: ${label} is ${raw} bytes"
exit 1
fi
done
js:
name: JS packages — build & test
# Run on every host OS so the native addon path is exercised everywhere.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
# DELIBERATE: @stable is intentionally not pinned to a SHA here. This is the
# only job that builds the native addon (ubuntu/macOS/Windows) against the
# stable compiler — providing the sole CI coverage of the compiler that actually
# ships in releases. Pinning to a fixed version (like the wasm job's 1.96.0)
# would silently remove coverage of the released compiler channel.
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
# Build the native addon for the host so @mdscript/mds resolves a backend.
# --no-js preserves the hand-maintained crates/mds-napi/index.js loader.
- name: Build native addon (host)
working-directory: crates/mds-napi
run: npx napi build --platform --release --no-js
# Build the unsuffixed mds-napi.node that __test__/index.spec.mjs loads
# directly via require('../mds-napi.node'). cargo is a cache hit here;
# napi just renames the already-built artifact without recompiling.
- name: Build native addon (unsuffixed, napi spec test)
run: npm run build:native -w @mdscript/mds-napi
# Build the WASM pkg so the WASM fallback (and wasm-backend tests) work.
- uses: ./.github/actions/setup-wasm
- name: Build WASM (nodejs)
run: wasm-pack build crates/mds-wasm --target nodejs --out-dir pkg
- name: Build TS packages
run: npm run build --workspaces --if-present
# Build the mds CLI binary so CF-SM2 can invoke it as the third parity
# surface. target/debug/mds is auto-discovered by findMdsCli(); no env
# var needed. The Rust toolchain + cache are already set up above, so
# this is an incremental build sharing the dep graph with the napi addon
# (avoids PF-007: CLI surface must actually run in CI, not skip silently).
- name: Build mds CLI (CF-SM2 parity producer)
run: cargo build -p mds-cli
# Install Python + the markdown_script binding so CF-SM2 can compare the Python
# output as the fourth parity surface (avoids PF-007). pip uses the
# maturin PEP 517 build backend declared in crates/mds-python/pyproject.toml;
# no pre-installed maturin needed. MDS_PYTHON_BIN is set to the exact
# executable that owns the installed module so findPythonForMarkdownScript()
# picks it up cross-platform (bin/ on Unix, Scripts/ on Windows).
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Install Python binding (CF-SM2 parity surface)
run: python -m pip install ./crates/mds-python
- name: Export MDS_PYTHON_BIN
shell: bash
run: |
PY=$(python -c "import sys; print(sys.executable)")
echo "MDS_PYTHON_BIN=$PY" >> "$GITHUB_ENV"
- name: Test
run: npm test --workspaces --if-present
python:
name: Python — build & test
# Build the abi3 extension and run the pytest suite on every host OS and on
# both the 3.11 floor and a recent release. (Wheel matrix + PyPI publishing is
# a deferred follow-up to #132; this job is dev/test only.)
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
python-version: ["3.11", "3.13"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
- name: Install test tooling
run: python -m pip install --upgrade pip pytest mypy pyright
# The mds CLI is the independent parity producer (Rust binary → mds-core);
# conftest auto-discovers target/debug/mds so the live parity tests run.
- name: Build mds CLI (parity producer)
run: cargo build -p mds-cli
# Build + install the extension via the maturin PEP 517 backend (maturin is
# pinned by build-system.requires). No virtualenv required on bare CI Python.
- name: Build & install the extension
run: python -m pip install ./crates/mds-python
- name: pytest
run: pytest crates/mds-python/tests -q -m "not perf"
# Wall-clock/throughput assertions are flake-prone on shared runners; keep them
# out of the gating step above and surface failures without redding the job.
- name: pytest (perf, advisory)
continue-on-error: true
run: pytest crates/mds-python/tests -q -m perf
# Build the CLI, compile every example that is expected to succeed, then assert
# that git sees no untracked files under examples/. If the compiler ever starts
# emitting a new extension that is not covered by the .gitignore catch-alls, this
# job fails with a concrete list of the uncovered files.
#
# What this catches: a new output extension added by the compiler that is not yet
# covered by .gitignore.
# What this does NOT catch: a hand-authored file being silently dropped by the
# catch-alls (that is a local-discipline concern; see the FOOTGUN WARNING in
# .gitignore).
#
# examples/stress-test/errors/ contains five intentionally-failing fixtures
# (bad-arity, bad-circular-a/b, bad-type, bad-undefined) — those are skipped.
examples-gitignore-coverage:
name: examples/ gitignore coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- name: Build mds CLI
run: cargo build -p mds-cli
- name: Compile examples (excluding intentionally-failing fixtures)
run: |
MDS=./target/debug/mds
# Directories that compile cleanly without runtime variables.
for dir in \
examples/ai-agent \
examples/api-docs \
examples/blog-generator \
examples/formatting \
examples/inheritance \
examples/linting \
examples/prompt-library \
examples/python \
examples/source-maps \
examples/watch \
examples/stress-test/agents \
examples/stress-test/edge \
examples/stress-test/lib \
examples/stress-test/shared; do
$MDS build "$dir"
done
# stress-test root entry point.
$MDS build examples/stress-test/main.mds
# edge-cases: supply vars.json so the runtime-vars example (08) compiles;
# vars are silently ignored by the other 30 templates.
$MDS build examples/edge-cases --vars examples/edge-cases/vars.json
- name: Assert all outputs are gitignored
run: |
UNTRACKED=$(git status --porcelain --untracked-files=all -- examples/)
if [ -n "$UNTRACKED" ]; then
echo "::error::examples/ has untracked files after mds build."
echo " A compiler output extension is not covered by .gitignore."
echo " Add a catch-all pattern AND any needed '!' exceptions."
echo ""
echo "$UNTRACKED"
exit 1
fi
echo "examples/ gitignore coverage check passed — all compiler outputs are gitignored."
python-wheel:
name: Python — wheel install smoke
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Install build tooling
run: python -m pip install --upgrade pip "maturin==1.13.3" pytest
- name: Build wheel
run: maturin build -m crates/mds-python/Cargo.toml --out dist
- name: Assert wheel is cp311-abi3
run: ls dist/ && ls dist/ | grep -q 'cp311-abi3' || (echo "expected a cp311-abi3 wheel" && exit 1)
- name: Install the built wheel (not editable) + import smoke
run: |
python -m pip install --find-links dist --no-index markdown-script
python -c "import markdown_script as m; r = m.compile('Hello {{n}}!', vars={'n': 'CI'}); assert r.kind == 'markdown', r.kind; assert r.output == 'Hello CI!\n', repr(r.output); print('smoke ok:', r.output)"
# Run the suite against the INSTALLED wheel (mypy/pyright deselected — they
# are covered in the develop job; parity CLI is built for the live check).
- name: Build mds CLI (parity producer)
run: cargo build -p mds-cli
- name: pytest against the installed wheel
run: pytest crates/mds-python/tests -q -m "not perf"
# Wall-clock/throughput assertions are flake-prone on shared runners; keep them
# out of the gating step above and surface failures without redding the job.
- name: pytest against the installed wheel (perf, advisory)
continue-on-error: true
run: pytest crates/mds-python/tests -q -m perf
# -------------------------------------------------------------------------
# #288: Source-hygiene gate — rejects hazardous codepoints (control bytes,
# bidi overrides, BOM) from tracked source. Scans the full tracked tree via
# `git ls-files`, reads content at codepoint level (pure Node; no grep -P
# which BSD grep lacks). Positive-control suite proves the check is live.
#
# D-CB7: BSD grep lacks -P and exits 2 with empty output, making the absence
# of hazard bytes indistinguishable from a broken invocation (avoids PF-013).
# D-CB5: Zero-files-scanned is exit 1, not exit 0 (avoids PF-016).
# -------------------------------------------------------------------------
source-hygiene:
name: Source hygiene
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: Scan tracked source for hazardous codepoints
run: node scripts/verify-no-control-bytes.mjs
- name: Run positive-control and class-completeness suite
run: npm run test:gates
# ---------------------------------------------------------------------------
# `mds watch` startup-race gate (#317).
#
# The `rust` job above runs this same file with default features, where the
# publish -> arm window is only the few microseconds between the last output
# write and the `watcher.watch()` syscall. `startup-race-probe` widens it to
# 200ms, which is what lets `watch_*_edit_during_startup_window_is_not_lost` —
# the two tests that deliberately skip the readiness handshake — land an edit
# inside it reliably. The feature is referenced nowhere else in CI, so without
# this job it is dead code.
#
# Linux-only on purpose: macOS FSEvents does not report reads and cannot
# reproduce this bug class at all (40/40 green on macOS pre-fix).
#
# Measured against the pre-fix arming order, on ubuntu aarch64 in Docker
# (per test, 6 runs with the probe / 3 without):
# probe on probe off
# file mode 6/6 red 3/3 red
# dir mode 6/6 red 1/3 red
# So the default run already catches a file-mode regression; this job is what
# makes the dir-mode one more than a coin flip. At this commit both are 6/6
# green with the probe on.
# ---------------------------------------------------------------------------
watch-startup-race:
name: Watch startup race (probe)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
- uses: Swatinem/rust-cache@v2
# Scoped to -p mds-cli --test cli_watch: `startup-race-probe` injects a sleep
# into `mds watch` startup, so a wider run would just be slower, not stronger.
- name: Watch suite with the startup-race probe enabled
run: cargo test -p mds-cli --test cli_watch --features startup-race-probe
# The probe must remain a clean, warning-free build path: it is compiled out
# entirely by default, so nothing else in CI would ever type-check it.
- name: Clippy with the probe enabled
run: cargo clippy -p mds-cli --all-targets --features startup-race-probe -- -D warnings