From d788f31b253612d0f725a5e6050fa74fd00f4aa8 Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Thu, 3 Sep 2026 18:16:33 -0400 Subject: [PATCH] Add CI; fix bench/ops.ts against the current mountApp signature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bench/ops.ts still called mountApp({ componentBytes, translator, root, onError }) — the pre-`source` shape. Every op failed all three attempts with "Cannot use 'in' operator to search for 'plan' in undefined", and because bench_worker.ts deliberately turns a failed op into medianMs: null rather than aborting the run, total breakage rendered as a table of N/A rather than as a crash. `deno check` catches it outright (TS2353). Nothing was running `deno check` over bench/: deno.json's check task covered host/src and host/tests only. Widened to include bench. That the hole survived at all is the more general problem — the repo had no CI, so every gate was "someone remembers to run it". Adds a workflow running the existing just recipes unchanged, so a green check means what `just check && just test` means locally. It builds the fixture and counter components because the suites that load them throw rather than skip when the .wasm is absent, and it runs both halves of the cross-language protocol gate: golden_matches_committed pins the Rust encoder to vectors/, and decoder_test.ts pins the TypeScript decoder to the same files. Toolchain versions are pinned, matching how the justfile pins POLYENGINE_REV and Cargo.toml pins dioxus/wit-bindgen: a floating toolchain can turn `clippy -D warnings` red with no commit to blame. --- .github/workflows/ci.yml | 94 ++++++++++++++++++++++++++++++++++++++++ bench/ops.ts | 19 ++++++-- deno.json | 2 +- 3 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..031fe56 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,94 @@ +# CI for polyengine-dioxus. +# +# Runs exactly the gates `just` runs locally, in the same order and through +# the same recipes, so a green check means what `just check && just test` +# means on a workstation. New gates belong in the justfile, not here; this +# file only supplies a machine to run them on. +# +# Why the build steps below are not optional: the two component-loading +# suites (host/tests/counter_test.ts, host/tests/fullstack_test.ts) THROW +# when their .wasm is missing rather than skipping it, so dropping a build +# step turns the suite red rather than quietly green. The one silent-skip +# in the suite is host/tests/decoder_test.ts's "golden vectors pending", +# which fires only if vectors/ is empty — it is committed, so the +# cross-language check against the Rust encoder always runs. +# +# Everything is version-pinned, matching how the rest of the repo pins +# (POLYENGINE_REV in the justfile, `=0.7.10` dioxus, `=0.60.0` +# wit-bindgen). A floating toolchain can turn `clippy -D warnings` red on a +# new lint with no commit to blame. + +name: ci + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + RUST_VERSION: "1.98.0" + DENO_VERSION: "2.9.5" + WASM_TOOLS_VERSION: "1.247.0" + JUST_VERSION: "1.54.0" + +jobs: + gates: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # wasm32-wasip2 builds the app components; wasm32-unknown-unknown + # builds polyengine's translator shim in `just deps`. + - name: Install Rust ${{ env.RUST_VERSION }} + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} + targets: wasm32-wasip2, wasm32-unknown-unknown + components: clippy + + - uses: denoland/setup-deno@v2 + with: + deno-version: ${{ env.DENO_VERSION }} + + - uses: taiki-e/install-action@v2 + with: + tool: just@${{ env.JUST_VERSION }},wasm-tools@${{ env.WASM_TOOLS_VERSION }} + + - uses: Swatinem/rust-cache@v2 + + # Saves the pinned checkout and the built translator_shim.wasm, so + # `just deps` becomes a fetch+checkout on a hit. Excludes the shim's + # own build dir (large, and only needed when the shim is absent). + # Keyed on the whole justfile rather than POLYENGINE_REV alone: + # conservative (an unrelated justfile edit misses), but it cannot go + # stale against a rev bump, which is the failure that would matter. + - name: Cache pinned polyengine checkout + uses: actions/cache@v6 + with: + path: | + .deps/polyengine + !.deps/polyengine/target + key: polyengine-${{ runner.os }}-${{ hashFiles('justfile') }} + + - run: just deps + + # cargo check + clippy -D warnings + deno check (host/src, host/tests, + # bench). + - run: just check + + - name: Build test fixture component + run: just fixtures + + - name: Build counter example component + run: just example counter + + # cargo test (incl. the golden-vector encoder check) + deno task test + # (incl. the same vectors against the TypeScript decoder). + - run: just test diff --git a/bench/ops.ts b/bench/ops.ts index a3fd44c..b7e126d 100644 --- a/bench/ops.ts +++ b/bench/ops.ts @@ -25,6 +25,13 @@ import { parseHTML } from "linkedom"; import { mountApp } from "../host/src/host.ts"; import type { Mounted } from "../host/src/host.ts"; +import type { UntranslatedArtifacts } from "@deltic/runtime/embedder"; + +/** Taken from the embedder's own type rather than restated, so this stays + * pinned to what `mountApp`'s `source` actually accepts. (The `unknown` it + * replaces did not hide the stale call shape below — `deno check` reports it + * fine; nothing was running `deno check` over bench/. See deno.json.) */ +type Translator = UntranslatedArtifacts["translator"]; export const RUNS = 5; // Below this, for an op touching >=1000 rows, the number is not credible @@ -199,11 +206,15 @@ export function median(xs: number[]): number { async function freshMount( t: TransportName, componentBytes: Uint8Array, - translator: unknown, + translator: Translator, ): Promise<{ root: Element; mounted: Mounted; errors: unknown[] }> { const root = makeRoot(); const errors: unknown[] = []; - const mounted = await mountApp({ componentBytes, translator, root, onError: (err) => errors.push(err) }); + const mounted = await mountApp({ + source: { componentBytes, translator }, + root, + onError: (err) => errors.push(err), + }); await waitFor(() => root.querySelector("#row-count") !== null, `${t}: initial mount`); if (errors.length > 0) { throw new Error(`${t}: onError fired during mount: ${Deno.inspect(errors)}`); @@ -377,7 +388,7 @@ const MAX_ATTEMPTS_PER_OP = 3; export async function runOp( t: TransportName, componentBytes: Uint8Array, - translator: unknown, + translator: Translator, op: OpDef, ): Promise { let lastErr: unknown; @@ -405,7 +416,7 @@ export async function runOp( async function runOpOnce( t: TransportName, componentBytes: Uint8Array, - translator: unknown, + translator: Translator, op: OpDef, ): Promise { const { root, mounted } = await freshMount(t, componentBytes, translator); diff --git a/deno.json b/deno.json index 8379998..9058c7a 100644 --- a/deno.json +++ b/deno.json @@ -11,7 +11,7 @@ "lib": ["deno.ns", "dom", "dom.iterable", "esnext"] }, "tasks": { - "check": "deno check host/src host/tests", + "check": "deno check host/src host/tests bench", "test": "deno test --allow-read=. host/tests/", "bench": "bash bench/run.sh" }