Skip to content

Commit bdd8017

Browse files
joaoh82claude
andauthored
ci: pin binaryen / wasm-opt for deterministic WASM builds (SQLR-58) (#138)
`jetli/wasm-pack-action` and wasm-pack's curl-installer both let wasm-pack pick whatever binaryen its internal cache happens to hold. When that copy predates multi-table WASM support, wasm-opt rejects rustc output with "Only 1 table definition allowed in MVP" — a flake whose hit/miss depended on runner image cache state (first surfaced on PR #135; rerun passed). Pin binaryen to version_122 in both the ci.yml `wasm-build` and release.yml `publish-wasm` jobs by downloading the official tarball before wasm-pack runs and prepending it to PATH. wasm-pack auto-detects wasm-opt on PATH, so no further wiring needed. Bump procedure documented in docs/release-plan.md so future-me knows to update both workflows in lockstep. Verified locally on darwin-arm64: `wasm-pack build --target web --release` reports `[INFO]: found wasm-opt at ".../binaryen-version_122/bin/wasm-opt"` and builds a 2.14 MiB bundle (in line with prior sizes). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b574546 commit bdd8017

3 files changed

Lines changed: 114 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,15 @@ jobs:
299299
wasm-build:
300300
name: wasm-build
301301
runs-on: ubuntu-latest
302+
# Pinned binaryen version — see docs/release-plan.md
303+
# ("Pinned binaryen / wasm-opt") for the bump procedure. Older
304+
# binaryen rejects rustc's multi-table WASM output with
305+
# "Only 1 table definition allowed in MVP", which the runner
306+
# image's cache state used to surface non-deterministically
307+
# (SQLR-58). Pinning keeps wasm-opt out of "whatever's cached"
308+
# territory.
309+
env:
310+
BINARYEN_VERSION: version_122
302311
steps:
303312
- uses: actions/checkout@v4
304313

@@ -313,12 +322,29 @@ jobs:
313322
workspaces: sdk/wasm
314323
shared-key: wasm-build
315324

325+
- name: Install pinned binaryen (wasm-opt)
326+
# MUST run before wasm-pack: wasm-pack picks up wasm-opt from
327+
# PATH if present, otherwise downloads whatever binaryen its
328+
# own internal cache happens to have. Pinning + prepending
329+
# to PATH forces a deterministic version across runner images.
330+
run: |
331+
set -euo pipefail
332+
curl -fsSL "https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \
333+
-o "$RUNNER_TEMP/binaryen.tar.gz"
334+
tar -xzf "$RUNNER_TEMP/binaryen.tar.gz" -C "$RUNNER_TEMP"
335+
echo "$RUNNER_TEMP/binaryen-${BINARYEN_VERSION}/bin" >> "$GITHUB_PATH"
336+
"$RUNNER_TEMP/binaryen-${BINARYEN_VERSION}/bin/wasm-opt" --version
337+
316338
- name: Install wasm-pack
317339
uses: jetli/wasm-pack-action@v0.4.0
318340

319341
- name: wasm-pack build --target web --release
320342
working-directory: sdk/wasm
321-
run: wasm-pack build --target web --release
343+
run: |
344+
# Sanity-check that the pinned wasm-opt is what wasm-pack sees.
345+
which wasm-opt
346+
wasm-opt --version
347+
wasm-pack build --target web --release
322348
323349
- name: Report .wasm size
324350
# Surfaces size regressions in PR logs. Not a hard limit yet;

.github/workflows/release.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,14 @@ jobs:
11891189
if: needs.detect.outputs.should_release == 'true'
11901190
runs-on: ubuntu-latest
11911191
environment: release
1192+
# Pinned binaryen version — see docs/release-plan.md
1193+
# ("Pinned binaryen / wasm-opt") for the bump procedure. Older
1194+
# binaryen rejects rustc's multi-table WASM output with
1195+
# "Only 1 table definition allowed in MVP" (SQLR-58). The
1196+
# release pipeline can't tolerate that flake — a failed
1197+
# publish-wasm leaves the rest of the release wave inconsistent.
1198+
env:
1199+
BINARYEN_VERSION: version_122
11921200
permissions:
11931201
# OIDC: required for npm trusted-publisher token exchange.
11941202
# Same flow proven in publish-nodejs after the v0.1.5–0.1.7
@@ -1207,6 +1215,20 @@ jobs:
12071215
shared-key: publish-wasm
12081216
workspaces: 'sdk/wasm -> target'
12091217

1218+
- name: Install pinned binaryen (wasm-opt)
1219+
# MUST run before wasm-pack: wasm-pack picks up wasm-opt
1220+
# from PATH if present, otherwise downloads whatever
1221+
# binaryen its own internal cache happens to have. Pinning
1222+
# + prepending to PATH forces a deterministic version
1223+
# across runner images.
1224+
run: |
1225+
set -euo pipefail
1226+
curl -fsSL "https://github.com/WebAssembly/binaryen/releases/download/${BINARYEN_VERSION}/binaryen-${BINARYEN_VERSION}-x86_64-linux.tar.gz" \
1227+
-o "$RUNNER_TEMP/binaryen.tar.gz"
1228+
tar -xzf "$RUNNER_TEMP/binaryen.tar.gz" -C "$RUNNER_TEMP"
1229+
echo "$RUNNER_TEMP/binaryen-${BINARYEN_VERSION}/bin" >> "$GITHUB_PATH"
1230+
"$RUNNER_TEMP/binaryen-${BINARYEN_VERSION}/bin/wasm-opt" --version
1231+
12101232
# Install wasm-pack — the canonical tool for building +
12111233
# packaging Rust crates as npm-publishable WASM modules.
12121234
# `cargo binstall` would be faster than `cargo install`
@@ -1239,6 +1261,9 @@ jobs:
12391261
- name: Build WASM package
12401262
working-directory: sdk/wasm
12411263
run: |
1264+
# Sanity-check that the pinned wasm-opt is on PATH (SQLR-58).
1265+
which wasm-opt
1266+
wasm-opt --version
12421267
wasm-pack build --release --target bundler --scope joaoh82
12431268
echo "--- generated pkg/ contents ---"
12441269
ls -la pkg/

docs/release-plan.md

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,10 @@ The "publish" half. Auto-fires on the release commit.
247247
GitHub Release `sqlrite-node-vX.Y.Z`.
248248
- **publish-wasm**`wasm-pack build --target bundler --release`,
249249
then `wasm-pack publish` via OIDC. Creates
250-
`sqlrite-wasm-vX.Y.Z` GitHub Release.
250+
`sqlrite-wasm-vX.Y.Z` GitHub Release. Installs a
251+
[pinned binaryen / wasm-opt](#pinned-binaryen--wasm-opt) before
252+
invoking `wasm-pack`, so the published bundle is byte-stable
253+
across runner image cache states.
251254
- **publish-go** — nothing to build on the Go side. Verifies
252255
`sdk/go/vX.Y.Z` was pushed correctly by `tag-all`. Pulls the
253256
per-platform `libsqlrite_c` tarballs produced by
@@ -285,6 +288,64 @@ The "publish" half. Auto-fires on the release commit.
285288
exist (because the real release happened weeks ago). Workflow
286289
aborts with a clear "tag already exists" error. No damage.
287290

291+
## Pinned binaryen / wasm-opt
292+
293+
The WASM build paths in `ci.yml` (`wasm-build`) and `release.yml`
294+
(`publish-wasm`) both install a **pinned version of binaryen**
295+
(which provides `wasm-opt`) before invoking `wasm-pack`. The pin
296+
lives in a `BINARYEN_VERSION` job-level `env:` in each workflow.
297+
298+
**Current pin: `version_122`** (released Feb 2025).
299+
300+
### Why this exists (SQLR-58)
301+
302+
`wasm-pack` invokes `wasm-opt` to size-optimize the published
303+
bundle. If `wasm-opt` is already on `PATH`, `wasm-pack` uses that
304+
one; otherwise it downloads its own copy into a per-runner cache.
305+
That cache is keyed on the runner image and survives across
306+
images opaquely — which means CI was getting whatever binaryen
307+
the cache happened to hold. When the cached copy was old enough
308+
to predate multi-table WASM support, `wasm-opt` would reject
309+
recent rustc output with:
310+
311+
> `[parse exception: Only 1 table definition allowed in MVP]`
312+
> `Fatal: error in parsing input`
313+
> `Error: failed to execute wasm-opt: exited with exit code: 1`
314+
315+
The failure was non-deterministic (re-runs frequently passed,
316+
because the new image had a different cache state), but it broke
317+
the release pipeline at least once before [PR #135](https://github.com/joaoh82/rust_sqlite/pull/135).
318+
Pinning binaryen + prepending it to `PATH` forces `wasm-pack`
319+
to always see the same `wasm-opt`, regardless of runner state.
320+
321+
### Bump procedure
322+
323+
1. Look at the [binaryen releases page](https://github.com/WebAssembly/binaryen/releases)
324+
and pick a recent stable version (avoid release candidates).
325+
2. Locally, download that release's `x86_64-linux` tarball and
326+
run `wasm-opt --version` to confirm it builds. Optional but
327+
nice: also run `wasm-pack build --target web --release` in
328+
`sdk/wasm` with the new `wasm-opt` on `PATH` and confirm the
329+
`.wasm` artifact size is in the same ballpark as before
330+
(regressions > 10% are worth investigating).
331+
3. Update `BINARYEN_VERSION` in both `.github/workflows/ci.yml`
332+
(job `wasm-build`) and `.github/workflows/release.yml` (job
333+
`publish-wasm`). Keep the two in lockstep — a divergence
334+
means CI and release produce subtly different artifacts.
335+
4. Update the "Current pin" line above to match.
336+
5. PR + merge. CI will exercise the new version on the WASM
337+
build job before the release pipeline ever sees it.
338+
339+
### Why a tarball, not apt-get
340+
341+
Ubuntu's apt-packaged `binaryen` is reliably 1–2 years behind
342+
upstream and pinning it requires the matching apt index, which is
343+
itself unstable across runner image refreshes. The official
344+
WebAssembly/binaryen GitHub release tarballs are stable URLs and
345+
sha-pinnable (we don't currently verify the sha256 — a follow-up
346+
if supply-chain integrity becomes a concern; the tarball is
347+
short-lived and contained to the runner).
348+
288349
## Secrets / one-time setup
289350

290351
With lockstep + OIDC-based trusted publishing, the only long-lived

0 commit comments

Comments
 (0)