diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml index 55f0c44..61c319e 100644 --- a/.gitea/workflows/docker-build.yml +++ b/.gitea/workflows/docker-build.yml @@ -36,47 +36,12 @@ jobs: id: meta run: echo "sha=${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT" - - name: Check wasm pkg drift - run: | - set -euo pipefail - BASE_SHA="${{ github.event.before }}" - HEAD_SHA="${{ github.sha }}" - if [ -n "$BASE_SHA" ] && git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then - RANGE="$BASE_SHA..$HEAD_SHA" - else - RANGE="HEAD~1..HEAD" - fi - - CHANGED="$(git diff --name-only "$RANGE")" - echo "Changed files:" - echo "$CHANGED" - - if echo "$CHANGED" | grep -Eq '^(solitaire_wasm/|solitaire_core/|Cargo\.toml|Cargo\.lock)$|^(solitaire_wasm/|solitaire_core/)'; then - if ! echo "$CHANGED" | grep -Eq '^solitaire_server/web/pkg/solitaire_wasm\.js$|^solitaire_server/web/pkg/solitaire_wasm_bg\.wasm$'; then - echo "error: wasm/core/Cargo changed but committed web pkg artifacts are missing." - echo "Run: wasm-pack build --target web --out-dir solitaire_server/web/pkg --no-typescript solitaire_wasm" - exit 1 - fi - fi - - # Hard check: solitaire_web/ is the direct Bevy WASM source — any - # change there MUST rebuild canvas_bg.wasm or the binary goes stale. - if echo "$CHANGED" | grep -Eq '^solitaire_web/'; then - if ! echo "$CHANGED" | grep -Eq '^solitaire_server/web/pkg/canvas_bg\.wasm$'; then - echo "error: solitaire_web/ changed but canvas_bg.wasm not updated." - echo "Run: ./build_wasm.sh (requires wasm-bindgen-cli + wasm32-unknown-unknown target)" - exit 1 - fi - fi - - # Advisory notice: solitaire_engine/ and solitaire_core/ changes often - # require a Bevy WASM rebuild but are not enforced (formatting-only - # commits should not be blocked). - if echo "$CHANGED" | grep -Eq '^(solitaire_engine/|solitaire_core/)' && \ - ! echo "$CHANGED" | grep -Eq '^solitaire_server/web/pkg/canvas_bg\.wasm$'; then - echo "notice: solitaire_engine/core changed without a canvas_bg.wasm rebuild." - echo " If the change affects gameplay run ./build_wasm.sh before pushing." - fi + # WASM artifact freshness is enforced precisely by the dedicated + # `web-wasm-freshness` workflow (rebuild-and-diff on every PR + master + # push), which supersedes the previous changed-files heuristic here. + # That heuristic only hard-failed on direct solitaire_web/ edits and + # treated solitaire_engine/_core changes as a non-blocking notice, which + # let the whole card_game migration ship a stale pkg/. - name: Log in to Gitea registry uses: docker/login-action@v3 diff --git a/.gitea/workflows/web-wasm-freshness.yml b/.gitea/workflows/web-wasm-freshness.yml new file mode 100644 index 0000000..b800455 --- /dev/null +++ b/.gitea/workflows/web-wasm-freshness.yml @@ -0,0 +1,91 @@ +name: Web WASM Freshness + +# Guards against the committed solitaire_server/web/pkg/ artifacts going stale. +# +# A fresh build on a pinned toolchain is byte-for-byte reproducible (verified), +# so this job rebuilds the artifacts and fails if they differ from what is +# committed. Unlike a changed-files heuristic, this catches *any* drift in the +# wasm-feeding crates — solitaire_core / _engine / _data / _sync / _wasm / _web — +# including gameplay-logic changes that do not alter the JS API surface (e.g. +# the v4->v5 save-schema change that the old heuristic let through as a +# non-blocking "notice", leaving the deployed web build stale for ~3 weeks). +# +# Tool versions are pinned to whatever produced the committed artifacts. When +# you regenerate the artifacts with a newer toolchain, bump these in lockstep: +# rust 1.95.0 · wasm-bindgen 0.2.120 · wasm-pack 0.14.0 · binaryen 130 + +on: + push: + branches: [master] + paths: + - 'solitaire_core/**' + - 'solitaire_engine/**' + - 'solitaire_data/**' + - 'solitaire_sync/**' + - 'solitaire_wasm/**' + - 'solitaire_web/**' + - 'solitaire_server/web/pkg/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'build_wasm.sh' + - '.gitea/workflows/web-wasm-freshness.yml' + pull_request: + paths: + - 'solitaire_core/**' + - 'solitaire_engine/**' + - 'solitaire_data/**' + - 'solitaire_sync/**' + - 'solitaire_wasm/**' + - 'solitaire_web/**' + - 'solitaire_server/web/pkg/**' + - 'Cargo.toml' + - 'Cargo.lock' + - 'build_wasm.sh' + - '.gitea/workflows/web-wasm-freshness.yml' + workflow_dispatch: + +jobs: + freshness: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust 1.95.0 (pinned for reproducible wasm output) + uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + targets: wasm32-unknown-unknown + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + + - name: Install wasm-bindgen-cli + wasm-pack (pinned) + uses: taiki-e/install-action@v2 + with: + tool: wasm-bindgen-cli@0.2.120,wasm-pack@0.14.0 + + - name: Install binaryen 130 (wasm-opt, pinned) + run: | + set -euo pipefail + curl -sSL \ + https://github.com/WebAssembly/binaryen/releases/download/version_130/binaryen-version_130-x86_64-linux.tar.gz \ + | tar xz + echo "$PWD/binaryen-version_130/bin" >> "$GITHUB_PATH" + + - name: Rebuild WASM artifacts + run: ./build_wasm.sh + + - name: Fail if committed pkg differs from a fresh build + run: | + set -euo pipefail + if [ -z "$(git status --porcelain -- solitaire_server/web/pkg/)" ]; then + echo "pkg/ artifacts match a fresh build." + else + echo "::error::solitaire_server/web/pkg/ is stale — a wasm-feeding crate changed without regenerating the artifacts." + echo "Fix: run ./build_wasm.sh and commit the updated solitaire_server/web/pkg/." + git status --porcelain -- solitaire_server/web/pkg/ + git diff --stat -- solitaire_server/web/pkg/ + exit 1 + fi