ci-ubicloud.yml used paths-ignore: ["docs/**", "**.md"] with
concurrency.cancel-in-progress: false, so every non-markdown edit under
site/ queued a full 6-tool KiCad WASM build plus the wxWidgets + KiCad
e2e suites — and queued rather than superseded, so a multi-commit push
stacked several of them up.
The marketing site shares no build inputs with the WASM tools and ships
from its own deploy-site.yml. Landed on its own, ahead of the Cloudflare
migration commits, so those don't pay the old cost.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LAmkjM7okPdScp9XLW1JVr
Squash of experiment/ff-big-modules vs main.
Big-module routing removed: native-EH shrank kicad_editor below
SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on
stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT
crutch are gone — kicad-firefox and kicad-chromium both run the full
suite, with the module compiled the way real users' browsers compile it.
Per-engine screenshots end to end: stableShot/shotPath write
test-results/<engine>/<name>.png; baselines move to
baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots
pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on
<engine>/<name>. Previously Firefox and Chromium renders of one spec
overwrote each other and Firefox renders were never actually gated.
Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium
web-suite shots); manifest generated from the baseline tree.
One merged playwright.config.ts (kicad/asyncify/coroutine/perf as
projects); ~25 dead npm scripts dropped. The web suite is gated in CI for
the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs
triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after
npm ci; last 26 blind-sleep violations fixed.
SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe
(--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag
is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable
without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader
transiently failed the first post-board-load draw and the recovery
cascade ended in a silent permanent Cairo fallback — that engine flip was
the "~1.2% changedRatio both directions" occ-export baseline flake.
Validated 160/160 across two 80-repeat rigs; full analysis in
docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium
baselines shift slightly on llvmpipe — promote once from the first green
run. Deflakes the new coverage exposed: presence baselines settle before
capture; presence fixtures declare current file formats; perf gets its
own outputDir so CI evidence survives; occ-export settles the board paint
before the export dialog; menu-item waits (waitForRenderedByLabel before
clickMenuItem) in 4 specs + the TESTING.md rule.
Web suite runs the PROD build, in parallel: webServer becomes backend
`start` + the standalone's e2e:preview (build-preview.mjs: link-wasm →
stash the public/wasm symlink aside during vite build, build-demo.mjs's
move — then vite preview as the persistent server). The wasm middleware
serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread
worker script's own response must carry COEP or Chrome kills it with
ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time;
VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default
workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed:
shared-page specs become serial groups; locks.spec grabs alice's exact
item via the new kicadCollabTestSelectByUuid hook (cross-tab "first
footprint" order is not a ysync invariant); quit specs poll page.url()
(quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox).
Suite: 51 passed / 12 skipped / 0 failed in 1.6m.
CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be
reachable from the npm scripts the workflows invoke — scraped from
.github/workflows/, resolved through package.json, coverage asked from
playwright --list itself. Rules: uncovered-spec + orphan-project (with a
documented LOCAL_ONLY_PROJECTS allowlist). Gating next to
lint:determinism; 138 spec files / 13 projects accounted for.
Product fixes kept from the investigations (reachable on real GPUs too):
wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always
propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush +
per-instance font texture + first-frame GL-error drain (GAL recovery
recovers instead of falling back to Cairo) + the user-facing eeschema
switch navigates again under __EMSCRIPTEN__ (project-sync's
FaceRegistered gate had rerouted it into the hidden sync player; caught
by the newly-gated web suite).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
VITE_APP_URL (build-editor --app-base, set to app.pcbjam.com in release CI)
turns on a redirect gate in App: home, project overview and mgmt-only paths
bounce to the mgmt app preserving path+search; tool boots, file deep-links,
lib editors and the @local pseudo-scope stay local. Unset (dev/demo) keeps
today's behavior. Policy is a pure helper (lib/redirect.ts) + vitest table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A42xfPFNdfsUt9eowkC9eM
All pcbjam properties now load the same new-format Plausible script
(https://plausible.io/js/pa-KjNS9YmidydULZTstsjRg.js — one dashboard,
segment by hostname) instead of per-domain data-domain sites:
- site: replace Vercel Analytics with the pa- snippet in BaseLayout
(is:inline, prod-only, crossorigin="anonymous" for the COEP
require-corp routes in vercel.json); drop @vercel/analytics dep.
Legal pages (cookies/privacy) rewritten Vercel -> Plausible.
- standalone: initAnalytics() rewritten for the new script format
(queue shim + plausible.init(), async, no data-domain). The gate is
now VITE_PLAUSIBLE_SRC (script URL, deploy-time opt-in) — unset means
no tracking, so dev checkouts and third-party GPL builds stay clean.
VITE_PLAUSIBLE_DOMAIN is gone.
- deploy: build-demo/build-editor --plausible now takes the script URL;
release.yml/deploy-demo.yml hardcode the (public) pa- URL, so the
repo Actions variable PLAUSIBLE_DOMAIN is no longer used.
Verified: standalone tsc + vite build (pa- URL present with env, absent
without, crossOrigin kept); astro build has the snippet on all layout
pages and no vercel/insights references; astro dev stays untracked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docker/build.sh "all" now includes kicad_tools (finalizes in-container, so
it never contends with the editor's wasm-opt critical path); .ci-cache-epoch
bumped — the cached FINAL output set changes. wasm-build.yml gains the
corpus-lint + CLI-contract gate step (run_tests leg): with the artifact now
built in CI, the skip-when-unbuilt scripts bite. Release runs
(upload_output) start shipping kicad_tools in the wasm-output artifact —
the closed repo's runner-image workflow consumes it from there.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
The tag-triggered release pipeline built the demo (and backed editor)
without --models-tag, so modelsSourceConfig() returned null and every
kind=model3d ensure logged "(unserved)" — 3D component models never
loaded on demo.pcbjam.com. The manual deploy-demo.yml already passed
it; mirror its MODELS_TAG env (10.0.3) in release.yml for both builds.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQRBzfidkoTU5yc7HnF9x7
build-editor.mjs already threads --plausible → VITE_PLAUSIBLE_DOMAIN; the
deploy-editor job now passes editor.pcbjam.com so the backed editor tracks
as its own Plausible site (separate from the demo's vars.PLAUSIBLE_DOMAIN).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dasZuqo6FStgT3rkC85im
New release.yml job `deploy-editor` (parallel to deploy-demo, same per-tag
WASM manifest): builds the standalone in REMOTE mode via the new
scripts/deploy/build-editor.mjs — projects/libs/auth from the closed API
(VITE_API_BASE_URL=https://api.pcbjam.com, VITE_LIBS_SOURCE=synced), Yjs
board rooms through the API host's path route (VITE_YJS_ENDPOINT=api origin,
doc source ydoc) — and ships it to the pcbjam-editor Pages project. The
demo deploy is unchanged. The closed stack itself (api./app.pcbjam.com)
deploys from pcbjam-private; when both sides change, that repo tags first.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014dasZuqo6FStgT3rkC85im
- build 3d-webgl test app after gal-webgl (same recipe: wx build + docker
sysroot headers); the capture spec already lives in the default e2e suite
and self-skips when the app is missing, so it now runs
- report-only parity + webgl-self step after the e2e suites
(continue-on-error: the committed baseline-webgl set is Mac-Chromium-
sourced — promote CI renders and flip gating if SwiftShader drifts past
the 0.005 floor)
- exclude tests/apps/3d-webgl from the test-apps cache (rebuilt every run,
like gal-webgl) and upload tests/3d-regression/output/** (renders, diffs,
the 3d:review triptych gallery) with the e2e artifacts
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All four editors (PCB / Footprint / Schematic / Symbol) are now runtime --frame
choices of a single kicad_editor.wasm (178 MB at -O1 vs 147+82 separate; shared
wx/common/boost linked once). One editor per page load, as before; frames pcb /
fpedit / sch / symedit.
- wasm/editor/: the merged executable target (single_top + both kiface library sets,
whole-archive pcbcommon) + the safety-net focus-walk Kiface() dispatch TU. Gated by
KICAD_WASM_MERGED_EDITOR (kicad submodule bump carries the fork side: per-engine
Kiface/getter binding + ODR renames + dual-kiface launcher).
- wasm/bindings/: per-editor collab entries renamed pcbCollab*/schCollab* (JS names
unchanged); duplicate kicadOpenFile/kicadCollabOnSave + shared-name registrations
guarded behind KICAD_MERGED_EMBIND; new kicad_editor_embind.cpp registers each
shared JS name once, dispatching on the live frame.
- Build: kicad_editor app (build wrapper, target case arms, 3-object embind compile
with the ABI-critical flags, STUB_APP=pcbnew); docker/build.sh "all" =
kicad_editor calculator pl_editor gerbview (pcbnew/eeschema stay as explicit debug
apps); scripts/kicad/audit-merged-symbols.sh = repeatable ODR-collision audit (run
on kicad bumps).
- Frontend: Bundle type (bundle ≠ tool); TOOL_BUNDLE maps all four editors to
kicad_editor; explicit --frame tokens for pcbnew (pcb) and eeschema (sch); publish
list = the 4 real bundles.
- Tests/CI: five harnesses load kicad_editor.js with explicit frame tokens;
PCBNEW_FAMILY_SPECS renamed BIG_MODULE_SPECS + the 8 eeschema-family specs (they
now boot the merged module — SpiderMonkey x86 CI OOM routing); frame-runtime spec
covers all four frames from the one bundle.
Validated so far: frame-runtime 4/4 (each frame boots with the right title, no
aborts, no duplicate embind registration); 24-spec merged-module regression green;
3D raytracer renders. Known pre-existing failure: 3d-viewer title-bar drag deadlock,
fixed on main by 7630c7e (2N+8 pthread pre-warm) — picked up by the follow-up rebase.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An e2e failure used to skip everything downstream: a wx failure skipped the
asyncify suite (&&-chain) and the kicad suite, and any failure skipped the
screenshot check + Discord report entirely — leaving only the bare text notice
even though the rendered screenshots were already on disk.
Now the three suites run as separate steps gated on !cancelled() + "previous
stage wasn't skipped" (build failures still skip all tests), and the screenshot
report runs whenever the suites ran, posting with an --e2e pass/fail badge
computed from the step outcomes — so a wrong or MISSING screenshot (spec died
before page.screenshot() => classified "removed") is visible on Discord on red
builds too. The text-only failure notice becomes a fallback for when the rich
report didn't post (build broke before tests, or the report errored) — no
duplicate ping. A failing suite still fails the job; gating is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTxTrY9on6A8gLvogFbGiy
The binopt cache from b4cb10b saved only binaryen-hoist-build/bin, but the
tools dynamically link lib/libbinaryen.so, so the first cache HIT (run
28585074335) failed all 7 tools' finalize with a loader error. Cache bin/ +
lib/, bump the key to binopt-v2 (Ubicloud never re-saves an exact-key hit, so
the poisoned v1 entry can't be repaired in place), and make the
BINARYEN_TRUST_PREBUILT guard exec both tools with --version instead of -x
existence checks so an incomplete restore falls through to a source build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTxTrY9on6A8gLvogFbGiy
Four new caches in wasm-build.yml, keyed on their real inputs:
- Binaryen post-process tools (bin/ only, keyed on the submodule SHA): on a
hit BINARYEN_TRUST_PREBUILT=1 tells build-wasm-opt.sh to skip cmake+ninja
and trust the restored binaries (~46s/run). The guard is env-gated so local
pass iteration (uncommitted sources, same SHA) is unaffected.
- Built wx test apps (tests/apps minus kicad/ staging + gal-webgl/): skips
the whole build step on a hit (~3 min — the post-link hoist+asyncify over
~74 apps dominates). Key covers wx SHA + kicad SHA (some apps compile real
KiCad sources: thread_pool.cpp, libcontext), binaryen SHA, tracked app
sources, and the build/post-link scripts + shims. Gated on a wx cache HIT:
the app build creates the libwx_*.a symlinks the GAL link needs, so on a
wx rebuild the apps must rebuild too.
- Host emsdk (tools/emsdk, keyed on the pinned EMSCRIPTEN_VERSION): mostly
availability insurance — a fresh install is only ~23s but pulls ~340 MB
from github.com + storage.googleapis.com on every run. downloads/ tarballs
are pruned before the post-job save.
- Playwright browsers (~/.cache/ms-playwright keyed on tests/package-lock).
Warm-cache runs should drop from ~12 min to ~8-8.5 min (e2e suites are the
floor). Measured baselines from runs 28577824366 / 28520845242.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeiSRRScdaox5jBueJNcyG
Per request: a screenshot difference shouldn't fail CI; the Discord post is the
signal. Revert the enforcing gate (3efcdcf): drop `--fail-on-change` + the
exit-status gating and restore `continue-on-error` on the on-success report step
(compare.ts exits 0 without the flag). Remove the now-moot fail-notice guard.
Build/e2e failures still post the "CI failed" notice; changed/added/removed still
post the captioned Discord report + perf.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- kicad-packages3D is STEP-only from the 10.x tags: ensureModelInMemfs
falls back from a missing .wrl to the same-stem .step, written and
answered under the .step path (the returned path's extension picks
the parsing plugin — no C++ awareness). Verified against the live
CDN with the gallery demo board (4/4 wrl refs served as step).
- upload-models-r2.sh: rclone/S3 bulk upload of a local publish layout
(wrangler-per-object can't move ~14k blobs); published 10.0.3 —
105 libs / 7,238 models, 3.4GB raw → 500MB brotli — to pcbjam-cdn.
- deploy-demo.yml MODELS_TAG=10.0.3 → build-demo --models-tag →
VITE_MODELS_MANIFEST_URL (matches LIB_TAG: model refs come from the
footprints at that release).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014AT7gVHRktDYoQ68S4x6A4
Finalize the screenshot review system after the first Linux re-baseline
(that run showed 355/356 stable — the placeholder floor is fine as-is):
- ENFORCE the gate: `screenshots:check --fail-on-change` now fails the build on any
changed/added/removed vs baselines (it still posts the drift report first, so the
failure is actionable; a real render change → re-promote). The generic "CI failed"
notice is suppressed for gate failures so we don't double-post on drift.
- Exclude retinascale-01-loaded: a fullPage HiDPI test whose captured height + DPR
scaling vary run-to-run (~60% inter-run diff) — a flaky test, not render noise.
IGNORE_SCREENSHOTS in config.ts; compare/promote/gen-manifest skip it; baseline removed.
- Kill the baseline-dir shadowing: drop e2e/baseline-screenshots/ from BASELINE_DIRS +
delete its 3 files (grid-tab-final, wxgrid-controls, wxgrid-dedicated-page) that
duplicated names in baseline-screenshots/ with different bytes.
- Prune 12 stale baselines (renamed/removed specs: wizard-01..04, gerbview-wizard-01..04,
zoom-pl_editor-*, popup-03-palette).
- Delete the dead compare-screenshots.sh / update-baseline-screenshots.sh.
- Regenerate screenshot-manifest.json (355 entries).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two fixes for the intermittent wx-build failure on CI (zipstrm.cpp: fatal
error: 'zlib.h' file not found, surviving the serial retry):
- build-wx-wasm.sh: --with-zlib=sys resolves to the Emscripten zlib PORT,
which only `embuilder build zlib` installs — and that ran only in the
configure branch. A pre-configured build dir (CI cache) + fresh emsdk
therefore died on any recompile of a zlib-using TU. Ensure the port
before every make; --force because embuilder stamps the port on libz.a
alone, so a half-populated cache would no-op.
- wasm-build.yml: the cache-restore `touch {} +` stamped files ns-apart in
readdir order; GNU make 4.x compares ns mtimes, so objects touched before
a generated header they depend on (wx/setup.h, pcre2.h via .deps/*.d)
recompiled — a random subset per run, which is what made the failure
flaky. Use one shared timestamp instead: equal mtimes = up to date.
Reproduced + validated locally (remove sysroot zlib.h + one .o → exact CI
failure; with fix, self-heals in 0.5s; Linux make ns/equal-mtime behavior
verified in a container).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XeiSRRScdaox5jBueJNcyG
The screenshot report went silent on green CI runs. CI runs four sequential
`playwright test` invocations (wx, asyncify, kicad, perf) that all default their
outputDir to `test-results`; Playwright clears the whole outputDir at the start
of every run, so each suite wiped the previous suite's screenshots. By the time
`compare` ran, only perf-*.json remained -> all 354 baselines read as "removed"
-> the no-render guard stayed silent.
- Gate Playwright's outputDir off test-results/ in CI only
(process.env.CI ? 'pw-artifacts/<suite>' : 'test-results') in the wx / asyncify
/ kicad configs, so the committed-baseline screenshots (page.screenshot to
'test-results/...') accumulate in test-results/ across the suites for compare.
Local single-suite behavior is unchanged.
- post-discord: replace the silent no-render guard with a loud, image-less
pipeline alert ("No screenshots produced this run") when 0 render; genuine
partial removals still post as the normal capped REMOVED list.
- gitignore pw-artifacts/; upload tests/pw-artifacts/** as a CI artifact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refer to demo.pcbjam.com from the landing: an accent "Live demo" link in the
header nav, an "Open the live editor" hero link, a note in the gerber section,
and a "Try the live demo now" line in the final CTA.
Rework the embedded gerber viewer (public/gerber-demo/boot.js) to source the
WASM from the deploy pipeline's versioned CDN instead of the hand-synced
assets.pcbjam.com bucket: resolve gerbview's content-addressed folder at runtime
from the release manifest (manifest-latest -> tag -> manifest-<tag> -> gerbview),
load all assets (glue + wasm + images.tar.gz) from cdn.pcbjam.com, and load the
cross-origin pthread worker via a same-origin blob importScripts shim (mirrors
web/standalone/src/wasm/boot.ts). Bump the config-seed KICAD_VERSION_DIR
9.99 -> 10.0 to match the deployed build. Drop the now-obsolete committed glue
mirror and the old assets.pcbjam.com sync/r2-deploy scripts.
Collapse the header nav to the hamburger below 1025px — the added demo link no
longer fits the 1024px-capped bar on a single row.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New tooling in tests/tools/screenshots/ (TypeScript via tsx):
- compare.ts: one pixelmatch engine (AA-excluded), connected-component
"where to look" boxes, old|new+boxes|heatmap triptych, per-engine floors.
- promote.ts: churn-free updater — overwrite a baseline only when decoded
pixels differ beyond the floor, copying CI bytes verbatim (no re-encode
churn); pulls a CI run via `gh run download` or a local --from dir.
- post-discord.ts: always-on CI-on-main report (SHA + e2e status + the
track-only runtime-perf table), then screenshot triptychs, batched +
size-capped + flood-collapsed + 429-aware.
- perf-report.ts: perf table with Δ vs the previous main run (via gh).
- changelog.ts: no-build git-history baseline differ (Discord trigger B).
- noise.ts / gen-manifest.ts: calibration + manifest generation.
CI wiring:
- wasm-build.yml: post-test step runs the gate + report on the already-
produced test-results (no extra build); report-only (continue-on-error),
posts only on push to main, inert without DISCORD_WEBHOOK_URL.
- ci-ubicloud.yml: secrets: inherit (pass the webhook through).
- screenshot-changelog.yml: ~30s no-build changelog on baseline changes.
screenshot-manifest.json: canonical 354-name set + best-effort engine tags
(313 chromium-swiftshader / 41 firefox-llvmpipe).
Normalize scale:'device'->'css' across 18 spec files (no-op at CI DSF=1)
so committed baselines are uniformly css-scaled.
Design: CI's Linux render is the single source of truth; no pinned
container (accept rare env drift -> re-promote); dev commits via promote.
Replaces the byte-cmp compare-screenshots.sh + file-size-proxy
update-baseline-screenshots.sh (kept for now until the first re-baseline).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Measures the current build's cold load, open+render, and pan/zoom FPS
(1x/4x/6x CPU-throttle sweep) for both editors, writing
tests/test-results/perf-*.json. Track-only: asserts only that the app booted
and the document opened — no perf thresholds, so it never gates CI.
Runs on a new Chromium `perf` Playwright project (CDP throttling; pcbnew needs
V8 anyway) via `npm run test:perf`, and is wired into wasm-build.yml as a
continue-on-error step so the numbers are captured/uploaded without flaking the
gate. Reuses the existing fixtures, ready-signal, fs-inject, and board-ready
helpers; FPS counter is cancel-able so a throttle sweep on one page doesn't
accumulate rAF loops.
.gitignore: ignore /benchmark-builds/ (disposable prebuilt-WASM + standalone
harness bundle used to reproduce the native-vs-JS-EH comparison locally).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
release.yml's publish-wasm job now uploads .wasm/.js with
`--compress br --quality 11` instead of gzip-6, and IMMUTABLE
content-addressed blobs carry `no-transform` so Cloudflare's edge
can't decompress + re-serve them at its on-the-fly br-4 (and the
original Content-Length passes through).
~15-20% smaller wasm on cdn.pcbjam.com. Forward-only: tool folders
already published stay gzip until their source changes (identity is
hashed over uncompressed bytes, so the compressor switch never
re-versions or re-uploads existing folders).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Binaryen wasm-opt shrink after Asyncify was the only -O2 in the
pipeline: -O2 by default (local + tag release) but -O1 in main CI. The
C++/wx/deps compile is already -O1 everywhere (DEBUG_BUILD defaults to 1;
nothing passes --release in CI/release). Pin the tail to -O1 too:
- apply-asyncify.sh: BINARYEN_OPT_LEVEL default -O2 -> -O1
- release.yml: opt_level -O2 -> -O1 (demo now ships -O1)
- ci-ubicloud.yml / wasm-build.yml / docker/build.sh: refresh stale -O2 comments
Because both callers now build at -O1, the FINAL cache key converges, so a
tag release FINAL-cache-hits main's build and skips the asyncify-tail
rebuild entirely (previously the ~1-2h -O2 wasm-opt was rerun per release).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the legacy Emscripten JS-exceptions model with native wasm-EH (legacy
encoding) across the whole build, keeping Asyncify coroutines working via a
from-source Binaryen --hoist-cpp-catches pre-pass. Net result: native-EH is the
only build mode, the 3D viewer is on by default, and pcbnew shrinks substantially.
Highlights:
- Binaryen submodule everywhere + --hoist-cpp-catches integration in apply-asyncify;
post-link Asyncify covers every app wasm (not just standalone test wasm).
- Build deps (incl. OpenCASCADE without OCC_CONVERT_SIGNALS) and all KiCad apps
with -fwasm-exceptions; emscripten_sleep added to the post-link asyncify-imports.
- libcontext fiber entry wired under native exceptions; while-loop main loop +
currData shim injected into all wx apps.
- Native-EH collab apply fixed: DEBUG-define the embind TU + match all out-of-CMake
C++ TUs' ABI flags to the core, fixing the vtable-layout skew / mis-dispatch.
- 3D viewer enabled by default (real raytracer linked, not the stub).
- Retire the EH-spike scaffolding; flip the asyncify-races ablation pins to
shim-redundancy pins (native-EH stays clean with the legacy shims ablated).
- Fix the asyncify-races quiescence check to not require Asyncify.currData==0:
under the native-EH per-frame-yield top loop the main stack is asyncify-suspended
every frame, so currData legitimately churns (a freed-but-not-yet-nulled buffer,
not a leak). Refresh the pcbnew toolbar screenshot baseline for the new kicad.
- CI: drop the obsolete binaryen_version input/env (the build uses the binaryen
submodule fork's wasm-opt, not a version download); key the wasm-output cache on
the binaryen submodule SHA instead.
Bumps the wxwidgets + binaryen submodules to their squashed feature commits.
Validated green: all 7 apps native-EH (real 3D in pcbnew); KiCad e2e 63/63
Firefox + Chromium (3D viewer renders); wx 336; coroutine 34/34 both engines;
asyncify 7/7 both engines.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Waitlist signup form on the home page (between Projects and Tools),
cross-posting to the landing site's /api/waitlist (CORS + OPTIONS added there,
gated by WAITLIST_ALLOWED_ORIGINS).
- Version badge gains a pcbjam.com landing-page link (VITE_LANDING_URL).
- Optional Plausible analytics from VITE_PLAUSIBLE_DOMAIN (off by default);
wired into build-demo + both deploy workflows via vars.PLAUSIBLE_DOMAIN.
- Opening a read-only gallery project auto-"moves to local": it forks into a
writable browser-local project at the same slug, shadowing the gallery row.
- Project + local-folder views use the same iconed tool launcher (ToolGrid)
and a navigable directory FileTree instead of a flat file list.
- Boot overlay shows real wasm download progress (Module.instantiateWasm +
streaming byte counter) and a "taking too long" state after 60s.
- Home Libraries section lists the active libs source (the read-only R2/CDN
set in the demo) with a name filter; useLibs now reads libsSourceConfig.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TPJYx4urFVhdMjEmYfMCMD
Fixes two coupled bugs: (1) a tag deployed the demo by snapshotting the STALE registry with no build/publish, so v0.1.1 shipped v0.1.0-era wasm; (2) publish-wasm built -O1/3D-off while ci-ubicloud built -O1/3D-on — two diverging recipes.
wasm-build.yml (reusable workflow_call): THE single build+test recipe, parameterized by opt_level / build_3d_viewer / run_tests / no_cache / upload_output. Two-tier output cache around build.sh's new --compile-only/--postprocess-only split: a BASE cache keyed opt-INDEPENDENTLY (compile output + sysroot headers, shared across -O1/-O2) and a FINAL cache keyed opt-SPECIFICALLY. final-hit ⇒ skip all; final-miss+base-hit ⇒ restore base, run only the asyncify/-O tail; full-miss ⇒ compile then postprocess. Both keys now include the 3D flag, closing the cache-poisoning divergence.
ci-ubicloud.yml: thin caller (main/PR/dispatch) → wasm-build at -O1. release.yml (tag v*): meta → build (wasm-build at -O2 + e2e gate, uploads output, reuses main's base cache so only the -O2 tail rebuilds) → publish-wasm (content-addressed push + manifest-<tag>.json) → deploy-demo (build-demo pinned to that manifest + libs/kicad/$LIB_TAG → Pages). So the shipped build is exactly the tested build, and the demo can never point at stale wasm.
deploy-demo.yml: demoted to manual dispatch-only re-deploy (keeps the --from-registry snapshot for re-shipping an already-published tag). publish-wasm.yml: deleted (folded into release.yml + wasm-build.yml).
Validated: actionlint clean (bar the known ubicloud custom-label note), YAML + needs-graph + reusable-path checks pass. NOT runtime-tested — the build can't run locally; needs a CI smoke (a PR exercises ci-ubicloud→wasm-build; a throwaway tag exercises release.yml) before relying on it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
publish-libs.ts: --clone <dir> shallow-clones kicad-symbols + kicad-footprints at --lib-tag (full, all libs) and uses them as sources, so CI publishes the whole set with one command.
publish-libs.yml: decoupled workflow_dispatch (mirrors publish-wasm.yml) — lib_tag input (default 10.0.3) + force; clones + publishes to R2, skip-if-exists keyed by the tag. Dependency-free import chain ⇒ no workspace install (just node + npx tsx + git + wrangler).
deploy-demo.yml: LIB_TAG=10.0.3 → build-demo --lib-tag, so the demo serves the full library set from libs/kicad/<LIB_TAG>. Validated locally over .cache: publish then skip-if-exists; both workflows valid YAML.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an output-cache layer to ci-ubicloud.yml so a build whose inputs are
unchanged (typical when only tests/ or web/ change) skips the in-Docker
compile + the 1-2h host asyncify/wasm-opt chain — the bulk of the ~1h52m
run. On a hit, output/ and the GAL sysroot headers are restored from cache
and the deps restore/seed, build.sh, and sysroot export are all gated on a
miss, so Docker is never started.
Key: kwasm-<os>-bin<ver><opt>-k<kicad-sha>-wx<wx-sha>-sc<hash>-e<epoch>.
The "sc" hash (scripts/deploy/wasm-cache-hash.mjs) folds in just the
build-logic files that shape the wasm bytes (host post-processing,
per-tool compile recipes, scripts/deps, docker/Dockerfile+build.sh) plus
itself; it is content-based, order-independent, and identical on macOS/CI.
*.wasm.debug.wasm (5+ GB, unused by tests) is excluded -> ~0.5 GB entry on
Ubicloud's transparent 30 GB/repo/week cache.
Invalidation:
- bump .ci-cache-epoch in a commit for durable busts (inputs the sc-hash
can't see: base-image/apt drift, a bad cache);
- [no-cache] or [rebuild-wasm] in the commit message / PR title, or
workflow_dispatch no_cache=true, for a one-off rebuild (split
restore/save so the bypass still refreshes the entry).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
GITHUB_REF_NAME is 'main' on workflow_dispatch, so the old ${REF:-input} order
labeled every dispatched deploy 'main'. Prefer github.event.inputs.tag.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Project not found errors when pcbjam-demo doesn't exist yet; create it (with the
production branch matching --branch) before deploying so the pipeline is
self-contained.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The repo root has no package.json, so pnpm/action-setup can't infer the version
(it lives in web/package.json). Pin it explicitly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The KiCad e2e collab specs rebuild apps/kicad/collab-bundle.js with esbuild
(collab/build.mjs), bundling web/standalone/src/wasm/collab/* and its
@pcbjam/shared import. That package is the web/pcbjam-shared submodule, resolved
through the web/ pnpm workspace — which CI never installed, so the build failed
with: [ERROR] Could not resolve "@pcbjam/shared". Install the web/ workspace
(links @pcbjam/shared + pulls the bundle's runtime deps zod/@ts-rest/core/yjs)
before the e2e step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Commit 7dc1bf2 renamed scripts/build-wxuniversal-wasm.sh to
build-wx-wasm.sh but missed the CI workflow, so the "Build wxWidgets
(wxUniversal WASM)" step ran a script that no longer exists. Update the
run command in ci-ubicloud.yml plus the two stale comments referencing
the old name (ci-ubicloud.yml:43, scripts/common/stages.sh:7).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ci-ubicloud.yml (cherry-picked from the ubicloud-ci experiment branch, green
on run 27375556034: 291 wx + 38 kicad tests in 1h52m) now triggers on main
pushes and replaces ci-full-build.yml and ci.yml. The Hetzner flow needed
create/delete-VM jobs, a PAT + hcloud token, and a shared one-slot
concurrency lock; the Ubicloud runner is a plain runs-on label and finished
faster end-to-end. wasm-opt-bench.yml stays (dispatch-only Hetzner bench
harness, unrelated to CI).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
build-gal-webgl-test.sh compiles kicad headers that need boost from
build-wasm/sysroot/include — a host dir on dev machines but inside the
kicad-build-cache docker volume on CI (run 27370316200: ptr_vector.hpp not
found). The Makefile uses only $(SYSROOT)/include, so headers suffice.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First wx-suite run (27359020746): 262/262 non-GL tests passed; all 29
gal-webgl failures were galTest.isReady() timeouts because
gal_webgl_test.{js,wasm} are gitignored artifacts built by their own script
(build-gal-webgl-test.sh), which the workflow never ran — the page loaded
but the wasm 404'd. The script self-sources the local emsdk like
build-wasm-test.sh. Those 29 timeouts x3 attempts were also ~1.4h of the
1.7h e2e wall-clock.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Host-side wxUniversal build + wx test apps (build-wxuniversal-wasm.sh /
build-wasm-test.sh, mirroring the dispatch-only ci.yml) followed by
`npm run test` before the KiCad e2e. The wx suite runs on bundled headless
Chromium; several specs use WebGL (gal-webgl.spec.ts), so the chromium
project gains a CI-gated --enable-unsafe-swiftshader launch arg — without
it newer headless Chromium refuses software WebGL on the GPU-less runner.
First time this suite runs on a working CI runner; treat as experimental.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same rationale as the Hetzner workflow change in 3168eff: headless Firefox
cannot create any GL context on the GPU-less runner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Run 27330725978 died at container create: "range of CPUs is from 0.01 to
30.00, as there are only 30 CPUs available" with KICAD_DOCKER_CPUS=30 on the
30-vcpu standard-30. The Hetzner box (fresh docker-ce) accepts cpus == nproc;
Ubicloud's preinstalled daemon doesn't. A 29/30 CFS quota is noise.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same recipe as ci-full-build.yml (all 6 tools, Binaryen 130 from source,
pipelined wasm-opt, KiCad e2e) but on ubicloud-standard-30: a plain runs-on
label, so no create/delete-Hetzner-VM jobs and no shared one-slot concurrency
lock. Push-triggered ONLY on this branch (plus workflow_dispatch) so it never
competes with the Hetzner main CI.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The xvfb/WebGL fix (f37ddc4) took CI from 22 to 11 failures (identical on
Hetzner 27343415331 and Ubicloud 27343416511) — everything left is the
pcbnew family, still dying at wasm compile: pcbnew's ~190M debug module
exceeds SpiderMonkey's per-process code budget on x86-64 even with the
baseline-only JIT (the same module compiles on arm64, whose denser code
fits — which is why the local arm64 repro passed). No pref raises that
limit; V8 handles the module fine.
New CI-only 'chromium-ci' project (bundled Chromium, headless, SwiftShader
via --enable-unsafe-swiftshader) carries pcbnew/pcbnew-collab/load-pcb/
load-pcb-probe; the firefox project ignores them on CI. Workflows call the
new test:kicad:ci script which runs both projects. Local test:kicad is
unchanged (firefox everywhere, system Chrome for headed runs).
Validated under x86 emulation (linux/amd64 playwright image on Rosetta):
the pcbnew wizard test passes on chromium-ci in 23s against the fresh
v130-postprocessed 192M module.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI runs (Hetzner 27329612719, Ubicloud 27330989479) failed 22/41 identically,
in two buckets, both firefox-environment issues (the suite is firefox-only:
test:kicad -> --project=firefox):
1. No WebGL: headless Firefox can't create ANY GL context on a GPU-less VM
(blocklist bypass still dies with FEATURE_FAILURE_WEBGL_EXHAUSTED_DRIVERS),
so the GAL canvas failed and an error dialog blocked every UI flow. Fix:
run headed under xvfb (GLX + Mesa llvmpipe software GL) with
webgl.force-enabled — workflows now wrap the e2e step in xvfb-run.
2. pcbnew wasm OOM: the ~190M module OOMs Firefox's optimizing wasm JIT at
compile ("InternalError: out of memory") and the app never boots. Fix:
javascript.options.wasm_optimizingjit=false (baseline-only compile).
Both prefs are CI-gated in playwright-kicad.config.ts; local runs keep stock
behavior. Boot got slower (baseline JIT), so the wizard helpers now wait for
the registry to have actual UI ENTRIES (the registry OBJECT exists pre-boot)
instead of clicking into a fixed 10x500ms window.
Validated in a local docker repro of the CI env (playwright:v1.57.0 image,
no GPU): pl_editor canvas, gerbview wizard+canvas, and pcbnew wizard (on a
properly asyncified 187M module) all pass; before the fix they reproduced the
exact CI signatures.
Known-fail left on CI: zoom-cursor — the zoom-in anchors at ~ -P in headed
Firefox (screen-vs-client coordinate mix-up in the wheel->GAL path, headed
mode only); marked test.fixme(CI) pending a wx-wasm-layer investigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per run, a fresh ccx53 VM is created (Cyclenerd/hcloud-github-runner),
builds + tests run on it, and it is always torn down — no persistent
infra, no lingering cost. Secrets: HCLOUD_TOKEN, HETZNER_RUNNER_PAT.
- ci-full-build.yml: the main-branch CI and the only push-triggered
workflow. Full build of all 6 tools (pipelined wasm-opt, self-built
Binaryen 130, ~1h15m) + KiCad e2e; docs-only pushes excluded via
paths-ignore.
- ci.yml (eeschema-only smoke) and wasm-opt-bench.yml (config sweeps):
workflow_dispatch-only, since every push-run costs a paid VM.
- Shared one-Hetzner-slot concurrency group across the workflows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>