Files
Toshit ChawdaandClaude Opus 5 961cc8fa91 install cargo and wasm-bindgen for the rewriter wasm build
node-worker's prepare-node.js ends by running the Rust crate's build.sh, which
wants cargo and a wasm-bindgen CLI matching the crate version exactly. The
runner has cargo but not wasm-bindgen, so the build died an hour in, after the
emsdk and Node work was already paid for. Set up ahead of that build, with the
version read out of build.sh so it cannot drift.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-05 15:13:02 -07:00

170 lines
7.8 KiB
YAML

name: Continuous build
on:
push:
branches: [master]
workflow_dispatch:
# The `continuous` tag is recreated on every run, so two runs must not overlap: the loser of the
# race would leave its assets attached to the other one's commit. Newer wins — a build of an older
# commit has nothing left to say once a newer one is underway.
concurrency:
group: continuous
cancel-in-progress: true
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
# Dominated by the node-worker build below, which is long enough that the default limit is not
# obviously sufficient.
timeout-minutes: 180
steps:
# Into a subdirectory rather than the workspace root: `node-worker` is a `link:../node-worker`
# dependency, so it has to end up *beside* this checkout, and actions/checkout cannot write
# outside the workspace.
- name: Check out the app
uses: actions/checkout@v5
with:
path: app
- name: Check out node-worker
uses: actions/checkout@v5
with:
repository: heyputer/node-worker
path: node-worker
# GITHUB_TOKEN can read a public repository, which is all this needs. The secret is the
# fallback for as long as node-worker is still private: GITHUB_TOKEN is scoped to this
# repository alone and cannot read another private one.
token: ${{ secrets.NODE_WORKER_TOKEN || github.token }}
# An Emscripten toolchain, a full Node checkout and the object files from a wasm link of Node
# do not comfortably fit alongside everything the runner image ships with. These two are the
# largest trees this build will never touch.
- name: Reclaim runner disk
run: sudo rm -rf /usr/share/dotnet /usr/local/lib/android
# Pinned rather than read from a `packageManager` field, which this repo does not set. Both
# lockfiles are lockfileVersion 9, so one pnpm serves both checkouts.
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 11.3.0
- name: Set up Node
uses: actions/setup-node@v5
with:
node-version: 24
# node-worker's build also compiles a Rust crate to wasm, via
# src/worker/node-rust/wasm/build.sh. That script wants cargo and a wasm-bindgen CLI whose
# version matches the crate's exactly — it checks, and refuses to run on a mismatch.
#
# Ahead of the node-worker build rather than folded into it, because build.sh runs at the
# *end* of prepare-node.js: a toolchain problem would otherwise surface an hour in, after the
# emsdk and Node work is already done and paid for.
- name: Set up Rust for the rewriter wasm build
run: |
# Read the version out of build.sh's own check rather than restating it here, so this
# cannot drift from whatever the sibling repo demands at the commit being built.
build_sh=node-worker/src/worker/node-rust/wasm/build.sh
version="$(sed -n 's/^WBG="wasm-bindgen \([0-9.]*\)"$/\1/p' "$build_sh")"
if [ -z "$version" ]; then
echo "::error::could not read the required wasm-bindgen version out of $build_sh"
exit 1
fi
echo "build.sh wants wasm-bindgen $version"
# nightly with rust-src, because build.sh builds std from source (`-Z build-std`) and
# passes unstable rustc flags. The crate's rust-toolchain.toml asks for exactly this;
# installing it up front beats leaving rustup to fetch it lazily mid-build.
rustup toolchain install nightly --profile minimal \
--component rust-src --target wasm32-unknown-unknown
# The prebuilt binary: `cargo install wasm-bindgen-cli` would compile it from source for
# several minutes to reach the same place.
tarball="wasm-bindgen-$version-x86_64-unknown-linux-musl"
base="https://github.com/wasm-bindgen/wasm-bindgen/releases/download/$version"
curl -sSfL -O "$base/$tarball.tar.gz" -O "$base/$tarball.tar.gz.sha256sum"
# Compared by hand rather than with `sha256sum -c`, whose input names a path that only
# ever existed on the machine that cut the release.
expected="$(awk '{print $1}' "$tarball.tar.gz.sha256sum")"
actual="$(sha256sum "$tarball.tar.gz" | awk '{print $1}')"
if [ "$expected" != "$actual" ]; then
echo "::error::wasm-bindgen $version checksum mismatch: expected $expected, got $actual"
exit 1
fi
mkdir -p "$HOME/.local/bin"
tar -xzf "$tarball.tar.gz" --strip-components=1 \
-C "$HOME/.local/bin" "$tarball/wasm-bindgen"
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
# `$GITHUB_PATH` only takes effect in later steps, so this is the first point at which the
# two binaries build.sh looks for can be confirmed to resolve. Cheap, and it fails here
# instead of at the very end of a long build.
- name: Check cargo and wasm-bindgen resolve
run: |
cargo --version
wasm-bindgen --version
# The long pole by a wide margin: clones emsdk and the Node v25.9.0 source, builds OpenSSL's
# libcrypto with emcc, links Node itself to wasm, and finally builds the Rust rewriter above.
# Deliberately uncached — see the note at the end of this file.
- name: Build node-worker
working-directory: node-worker
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Install
working-directory: app
run: pnpm install --frozen-lockfile
# `prebuild` bakes the project templates first, fetching create-vite and its dependency tree
# from the npm registry, so this needs network as well as CPU.
- name: Build
working-directory: app
run: pnpm build
# `-C app dist` rather than `-C app/dist .`, so every entry in the archive is prefixed
# `dist/`. Asserted rather than assumed: getting this wrong produces an archive that looks
# fine until it is unpacked over somebody's web root.
- name: Package dist
run: |
tar -czf dist.tar.gz -C app dist
tar -tzf dist.tar.gz | grep -qx 'dist/index.html'
# Deleted and recreated rather than edited, because the tag has to move to this commit and
# `gh release edit --target` will not move a tag that already exists. `--repo` throughout
# because the workspace root is not itself a git checkout.
- name: Publish the continuous release
env:
GH_TOKEN: ${{ github.token }}
run: |
# Read from the checkout rather than from `github.event`, which has no commit message on
# a workflow_dispatch run, and passed to printf as an argument so nothing in it is
# interpreted as shell.
subject="$(git -C app log -1 --pretty=%s)"
{
printf 'Built from `%s` — %s\n\n' "$GITHUB_SHA" "$subject"
printf '`dist.tar.gz` unpacks to `dist/`.\n\n'
printf 'The build uses a relative base, so it can be served from any path — `/`,\n'
printf '`/labs/node/`, anywhere — provided the URL it is served at ends in a slash.\n'
} > notes.md
gh release delete continuous --repo "$GITHUB_REPOSITORY" --cleanup-tag --yes || true
gh release create continuous dist.tar.gz \
--repo "$GITHUB_REPOSITORY" \
--target "$GITHUB_SHA" \
--prerelease \
--title "Continuous build" \
--notes-file notes.md
# On caching: nothing here is cached, so every run pays for the full node-worker build. The
# alternative is caching `node-worker/{dist,emsdk,node_core}` keyed on that repo's commit SHA,
# which would turn most runs into a cache restore. Worth revisiting if the wait starts to hurt.