Skip to content

feat(plugins): add native-default engine selection infrastructure #2657

feat(plugins): add native-default engine selection infrastructure

feat(plugins): add native-default engine selection infrastructure #2657

Workflow file for this run

name: Publish Package to npm
on:
push:
branches:
- main
pull_request:
branches:
- main
- feat/napi-binding
# Pull requests and every job below run read-only. Only the `release` and
# `deploy-pages` jobs get write permissions, and they run on pushes to main
# alone, so no step a pull request controls can publish, tag, comment or deploy.
permissions:
contents: read
# CI always runs the latest releases: the `ubuntu-latest` runner, the newest
# major version of every action, and the latest stable Rust, Bun, Node.js,
# wasm-pack, cargo-binstall, cargo-tarpaulin and changepacks.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
jobs:
# Independent read-only lane; no existing validation job waits on it.
native-build:
runs-on: ${{ matrix.runner }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- { suffix: linux-x64-gnu, target: x86_64-unknown-linux-gnu, runner: ubuntu-latest }
- { suffix: linux-arm64-gnu, target: aarch64-unknown-linux-gnu, runner: ubuntu-latest }
- { suffix: linux-x64-musl, target: x86_64-unknown-linux-musl, runner: ubuntu-latest }
- { suffix: linux-arm64-musl, target: aarch64-unknown-linux-musl, runner: ubuntu-latest }
- { suffix: darwin-x64, target: x86_64-apple-darwin, runner: macos-latest }
- { suffix: darwin-arm64, target: aarch64-apple-darwin, runner: macos-latest }
- { suffix: win32-x64-msvc, target: x86_64-pc-windows-msvc, runner: windows-latest }
# Windows ARM has no -latest alias; this is its newest exposed image, not a pin.
- { suffix: win32-arm64-msvc, target: aarch64-pc-windows-msvc, runner: windows-11-vs2026-arm }
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.autocrlf
GIT_CONFIG_VALUE_0: "false"
CARGO_BUILD_JOBS: "2"
RUSTUP_TOOLCHAIN: stable
steps:
- name: Start durable build evidence
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/native-build-${{ matrix.suffix }}"
printf '%s\n' "$GITHUB_SHA $RUNNER_OS $RUNNER_ARCH $ImageOS $ImageVersion" > "$RUNNER_TEMP/native-build-${{ matrix.suffix }}/runner.txt"
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
target: ${{ matrix.target }}
cache: false
- name: Install latest stable Zig and cargo-zigbuild
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
curl -fsSL https://ziglang.org/download/index.json -o "$RUNNER_TEMP/zig-index.json"
version=$(jq -r 'keys[] | select(test("^[0-9]+\\.[0-9]+\\.[0-9]+$"))' "$RUNNER_TEMP/zig-index.json" | sort -V | tail -1)
jq --arg v "$version" '.[$v]["x86_64-linux"]' "$RUNNER_TEMP/zig-index.json" > "$RUNNER_TEMP/zig-asset.json"
cp "$RUNNER_TEMP/zig-asset.json" "$RUNNER_TEMP/native-build-${{ matrix.suffix }}/zig-asset.json"
curl -fsSL "$(jq -r .tarball "$RUNNER_TEMP/zig-asset.json")" -o "$RUNNER_TEMP/zig.tar.xz"
echo "$(jq -r .shasum "$RUNNER_TEMP/zig-asset.json") $RUNNER_TEMP/zig.tar.xz" | sha256sum -c -
mkdir "$RUNNER_TEMP/zig"
tar -xf "$RUNNER_TEMP/zig.tar.xz" --strip-components=1 -C "$RUNNER_TEMP/zig"
echo "$RUNNER_TEMP/zig" >> "$GITHUB_PATH"
CARGO_TARGET_DIR="$RUNNER_TEMP/native-zigbuild-tool-target" cargo install cargo-zigbuild --locked --jobs 2
- name: Build fresh target and retain exact receipt
shell: bash
env:
CARGO_TARGET_DIR: ${{ runner.temp }}/native-target-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.suffix }}
NATIVE_SUFFIX: ${{ matrix.suffix }}
NATIVE_TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
test "$GITHUB_RUN_ATTEMPT" = 1 # Never replace original payloads on release-only retries.
test ! -e "$CARGO_TARGET_DIR"
if [[ "$RUNNER_OS" == macOS ]]; then export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)"; fi
if [[ "$NATIVE_SUFFIX" == win32-arm64-msvc ]]; then node -e "require('node:assert/strict').equal(process.arch,'arm64')"; fi
flags=()
if [[ "$RUNNER_OS" == Linux ]]; then flags+=(--zigbuild); fi
node e2e/binding/package/provenance.mjs "$NATIVE_TARGET" "$RUNNER_TEMP/native-build-$NATIVE_SUFFIX/payload" "$GITHUB_RUN_ID/1" "${flags[@]}"
- uses: actions/upload-artifact@v7
if: always()
with:
name: native-build-${{ matrix.suffix }}-${{ github.run_id }}
path: ${{ runner.temp }}/native-build-${{ matrix.suffix }}/*
if-no-files-found: error
retention-days: 7
native-packs:
needs: [native-build]
runs-on: ubuntu-latest
timeout-minutes: 20
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.autocrlf
GIT_CONFIG_VALUE_0: "false"
steps:
- name: Start durable collector evidence
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/native-pack-evidence"
printf '%s\n' "$GITHUB_SHA $GITHUB_RUN_ID $GITHUB_RUN_ATTEMPT $RUNNER_OS $RUNNER_ARCH $ImageOS $ImageVersion" > "$RUNNER_TEMP/native-pack-evidence/runner.txt"
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/download-artifact@v8
with:
pattern: native-build-*-${{ github.run_id }}
path: ${{ runner.temp }}/native-inputs
- name: Collect once from clean source and run real-artifact controls
shell: bash
run: |
set -euo pipefail
test "$GITHUB_RUN_ATTEMPT" = 1
mkdir "$RUNNER_TEMP/native-artifacts"
for suffix in $(node --input-type=module -e "import {targets} from './e2e/binding/package/matrix.mjs'; console.log(targets.map(r=>r.suffix).join(' '))"); do
mv "$RUNNER_TEMP/native-inputs/native-build-$suffix-$GITHUB_RUN_ID/payload" "$RUNNER_TEMP/native-artifacts/$suffix"
done
mkdir -p bindings/devup-ui-binding/pkg
node bindings/devup-ui-binding/script.cjs --public "$PWD/bindings/devup-ui-binding/pkg/index.cjs"
node e2e/binding/package/collect.mjs "$RUNNER_TEMP/native-artifacts" "$RUNNER_TEMP/native-bundle" "$GITHUB_RUN_ID/1"
mkdir "$RUNNER_TEMP/native-controls"
cp -R "$RUNNER_TEMP/native-artifacts/linux-x64-gnu" "$RUNNER_TEMP/native-controls/linux-x64-gnu"
NATIVE_PROOF_ARTIFACTS="$RUNNER_TEMP/native-controls" NATIVE_PROOF_BUNDLE="$RUNNER_TEMP/native-bundle" node --test e2e/binding/package/package.test.mjs e2e/binding/package/publish.test.mjs e2e/binding/package/elf.test.mjs
sha256sum "$RUNNER_TEMP/native-bundle/"*.tgz "$RUNNER_TEMP/native-bundle/bundle.json" | tee "$RUNNER_TEMP/native-bundle/SHA256SUMS"
du -b "$RUNNER_TEMP/native-bundle/"*
- uses: actions/upload-artifact@v7
if: always()
with:
name: native-packs-${{ github.run_id }}
path: ${{ runner.temp }}/native-bundle/*
if-no-files-found: error
retention-days: 7
- uses: actions/upload-artifact@v7
if: always()
with:
name: native-pack-evidence-${{ github.run_id }}
path: ${{ runner.temp }}/native-pack-evidence/*
if-no-files-found: error
retention-days: 7
native-consumer:
needs: [native-packs]
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- { suffix: linux-x64-gnu, runner: ubuntu-latest, cpu: x64, docker: amd64, image: "node:current", bun: bun-linux-x64.zip, mode: native-container }
- { suffix: linux-arm64-gnu, runner: ubuntu-latest, cpu: arm64, docker: arm64, image: "node:current", bun: bun-linux-aarch64.zip, mode: QEMU-emulated }
- { suffix: linux-x64-musl, runner: ubuntu-latest, cpu: x64, docker: amd64, image: "node:current-alpine", bun: bun-linux-x64-musl.zip, mode: native-container }
- { suffix: linux-arm64-musl, runner: ubuntu-latest, cpu: arm64, docker: arm64, image: "node:current-alpine", bun: bun-linux-aarch64-musl.zip, mode: QEMU-emulated }
- { suffix: darwin-x64, runner: macos-latest, cpu: x64, bun: bun-darwin-x64.zip, mode: Rosetta }
- { suffix: darwin-arm64, runner: macos-latest, cpu: arm64, bun: bun-darwin-aarch64.zip, mode: native }
- { suffix: win32-x64-msvc, runner: windows-latest, cpu: x64, bun: bun-windows-x64.zip, mode: native }
# Windows ARM has no -latest alias; this is its newest exposed image, not a pin.
- { suffix: win32-arm64-msvc, runner: windows-11-vs2026-arm, cpu: arm64, bun: bun-windows-aarch64.zip, mode: native }
env:
NATIVE_SUFFIX: ${{ matrix.suffix }}
NATIVE_CPU: ${{ matrix.cpu }}
NATIVE_MODE: ${{ matrix.mode }}
steps:
- name: Start durable consumer evidence
shell: bash
run: |
mkdir -p "$RUNNER_TEMP/native-evidence"
printf '%s\n' "$NATIVE_MODE $GITHUB_SHA $RUNNER_OS $RUNNER_ARCH" > "$RUNNER_TEMP/native-evidence/start.txt"
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/download-artifact@v8
with:
name: native-packs-${{ github.run_id }}
path: ${{ runner.temp }}/native-bundle
- name: Enable current ARM emulation
if: runner.os == 'Linux' && matrix.cpu == 'arm64'
uses: docker/setup-qemu-action@v4
with:
platforms: arm64
- name: Enable Rosetta for both x64 runtimes
if: matrix.mode == 'Rosetta'
run: sudo softwareupdate --install-rosetta --agree-to-license
- uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
architecture: ${{ runner.os == 'Linux' && 'x64' || matrix.cpu }}
- name: Download matching latest official Bun with digest
shell: bash
env:
GH_TOKEN: ${{ github.token }}
BUN_ASSET: ${{ matrix.bun }}
run: |
set -euo pipefail
mkdir "$RUNNER_TEMP/native-tools"
gh api repos/oven-sh/bun/releases/latest > "$RUNNER_TEMP/native-evidence/bun-release.json"
node -e "const fs=require('node:fs'),r=JSON.parse(fs.readFileSync(process.env.RUNNER_TEMP+'/native-evidence/bun-release.json'));const a=r.assets.find(a=>a.name===process.env.BUN_ASSET);if(!a?.digest?.startsWith('sha256:'))throw Error('missing official digest');fs.writeFileSync(process.env.RUNNER_TEMP+'/native-evidence/bun-asset.json',JSON.stringify(a))"
url=$(node -p "JSON.parse(require('node:fs').readFileSync(process.env.RUNNER_TEMP+'/native-evidence/bun-asset.json')).browser_download_url")
curl -fsSL "$url" -o "$RUNNER_TEMP/native-tools/bun.zip"
node -e "const fs=require('node:fs'),c=require('node:crypto'),p=process.env.RUNNER_TEMP;require('node:assert/strict').equal('sha256:'+c.createHash('sha256').update(fs.readFileSync(p+'/native-tools/bun.zip')).digest('hex'),JSON.parse(fs.readFileSync(p+'/native-evidence/bun-asset.json')).digest)"
unzip -q "$RUNNER_TEMP/native-tools/bun.zip" -d "$RUNNER_TEMP/native-tools"
bun_dir="$RUNNER_TEMP/native-tools/${BUN_ASSET%.zip}"
echo "$bun_dir" >> "$GITHUB_PATH"
printf '%s\n' "$NATIVE_MODE" > "$RUNNER_TEMP/native-evidence/execution-mode.txt"
node -e "require('node:fs').writeFileSync(process.env.RUNNER_TEMP+'/native-evidence/runner.json',JSON.stringify(Object.fromEntries(['GITHUB_SHA','GITHUB_RUN_ID','GITHUB_RUN_ATTEMPT','RUNNER_OS','RUNNER_ARCH','ImageOS','ImageVersion','NATIVE_MODE','NATIVE_SUFFIX'].map(k=>[k,process.env[k]]))))"
- name: Consume on matching macOS or Windows runtimes
if: runner.os != 'Linux'
shell: bash
run: |
set -euo pipefail
bun install --frozen-lockfile --ignore-scripts
node --input-type=module <<'NODE' 2>&1 | tee "$RUNNER_TEMP/native-evidence/consumer.log"
import assert from 'node:assert/strict'
import { dirname, join } from 'node:path'
import { realpathSync } from 'node:fs'
import getExePath from './node_modules/@typescript/native/lib/getExePath.js'
import { command } from './e2e/binding/package/provenance.mjs'
import { consume } from './e2e/binding/package/consumer.mjs'
assert.equal(process.arch, process.env.NATIVE_CPU)
assert.equal(command('bun', ['-p', 'process.arch']), process.env.NATIVE_CPU)
const tsc = getExePath()
console.log({ mode: process.env.NATIVE_MODE, tsc, version: command(tsc, ['--version']) })
const npmCli = process.platform === 'win32' ? join(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js') : realpathSync(command('which', ['npm']))
await consume({ bundle: join(process.env.RUNNER_TEMP, 'native-bundle'), suffix: process.env.NATIVE_SUFFIX, report: join(process.env.RUNNER_TEMP, 'native-evidence/consumer.json'), tsc, npmCli })
NODE
- name: Consume current GNU or musl container with matching Node and Bun
if: runner.os == 'Linux'
shell: bash
env:
CONTAINER_IMAGE: ${{ matrix.image }}
CONTAINER_CPU: ${{ matrix.docker }}
BUN_ASSET: ${{ matrix.bun }}
run: |
set -euo pipefail
docker pull --platform "linux/$CONTAINER_CPU" "$CONTAINER_IMAGE"
docker image inspect "$CONTAINER_IMAGE" > "$RUNNER_TEMP/native-evidence/container.json"
docker run --rm -i --platform "linux/$CONTAINER_CPU" -v "$PWD:/repo" -v "$RUNNER_TEMP:/evidence" -w /repo -e NATIVE_SUFFIX -e NATIVE_CPU -e NATIVE_MODE -e BUN_ASSET "$CONTAINER_IMAGE" sh -eu <<'SH' 2>&1 | tee "$RUNNER_TEMP/native-evidence/consumer.log"
if command -v apk >/dev/null; then apk add --no-cache libstdc++ tar; else apt-get update && apt-get install -y --no-install-recommends libatomic1; fi
export PATH="/evidence/native-tools/${BUN_ASSET%.zip}:$PATH"
bun install --frozen-lockfile --ignore-scripts
node --input-type=module <<'NODE'
import assert from 'node:assert/strict'
import { realpathSync } from 'node:fs'
import getExePath from './node_modules/@typescript/native/lib/getExePath.js'
import { command } from './e2e/binding/package/provenance.mjs'
import { consume } from './e2e/binding/package/consumer.mjs'
assert.equal(process.arch, process.env.NATIVE_CPU)
assert.equal(command('bun', ['-p', 'process.arch']), process.env.NATIVE_CPU)
const tsc = getExePath()
console.log({ mode: process.env.NATIVE_MODE, tsc, version: command(tsc, ['--version']) })
await consume({ bundle: '/evidence/native-bundle', suffix: process.env.NATIVE_SUFFIX, report: '/evidence/native-evidence/consumer.json', tsc, npmCli: realpathSync(command('which', ['npm'])) })
NODE
SH
- uses: actions/upload-artifact@v7
if: always()
with:
name: native-consumer-${{ matrix.suffix }}-${{ github.run_id }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/native-evidence/*
if-no-files-found: error
retention-days: 7
benchmark:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-benchmark-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
name: Install bun
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-benchmark-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- &install-wasm-pack
name: Install and verify latest stable wasm-pack
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
export PATH="${CARGO_HOME:-$HOME/.cargo}/bin:$PATH"
cargo binstall wasm-pack --no-confirm --force
latest="$(curl -fsSL -A 'devup-ui CI (https://github.com/dev-five-git/devup-ui)' https://crates.io/api/v1/crates/wasm-pack | jq -er '.crate.max_stable_version | select(type == "string" and length > 0)')"
installed="$(wasm-pack --version)"
echo "Installed $installed; crates.io latest stable: $latest"
test "$installed" = "wasm-pack $latest"
- name: Install Node.js
uses: actions/setup-node@v7
with:
registry-url: "https://registry.npmjs.org"
# `latest` can select alphas from https://nodejs.org/dist/index.json;
# see https://github.com/actions/setup-node/issues/1657.
# The stable manifest can briefly trail nodejs.org after a stable release.
node-version: '*'
check-latest: true
- run: bun install
- run: bun run build
# Every row is the median of the same number of cold samples taken in
# rotating order (benchmark-manifest.json). The run goes to an artifact and
# the Devup UI rows are compared with the checked baseline at the end of the
# job, so the profile and the Rust measurement still run when it fails.
- name: Benchmark
run: bun run benchmark
- name: Profile Devup UI Turbopack
env:
DEVUP_UI_PROFILE: "1"
run: |
rm -rf benchmark/next-devup-ui-single-turbo/.next \
benchmark/next-devup-ui-single-turbo/df \
benchmark/next-devup-ui-single-turbo/tsconfig.tsbuildinfo
bun run --filter next-devup-ui-single-turbo-benchmark build
- name: Upload the measured run
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: benchmark-run
path: benchmark-run.json
if-no-files-found: ignore
retention-days: 30
- name: Measure the SemanticBuilder passes of the extractor
run: cargo bench -p extractor --bench semantic_pass_benchmark -- --quick
- name: Compare the Devup UI rows with the baseline
run: bun benchmark-check.js
wasm-size:
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
env:
CARGO_BUILD_JOBS: "2"
RUSTUP_TOOLCHAIN: stable
steps:
- name: Checkout the tested source and both histories
uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
target: wasm32-unknown-unknown
cache: false
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- name: Resolve and report exact WASM comparison sources
run: node resolve-wasm-size-sources.js
- *install-wasm-pack
- name: Cache registry downloads only
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-wasm-size-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Install the latest shared optimizer once
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/wasm-size-tools/binaryen"
gh release download --repo WebAssembly/binaryen --pattern '*-x86_64-linux.tar.gz' --dir "$RUNNER_TEMP/wasm-size-tools"
tar -xzf "$RUNNER_TEMP"/wasm-size-tools/*.tar.gz --strip-components=1 -C "$RUNNER_TEMP/wasm-size-tools/binaryen"
echo "$RUNNER_TEMP/wasm-size-tools/binaryen/bin" >> "$GITHUB_PATH"
- name: Build exact sources with shared tools and lock-matched bindgen
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
export SIZE_ROOT="$RUNNER_TEMP/wasm-size"
mkdir -p "$SIZE_ROOT"
stage=source
trap 'echo "## WASM size: FAIL - $stage build or provenance failed; no comparison was skipped or substituted." >> "$GITHUB_STEP_SUMMARY"' ERR
for side in base candidate; do
if [[ "$side" == base ]]; then sha="$BASE_SHA"; else sha="$CANDIDATE_SHA"; fi
git cat-file -e "$sha^{commit}"
mkdir -p "$SIZE_ROOT/$side"
git archive "$sha" | tar -x -C "$SIZE_ROOT/$side"
done
cat > "$SIZE_ROOT/provenance.mjs" <<'NODE'
import { execFileSync } from 'node:child_process'
import { createHash } from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
const run = (command, args) => execFileSync(command, args, { encoding: 'utf8' }).trim()
const tools = Object.fromEntries([
['rustc', 'rustc', ['--version', '--verbose']],
['wasmPack', 'wasm-pack', ['--version']],
['wasmBindgen', 'wasm-bindgen', ['--version']],
['wasmOpt', 'wasm-opt', ['--version']],
['bun', 'bun', ['--version']],
['node', 'node', ['--version']],
].map(([key, command, args]) => {
const path = run('which', [command])
const hash = createHash('sha256').update(readFileSync(path)).digest('hex')
return [key, `${run(command, args)}\n${path}\nsha256:${hash}`]
}))
const side = process.argv[2]
const file = join(process.env.SIZE_ROOT, 'metadata.json')
const metadata = side === 'base' ? {
jobId: `${process.env.GITHUB_RUN_ID}/${process.env.GITHUB_RUN_ATTEMPT}/${process.env.GITHUB_JOB}`,
buildCommand: 'cargo metadata --locked --format-version 1 > /dev/null && wasm-pack build --mode no-install --release --target nodejs --out-dir ./pkg --out-name index --locked && node script.js',
} : JSON.parse(readFileSync(file, 'utf8'))
const sha = side === 'base' ? process.env.BASE_SHA : process.env.CANDIDATE_SHA
metadata[side] = {
commit: run('git', ['rev-parse', `${sha}^{commit}`]),
tree: run('git', ['rev-parse', `${sha}^{tree}`]),
wasmBindgenVersion: process.env.WASM_BINDGEN_VERSION,
packageDir: join(process.env.SIZE_ROOT, side, 'bindings/devup-ui-wasm/pkg'), tools,
}
writeFileSync(file, JSON.stringify(metadata, null, 2) + '\n')
NODE
shared_path="$PATH"
for side in base candidate; do
stage="$side"
(
WASM_BINDGEN_VERSION="$(bun resolve-wasm-bindgen.js "$SIZE_ROOT/$side/Cargo.lock")"
export WASM_BINDGEN_VERSION
bindgen_root="$SIZE_ROOT/abi-tools/$WASM_BINDGEN_VERSION"
if [[ ! -x "$bindgen_root/bin/wasm-bindgen" ]]; then
cargo binstall wasm-bindgen-cli --version "=$WASM_BINDGEN_VERSION" --root "$bindgen_root" --no-confirm --force
fi
export PATH="$bindgen_root/bin:$shared_path"
node "$SIZE_ROOT/provenance.mjs" "$side"
cd "$SIZE_ROOT/$side/bindings/devup-ui-wasm"
export CARGO_TARGET_DIR="$SIZE_ROOT/$side-target"
cargo metadata --locked --format-version 1 > /dev/null
wasm-pack build --mode no-install --release --target nodejs --out-dir ./pkg --out-name index --locked
node script.js
) 2>&1 | tee "$SIZE_ROOT/$side-build.log"
done
- name: Compare all four relative size metrics
run: |
node verify-wasm-size.js \
--base-pkg "$RUNNER_TEMP/wasm-size/base/bindings/devup-ui-wasm/pkg" \
--candidate-pkg "$RUNNER_TEMP/wasm-size/candidate/bindings/devup-ui-wasm/pkg" \
--metadata "$RUNNER_TEMP/wasm-size/metadata.json" \
--report "$RUNNER_TEMP/wasm-size/wasm-size.json"
- name: Upload size measurements and build evidence
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: wasm-size
path: |
${{ runner.temp }}/wasm-size/metadata.json
${{ runner.temp }}/wasm-size/wasm-size.json
${{ runner.temp }}/wasm-size/*-build.log
if-no-files-found: error
retention-days: 30
# The landing app is the only real-scale acceptance target in this repo, and
# `publish` runs it against the deployed vinext/@devup-ui/vite-plugin build.
# This job runs the SAME 128 assertions and the SAME screenshot baselines
# against a Next 16 + Turbopack build, so @devup-ui/next-plugin's production
# CSS path stays gated at 64 routes - the scale where extraction-wave races
# actually reproduce. It runs in parallel so it stays off the publish critical
# path, and its artifact is never deployed.
landing-next-e2e:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-landing-next-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
name: Install bun
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-landing-next-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- *install-wasm-pack
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- run: bun install
- run: bun run build
- name: Build Landing (Next)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bun run --filter @devup-ui/components build-storybook
mv ./packages/components/storybook-static ./apps/landing/public/storybook
bun run --filter landing build:next
- name: Cache Playwright Browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-landing-next-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Playwright Browsers
run: bunx playwright install chromium --with-deps
- name: Run E2E Tests (Next)
run: bun run test:e2e:next
- name: Run Landing dev navigation tests (Next)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LANDING_BUILD_MODE: next
run: bunx playwright test -c playwright.landing-dev.config.ts
- name: Build Landing (Next, singleCss)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -rf apps/landing/out apps/landing/.next apps/landing/df
DEVUP_SINGLE_CSS=1 bun run --filter landing build:next
- name: Run E2E Tests (Next, singleCss)
run: bun run test:e2e:next
- name: Upload Next Playwright Report
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report-next
path: playwright-report/
retention-days: 1
# The packages take React ^18 || ^19 as a peer. This builds a React 18.3 app
# with the Vite plugin and the UI kit, then checks refs, context and events
# in Chromium, so React-19-only APIs cannot slip back in.
react-18:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-react-18-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
name: Install bun
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-react-18-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- *install-wasm-pack
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- run: bun install
- run: bun run build
- name: Type-check and build the React 18 app
working-directory: apps/react-18
run: |
bun ../../node_modules/@typescript/native/bin/tsc
bun run build
- name: Cache Playwright Browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-react-18-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Playwright Browsers
run: bunx playwright install chromium --with-deps
- name: Run React 18 E2E Tests
working-directory: apps/react-18
run: bun run test:e2e
# @devup-ui/vite-plugin must keep the CSS every server page needs when the
# build is vinext/RSC. The fixture shares one Devup UI header between server
# list pages and a client page, and the tests read the styles the browser
# actually applies when each route is entered directly, so a dropped
# stylesheet dependency fails here whatever the CSS files are called.
vinext-rsc-css-e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
name: Install bun
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- *install-wasm-pack
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- run: bun install
- run: bun run build
- name: Cache Playwright Browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-vinext-rsc-css-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Playwright Browsers
run: bunx playwright install chromium --with-deps
- name: Build fixture (per-file CSS)
run: bun run --filter vinext-rsc-css-repro build
- name: Run E2E Tests (per-file CSS)
run: bun run test:e2e:rsc-css
- name: Build fixture (singleCss)
run: |
rm -rf apps/vinext-rsc-css-repro/dist apps/vinext-rsc-css-repro/.vinext apps/vinext-rsc-css-repro/df
DEVUP_SINGLE_CSS=1 bun run --filter vinext-rsc-css-repro build
- name: Run E2E Tests (singleCss)
run: bun run test:e2e:rsc-css
release-compat:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
CARGO_BUILD_JOBS: "2"
steps:
- name: Checkout
uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-release-compat-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-release-compat-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- *install-wasm-pack
- uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- run: bun install
- name: Test exited-process memory sampling
run: node --test e2e/release/process-memory.test.mjs
- name: Build packages under the memory watchdog
run: node e2e/release/build-packages.mjs
- name: Cache Playwright browsers
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-release-compat-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Playwright browsers
run: bunx playwright install chromium firefox webkit --with-deps
- name: Build packed consumers and check production computed styles
run: bun run test:release
- name: Upload release compatibility evidence
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: release-compat-results
path: |
release-package-build.json
release-results.json
if-no-files-found: error
retention-days: 30
webpack-extends-theme:
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read
env:
CARGO_BUILD_JOBS: "2"
steps:
- name: Configure isolated paths
run: |
printf 'DEVUP_PLAYWRIGHT_ROOT=%s/webpack-extends-playwright\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
printf 'PLAYWRIGHT_BROWSERS_PATH=%s/webpack-extends-browsers\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
printf 'DEVUP_EXTENDS_RESULTS=%s/webpack-extends-results\n' "$RUNNER_TEMP" >> "$GITHUB_ENV"
- name: Checkout
uses: actions/checkout@v7
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-webpack-extends-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-webpack-extends-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- *install-wasm-pack
- name: Install Node.js
uses: actions/setup-node@v7
with:
node-version: '*'
check-latest: true
- run: bun install
- name: Build fresh WASM
working-directory: bindings/devup-ui-wasm
run: bun run build
- run: bun run build
- name: Install latest private Playwright and Chromium
run: |
mkdir -p "$DEVUP_PLAYWRIGHT_ROOT" "$DEVUP_EXTENDS_RESULTS"
printf '{"private":true,"type":"module"}\n' > "$DEVUP_PLAYWRIGHT_ROOT/package.json"
bun add --cwd "$DEVUP_PLAYWRIGHT_ROOT" @playwright/test@latest
node "$DEVUP_PLAYWRIGHT_ROOT/node_modules/@playwright/test/cli.js" install chromium --with-deps
- name: Installed formats and production theme delivery regressions
run: |
set -euo pipefail
env -u CI node --test packages/webpack-plugin/src/__tests__/extends-theme/extends-theme-regression.mjs 2>&1 | tee "$DEVUP_EXTENDS_RESULTS/matrix.log"
- name: Upload packed-consumer matrix evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: webpack-extends-theme-evidence
path: |
${{ runner.temp }}/webpack-extends-results/matrix.log
${{ runner.temp }}/webpack-extends-results/extends-theme-*/*.json
${{ runner.temp }}/webpack-extends-results/extends-theme-*/*/identity-*.json
${{ runner.temp }}/webpack-extends-results/extends-theme-*/*/bun.lock
${{ runner.temp }}/webpack-extends-results/extends-theme-*/packs/*/*.tgz
if-no-files-found: error
retention-days: 7
# Validation: runs on pull requests and on pushes to main, read-only. The job
# id stays `publish` because it is the status check the repository requires;
# the privileged release steps live in `release` and `deploy-pages` below.
publish:
runs-on: ubuntu-latest
timeout-minutes: 90
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# changepacks diffs HEAD against the previous release commit. The
# default shallow fetch grafts away every parent, so that lookup dies
# with "Could not find a merge-base".
fetch-depth: 0
fetch-tags: true
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- *install-wasm-pack
- name: Install cargo-tarpaulin
env:
GH_TOKEN: ${{ github.token }}
run: cargo binstall cargo-tarpaulin --no-confirm --force
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-publish-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
name: Install bun
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-publish-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Node.js
uses: actions/setup-node@v7
with:
registry-url: "https://registry.npmjs.org"
node-version: '*'
check-latest: true
- run: bun install --frozen-lockfile
- run: bun run build
- name: Verify built package entry points
run: bun run verify:dist
- name: Verify the README benchmark tables
run: bun render-benchmark-readme.js --check
- name: Verify publish tarballs
shell: bash
run: |
set -euo pipefail
for manifest in bindings/*/package.json packages/*/package.json; do
# Exactly the nine native names delegate to native-packs/consumer.
name=$(node -p "require('./$manifest').name")
case "$name" in
@devup-ui/binding|@devup-ui/binding-linux-x64-gnu|@devup-ui/binding-linux-arm64-gnu|@devup-ui/binding-linux-x64-musl|@devup-ui/binding-linux-arm64-musl|@devup-ui/binding-darwin-x64|@devup-ui/binding-darwin-arm64|@devup-ui/binding-win32-x64-msvc|@devup-ui/binding-win32-arm64-msvc) continue ;;
esac
package_dir="$(dirname "$manifest")"
pack_dir="$RUNNER_TEMP/publish-verify/${package_dir//\//-}"
mkdir -p "$pack_dir"
(cd "$package_dir" && bun pm pack --quiet --destination "$pack_dir")
tarballs=("$pack_dir"/*.tgz)
if [[ "${#tarballs[@]}" -ne 1 ]]; then
echo "Expected one tarball for $package_dir" >&2
exit 1
fi
packed_manifest="$(tar -xOf "${tarballs[0]}" package/package.json)"
if grep -q 'workspace:' <<< "$packed_manifest"; then
echo "Unresolved workspace dependency in $package_dir" >&2
exit 1
fi
done
- run: |
bun run lint
# rust coverage issue
echo 'max_width = 100000' > .rustfmt.toml
echo 'tab_spaces = 4' >> .rustfmt.toml
echo 'newline_style = "Unix"' >> .rustfmt.toml
echo 'fn_call_width = 100000' >> .rustfmt.toml
echo 'fn_params_layout = "Compressed"' >> .rustfmt.toml
echo 'chain_width = 100000' >> .rustfmt.toml
echo 'merge_derives = true' >> .rustfmt.toml
echo 'use_small_heuristics = "Default"' >> .rustfmt.toml
cargo fmt
- run: bun run test
- name: Regression test (bun-plugin preload race)
run: bun run --filter @devup-ui/bun-plugin test:regression
- name: Format Rollback
run: |
rm -rf .rustfmt.toml
# Formatting with the temporary coverage config and then with the
# default config is not a reversible round trip. Restore the exact
# checked-in sources so the checkout is clean again.
git restore --source=HEAD --staged --worktree -- .
cargo fmt --all -- --check
git diff --exit-code
- name: Check the changepacks of this change
if: github.event_name == 'pull_request'
run: |
# changepacks compares the change against a local main, which a pull request checkout does not have
git fetch --no-tags origin main:refs/heads/main
bunx @changepacks/cli@latest check --format json
- name: Build Landing
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bun run --filter @devup-ui/components build-storybook
mv ./packages/components/storybook-static ./apps/landing/public/storybook
bun run --filter landing build
- name: Cache Playwright Browsers
id: playwright-cache
uses: actions/cache@v6
with:
path: ~/.cache/ms-playwright
key: playwright-publish-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- name: Install Playwright Browsers
run: bunx playwright install chromium --with-deps
# A run only compares against the screenshots checked into the repository:
# it never creates or commits them. A new visual test needs its baselines
# generated by a maintainer (`bun run test:e2e:update`) and committed.
- name: Require the E2E baselines
run: |
SNAPSHOT_COUNT=$(find e2e -name '*-linux.png' -path '*-snapshots/*' 2>/dev/null | wc -l)
echo "Found $SNAPSHOT_COUNT linux baseline snapshot(s)"
if [ "$SNAPSHOT_COUNT" -eq 0 ]; then
echo "No checked-in baselines under e2e/**/*-snapshots; generate them with 'bun run test:e2e:update' and commit them." >&2
exit 1
fi
- name: Run E2E Tests
run: bun run test:e2e
- name: Run Landing dev navigation tests
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: bunx playwright test -c playwright.landing-dev.config.ts
- name: Build Landing (singleCss)
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
rm -rf apps/landing/dist apps/landing/.next apps/landing/.vinext apps/landing/df
DEVUP_SINGLE_CSS=1 bun run --filter landing build
- name: Run E2E Tests (singleCss)
run: bun run test:e2e
- name: Upload singleCss Playwright Report
uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report-singlecss
path: playwright-report/
retention-days: 1
- name: Upload the site for deployment
uses: actions/upload-pages-artifact@v5
with:
path: ./apps/landing/dist/client
retention-days: 1
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
- name: Upload to codecov.io
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
files: ./coverage/root/lcov.info,./coverage/vite/lcov.info
# Pages deployment: the only job with `pages` and `id-token` write, and it
# only runs for a push to main whose validation passed.
deploy-pages:
needs: [publish]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/deploy-pages@v5
# Publishes through npm OIDC trusted publishing. The job's `id-token: write`
# permission lets npm exchange GitHub's short-lived OIDC token, and public
# packages receive provenance automatically.
#
# PREREQUISITE: every @devup-ui/* package must be registered as a Trusted
# Publisher on npmjs.com against repo dev-five-git/devup-ui and workflow
# file `publish.yml`. npm matches that filename exactly, so renaming or
# moving this file breaks publishing until the entries are updated.
#
# The publish command itself is selected in .changepacks/config.json.
# Bun packs each workspace so dependency ranges are materialized, then
# npm publishes that tarball with OIDC (Bun cannot do OIDC itself:
# oven-sh/bun#22423).
#
# It runs only for a push to main whose validation and end-to-end jobs
# passed, and is the only job that can write contents, open pull requests or
# mint an OIDC token for npm.
release:
needs: [publish, landing-next-e2e, react-18, release-compat, native-consumer]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 45
env:
GIT_CONFIG_COUNT: "1"
GIT_CONFIG_KEY_0: core.autocrlf
GIT_CONFIG_VALUE_0: "false"
permissions:
contents: write
id-token: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v7
with:
# changepacks diffs HEAD against the previous release commit. The
# default shallow fetch grafts away every parent, so that lookup dies
# with "Could not find a merge-base" and the release never publishes.
fetch-depth: 0
fetch-tags: true
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
name: Install bun
- uses: actions-rust-lang/setup-rust-toolchain@v2
with:
toolchain: stable
components: clippy, rustfmt
- name: Cache cargo registry + target
uses: actions/cache@v6
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: cargo-release-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
- name: Cache bun dependencies
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-release-${{ runner.os }}-${{ hashFiles('**/bun.lock') }}
- *install-wasm-pack
- name: Install Node.js
uses: actions/setup-node@v7
with:
registry-url: "https://registry.npmjs.org"
node-version: '*'
check-latest: true
- run: bun install --frozen-lockfile
- run: bun run build
- name: Verify built package entry points
run: bun run verify:dist
- name: Verify clean publish checkout
run: git diff --exit-code
# Stable artifact name deliberately survives a release-only run_attempt retry.
- uses: actions/download-artifact@v8
with:
name: native-packs-${{ github.run_id }}
path: ${{ runner.temp }}/native-release-bundle
- uses: changepacks/action@main
id: changepacks
env:
NATIVE_RELEASE_BUNDLE: ${{ runner.temp }}/native-release-bundle
with:
publish: true