Files
fx/.github/workflows/ci.yml
T
Pranit b1d515851a Upgrade to Zig 0.17
Build fx with Zig 0.17.0. build.zig.zon requires it, every workflow
installs it, and PGSO qualification uses LLVM 22.1.8 to match.

Zig 0.17 removes `**`, `errdefer |err|`, `@cImport`, and the old type
info field lists. The source moves to `@splat`, a comptime
`text_utils.repeat` helper, split cleanup functions, and the new
field_names and field_types arrays. The Node-API addon translates
node_api.h with a build step. Test fakes checked with `@hasDecl` become
pub, because `@hasDecl` no longer sees private declarations.

`zig build test` gives the test binary its fx and source paths through
a generated test_paths module, since the build no longer exposes install
paths for environment variables.

Zig 0.17's SafeAllocator grows a small allocation in place only when it
sits at the end of a bucket, and buckets are never reused, so an
allocation-failure sweep could count a different number of allocations
on each run. Sweeps and allocation-count tests now use
testing_allocator.no_resize, which never resizes in place.

allocRemaining now treats its limit as inclusive. readFileToEnd keeps
rejecting a file of exactly max_bytes by reading through
appendRemaining. Direct allocRemaining callers for remote responses now
accept a body exactly at their limit.

Zig 0.17's memset stores a full vector per iteration, so the Linux
memset replacement is removed. macOS release binaries now require macOS
15, Zig 0.17's default minimum.
2026-10-05 19:44:53 -04:00

670 lines
21 KiB
YAML

name: CI
# Linux is the gate for every change. Behavior that can differ on macOS runs in
# .github/workflows/macos.yml only when a change needs it.
on:
workflow_dispatch:
inputs:
acp_live:
description: Also run the full ACP suite with live model calls
type: boolean
default: false
push:
branches: [main]
pull_request:
concurrency:
# A manual dispatch gets its own group so it cannot cancel the push run on
# main.
group: ci-${{ github.event_name == 'workflow_dispatch' && 'dispatch-' || '' }}${{ github.ref }}
cancel-in-progress: true
jobs:
zig_mode:
name: Build & Test (${{ matrix.optimize }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- optimize: ReleaseSafe
test_timeout_seconds: 1500
steps:
- uses: actions/checkout@v4
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-unit-${{ matrix.optimize }}
- name: Check formatting
run: zig fmt --check src/
- name: Validate PGSO corpus classification
run: python3 -m scripts.pgso.corpus --manifest scripts/pgso/corpus.json --list
- name: Audit public surface
run: ./scripts/check-public-surface.sh
- name: Check compactor boundary
run: ./scripts/check-compactor-boundary.sh
- name: Test CI and release scripts
run: python3 -m unittest scripts.tests.test_macos_signing scripts.tests.test_signed_macos scripts.tests.test_detect_macos_need -q
- name: Build
run: zig build -Doptimize=${{ matrix.optimize }}
- name: Test
env:
TEST_TIMEOUT_SECONDS: ${{ matrix.test_timeout_seconds }}
run: |
started_at=$SECONDS
trap 'printf "Unit tests elapsed: %ss\n" "$((SECONDS - started_at))"' EXIT
timeout "${TEST_TIMEOUT_SECONDS}s" zig build test -Doptimize=${{ matrix.optimize }}
- name: Save Zig cache
uses: ./.github/actions/save-zig-cache
with:
cache-key: ${{ steps.zig.outputs.cache-key }}
cache-hit: ${{ steps.zig.outputs.cache-hit }}
# The SDK jobs are part of this required check because Publish libfx relies
# on them instead of rerunning the package qualification before publishing.
zig:
name: Build & Test
if: ${{ always() }}
needs: [zig_mode, sdk-node-wasm, sdk-node-napi, sdk-bun, sdk-browser-wasm]
runs-on: ubuntu-latest
steps:
- name: Require unit tests and every SDK job
env:
RESULTS: ${{ toJSON(needs.*.result) }}
run: |
printf '%s\n' "$RESULTS"
jq -e 'length == 5 and all(.[]; . == "success")' <<<"$RESULTS"
build:
name: Build fx (ReleaseSafe)
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
AI_GATEWAY_API_KEY: ""
VERCEL_OIDC_TOKEN: ""
steps:
- uses: actions/checkout@v4
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-build-ReleaseSafe
# Other runners execute this binary, and their CPUs can differ from this
# one. A native build can emit instructions such as AVX-512 that those
# runners lack, so target the baseline CPU like the release builds do.
- name: Build
run: zig build -Doptimize=ReleaseSafe -Dcpu=baseline
- name: Smoke test fresh binary
run: scripts/smoke-binary.sh zig-out/bin/fx
- name: Share the binary with E2E jobs
uses: actions/upload-artifact@v4
with:
name: fx-linux-x86_64
path: zig-out/bin/fx
if-no-files-found: error
retention-days: 1
- name: Save Zig cache
uses: ./.github/actions/save-zig-cache
with:
cache-key: ${{ steps.zig.outputs.cache-key }}
cache-hit: ${{ steps.zig.outputs.cache-hit }}
sdk-wasm:
name: SDK build (WASM)
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-sdk-wasm
- name: Build WASM surfaces (ReleaseSmall)
run: |
zig build fx-core-wasm -Dwasm-surface=core -Doptimize=ReleaseSmall
zig build fx-term-wasm -Dwasm-surface=term -Doptimize=ReleaseSmall
- name: Share WASM surfaces with SDK jobs
uses: actions/upload-artifact@v4
with:
name: sdk-wasm
path: |
zig-out/bin/fx-core.wasm
zig-out/bin/fx-term.wasm
if-no-files-found: error
retention-days: 1
- name: Save Zig cache
uses: ./.github/actions/save-zig-cache
with:
cache-key: ${{ steps.zig.outputs.cache-key }}
cache-hit: ${{ steps.zig.outputs.cache-hit }}
sdk-napi:
name: SDK build (N-API, ${{ matrix.platform }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- platform: linux-x64
target_flags: -Dtarget=x86_64-linux-gnu.2.34 -Dcpu=baseline
- platform: linux-arm64
target_flags: -Dtarget=aarch64-linux-gnu.2.34 -Dcpu=baseline
- platform: darwin-x64
target_flags: -Dtarget=x86_64-macos -Dcpu=baseline
- platform: darwin-arm64
target_flags: -Dtarget=aarch64-macos -Dcpu=baseline
steps:
- uses: actions/checkout@v4
# build.zig locates node_api.h through the node on PATH.
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-sdk-napi-${{ matrix.platform }}
- name: Build N-API addon (ReleaseSafe)
run: |
zig build libfx-napi -Dnapi-surface=core -Doptimize=ReleaseSafe ${{ matrix.target_flags }}
mkdir -p native-addons
cp zig-out/lib/libfx.node native-addons/libfx.${{ matrix.platform }}.node
- name: Share the addon with SDK jobs
uses: actions/upload-artifact@v4
with:
name: sdk-napi-${{ matrix.platform }}
path: native-addons/libfx.${{ matrix.platform }}.node
if-no-files-found: error
retention-days: 1
- name: Save Zig cache
uses: ./.github/actions/save-zig-cache
with:
cache-key: ${{ steps.zig.outputs.cache-key }}
cache-hit: ${{ steps.zig.outputs.cache-hit }}
sdk-node-wasm:
name: SDK (Node + WASM)
needs: sdk-wasm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Download WASM surfaces
uses: actions/download-artifact@v4
with:
name: sdk-wasm
path: zig-out/bin
- name: Install terminal test dependencies
run: npm ci --prefix sdk/node
- name: Test Node + WASM
run: npm run --prefix sdk test:node-wasm
- name: Test libfx Wasm integrations
run: |
node --experimental-wasm-jspi sdk/tests/test-host-tools.mjs wasm
node --experimental-wasm-jspi sdk/tests/test-host-tool-cancel.mjs wasm
node --experimental-wasm-jspi sdk/tests/test-mcp-adapter.mjs http wasm
node --experimental-wasm-jspi sdk/tests/test-skills-adapter.mjs host wasm
sdk-node-napi:
name: SDK (Node + N-API)
needs: sdk-napi
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Download Linux addon
uses: actions/download-artifact@v4
with:
name: sdk-napi-linux-x64
path: native-addons
- name: Install the addon
run: |
mkdir -p zig-out/lib
cp native-addons/libfx.linux-x64.node zig-out/lib/libfx.node
- name: Test Node + N-API
run: npm run --prefix sdk test:node-napi
- name: Test minimal native integrations
run: |
node sdk/tests/test-minimal-api.mjs
node sdk/tests/test-host-tools.mjs native
node sdk/tests/test-host-tool-cancel.mjs native
node sdk/tests/test-mcp-adapter.mjs stdio native
node sdk/tests/test-skills-adapter.mjs disk native
sdk-bun:
name: SDK (Bun + N-API/WASM)
needs: [sdk-wasm, sdk-napi]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.4.2"
- name: Download WASM surfaces
uses: actions/download-artifact@v4
with:
name: sdk-wasm
path: zig-out/bin
- name: Download native addons
uses: actions/download-artifact@v4
with:
pattern: sdk-napi-*
path: native-addons
merge-multiple: true
- name: Install the Linux addon
run: |
mkdir -p zig-out/lib
cp native-addons/libfx.linux-x64.node zig-out/lib/libfx.node
- name: Install terminal test dependencies
run: npm ci --prefix sdk/node
- name: Test Bun + N-API
run: bun sdk/tests/test-node-napi.mjs
- name: Test Bun + WASM
run: bun sdk/tests/test-node-wasm.mjs
- name: Test Bun benchmark path
run: bun sdk/tests/test-libfx-benchmark.mjs
- name: Test Bun cross-backend integrations
run: |
bun sdk/tests/test-agent-bootstrap.mjs auto
bun sdk/tests/test-checkpoint.mjs native wasm
bun sdk/tests/test-checkpoint.mjs wasm native
bun sdk/tests/test-host-tools.mjs native
bun sdk/tests/test-host-tools.mjs wasm
bun sdk/tests/test-host-tool-cancel.mjs native
bun sdk/tests/test-host-tool-cancel.mjs wasm
bun sdk/tests/test-mcp-adapter.mjs stdio native
bun sdk/tests/test-mcp-adapter.mjs http wasm
bun sdk/tests/test-skills-adapter.mjs disk native
bun sdk/tests/test-skills-adapter.mjs host wasm
- name: Check Linux addon compatibility
run: |
python3 sdk/tests/test-linux-abi.py
python3 sdk/scripts/check-linux-abi.py zig-out/lib/libfx.node
docker run --rm --network none --entrypoint /bin/bash \
--volume "$PWD:/workspace:ro" --workdir /workspace \
public.ecr.aws/lambda/nodejs@sha256:2472f0273d8848d8b2aa6528f5526b8a514b70703596199efe5a4b84d3d59b0b -euc '
node sdk/tests/test-native-core-ready.mjs
node sdk/tests/test-host-tools.mjs native
node sdk/tests/test-host-tool-cancel.mjs native
'
- name: Test Node package and backend diagnostics
run: |
node --experimental-wasm-jspi sdk/tests/test-backend-info.mjs
node --experimental-wasm-jspi sdk/tests/test-node-bundled-assets.mjs
node sdk/tests/test-node-package.mjs
node sdk/tests/test-term-demo-package.mjs
node sdk/scripts/package-libfx.mjs sdk/dist/libfx native-addons/libfx.linux-x64.node native-addons/libfx.linux-arm64.node native-addons/libfx.darwin-x64.node native-addons/libfx.darwin-arm64.node
node --experimental-wasm-jspi sdk/tests/test-node-async-assets.mjs sdk/dist/libfx
npm pack ./sdk/dist/libfx --ignore-scripts --json > /tmp/libfx-pack.json
mv "$(node -p 'require("/tmp/libfx-pack.json")[0].filename')" /tmp/libfx-package.tgz
node sdk/tests/test-packed-example.mjs /tmp/libfx-package.tgz native esm
node --no-experimental-require-module sdk/tests/test-packed-example.mjs /tmp/libfx-package.tgz native cjs
node --experimental-wasm-jspi sdk/tests/test-packed-example.mjs /tmp/libfx-package.tgz wasm esm
node --experimental-wasm-jspi --no-experimental-require-module sdk/tests/test-packed-example.mjs /tmp/libfx-package.tgz wasm cjs
- name: Test package recovery and lifecycle boundaries
run: |
node sdk/tests/test-package-resilience.mjs --package /tmp/libfx-package.tgz \
--cheap-cycles 100 --tool-workflows 20 --duration-ms 5000 --concurrency 4 \
--json-out "${{ runner.temp }}/libfx-package-evidence/resilience.json"
- name: Install Next fixture package manager
run: npm install --global pnpm@11.1.1
- name: Test installed package through Next
env:
LIBFX_TEST_ARTIFACT_ROOT: ${{ runner.temp }}/libfx-package-evidence
run: |
node sdk/tests/test-next-package.mjs /tmp/libfx-package.tgz
node sdk/tests/test-next-package.mjs /tmp/libfx-package.tgz --webpack
node sdk/tests/test-next-package.mjs /tmp/libfx-package.tgz --next15
- name: Retain package qualification evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: libfx-next-package-evidence
path: |
${{ runner.temp }}/libfx-package-evidence/resilience.json
${{ runner.temp }}/libfx-package-evidence/libfx-next-*/*.log
${{ runner.temp }}/libfx-package-evidence/libfx-next-*/results.json
${{ runner.temp }}/libfx-package-evidence/libfx-trace-*/*.log
${{ runner.temp }}/libfx-package-evidence/libfx-trace-*/results.json
if-no-files-found: ignore
sdk-browser-wasm:
name: SDK (Browser + WASM)
needs: sdk-wasm
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "24"
- name: Setup Chrome
id: setup-chrome
uses: browser-actions/setup-chrome@v2
- name: Download WASM surfaces
uses: actions/download-artifact@v4
with:
name: sdk-wasm
path: zig-out/bin
- name: Test Browser + WASM
env:
CHROME_PATH: ${{ steps.setup-chrome.outputs.chrome-path }}
run: npm run --prefix sdk test:browser-wasm
e2e_mode:
name: E2E (deterministic, shard ${{ matrix.shard.label }})
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
shard:
- index: 0
label: "1/4"
- index: 1
label: "2/4"
- index: 2
label: "3/4"
- index: 3
label: "4/4"
env:
AI_GATEWAY_API_KEY: ""
VERCEL_OIDC_TOKEN: ""
FX_REQUIRE_TMUX: "1"
SHARD_INDEX: ${{ matrix.shard.index }}
SHARD_COUNT: 4
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download fx
uses: actions/download-artifact@v4
with:
name: fx-linux-x86_64
path: zig-out/bin
- name: Restore executable mode
run: chmod +x zig-out/bin/fx
# Some E2E files build their own Zig fixtures.
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-e2e-fixtures
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Install E2E system dependencies
timeout-minutes: 17
run: |
missing=()
command -v tmux >/dev/null 2>&1 || missing+=(tmux)
command -v zsh >/dev/null 2>&1 || missing+=(zsh)
if [ "${#missing[@]}" -gt 0 ]; then
retry() {
local attempt
for attempt in 1 2; do
if "$@"; then
return 0
fi
if [ "$attempt" -lt 2 ]; then
printf 'Dependency install attempt %s failed; retrying in 5s\n' "$attempt" >&2
sleep 5
fi
done
return 1
}
# The Azure mirror can stall after APT has already fetched release
# metadata from the direct Ubuntu fallback. Prefer that fallback
# for the whole update when the runner's mirror list provides it.
if [ -f /etc/apt/apt-mirrors.txt ] &&
grep -Eq '^https?://(archive|ports)\.ubuntu\.com/' /etc/apt/apt-mirrors.txt
then
sudo sed -i '/:\/\/azure\..*ubuntu\.com\//d' /etc/apt/apt-mirrors.txt
fi
retry sudo timeout --kill-after=15s 4m apt-get \
-o Acquire::Retries=3 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
update
retry sudo timeout --kill-after=15s 3m apt-get \
-o Dpkg::Use-Pty=0 \
install -y "${missing[@]}"
fi
printf 'set-option -g history-limit 100000\n' > "$HOME/.tmux.conf"
tmux -V
zsh --version
- name: Install E2E dependencies
run: bun install --frozen-lockfile
working-directory: tests/e2e
- name: Run deterministic E2E shard
working-directory: tests/e2e
run: |
selected=()
selection_file="$RUNNER_TEMP/fx-e2e-shard-${SHARD_INDEX}.txt"
bun ci-shards.ts \
--shard-count "$SHARD_COUNT" \
--shard-index "$SHARD_INDEX" > "$selection_file"
while IFS= read -r test_file; do
selected+=("$test_file")
done < "$selection_file"
test "${#selected[@]}" -gt 0
printf 'Running E2E shard %s/%s (%s files):\n' "$((SHARD_INDEX + 1))" "$SHARD_COUNT" "${#selected[@]}"
printf ' %s\n' "${selected[@]}"
./ci-run-files.sh "${selected[@]}"
- name: Save Zig cache
if: matrix.shard.index == 0
uses: ./.github/actions/save-zig-cache
with:
cache-key: ${{ steps.zig.outputs.cache-key }}
cache-hit: ${{ steps.zig.outputs.cache-hit }}
e2e_conformance:
name: E2E (MCP conformance)
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
AI_GATEWAY_API_KEY: ""
VERCEL_OIDC_TOKEN: ""
FX_SOUND: "0"
steps:
- uses: actions/checkout@v4
- name: Download fx
uses: actions/download-artifact@v4
with:
name: fx-linux-x86_64
path: zig-out/bin
- name: Restore executable mode
run: chmod +x zig-out/bin/fx
# The client-request-state scenario builds a Zig dispatcher fixture.
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-mcp-fixtures
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Install E2E dependencies
run: bun install --frozen-lockfile
working-directory: tests/e2e
- name: Install isolated MCP conformance package
run: npm ci
working-directory: tests/e2e/conformance
- name: Run isolated MCP conformance baseline
run: npm test
working-directory: tests/e2e/conformance
- name: Save Zig cache
uses: ./.github/actions/save-zig-cache
with:
cache-key: ${{ steps.zig.outputs.cache-key }}
cache-hit: ${{ steps.zig.outputs.cache-hit }}
e2e:
name: E2E (deterministic)
if: ${{ always() }}
needs: [e2e_mode, e2e_conformance]
runs-on: ubuntu-latest
steps:
- name: Require every shard and the conformance baseline
env:
SHARD_RESULT: ${{ needs.e2e_mode.result }}
CONFORMANCE_RESULT: ${{ needs.e2e_conformance.result }}
run: |
test "$SHARD_RESULT" = success
test "$CONFORMANCE_RESULT" = success
lint-scripts:
name: Shellcheck
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Shellcheck Fx scripts
run: |
scripts=(
benchmarks/startup.sh
scripts/check-compactor-boundary.sh
scripts/check-public-surface.sh
scripts/detect-macos-need.sh
scripts/smoke-binary.sh
tests/e2e/ci-run-files.sh
)
# bash -n parses only its first argument, so check each script.
for script in "${scripts[@]}"; do
bash -n "$script"
done
shellcheck "${scripts[@]}"
e2e_acp_live:
name: E2E ACP (live models)
if: github.event_name == 'workflow_dispatch' && inputs.acp_live
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Zig
id: zig
uses: ./.github/actions/setup-zig
with:
version: "0.17.0"
cache-key: ci-unit-ReleaseSafe
- name: Build
run: zig build -Doptimize=ReleaseSafe
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Install e2e deps
run: bun install
working-directory: tests/e2e
- name: Run full ACP tests
run: bun test acp.test.ts
working-directory: tests/e2e
env:
AI_GATEWAY_API_KEY: ${{ secrets.AI_GATEWAY_API_KEY }}