-
Notifications
You must be signed in to change notification settings - Fork 3.6k
366 lines (324 loc) · 14.8 KB
/
Copy pathlabs-validate-dev.yml
File metadata and controls
366 lines (324 loc) · 14.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
name: '🔮 Labs · ✅ Validate (Dev)'
# =============================================================================
# Labs — Notebook & Build Validation
# =============================================================================
#
# Validates Marimo notebook integrity, mlsysim engine compatibility, WASM
# export capability, and Quarto site build output.
#
# Flow:
# 1. VALIDATE_NOTEBOOKS — Static analysis + engine + widget + protocol tests
# 2. BUILD_SITE — Quarto HTML render succeeds
# 3. WASM_SMOKE_TEST — Export representative labs to WASM + Pyodide check
# 4. SUMMARY — Aggregate results
#
# Triggers:
# - push: dev branch, labs/** or mlsysim/** paths
# - pull_request: labs/** or mlsysim/** or workflow file changes
# - workflow_dispatch: manual
#
# Deploys to: N/A (validate only)
#
# Related:
# - labs-preview-dev.yml — Gated on this workflow passing
# - labs-publish-live.yml — Production deploy
#
# =============================================================================
on:
workflow_dispatch:
pull_request:
paths:
- 'labs/**'
- 'mlsysim/**'
- 'mlsysim/**'
- '.github/workflows/labs-validate-dev.yml'
- '.github/workflows/labs-preview-dev.yml'
push:
branches: [dev]
paths:
- 'labs/**'
- 'mlsysim/**'
- 'mlsysim/**'
- '.github/workflows/labs-validate-dev.yml'
- '.github/workflows/labs-preview-dev.yml'
permissions:
contents: read
concurrency:
# `head_ref || run_id` keeps PR cancel-on-amend behavior while making
# push and workflow_dispatch runs unique per-run, so a manual dispatch
# on the same SHA as a push doesn't collide on a shared group and leave
# the README badge red on a healthy SHA. See kits-validate-dev.yml.
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
# Workflow-level environment. Defined here rather than as a repository variable
# because repository variables are not exposed to workflows triggered by
# pull_request events from forks (GitHub security default). Workflow-level
# env vars are available in all contexts.
env:
LABS_ROOT: labs
jobs:
# ===========================================================================
# Stage 1: Python syntax + import validation
# ===========================================================================
validate-notebooks:
name: '🧪 Validate Notebooks'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: 🐍 Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ vars.PYTHON_VERSION || '3.12' }}
- name: 📦 Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r labs/requirements.txt
# Install mlsysim from source (the engine that powers all labs)
pip install "mlsysim/[full]"
- name: 🧪 Run Level 1 tests (static analysis)
run: |
echo "🧪 Running static analysis tests..."
python3 -m pytest labs/tests/test_static.py -v --tb=short --junitxml=test-results-static.xml
- name: 🧪 Run Level 2 tests (engine execution)
run: |
echo "🧪 Running engine execution tests..."
python3 -m pytest labs/tests/test_engine.py -v --tb=short -k "engine" --junitxml=test-results-engine.xml
- name: 🧪 Run Level 3 tests (widget structure)
run: |
echo "🧪 Running widget structure tests..."
python3 -m pytest labs/tests/test_widget.py -v --tb=short -k "widget" --junitxml=test-results-widget.xml
continue-on-error: true # Widget tests are aspirational; many labs still WIP
- name: 🧪 Run Level 4 tests (protocol compliance)
run: |
echo "🧪 Running protocol invariant tests..."
python3 -m pytest labs/tests/test_protocol.py -v --tb=short -k "protocol" --junitxml=test-results-protocol.xml
- name: 📊 Upload test results
uses: actions/upload-artifact@v7
if: always()
with:
name: lab-test-results
path: test-results-*.xml
# ===========================================================================
# Stage 2: Quarto site build validation
# ===========================================================================
build-site:
name: '🔨 Build Labs Site'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: 🔧 Setup Quarto
uses: quarto-dev/quarto-actions/setup@v2
- name: 🔨 Build Labs Site
working-directory: ${{ env.LABS_ROOT }}
run: quarto render
- name: 🔍 Validate build output
run: |
LABS_ROOT="${{ env.LABS_ROOT }}"
echo "🔎 Resolved LABS_ROOT: '${LABS_ROOT}'"
if [ -z "${LABS_ROOT}" ]; then
echo "❌ FATAL: LABS_ROOT resolved to empty string."
echo " If this is a fork PR, check that LABS_ROOT is declared in the workflow-level env: block."
echo " Repository variables are not available on fork PR triggers."
exit 1
fi
if [ ! -f "${LABS_ROOT}/_build/index.html" ]; then
echo "❌ CRITICAL: ${LABS_ROOT}/_build/index.html missing from build output."
echo " Quarto render may have exited nonzero. Check the build step above."
exit 1
fi
echo "✅ Site built successfully"
echo "📊 Build size: $(du -sh ${LABS_ROOT}/_build | cut -f1)"
# ===========================================================================
# Stage 3: WASM export smoke test
# ===========================================================================
wasm-smoke-test:
name: '🌐 WASM Export Smoke Test'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: 📥 Checkout
uses: actions/checkout@v6
- name: 🐍 Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ vars.PYTHON_VERSION || '3.12' }}
- name: 📦 Install dependencies
run: |
pip install build marimo uv
pip install -r labs/requirements.txt
pip install mlsysim/
- name: 📦 Build mlsysim wheel
run: |
cd mlsysim && python3 -m build --wheel
echo "✅ Wheel built:"
ls -la dist/*.whl
- name: 📦 Build mlsysbook-labs helper wheel
run: |
python3 -m build --wheel labs --outdir labs/dist
echo "✅ Lab helper wheel built:"
ls -la labs/dist/*.whl
- name: 📦 Setup Node.js for Pyodide test
uses: actions/setup-node@v6
with:
node-version: ${{ vars.NODE_VERSION || '20' }}
- name: 🧪 Test Pyodide wheel install
run: |
npm install pyodide
WHEEL_FILE=$(ls mlsysim/dist/*.whl | head -n 1)
cat << 'EOF' > test_pyodide.js
const { loadPyodide } = require("pyodide");
const path = require("path");
async function main() {
const pyodide = await loadPyodide();
await pyodide.loadPackage("micropip");
const micropip = pyodide.pyimport("micropip");
console.log("Installing dependencies...");
await pyodide.loadPackage(["pandas", "numpy"]);
await micropip.install(["pydantic", "pint"], {keep_going: false});
console.log("Installing mlsysim wheel...");
const wheelPath = process.argv[2];
await micropip.install("file://" + path.resolve(wheelPath), {keep_going: false});
console.log("Testing imports...");
pyodide.runPython(`
import mlsysim
from mlsysim import Engine
print("✅ mlsysim imported successfully in Pyodide!")
`);
}
main().catch(err => {
console.error("❌ Pyodide test failed:", err);
process.exit(1);
});
EOF
node test_pyodide.js "$WHEEL_FILE"
- name: 🌐 Export representative labs to WASM HTML
run: |
# lab_05_dist_train is the regression guard: it shipped broken to
# prod when plotly was imported before micropip.install(). See #1353.
SMOKE_LABS="labs/vol1/lab_00_introduction.py labs/vol1/lab_01_ml_intro.py labs/vol2/lab_01_introduction.py labs/vol2/lab_05_dist_train.py"
FAILED=""
# Setup wheel directory so relative paths work. Labs bootstrap with
# BOTH the mlsysim engine wheel and the mlsysbook-labs helper wheel,
# so both must be served or micropip 404s and Pyodide raises BadZipFile.
mkdir -p /tmp/wasm-smoke/wheels
cp mlsysim/dist/*.whl /tmp/wasm-smoke/wheels/
cp labs/dist/mlsysbook_labs-*.whl /tmp/wasm-smoke/wheels/
for lab in $SMOKE_LABS; do
name=$(basename "$lab" .py)
echo " Exporting ${name}..."
mkdir -p "/tmp/wasm-smoke/${name}"
if marimo export html-wasm "$lab" \
-o "/tmp/wasm-smoke/${name}/index.html" \
--mode run --no-show-code; then
# Verify the HTML is non-trivial (>10KB)
SIZE=$(wc -c < "/tmp/wasm-smoke/${name}/index.html")
if [ "$SIZE" -lt 10000 ]; then
echo "❌ ${name}: index.html is only ${SIZE} bytes (expected >10KB)"
FAILED="${FAILED} ${name}"
else
echo "✅ ${name}: ${SIZE} bytes"
fi
else
echo "❌ ${name}: marimo export failed"
FAILED="${FAILED} ${name}"
fi
done
if [ -n "$FAILED" ]; then
echo ""
echo "❌ WASM smoke test failed for:${FAILED}"
exit 1
fi
echo ""
echo "✅ All WASM smoke tests passed"
# =====================================================================
# Browser-level Pyodide verification
# =====================================================================
# lab_05 shipped broken to prod (#1353): plotly imported before
# micropip.install() in a WASM runtime. Every static/engine/Node-Pyodide
# check passed — only a real browser caught it. This step runs the
# exported labs in headless Chromium behind the same cross-origin
# isolation headers the prod dev-preview uses, so SharedArrayBuffer is
# enabled and Pyodide threading works. A lab is considered healthy when
# a marimo DOM signal (tab, cell, or island) attaches within 180s.
- name: 🎭 Install Playwright + Chromium
env:
# Fixed, HOME-independent location. labs/tests/conftest.py
# redirects $HOME for test isolation (so lab tests don't touch a
# real ~/.mlsys), which would otherwise make Playwright look for
# its browser cache in the wrong place once pytest runs. See the
# WASM persistence regression test step below.
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.pw-browsers
run: |
pip install playwright
python3 -m playwright install --with-deps chromium
- name: 🌐 Browser smoke test (real Chromium + Pyodide)
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.pw-browsers
run: |
python3 labs/tests/browser_smoke.py --labs-dir /tmp/wasm-smoke
# =====================================================================
# WASM/IndexedDB persistence regression test (#1985 / PR #1988)
# =====================================================================
# DesignLedger.save_async() previously reported success while
# silently failing to persist to IndexedDB, due to a Python
# name-mangling bug (globalThis.__mlsys_temp_state written inside the
# class body was rewritten to globalThis._DesignLedger__mlsys_temp_state,
# desyncing it from the plain name the embedded JS read). Mocked unit
# tests (mlsysim/tests/test_state.py) can't catch this class of bug
# since they replace save_async() entirely. This runs the real
# save_async() against real Pyodide + real IndexedDB in headless
# Chromium and reads the write back through a separate connection.
- name: 🧪 WASM persistence regression test (real IndexedDB)
env:
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.pw-browsers
run: |
python3 -m pytest labs/tests/test_wasm_persistence.py -v --override-ini="addopts="
# ===========================================================================
# Stage 4: Link integrity (Tier 2 — non-blocking baseline)
# ===========================================================================
# Tier 1 pre-commit (shared/scripts/check-internal-links.py) already blocks
# broken internal links + anchors. This pass adds external reachability as a
# warning. Flip fail_on_broken=true once the baseline is clean.
check-links:
name: '🔗 Check Links'
uses: ./.github/workflows/infra-link-check.yml
with:
path_pattern: './labs/**/*.qmd'
lycheeignore_path: 'shared/config/.lycheeignore'
fail_on_broken: false
max_concurrency: 8
# ===========================================================================
# Summary
# ===========================================================================
summary:
name: '📊 Summary'
runs-on: ubuntu-latest
needs: [validate-notebooks, build-site, wasm-smoke-test, check-links]
if: always()
steps:
- name: 📊 Generate Summary
run: |
echo "## 🔮 Labs Validation Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| 🧪 Notebook Validation | ${{ needs.validate-notebooks.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| 🔨 Site Build | ${{ needs.build-site.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| 🌐 WASM Export | ${{ needs.wasm-smoke-test.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| 🔗 Link Check | ${{ needs.check-links.result }} (non-blocking) |" >> $GITHUB_STEP_SUMMARY
- name: ❌ Check for failures
run: |
if [ "${{ needs.validate-notebooks.result }}" = "failure" ] || \
[ "${{ needs.build-site.result }}" = "failure" ] || \
[ "${{ needs.wasm-smoke-test.result }}" = "failure" ]; then
echo "❌ Validation failed"
exit 1
fi
if [ "${{ needs.check-links.result }}" = "failure" ]; then
echo "⚠️ Link check found issues (non-blocking)"
fi
echo "✅ Core checks passed"