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.