diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2edd9ec4..29ed76d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -171,14 +171,32 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + # Pinned to match wasm/oxicloud-plugin-hello/rust-toolchain.toml. + # Reproducibility of the committed .wasm fixtures depends on both + # sides using the same rustc — even a patch bump shifts codegen. + # Bump both together. + - uses: dtolnay/rust-toolchain@1.96.1 with: targets: wasm32-unknown-unknown - uses: Swatinem/rust-cache@v2 - name: Rebuild committed wasm fixtures run: bash scripts/build-plugin-hello.sh - - name: Fail if fixtures are stale (rebuild + commit them) - run: git diff --exit-code tests/fixtures/plugins/ + # NOTE: no `git diff --exit-code` staleness check. + # + # Cross-host wasm builds (contributor aarch64-macOS vs CI + # x86_64-linux, same rustc 1.96.1, same `--remap-path-prefix`, + # same `CARGO_INCREMENTAL=0`, same profile) still produce + # byte-different .wasm — plain `cargo build` doesn't guarantee + # bit-reproducible cross-host wasm output. The proper fixes + # (containerised builds, or dropping the committed fixtures and + # rebuilding from source everywhere) are deferred; the frontend + # wasm crate (`wasm/oxicloud-hash`) will hit the same wall when + # we add a similar check for it, so we'll tackle both together. + # For now: CI rebuilds the fixtures fresh above and uses those + # for the plugin runtime tests below. The versions committed at + # HEAD are a convenience for local dev without the wasm32 + # toolchain — they may drift from what CI produces, which is + # fine as long as the runtime tests pass. - name: Run plugin runtime tests # Quote: the trailing `::` confuses GitHub's YAML parser (mapping # values not allowed) and aborts the whole workflow at load time. diff --git a/scripts/build-plugin-hello.sh b/scripts/build-plugin-hello.sh index 737df488..11d59074 100755 --- a/scripts/build-plugin-hello.sh +++ b/scripts/build-plugin-hello.sh @@ -23,6 +23,19 @@ OUT=tests/fixtures/plugins export CARGO_TARGET_DIR="$PWD/$CRATE/target" ARTIFACT="$CRATE/target/wasm32-unknown-unknown/release/oxicloud_plugin_hello.wasm" +# Reproducibility: the CI plugins job runs `git diff --exit-code` against +# the committed fixtures, so any environment drift (absolute paths in +# debug info, incremental caches, host-specific codegen) breaks the check. +# * `--remap-path-prefix` strips the source directory from any residual +# path strings (panic message file paths mostly). +# * `CARGO_INCREMENTAL=0` forces a from-scratch compilation — incremental +# artifacts are not bit-reproducible across cache states. +# The Rust version itself is pinned via +# `wasm/oxicloud-plugin-hello/rust-toolchain.toml`; keep it in sync with +# the CI action tag in `.github/workflows/ci.yml` (`plugins` job). +export CARGO_INCREMENTAL=0 +export RUSTFLAGS="${RUSTFLAGS:-} --remap-path-prefix=$PWD/$CRATE=." + # Needs the wasm32-unknown-unknown target's std. In the devenv this comes from # `languages.rust.targets` in devenv.nix; otherwise run # `rustup target add wasm32-unknown-unknown`. cargo emits a clear "can't find diff --git a/tests/fixtures/plugins/hello.wasm b/tests/fixtures/plugins/hello.wasm index f1518195..f9d10e2c 100755 Binary files a/tests/fixtures/plugins/hello.wasm and b/tests/fixtures/plugins/hello.wasm differ diff --git a/tests/fixtures/plugins/net.wasm b/tests/fixtures/plugins/net.wasm index d7100fa7..99ebc896 100755 Binary files a/tests/fixtures/plugins/net.wasm and b/tests/fixtures/plugins/net.wasm differ diff --git a/tests/fixtures/plugins/omit_login.wasm b/tests/fixtures/plugins/omit_login.wasm index 686e1812..ba4e3c1d 100755 Binary files a/tests/fixtures/plugins/omit_login.wasm and b/tests/fixtures/plugins/omit_login.wasm differ diff --git a/tests/fixtures/plugins/panic.wasm b/tests/fixtures/plugins/panic.wasm index 18f47360..d5b7c77e 100755 Binary files a/tests/fixtures/plugins/panic.wasm and b/tests/fixtures/plugins/panic.wasm differ diff --git a/tests/fixtures/plugins/sleep.wasm b/tests/fixtures/plugins/sleep.wasm index a6527a35..1eeea87e 100755 Binary files a/tests/fixtures/plugins/sleep.wasm and b/tests/fixtures/plugins/sleep.wasm differ diff --git a/tests/fixtures/plugins/wrong_abi.wasm b/tests/fixtures/plugins/wrong_abi.wasm index f3108bc3..99053578 100755 Binary files a/tests/fixtures/plugins/wrong_abi.wasm and b/tests/fixtures/plugins/wrong_abi.wasm differ diff --git a/wasm/oxicloud-plugin-hello/src/lib.rs b/wasm/oxicloud-plugin-hello/src/lib.rs index cb6b5bce..37b9fa7f 100644 --- a/wasm/oxicloud-plugin-hello/src/lib.rs +++ b/wasm/oxicloud-plugin-hello/src/lib.rs @@ -37,8 +37,12 @@ pub fn abi_version() -> FnResult { } /// Handler for the `file.uploaded` event. +/// +/// `#[plugin_fn]` rewrites the fn signature, so an outer `#[allow]` doesn't +/// reach the inner scope where `input` is bound — hence the `_` prefix on the +/// parameter. The well-behaved tail rebinds it as `input` locally. #[plugin_fn] -pub fn on_file_uploaded(input: String) -> FnResult { +pub fn on_file_uploaded(_input: String) -> FnResult { // --- misbehaving variants (compiled in only under their feature) --------- #[cfg(feature = "panic")] panic!("intentional panic: exercises host failure isolation"); @@ -53,27 +57,33 @@ pub fn on_file_uploaded(input: String) -> FnResult { } } - #[cfg(feature = "net")] + // The well-behaved tail is unreachable under the diverging variants above; + // gate it so the compiler doesn't flag input/tail as unused/dead. + #[cfg(not(any(feature = "panic", feature = "sleep")))] { - // Attempt an outbound HTTP call. The host grants no `allowed_hosts`, so - // Extism denies this before any socket is opened (offline-deterministic) - // and the error propagates out of the handler. - let req = HttpRequest::new("https://example.com/"); - let _ = http::request::<()>(&req, None)?; - } + let input = _input; - // --- well-behaved path --------------------------------------------------- - let ev: serde_json::Value = serde_json::from_str(&input)?; - let path = ev["payload"]["path"].as_str().unwrap_or(""); - let size = ev["payload"]["size"].as_u64().unwrap_or(0); + #[cfg(feature = "net")] + { + // Attempt an outbound HTTP call. The host grants no `allowed_hosts`, + // so Extism denies this before any socket is opened + // (offline-deterministic) and the error propagates out. + let req = HttpRequest::new("https://example.com/"); + let _ = http::request::<()>(&req, None)?; + } - unsafe { - log( - "info".to_string(), - format!("hello plugin saw upload: {path} ({size} bytes)"), - )?; + let ev: serde_json::Value = serde_json::from_str(&input)?; + let path = ev["payload"]["path"].as_str().unwrap_or(""); + let size = ev["payload"]["size"].as_u64().unwrap_or(0); + + unsafe { + log( + "info".to_string(), + format!("hello plugin saw upload: {path} ({size} bytes)"), + )?; + } + Ok(serde_json::json!({ "ok": true }).to_string()) } - Ok(serde_json::json!({ "ok": true }).to_string()) } /// Handler for the `user.login` event. Dropped by the `omit_login` variant so