openpencil/.github/workflows/wasm-bundle-build.yml
Kayshen-X d023dbf1de feat(web): fetch scene templates and the icon catalog at runtime
The 58 template documents and the core iconify catalog follow the
previews out of the wasm binary behind the same asset seam: templates
resolve by id on both platforms but carry bytes only natively, so the
wasm boot check validates routes instead of rejecting the catalogue,
and a click on an unfetched card requests the asset and instantiates
on the install edge (failure raises the retryable toast). The icon
picker prefetches while open, distinguishes "still loading" from "no
match", and keys its search memo on catalog readiness so the empty
pre-fetch result cannot be memoized for the session. The split is a
feature (runtime-icon-catalog) enabled only by op-host-web — the
web-sdk viewer has no daemon to fetch from and keeps the embed, which
a first cut silently broke. Bundle: 5.10 → 4.93 MiB gzip; the sdk
bundle (5.20 MiB measured) gets a calibrated 6 MiB tripwire.
2026-08-09 12:26:39 +08:00

164 lines
7.7 KiB
YAML

name: WASM bundle build (#56 — real canvaskit deployable)
# Builds the REAL production web bundle (the `canvaskit` cargo feature, not the
# `web` compile stub), size-gates it, and uploads it as a reproducible
# deployable artifact. This is the CI counterpart of the local
# `tools/check-wasm-bundle.sh` developer gate.
#
# Why a NEW file instead of un-deferring the block in `rust-release.yml`
# / `rust-multiplatform.yml`: those deferred blocks describe the RETIRED
# from-scratch skia pipeline (`--features skia`, `vendor/skia-safe-op`,
# `crates/wasm-libc-shim`, brew-emscripten + EMSDK + the `.wasm.a` → `.a`
# symlink hack). That whole path was retired 2026-06-17 (see the workspace
# `Cargo.toml` note + `tools/check-wasm-bundle.sh` lines 57-59): the editor now
# renders through the official CanvasKit skia WASM, so the Rust bundle is pure
# logic and needs NO EMSDK / skia-safe / libc shim. Un-deferring the stale
# blocks would mean rewriting them top to bottom; a clean new job that mirrors
# the CURRENT local script is the honest option.
#
# Authoritative recipe mirrored from `tools/check-wasm-bundle.sh`:
# prerequisites: cargo, wasm-bindgen, wasm-opt, node, gzip (NO EMSDK)
# 1. cargo build -p op-host-web --target wasm32-unknown-unknown \
# --no-default-features --features canvaskit --release
# 2. wasm-bindgen --target web --out-dir crates/op-host-web/pkg <target.wasm>
# 3. assert 0 env.* imports (LinkError guard)
# 4. wasm-opt -Oz with the rustc-emitted WebAssembly feature flags, then
# gzip size <= ceiling (default 6291456 bytes = 6 MiB, overridable via
# STEP1B_SHELL_WASM_GZIP_LIMIT_BYTES)
# The job calls the script directly rather than duplicating that logic — the
# script already runs non-interactively (`set -euo pipefail`, exit 0/1/2) and
# is the single source of truth for the gate.
on:
pull_request:
paths:
- 'Cargo.toml'
- 'Cargo.lock'
- 'crates/**'
- 'deny.toml'
- 'tools/check-wasm-bundle.sh'
- '.github/workflows/wasm-bundle-build.yml'
push:
branches: ['**']
paths:
- 'Cargo.toml'
- 'Cargo.lock'
- 'crates/**'
- 'tools/check-wasm-bundle.sh'
- '.github/workflows/wasm-bundle-build.yml'
# Allow on-demand runs so a release can produce the artifact without a code
# change (e.g. to re-bundle against an updated toolchain).
workflow_dispatch:
jobs:
build-canvaskit-bundle:
name: build + size-gate canvaskit web bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: '1.94'
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
with:
shared-key: wasm-bundle-build
# The CanvasKit bundle is pure Rust + web_sys + serde — no skia-safe, no
# emscripten, no system GL libs. So the toolchain footprint is just the
# wasm-bindgen CLI + wasm-opt (binaryen) + node, exactly the prerequisites
# `tools/check-wasm-bundle.sh` asserts via its `need` checks.
- name: Install wasm-bindgen-cli (pinned to the locked version)
run: |
# Pin the CLI to the same version as the `wasm-bindgen` crate in
# Cargo.lock so the generated JS glue matches the linked runtime —
# a version skew between the two is the classic `wasm-bindgen`
# "schema version mismatch" panic at mount time. Read the locked
# version straight out of Cargo.lock (the `[[package]]` block whose
# name is exactly `wasm-bindgen`).
version="$(cargo metadata --format-version 1 --locked --no-deps >/dev/null 2>&1; \
awk '/^name = "wasm-bindgen"$/{found=1; next} found && /^version = /{gsub(/[" ]/,"",$3); print $3; exit}' Cargo.lock)"
if [ -z "$version" ]; then
echo "::error::could not resolve wasm-bindgen version from Cargo.lock"
exit 1
fi
echo "Installing wasm-bindgen-cli $version"
cargo install wasm-bindgen-cli --version "$version" --locked
- name: Install binaryen (pinned modern release) + headless server runtime deps
run: |
sudo apt-get update
sudo apt-get install -y \
pkg-config \
libfreetype-dev \
libfontconfig1-dev \
fonts-noto-cjk
# Ubuntu's apt `binaryen` is v108 (2022) — far too old for rustc
# 1.94's wasm feature set (memory.copy/fill from bulk-memory-opt),
# so `wasm-opt` rejects the bundle with "all used features should be
# allowed". Pin a modern release matching the local dev toolchain.
BINARYEN_VERSION=version_123
curl -fsSL \
"https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \
| tar -xz
echo "${GITHUB_WORKSPACE}/binaryen-${BINARYEN_VERSION}/bin" >> "$GITHUB_PATH"
"${GITHUB_WORKSPACE}/binaryen-${BINARYEN_VERSION}/bin/wasm-opt" --version
- name: Verify node + gzip + Chrome are present (script prerequisites)
run: |
node --version
gzip --version | head -n1
google-chrome --version || chromium --version || chromium-browser --version
# Single source of truth: run the local gate script verbatim. It performs
# cargo build (canvaskit) -> wasm-bindgen --target web -> 0-env-import
# assert -> wasm-opt -Oz with rustc's WebAssembly feature flags -> gzip
# size <= ceiling, and exits non-zero on any breach, which fails the job.
# The ceiling default (6 MiB gzip) lives in the script; override here only
# if a release intentionally re-baselines.
- name: Build + size-gate the canvaskit bundle
run: bash tools/check-wasm-bundle.sh
- name: Browser smoke the canvaskit bundle + daemon
run: OPENPENCIL_SKIP_WASM_BUILD=1 bash tools/check-web-browser-smoke.sh
# Assemble the deployable layout the daemon's `web_static.rs` expects: the
# wasm-bindgen `pkg/` output PLUS the vendored CanvasKit artifact under a
# `canvaskit/` subdir, all inside a single `web-bundle/` directory. This
# mirrors the `<exe_dir>/web-bundle` + `web-bundle/canvaskit` resolution
# order in `crates/op-host-services/src/web_static.rs`.
#
# `pkg/` also carries `assets/` — the preview JPEGs the wasm bundle no
# longer embeds and the browser fetches from `/pkg/assets/…` (staged by
# `tools/stage-web-assets.sh`, which the gate script above runs). The
# assertion below is what stops a bundle shipping without them.
- name: Assemble deployable web-bundle/
run: |
set -euo pipefail
rm -rf web-bundle
mkdir -p web-bundle
cp -R crates/op-host-web/pkg/. web-bundle/
cp -R crates/op-host-web/assets/canvaskit web-bundle/canvaskit
for required in assets/prompt_center_previews assets/scene_template_previews \
assets/scene_templates assets/iconify-catalog-core.json; do
test -e "web-bundle/$required" \
|| { echo "::error::web-bundle is missing $required"; exit 1; }
done
echo "web-bundle contents:"
find web-bundle -maxdepth 2 -type f | sort
echo "staged runtime assets: $(find web-bundle/assets -type f | wc -l) files"
- name: Upload deployable web-bundle artifact
uses: actions/upload-artifact@v4
with:
name: op-web-bundle
path: web-bundle
if-no-files-found: error
# The bundle is deterministic from the inputs; keep it briefly so a
# release / Docker build can pull it without rebuilding.
retention-days: 14