From 414959c7dd984db1b5c3f8bd6a73d1dd8aeb976d Mon Sep 17 00:00:00 2001 From: Alex Kesling Date: Thu, 6 Aug 2026 10:51:50 -0400 Subject: [PATCH] ci: pin the emscripten toolchain; stop the format gate swallowing rustfmt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wasm build installed `latest` and cached it under a constant key, so an expired cache silently swapped compilers. Emscripten's wasm exception-handling ABI has since moved, and the fresh `latest` fails to link with undefined __cpp_exception / _Unwind_* symbols — main is exposed to this on its next deploy, not just one branch. .emsdk-version is now the single source of truth: the build script installs that version and the deploy workflow keys its emsdk cache on the file's hash, so a cache miss reinstalls the same compiler and a bump invalidates the cache. A mismatched emscripten already on PATH warns (naming both versions and the link errors it causes) rather than failing, so developer machines still build. The format gate returned only prettier's status, so `cargo fmt --check` failures reported PASS — two files have been unformatted on main unnoticed and are reformatted here. Both checks now always run and either failing fails the gate. The gate's bare `cd` also leaked the working directory into every later gate; it and the site gate's now run in subshells. --- .emsdk-version | 1 + .github/workflows/deploy-site.yml | 5 ++++- crates/toolpath-codex/src/io.rs | 6 +++++- crates/toolpath-codex/src/paths.rs | 5 ++++- scripts/build-wasm.sh | 29 ++++++++++++++++++++++++++--- scripts/quality_gates.sh | 13 +++++++++---- 6 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 .emsdk-version diff --git a/.emsdk-version b/.emsdk-version new file mode 100644 index 00000000..6b244dcd --- /dev/null +++ b/.emsdk-version @@ -0,0 +1 @@ +5.0.1 diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml index 58c6ff61..7f2d04c6 100644 --- a/.github/workflows/deploy-site.yml +++ b/.github/workflows/deploy-site.yml @@ -62,11 +62,14 @@ jobs: key: cargo-wasm-${{ hashFiles('Cargo.lock') }} restore-keys: cargo-wasm- + # Keyed on the pin, not a constant: a cache miss re-installs the same + # emscripten instead of silently picking up whatever `latest` resolves to + # that day, and bumping .emsdk-version invalidates the cache. - name: Cache emsdk uses: actions/cache@v4 with: path: local/emsdk - key: emsdk-latest + key: emsdk-${{ hashFiles('.emsdk-version') }} - name: Build wasm run: scripts/build-wasm.sh diff --git a/crates/toolpath-codex/src/io.rs b/crates/toolpath-codex/src/io.rs index 25441a9a..bc5340a4 100644 --- a/crates/toolpath-codex/src/io.rs +++ b/crates/toolpath-codex/src/io.rs @@ -173,7 +173,11 @@ mod tests { let (_t, io) = setup(); let day = io.resolver().sessions_root().unwrap().join("2026/04/21"); fs::create_dir_all(&day).unwrap(); - fs::write(day.join("rollout-2026-04-21T09-00-00-bbb.jsonl"), "not json").unwrap(); + fs::write( + day.join("rollout-2026-04-21T09-00-00-bbb.jsonl"), + "not json", + ) + .unwrap(); let ids = io.list_session_ids().unwrap(); assert_eq!(ids.len(), 2); diff --git a/crates/toolpath-codex/src/paths.rs b/crates/toolpath-codex/src/paths.rs index 610b2638..c047a0af 100644 --- a/crates/toolpath-codex/src/paths.rs +++ b/crates/toolpath-codex/src/paths.rs @@ -141,7 +141,10 @@ impl PathResolver { /// isn't stem-shaped or the file isn't at its dated path (the caller /// falls back to the tree walk). fn rollout_file_for_stem(&self, session_id: &str) -> Result> { - let Some(date) = session_id.strip_prefix("rollout-").and_then(|r| r.get(..10)) else { + let Some(date) = session_id + .strip_prefix("rollout-") + .and_then(|r| r.get(..10)) + else { return Ok(None); }; let parts: Vec<&str> = date.split('-').collect(); diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index 412a368a..1c9b9a4b 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -6,6 +6,13 @@ _wasm_js="${_root}/site/wasm/path.js" _wasm_bin="${_root}/site/wasm/path.wasm" _emsdk_dir="${_root}/local/emsdk" +# The single source of truth for the emscripten toolchain: this script installs +# it and the deploy workflow keys its emsdk cache on the file's hash, so a bump +# here invalidates the cache instead of silently reusing another compiler. +# `latest` is not usable as a pin — emscripten's wasm exception-handling ABI +# has broken this build across releases. +_emsdk_version="$(tr -d '[:space:]' < "${_root}/.emsdk-version")" + # --- Parse flags -------------------------------------------------------------- # --if-changed Skip build if outputs are newer than all Rust sources # --dev Use dev profile (fast incremental builds, no LTO/strip) @@ -48,9 +55,23 @@ fi # --- Ensure emsdk is available ------------------------------------------------ +# Warns rather than fails on a mismatch: developer machines carry their own +# emscripten, and refusing to build there would be worse than a link error the +# warning already explains. +warn_unless_pinned() { + local _active + _active="$(emcc -dumpversion 2>/dev/null || true)" + + if [ "${_active}" != "${_emsdk_version}" ]; then + echo "wasm: WARNING active emscripten is ${_active:-unknown}, pinned is ${_emsdk_version} (.emsdk-version)" >&2 + echo "wasm: WARNING link errors about __cpp_exception or _Unwind_* mean this skew" >&2 + fi +} + ensure_emsdk() { # Already on PATH? if command -v emcc &>/dev/null; then + warn_unless_pinned return 0 fi @@ -59,16 +80,18 @@ ensure_emsdk() { echo "wasm: Activating local emsdk..." # shellcheck source=/dev/null source "${_emsdk_dir}/emsdk_env.sh" 2>/dev/null + warn_unless_pinned return 0 fi # Bootstrap: clone + install + activate - echo "wasm: Installing emsdk to target/emsdk (one-time)..." + echo "wasm: Installing emsdk ${_emsdk_version} to local/emsdk (one-time)..." git clone --depth 1 https://github.com/emscripten-core/emsdk.git "${_emsdk_dir}" - "${_emsdk_dir}/emsdk" install latest - "${_emsdk_dir}/emsdk" activate latest + "${_emsdk_dir}/emsdk" install "${_emsdk_version}" + "${_emsdk_dir}/emsdk" activate "${_emsdk_version}" # shellcheck source=/dev/null source "${_emsdk_dir}/emsdk_env.sh" 2>/dev/null + warn_unless_pinned } ensure_emsdk diff --git a/scripts/quality_gates.sh b/scripts/quality_gates.sh index 0ad241d5..45a72012 100755 --- a/scripts/quality_gates.sh +++ b/scripts/quality_gates.sh @@ -47,11 +47,16 @@ _all_gates=(format shellcheck clippy test doc examples plugin site) # shellcheck disable=SC2329 gate_format() { + # Both checks always run so one pass reports every formatting problem, and + # either failing fails the gate — the gate's status must not be just the + # last command's. + local _failed=0 echo "--- cargo fmt ---" - cargo fmt --all --manifest-path "${_root}/Cargo.toml" --check 2>&1 + cargo fmt --all --manifest-path "${_root}/Cargo.toml" --check 2>&1 || _failed=1 echo "--- prettier ---" - cd "${_root}/site" || return 1 - npx --yes prettier --check --no-color "**/*.{md,css,json,js}" 2>&1 + # Subshell: gates run in this shell, so a bare `cd` would leak to the rest. + (cd "${_root}/site" && npx --yes prettier --check --no-color "**/*.{md,css,json,js}" 2>&1) || _failed=1 + return "${_failed}" } # shellcheck disable=SC2329 @@ -101,7 +106,7 @@ gate_site() { # (notably, every new worktree). Without it the build fails with # `sh: eleventy: command not found` and a stale "did you mean to install?" # warning that doesn't say which directory. - cd "${_root}/site" && pnpm install --frozen-lockfile 2>&1 && pnpm run build 2>&1 + (cd "${_root}/site" && pnpm install --frozen-lockfile 2>&1 && pnpm run build 2>&1) } # ── Runner ────────────────────────────────────────────────────────────────────