Files
vercel__next.js/.github/workflows/build_reusable.yml
Niklas Mischkulnig 3bf71ee3fd test: run cargo unit tests with miri as well (#97535)
### What?

Adds a CI job that runs selected Rust unit tests under
[Miri](https://github.com/rust-lang/miri) to detect undefined behavior
in unsafe code.

The job currently covers the low-level crates that are compatible with
Miri:

- `turbo-persistence`
- `turbo-rcstr`
- `turbo-tasks-malloc`

It also makes those crates Miri-compatible by:

- using provenance-free tagged-pointer operations in `turbo-rcstr`;
- disabling mimalloc and native compression under Miri;
- making mmap support a default-enabled Cargo feature that is disabled
for Miri and wasm;
- running persistence through file I/O when mmap is disabled;
- skipping only tests measured to exceed the Miri time budget;
- removing leaked test arenas from the analyzer predicate tests.

### Why?

Miri can detect invalid memory access and other undefined behavior that
normal Rust tests may not expose. Running it in CI gives low-level
unsafe code an additional correctness check.

The job uses an explicit package allowlist because Turbo Tasks builds
its generated registry from linker sections, and Miri does not support
the linker-defined section symbols required by that registry.

### How?

- Installs the `miri` Rust component in CI and the development
container.
- Adds a dedicated `test-cargo-unit-miri` task and reusable workflow
configuration.
- Makes `memmap2` optional behind the default `mmap` feature and
disables that feature in Miri CI and the wasm dependency graph.
- Uses structural `cfg(miri)` branches for allocator and compression
paths Miri cannot execute.
- Re-enables three persistence compaction tests after measuring them
successfully under Miri.
- Keeps normal native behavior unchanged and documents measured Miri
exclusions at the affected tests.

Verification included focused normal and Miri tests for persistence,
rcstr, allocation accounting, compression, and the refactored leak-free
predicate cases.

<!-- NEXT_JS_LLM -->


<!-- fleet fb42942f-173a-484c-b4de-4e18354834c6 -->

---------

Co-authored-by: vercel-fleet-prod[bot] <318278635+vercel-fleet-prod[bot]@users.noreply.github.com>
Co-authored-by: Tobias Koppers <1365881+sokra@users.noreply.github.com>
2026-09-18 09:29:06 +02:00

529 lines
22 KiB
YAML

name: Build Reusable
on:
workflow_call:
inputs:
afterBuild:
required: false
description: 'additional steps to run'
type: string
skipInstallBuild:
required: false
description: 'whether to skip pnpm install && pnpm build'
type: string
skipNativeBuild:
required: false
description: 'whether to skip building native modules'
type: string
skipNativeInstall:
required: false
description: 'whether to skip native postinstall script'
type: string
default: 'yes'
uploadAnalyzerArtifacts:
required: false
description: 'whether to upload analyzer artifacts'
type: string
nodeVersion:
required: false
description: 'version of Node.js to use'
type: string
needsRust:
required: false
description: 'if rust is needed'
type: string
needsNextest:
required: false
description: 'if nextest rust dep is needed'
type: string
needsRustJsInstall:
required: false
description: 'if Rust JS dependencies should be installed'
type: string
rustBuildProfile:
required: false
description: 'The profile to use for the build, default is `release-with-assertions`, also supports `` for debug and `release` for normal release'
type: string
default: 'release-with-assertions'
uploadSwcArtifact:
required: false
description: 'if swc artifact needs uploading'
type: string
rustCacheKey:
required: false
description: 'rustCacheKey to cache shared target assets'
type: string
stepName:
required: true
description: 'name of the step, to be used for the upload artifact unique key '
type: string
timeout_minutes:
description: 'Timeout in minutes'
required: false
type: number
default: 30
runs_on_labels:
description: 'List of runner labels'
required: false
type: string
default: '["ubuntu-latest-16-core-arm-oss"]'
overrideProxyAddress:
description: Override the proxy address to use for the test
required: false
type: string
default: ''
testTimingsArtifact:
description: >
Name of an uploaded artifact containing test-timings.json. When set,
download it instead of fetching via turbo. This can be useful in
ensuring that when failed jobs are retried, they end up with exactly
the same set of timings, and therefore run the same set of tests.
required: false
type: string
default: ''
testReportsArtifactPrefix:
description: 'Artifact name prefix for uploading test `*.results.json` files. Empty string disables upload.'
required: false
type: string
default: ''
uploadNativeArtifact:
description: >
When true, build and upload the native `next-swc.*.node` files as an
artifact named `next-swc-<target>`. When false, we may attempt to
download this artifact, falling back to rebuilding if the artifact is
not found.
required: false
type: boolean
default: false
browser:
description: 'Browser to use for tests'
required: false
type: string
default: 'chromium'
needsPlaywright:
description: 'whether Playwright browser installation is needed'
required: false
type: string
default: 'yes'
env:
# Newest @napi-rs/cli that runs on Node.js 20.9.0 (NODE_LTS_VERSION below):
# 3.5.0+ depends on @inquirer/prompts 8, whose @inquirer/core 11 requires
# `util.styleText` (Node >= 20.12) and crashes at CLI startup. The CLI here
# only orchestrates cargo and names artifacts (types are committed), so
# drifting from the workspace devDependency (3.7.2) is acceptable.
NAPI_CLI_VERSION: 3.4.1
TURBO_VERSION: 2.9.4
TURBO_ARGS: '-v --env-mode loose --remote-cache-timeout 300 --log-order stream'
NODE_LTS_VERSION: 20.9.0
# run-tests.js reads `TEST_CONCURRENCY` if no explicit `--concurrency` or `-c`
# argument is provided
TEST_CONCURRENCY: 8
# overrides `turbo_tasks::parallel::available_parallelism`. Smaller values are more CPU and memory
# efficient, but won't fully utilize all available CPU cores. A small value here makes sense since
# we're running many tests in parallel.
TURBO_TASKS_AVAILABLE_PARALLELISM: 4
# disable backtrace for test snapshots
RUST_BACKTRACE: 0
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# Prefer shared remote cache across runs, but keep local cache enabled so jobs
# degrade gracefully if the remote cache or token is unavailable.
TURBO_CACHE: 'local:rw,remote:rw'
NEXT_TELEMETRY_DISABLED: 1
# `skipNativeInstall: 'no'` must force the download even though CI otherwise
# skips it by default (see scripts/install-native.mjs), so emit an explicit
# '0' rather than an empty value.
NEXT_SKIP_NATIVE_POSTINSTALL: ${{ inputs.skipNativeInstall == 'yes' && '1' || '0' }}
DATADOG_API_KEY: ${{ secrets.DATA_DOG_API_KEY }}
NEXT_JUNIT_TEST_REPORT: 'true'
DD_ENV: 'ci'
# Vercel KV Store for test timings
KV_REST_API_URL: ${{ secrets.KV_REST_API_URL }}
KV_REST_API_TOKEN: ${{ secrets.KV_REST_API_TOKEN }}
NEXT_TEST_JOB: 1
# Per-job memoization of passed test files.
NEXT_TEST_PASSED_FILE: .next-test-passed.txt
VERCEL_TEST_TOKEN: ${{ secrets.VERCEL_TEST_TOKEN }}
VERCEL_TEST_TEAM: vtest314-next-e2e-tests
VERCEL_ADAPTER_TEST_TOKEN: ${{ secrets.VERCEL_ADAPTER_TEST_TOKEN }}
VERCEL_ADAPTER_TEST_TEAM: vtest314-next-adapter-e2e-tests
VERCEL_TURBOPACK_TEST_TOKEN: ${{ secrets.VERCEL_TURBOPACK_TEST_TOKEN }}
VERCEL_TURBOPACK_TEST_TEAM: vtest314-next-turbo-e2e-tests
PREVIEW_BUILDS_ACCESS: ${{ vars.PREVIEW_BUILDS_ACCESS }}
NEXT_TEST_PREFER_OFFLINE: 1
NEXT_CI_RUNNER: ${{ inputs.runs_on_labels }}
NEXT_TEST_PROXY_ADDRESS: ${{ inputs.overrideProxyAddress || '' }}
# defaults to 256, but we run a lot of tests in parallel, so the limit should be lower
NEXT_TURBOPACK_IO_CONCURRENCY: 64
# Disable warnings from baseline-browser-mapping
# https://github.com/web-platform-dx/baseline-browser-mapping/blob/ec8136ae9e034b332fab991d63a340d2e13b8afc/README.md?plain=1#L34
BASELINE_BROWSER_MAPPING_IGNORE_OLD_DATA: 1
jobs:
build:
timeout-minutes: ${{ inputs.timeout_minutes }}
runs-on: ${{ fromJson(inputs.runs_on_labels) }}
defaults:
run:
shell: bash -leuo pipefail {0}
outputs:
input_step_key: ${{ steps.var.outputs.input_step_key }}
steps:
# enforce consistent line endings for git on windows
- name: Configure git to use LF endings
if: ${{ runner.os == 'Windows' }}
run: |
git config --global core.autocrlf false
git config --global core.eol lf
shell: bash
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
# Changing the checked out ref has wide implications:
# Be aware that all git-diff operations in the repo rely on the
# default behavior of GitHub actions (ref on push, merge-commit on PR).
fetch-depth: 25
persist-credentials: false
- name: Set up Turborepo remote cache
if: ${{ vars.TURBO_TEAM != '' }}
continue-on-error: true
uses: vercel/setup-turborepo-remote-cache-action@135046f5efcadb17337d9b29b8d5b4035ae64b10 # v1.0.0
with:
team: ${{ vars.TURBO_TEAM }}
# Depends on rust-toolchain.toml in the checkout
- name: Install Rust
id: install-rust
uses: ./.github/actions/setup-rust
background: true
if: ${{ inputs.uploadNativeArtifact || inputs.needsNextest == 'yes' || inputs.needsRust == 'yes' }}
- name: Install fnm
run: |
FNM_VERSION=v1.39.0
FNM_BIN_DIR="$HOME/.local/share/fnm/bin"
case "$RUNNER_OS" in
Linux)
# The asset names and sha256 are available on the GH releases page
# https://github.com/Schniz/fnm/releases
ARCH="$(uname -m)"
case "$ARCH" in
aarch64)
FNM_ASSET=fnm-arm64.zip
FNM_SHA256=4eaff58b2c5bf30d0934027572dd0b5bbb60d2a1af309230b53662d4b1d45599
;;
x86_64)
FNM_ASSET=fnm-linux.zip
FNM_SHA256=7807664f39d39fc518da1c35ba0181e4b3267603c4b1dedeb4b5fc6ae440a224
;;
*)
echo "::error::Unsupported architecture: $ARCH"
exit 1
;;
esac
FNM_EXE=fnm
;;
Windows)
FNM_ASSET=fnm-windows.zip
FNM_SHA256=8183bed4348cb78fdfd8abb3d1247fbeab7b2082f941363929c61e747c001e10
FNM_EXE=fnm.exe
;;
*)
echo "::error::Unsupported RUNNER_OS: $RUNNER_OS"
exit 1
;;
esac
FNM_BIN="$FNM_BIN_DIR/$FNM_EXE"
mkdir -p "$FNM_BIN_DIR"
curl -L --fail --retry 3 -o /tmp/fnm.zip \
"https://github.com/Schniz/fnm/releases/download/$FNM_VERSION/$FNM_ASSET"
echo "$FNM_SHA256 /tmp/fnm.zip" | sha256sum --check --status
unzip /tmp/fnm.zip "$FNM_EXE" -d "$FNM_BIN_DIR"
rm /tmp/fnm.zip
chmod +x "$FNM_BIN"
echo "$FNM_BIN_DIR" >> $GITHUB_PATH
"$FNM_BIN" env --corepack-enabled --json | \
jq -r 'to_entries|map("\(.key)=\(.value|tostring|@sh)")|.[]' | \
xargs -I {} echo "{}" >> $GITHUB_ENV
- name: Use Node.js ${{ inputs.nodeVersion || env.NODE_LTS_VERSION }}
run: |
fnm use --install-if-missing "$NODE_VERSION"
fnm default "$NODE_VERSION"
eval "$(fnm env)"
echo "$(dirname "$(which node)")" >> "$GITHUB_PATH"
env:
NODE_VERSION: ${{ inputs.nodeVersion || env.NODE_LTS_VERSION }}
# Debug used Node.js version in a separate step to ensure the Node.js
# version is set for the entire job
- name: Verify Node.js version
run: |
which node
node --version
pnpm --version
- run: corepack prepare --activate yarn@1.22.19 && npm i -g "@napi-rs/cli@${NAPI_CLI_VERSION}"
# clean up any previous artifacts to avoid hitting disk space limits
- run: git clean -xdf && rm -rf /tmp/next-repo-*; rm -rf /tmp/next-install-* /tmp/yarn-* /tmp/ncc-cache target
# Configure a git user so that Create Next App can initialize git repos during integration tests.
- name: Set CI git user
run: |
git config --global user.name "vercel-ci-bot"
git config --global user.email "infra+ci@vercel.com"
# normalize versions before build-native for better cache hits
- run: node scripts/normalize-version-bump.js
name: normalize versions
- name: Wait for Rust toolchain
wait: install-rust
- name: Install nextest
if: ${{ inputs.needsNextest == 'yes' }}
run: cargo binstall --no-confirm --locked cargo-nextest@0.9.133
- run: rustc --version
if: ${{ inputs.uploadNativeArtifact || inputs.needsNextest == 'yes' || inputs.needsRust == 'yes' }}
- name: Start sccache
uses: ./.github/actions/sccache
if: ${{ runner.os != 'Windows' && (inputs.uploadNativeArtifact || inputs.needsNextest == 'yes' || inputs.needsRust == 'yes') }}
# Infer the Rust target triple from the runner's OS/arch. napi would build
# for the host target if `--target` were omitted, but Turborepo does not
# segment its cache by OS/arch, so we pass an explicit `--target` to keep
# the cache key distinct per architecture. The native artifact is also
# named after this target so its name can't diverge from what we built.
- name: Determine native build target
id: native-target
run: |
case "$RUNNER_OS-$RUNNER_ARCH" in
Linux-X64) target=x86_64-unknown-linux-gnu ;;
Linux-ARM64) target=aarch64-unknown-linux-gnu ;;
Windows-X64) target=x86_64-pc-windows-msvc ;;
macOS-ARM64) target=aarch64-apple-darwin ;;
macOS-X64) target=x86_64-apple-darwin ;;
*) echo "Unsupported runner: $RUNNER_OS-$RUNNER_ARCH" >&2; exit 1 ;;
esac
echo "target=$target" >> "$GITHUB_OUTPUT"
- name: Download pre-built native binary
if: ${{ inputs.skipNativeBuild != 'yes' && !inputs.uploadNativeArtifact }}
id: native-download
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: next-swc-${{ steps.native-target.outputs.target }}
path: packages/next-swc/native/
- name: Build native binary
if: ${{ inputs.skipNativeBuild != 'yes' && (inputs.uploadNativeArtifact || steps.native-download.outcome != 'success') }}
env:
RUST_BUILD_PROFILE: ${{ inputs.rustBuildProfile }}
NATIVE_TARGET: ${{ steps.native-target.outputs.target }}
run: pnpm dlx turbo@${TURBO_VERSION} run "build-native-${RUST_BUILD_PROFILE}" ${TURBO_ARGS} --summarize -- --target "${NATIVE_TARGET}"
- name: Upload native artifact
if: ${{ inputs.uploadNativeArtifact && inputs.skipNativeBuild != 'yes' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: next-swc-${{ steps.native-target.outputs.target }}
path: packages/next-swc/native/next-swc.*.node
retention-days: 1
- name: Upload next-swc artifact
if: ${{ inputs.uploadSwcArtifact == 'yes' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: next-swc-binary-${{ runner.os }}-${{ runner.arch }}
path: packages/next-swc/native/next-swc.*.node
# undo normalize version changes for install/build
- run: git checkout .
if: ${{ inputs.skipInstallBuild != 'yes' }}
- name: Get pnpm store directory
id: get-store-path
run: echo STORE_PATH=$(pnpm store path) >> $GITHUB_OUTPUT
- name: Cache pnpm store
# conditions must be subset of runs executing pnpm install, otherwise we
# risk caching an incomplete store
# If keep conditions in sync breaks, we can split into restore and save
# steps where saving runs based on the outcome of the install step
if: ${{ inputs.skipInstallBuild != 'yes' || inputs.needsRustJsInstall == 'yes' }}
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
timeout-minutes: 5
id: cache-pnpm-store
with:
path: ${{ steps.get-store-path.outputs.STORE_PATH }}
key: pnpm-store-root-v1-${{ hashFiles('pnpm-lock.yaml') }}
# Do not use restore-keys since it leads to indefinite growth of the cache.
- run: pnpm install
# condititions must be superset of cache step, otherwise we risk running install without a cache and then caching the incomplete store.
if: ${{ inputs.skipInstallBuild != 'yes' || inputs.needsRustJsInstall == 'yes' }}
- name: Install node-file-trace test dependencies
if: ${{ inputs.needsRustJsInstall == 'yes' }}
working-directory: turbopack/crates/turbopack-tracing/tests/node-file-trace
run: pnpm install --recursive
- name: Install Playwright browsers
background: true
if: ${{ inputs.skipInstallBuild != 'yes' && inputs.needsPlaywright != 'no' }}
env:
PLAYWRIGHT_BROWSERS: ${{ inputs.browser }}
WITH_DEPS: ${{ inputs.browser != 'chromium' && '--with-deps' || '' }}
run: |
# PLAYWRIGHT_BROWSERS can be a space-separated list, should expand
# into multiple arguments
pnpm playwright install --only-shell $WITH_DEPS $PLAYWRIGHT_BROWSERS
- name: Download pre-built test timings
if: ${{ inputs.testTimingsArtifact != '' }}
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ inputs.testTimingsArtifact }}
- name: Verify test timings
if: ${{ inputs.testTimingsArtifact != '' }}
run: |
if [ ! -f test-timings.json ]; then
echo "::error::test-timings.json not found"
exit 1
fi
echo "Test timings loaded ($(wc -c < test-timings.json) bytes)"
- name: Fetch test timings via turbo
background: true
if: ${{ inputs.testTimingsArtifact == '' }}
run: pnpm dlx turbo@${TURBO_VERSION} run get-test-timings -- --build ${{ github.sha }}
- run: ANALYZE=1 pnpm build
if: ${{ inputs.skipInstallBuild != 'yes' }}
- name: Normalize input step names into path key
uses: actions/github-script@d746ffe35508b1917358783b479e04febd2b8f71 # v9.0.0
id: var
with:
script: |
core.setOutput('input_step_key', '${{ inputs.stepName }}'.toLowerCase().replaceAll(/[/.]/g, '-').trim('-'));
# Restore the list of tests that already passed in an earlier
# attempt of this same workflow run.
- name: Restore passed-tests cache
if: ${{ inputs.afterBuild }}
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.NEXT_TEST_PASSED_FILE }}
key: next-test-passed-${{ steps.var.outputs.input_step_key }}-${{ github.run_id }}-attempt${{ github.run_attempt }}
restore-keys: |
next-test-passed-${{ steps.var.outputs.input_step_key }}-${{ github.run_id }}-
- name: Wait for background setup steps
wait-all:
- run: ${{ inputs.afterBuild }}
id: after-build
# defaults.run.shell sets a stronger options (`-leo pipefail`)
# Set this back to github action's weaker defaults:
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell
#
# We must use a login shell: fnm installation may modify the `.profile`
shell: bash -le {0}
timeout-minutes: ${{ inputs.timeout_minutes }}
# Save the passed-tests file so the next attempt (if any) can skip
# them. `always()` makes this run on success, failure, or cancellation.
# run-tests.js emits the `passed_tests_file` step output when it creates
# the file (before running any test), so this step is skipped for jobs
# that never run run-tests.js or that disable the result cache (e.g.
# flake detection) instead of warning about a missing path.
- name: Save passed-tests cache
if: ${{ always() && steps.after-build.outputs.passed_tests_file != '' }}
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ steps.after-build.outputs.passed_tests_file }}
key: next-test-passed-${{ steps.var.outputs.input_step_key }}-${{ github.run_id }}-attempt${{ github.run_attempt }}
- name: Upload test result artifacts
if: ${{ inputs.testReportsArtifactPrefix != '' && inputs.afterBuild && always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ inputs.testReportsArtifactPrefix }}-${{ steps.var.outputs.input_step_key }}
path: test/**/*.results.json
if-no-files-found: ignore
retention-days: 1
# This file messes up the tests because it influences the build root autodetection.
# Jest has a global cache, so PRs that poison the cache can bring down CI
- name: Clean up stray files
if: ${{ always() }}
run: rm -f /tmp/package-lock.json
- name: Upload Turborepo summary
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: turbo-run-summary-${{ steps.var.outputs.input_step_key }}
path: .turbo/runs
if-no-files-found: ignore
- name: Upload bundle analyzer artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ inputs.uploadAnalyzerArtifacts == 'yes' }}
with:
name: webpack bundle analysis stats-${{ steps.var.outputs.input_step_key }}
path: packages/next/dist/compiled/next-server/report.*.html
- name: Install datadog-ci
if: ${{ inputs.afterBuild && always() && !github.event.pull_request.head.repo.fork && env.DATADOG_API_KEY != '' }}
uses: ./.github/actions/setup-datadog-ci
- name: Upload test report to datadog
if: ${{ inputs.afterBuild && always() && !github.event.pull_request.head.repo.fork && env.DATADOG_API_KEY != '' }}
run: |
# Add a `test.type` tag to distinguish between turbopack and next.js runs
# Add a `nextjs.test_session.name` tag to help identify the job
if [ -d ./test/test-junit-report ]; then
"$DATADOG_CI_PATH" junit upload \
--skip-git-metadata-upload \
--service nextjs \
--tags test.type:nextjs \
--tags test_session.name:"${{ inputs.stepName }}" \
--tags runner.name:"${{ runner.name }}" \
./test/test-junit-report
fi
if [ -d ./test/turbopack-test-junit-report ]; then
"$DATADOG_CI_PATH" junit upload \
--skip-git-metadata-upload \
--service nextjs \
--tags test.type:turbopack \
--tags test_session.name:"${{ inputs.stepName }}" \
--tags runner.name:"${{ runner.name }}" \
./test/turbopack-test-junit-report
fi
- name: Upload Playwright Snapshots
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: ${{ inputs.afterBuild && always() }}
with:
name: test-playwright-snapshots-${{ steps.var.outputs.input_step_key }}
path: |
test/traces
if-no-files-found: ignore