mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-28 10:15:17 +08:00
Close macOS detection gaps and stop pull requests saving Zig caches
The macOS detector now fails the check when it cannot read either revision or the platform test list, instead of reporting that macOS is not needed. It also treats Linux and BSD code paths as platform code, because macOS takes the other side of a Linux branch. Windows-only branches still skip macOS. Replaying the last 150 commits on main moves macOS runs from 72 to 76. The macOS platform E2E files now run in two weighted shards that share one baseline-CPU build. tui-performance and tui-resume join the list because they branch on the platform. Linux Zig jobs restore a cache keyed by the Zig version and build files, and only runs on main save it. Pull request runs no longer add cache entries that push main's out of the quota. The macOS jobs never run on main, so their Zig cache is off. Build & Test now also requires the SDK jobs, which Publish libfx relies on. Manual dispatches get their own concurrency group so they cannot cancel the push run on main. The N-API build pins Node 24, Binary Size smoke-tests every target, and the shellcheck job syntax-checks each script instead of only the first.
This commit is contained in:
13 files changed
+544
-60
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: Save Zig cache
|
||||
description: >-
|
||||
Save the Zig cache restored by the setup-zig action. Saves only on main and
|
||||
only when the exact key was missing, so the cache gets a new entry when the
|
||||
Zig version or build files change instead of on every run.
|
||||
|
||||
inputs:
|
||||
cache-key:
|
||||
description: The cache-key output of the setup-zig action
|
||||
required: true
|
||||
cache-hit:
|
||||
description: The cache-hit output of the setup-zig action
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Save Zig cache
|
||||
if: github.ref == 'refs/heads/main' && inputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.zig-cache
|
||||
key: ${{ inputs.cache-key }}
|
||||
@@ -0,0 +1,52 @@
|
||||
name: Setup Zig
|
||||
description: >-
|
||||
Install Zig and restore the job's Zig cache. Only runs on main save the cache,
|
||||
through save-zig-cache, so pull requests reuse main's cache without adding
|
||||
entries of their own. The key changes only with the Zig version and the build
|
||||
files: fx itself recompiles on every change, so the cache pays off through the
|
||||
dependencies, compiler_rt, and libc it keeps.
|
||||
|
||||
inputs:
|
||||
version:
|
||||
description: Zig version to install
|
||||
required: true
|
||||
cache-key:
|
||||
description: Cache name for the job, unique per build configuration
|
||||
required: true
|
||||
|
||||
outputs:
|
||||
cache-key:
|
||||
description: Exact cache key for save-zig-cache
|
||||
value: ${{ steps.key.outputs.key }}
|
||||
cache-hit:
|
||||
description: Whether the exact cache key was restored
|
||||
value: ${{ steps.restore.outputs.cache-hit }}
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Install Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: ${{ inputs.version }}
|
||||
use-cache: false
|
||||
|
||||
- name: Compute Zig cache key
|
||||
id: key
|
||||
shell: bash
|
||||
env:
|
||||
PREFIX: zig-v1-${{ runner.os }}-${{ runner.arch }}-${{ inputs.version }}-${{ inputs.cache-key }}-
|
||||
BUILD_HASH: ${{ hashFiles('build.zig', 'build.zig.zon') }}
|
||||
run: |
|
||||
printf 'prefix=%s\n' "$PREFIX" >> "$GITHUB_OUTPUT"
|
||||
printf 'key=%s%s\n' "$PREFIX" "$BUILD_HASH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Main starts from an empty cache when the key misses, so each saved
|
||||
# snapshot holds one build instead of growing with every build change.
|
||||
- name: Restore Zig cache
|
||||
id: restore
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ${{ github.workspace }}/.zig-cache
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
restore-keys: ${{ github.ref != 'refs/heads/main' && steps.key.outputs.prefix || '' }}
|
||||
@@ -28,7 +28,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: bench-${{ github.ref }}${{ (inputs.pgso_control_artifact != '' || inputs.pgso_candidate_artifact != '') && '-pgso-artifacts' || '' }}
|
||||
group: bench-${{ github.event_name == 'workflow_dispatch' && 'dispatch-' || '' }}${{ github.ref }}${{ (inputs.pgso_control_artifact != '' || inputs.pgso_candidate_artifact != '') && '-pgso-artifacts' || '' }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
@@ -43,9 +43,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: bench-ReleaseSafe
|
||||
|
||||
- name: Build (ReleaseSafe)
|
||||
run: zig build -Doptimize=ReleaseSafe
|
||||
@@ -85,6 +87,12 @@ jobs:
|
||||
path: ${{ runner.temp }}/tui-performance.json
|
||||
if-no-files-found: warn
|
||||
|
||||
- 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 }}
|
||||
|
||||
long-turn-memory:
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.long_turn_memory && inputs.signed_run == '' && inputs.pgso_control_artifact == '' && inputs.pgso_candidate_artifact == ''
|
||||
name: Long-turn memory
|
||||
@@ -94,9 +102,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: bench-ReleaseSafe
|
||||
|
||||
- name: Build (ReleaseSafe)
|
||||
run: zig build -Doptimize=ReleaseSafe
|
||||
@@ -126,9 +136,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: bench-ReleaseSafe
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
|
||||
@@ -83,10 +83,9 @@ jobs:
|
||||
fi
|
||||
cat "$evidence_dir/head-sections.txt"
|
||||
|
||||
# Linux x86_64 gets the same smoke test in CI. The other platforms run
|
||||
# These are release-style builds. Linux arm64 and both macOS targets run
|
||||
# nowhere else on a pull request, so this is where a broken binary shows up.
|
||||
- name: Smoke test native binary
|
||||
if: matrix.name != 'linux-x86_64'
|
||||
env:
|
||||
AI_GATEWAY_API_KEY: ""
|
||||
VERCEL_OIDC_TOKEN: ""
|
||||
|
||||
+77
-17
@@ -15,7 +15,9 @@ on:
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
# A manual dispatch on main must not cancel the push run that Dev Release
|
||||
# and Publish libfx wait on.
|
||||
group: ci-${{ github.event_name == 'workflow_dispatch' && 'dispatch-' || '' }}${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -32,7 +34,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-unit-${{ matrix.optimize }}
|
||||
@@ -49,8 +52,8 @@ jobs:
|
||||
- name: Check compactor boundary
|
||||
run: ./scripts/check-compactor-boundary.sh
|
||||
|
||||
- name: Test signing and release routing
|
||||
run: python3 -m unittest scripts.tests.test_macos_signing scripts.tests.test_signed_macos -q
|
||||
- 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 }}
|
||||
@@ -63,16 +66,26 @@ jobs:
|
||||
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
|
||||
needs: [zig_mode, sdk-node-wasm, sdk-node-napi, sdk-bun, sdk-browser-wasm]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require ReleaseSafe
|
||||
- name: Require unit tests and every SDK job
|
||||
env:
|
||||
MATRIX_RESULT: ${{ needs.zig_mode.result }}
|
||||
run: test "$MATRIX_RESULT" = success
|
||||
RESULTS: ${{ toJSON(needs.*.result) }}
|
||||
run: |
|
||||
printf '%s\n' "$RESULTS"
|
||||
jq -e 'length == 5 and all(.[]; . == "success")' <<<"$RESULTS"
|
||||
|
||||
build:
|
||||
name: Build fx (ReleaseSafe)
|
||||
@@ -85,7 +98,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-build-ReleaseSafe
|
||||
@@ -107,6 +121,12 @@ jobs:
|
||||
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
|
||||
@@ -115,9 +135,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-sdk-wasm
|
||||
|
||||
- name: Build WASM surfaces (ReleaseSmall)
|
||||
run: |
|
||||
@@ -134,6 +156,12 @@ jobs:
|
||||
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
|
||||
@@ -153,8 +181,15 @@ jobs:
|
||||
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
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-sdk-napi-${{ matrix.platform }}
|
||||
@@ -173,6 +208,12 @@ jobs:
|
||||
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
|
||||
@@ -422,7 +463,8 @@ jobs:
|
||||
|
||||
# Some E2E files build their own Zig fixtures.
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-e2e-fixtures
|
||||
@@ -494,6 +536,13 @@ jobs:
|
||||
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
|
||||
@@ -517,10 +566,11 @@ jobs:
|
||||
|
||||
# The client-request-state scenario builds a Zig dispatcher fixture.
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-e2e-fixtures
|
||||
cache-key: ci-mcp-fixtures
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
@@ -544,6 +594,12 @@ jobs:
|
||||
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() }}
|
||||
@@ -574,7 +630,10 @@ jobs:
|
||||
scripts/smoke-binary.sh
|
||||
tests/e2e/ci-run-files.sh
|
||||
)
|
||||
bash -n "${scripts[@]}"
|
||||
# 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:
|
||||
@@ -585,10 +644,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
id: zig
|
||||
uses: ./.github/actions/setup-zig
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: ci-acp-full-ReleaseSafe
|
||||
cache-key: ci-unit-ReleaseSafe
|
||||
|
||||
- name: Build
|
||||
run: zig build -Doptimize=ReleaseSafe
|
||||
|
||||
+67
-15
@@ -64,7 +64,7 @@ jobs:
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: macos-unit-ReleaseSafe
|
||||
use-cache: false
|
||||
|
||||
- name: Test signing and release routing
|
||||
run: python3 -m unittest scripts.tests.test_macos_signing scripts.tests.test_signed_macos -q
|
||||
@@ -72,33 +72,80 @@ jobs:
|
||||
- name: Run unit tests
|
||||
run: zig build test -Doptimize=ReleaseSafe
|
||||
|
||||
e2e:
|
||||
name: macOS arm64 platform E2E
|
||||
build:
|
||||
name: macOS arm64 build
|
||||
needs: detect
|
||||
if: needs.detect.outputs.needed == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 90
|
||||
timeout-minutes: 30
|
||||
env:
|
||||
AI_GATEWAY_API_KEY: ""
|
||||
VERCEL_OIDC_TOKEN: ""
|
||||
FX_REQUIRE_TMUX: "1"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: "0.16.0"
|
||||
cache-key: macos-e2e-ReleaseSafe
|
||||
use-cache: false
|
||||
|
||||
# Both E2E shards run this binary on other runners, so build for the
|
||||
# baseline CPU rather than the build runner's.
|
||||
- name: Build
|
||||
run: zig build -Doptimize=ReleaseSafe
|
||||
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 the E2E shards
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fx-macos-arm64
|
||||
path: zig-out/bin/fx
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
e2e:
|
||||
name: macOS arm64 platform E2E (shard ${{ matrix.shard.label }})
|
||||
needs: [detect, build]
|
||||
if: needs.detect.outputs.needed == 'true'
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard:
|
||||
- index: 0
|
||||
label: "1/2"
|
||||
- index: 1
|
||||
label: "2/2"
|
||||
env:
|
||||
AI_GATEWAY_API_KEY: ""
|
||||
VERCEL_OIDC_TOKEN: ""
|
||||
FX_REQUIRE_TMUX: "1"
|
||||
SHARD_INDEX: ${{ matrix.shard.index }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Download the shared binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: fx-macos-arm64
|
||||
path: zig-out/bin
|
||||
|
||||
- name: Restore the executable bit
|
||||
run: chmod +x zig-out/bin/fx
|
||||
|
||||
# Some E2E fixtures build small Zig helpers.
|
||||
- name: Setup Zig
|
||||
uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: "0.16.0"
|
||||
use-cache: false
|
||||
|
||||
- name: Setup Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
@@ -115,26 +162,29 @@ jobs:
|
||||
run: bun install --frozen-lockfile
|
||||
working-directory: tests/e2e
|
||||
|
||||
- name: Run macOS platform E2E files
|
||||
- name: Run this shard of the macOS platform E2E files
|
||||
working-directory: tests/e2e
|
||||
shell: bash
|
||||
run: |
|
||||
bun ci-shards.ts --shard-count 2 --shard-index "$SHARD_INDEX" \
|
||||
--only macos-platform-tests.json > "$RUNNER_TEMP/fx-macos-shard.txt"
|
||||
files=()
|
||||
while IFS= read -r test_file; do
|
||||
files+=("$test_file")
|
||||
done < <(jq -r '.[]' tests/e2e/macos-platform-tests.json)
|
||||
done < "$RUNNER_TEMP/fx-macos-shard.txt"
|
||||
test "${#files[@]}" -gt 0
|
||||
printf 'Running %s macOS platform E2E files:\n' "${#files[@]}"
|
||||
printf ' %s\n' "${files[@]}"
|
||||
tests/e2e/ci-run-files.sh "${files[@]}"
|
||||
./ci-run-files.sh "${files[@]}"
|
||||
|
||||
- name: Setup Node
|
||||
if: needs.detect.outputs.sdk_native == 'true'
|
||||
if: needs.detect.outputs.sdk_native == 'true' && matrix.shard.index == 0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Build and load the native SDK addon
|
||||
if: needs.detect.outputs.sdk_native == 'true'
|
||||
if: needs.detect.outputs.sdk_native == 'true' && matrix.shard.index == 0
|
||||
run: |
|
||||
zig build libfx-napi -Dnapi-surface=core -Doptimize=ReleaseSafe
|
||||
npm run --prefix sdk test:node-napi
|
||||
@@ -142,7 +192,7 @@ jobs:
|
||||
macos:
|
||||
name: macOS arm64
|
||||
if: ${{ always() }}
|
||||
needs: [detect, unit, e2e]
|
||||
needs: [detect, unit, build, e2e]
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
@@ -151,6 +201,7 @@ jobs:
|
||||
DETECT_RESULT: ${{ needs.detect.result }}
|
||||
NEEDED: ${{ needs.detect.outputs.needed }}
|
||||
UNIT_RESULT: ${{ needs.unit.result }}
|
||||
BUILD_RESULT: ${{ needs.build.result }}
|
||||
E2E_RESULT: ${{ needs.e2e.result }}
|
||||
run: |
|
||||
test "$DETECT_RESULT" = success
|
||||
@@ -159,4 +210,5 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
test "$UNIT_RESULT" = success
|
||||
test "$BUILD_RESULT" = success
|
||||
test "$E2E_RESULT" = success
|
||||
@@ -278,11 +278,13 @@ Do not run the complete deterministic test suite locally as the default developm
|
||||
|
||||
After the focused checks pass, create a clean checkpoint commit, push the non-`main` feature branch, and open a draft PR immediately. Linux is the gate for every change because most fx behavior is identical on every platform:
|
||||
|
||||
* `.github/workflows/ci.yml` runs on Linux x86_64. It checks formatting, the PGSO corpus, the public surface, and the compactor boundary, then runs the ReleaseSafe unit tests. It builds fx once, smoke-tests it, and shares that binary with four duration-balanced E2E shards and the MCP conformance baseline. Checked-in weights assign every E2E file to exactly one shard, and files inside each shard run sequentially in separate Bun processes so terminal fixtures and process state cannot leak between files. A failed file receives one retry after its tmux server is reset. A file that passes only on retry gets a warning annotation; investigate it as a possible race. The SDK jobs build WASM and the Node-API addons once and run the complete package qualification.
|
||||
* `.github/workflows/ci.yml` runs on Linux x86_64. It checks formatting, the PGSO corpus, the public surface, and the compactor boundary, then runs the ReleaseSafe unit tests. It builds fx once, smoke-tests it, and shares that binary with four duration-balanced E2E shards and the MCP conformance baseline. Checked-in weights assign every E2E file to exactly one shard, and files inside each shard run sequentially in separate Bun processes so terminal fixtures and process state cannot leak between files. A failed file receives one retry after its tmux server is reset. A file that passes only on retry gets a warning annotation; investigate it as a possible race. The SDK jobs build WASM and the Node-API addons once and run the complete package qualification. `Build & Test` passes only when the unit tests and every SDK job pass, because Publish libfx relies on that result instead of qualifying the package again.
|
||||
* `.github/workflows/binary-size.yml` builds each release target on its native runner, reports the size change, and smoke-tests the Linux arm64, macOS x86_64, and macOS arm64 binaries. See **Binary Size Observability**.
|
||||
* `.github/workflows/bench.yml` enforces the startup latency budget. See **Benchmarks**.
|
||||
|
||||
macOS runs only when a change can behave differently there. `.github/workflows/macos.yml` uses `scripts/detect-macos-need.sh` to check the diff. The macOS arm64 unit tests and the E2E files in `tests/e2e/macos-platform-tests.json` run when the change touches a Zig file with macOS-specific code, `build.zig`, the macOS signing script, the native SDK addon, a listed E2E file, or the macOS checks themselves. Native SDK addon changes also build and load the addon on macOS. To request macOS for any other change, add the `ci:macos` label or run `gh workflow run macos.yml --ref <branch>`. When the change does not need macOS, the `macOS arm64` check passes without starting a macOS runner. Every E2E file that branches on `process.platform === "darwin"` must appear in `tests/e2e/macos-platform-tests.json`, and `tests/e2e/ci-shards.test.ts` enforces that.
|
||||
Linux jobs install Zig through `.github/actions/setup-zig`, which restores the Zig cache that runs on `main` save with `.github/actions/save-zig-cache`. Pull requests never save a Zig cache, so they cannot push `main`'s entries out of the repository's cache quota.
|
||||
|
||||
macOS runs only when a change can behave differently there. `.github/workflows/macos.yml` uses `scripts/detect-macos-need.sh` to check the diff. The macOS arm64 unit tests and the E2E files in `tests/e2e/macos-platform-tests.json` run when the change touches a Zig file with a macOS, BSD, or Linux code path, `build.zig`, the macOS signing script, the native SDK addon, a listed E2E file, or the macOS checks themselves. A Linux-only branch counts because macOS takes its other path; a Windows-only branch does not. The listed E2E files run in two weighted shards that share one macOS build. Native SDK addon changes also build and load the addon on macOS. If the detector cannot read the change, the `macOS arm64` check fails instead of passing. To request macOS for any other change, add the `ci:macos` label or run `gh workflow run macos.yml --ref <branch>`. When the change does not need macOS, the `macOS arm64` check passes without starting a macOS runner. Every E2E file that branches on `process.platform === "darwin"` must appear in `tests/e2e/macos-platform-tests.json`, and `tests/e2e/ci-shards.test.ts` enforces that.
|
||||
|
||||
The macOS arm64 PGSO workflow runs on a pull request only when the PGSO pipeline changes: `scripts/pgso/` other than `corpus.json`, the `setup-pgso` action, or `.github/workflows/pgso-macos-arm64.yml`. To qualify any other change, run `gh workflow run pgso-macos-arm64.yml --ref <branch>`. The stable release always runs it.
|
||||
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ Keep the local development loop focused: run the narrowest test that covers the
|
||||
|
||||
Once the focused checks pass, create a clean checkpoint commit, push the non-`main` feature branch, and open a draft PR immediately. **CI** runs the complete deterministic suite on Linux x86_64, which is the gate for every change. It checks formatting, the PGSO corpus, the public surface, and the compactor boundary, runs the ReleaseSafe unit tests, and builds fx once for the E2E jobs. Four duration-balanced E2E shards use checked-in weights to assign every Bun test file once, and files inside each shard run sequentially in separate Bun processes so terminal fixtures and process state cannot leak between files. A failed file receives one retry after tmux is reset, and a file that passes only on retry gets a warning annotation.
|
||||
|
||||
macOS checks run only when a change can behave differently on macOS: a Zig file with macOS-specific code, `build.zig`, the macOS signing script, the native SDK addon, or an E2E file listed in `tests/e2e/macos-platform-tests.json`. Add the `ci:macos` label or run `gh workflow run macos.yml --ref <branch>` to request them for any other change. The `macOS arm64` check passes without a macOS runner when the change does not need one.
|
||||
macOS checks run only when a change can behave differently on macOS: a Zig file with a macOS, BSD, or Linux code path, `build.zig`, the macOS signing script, the native SDK addon, or an E2E file listed in `tests/e2e/macos-platform-tests.json`. Add the `ci:macos` label or run `gh workflow run macos.yml --ref <branch>` to request them for any other change. The `macOS arm64` check passes without a macOS runner when the change does not need one.
|
||||
|
||||
Do not mark the draft PR ready until `Build & Test`, `E2E (deterministic)`, `Shellcheck`, `Startup Latency`, `macOS arm64`, and the final ship gate have succeeded for the exact current commit. A result from an older commit does not count. Live model evals are separate from this gate because they require credentials and are not deterministic.
|
||||
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
# Usage: detect-macos-need.sh <base-revision> <head-revision>
|
||||
#
|
||||
# macOS runs only when a change touches behavior that can differ on macOS: a
|
||||
# Zig file with macOS-specific code before or after the change, build.zig, the
|
||||
# macOS signing script, the native SDK addon, an E2E file listed in
|
||||
# tests/e2e/macos-platform-tests.json, or the macOS checks themselves. Set
|
||||
# MACOS_REQUESTED to a reason to force the checks, and SDK_NATIVE_REQUESTED=true
|
||||
# to force the native SDK addon check.
|
||||
# Zig file with a macOS, BSD, or Linux code path before or after the change,
|
||||
# build.zig, the macOS signing script, the native SDK addon, an E2E file listed
|
||||
# in tests/e2e/macos-platform-tests.json, or the macOS checks themselves. A Linux
|
||||
# branch counts because macOS takes its other path. A Windows-only branch does
|
||||
# not, because macOS and Linux share its other path. Set MACOS_REQUESTED to a
|
||||
# reason to force the checks, and SDK_NATIVE_REQUESTED=true to force the native
|
||||
# SDK addon check.
|
||||
#
|
||||
# Writes needed=true|false and sdk_native=true|false to $GITHUB_OUTPUT and the
|
||||
# reasons to $GITHUB_STEP_SUMMARY when those files are set.
|
||||
# reasons to $GITHUB_STEP_SUMMARY when those files are set. Exits non-zero when
|
||||
# either revision cannot be read, so the macOS check fails instead of passing
|
||||
# without having looked at the change.
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
@@ -37,12 +41,29 @@ require_sdk_native() {
|
||||
require "$1"
|
||||
}
|
||||
|
||||
has_macos_code() {
|
||||
has_platform_code() {
|
||||
local content
|
||||
content="$({ git show "$head:$1" 2>/dev/null; git show "$base:$1" 2>/dev/null; } || true)"
|
||||
grep -Eq '\.macos|isDarwin|[Dd]arwin' <<<"$content"
|
||||
grep -Eq '\.linux([^A-Za-z0-9_]|$)|isBSD|\.macos|isDarwin|[Dd]arwin' <<<"$content"
|
||||
}
|
||||
|
||||
if ! jq -e 'type == "array"' "$platform_list" >/dev/null 2>&1; then
|
||||
printf 'error: %s is missing or not a JSON array; the macOS check cannot decide\n' "$platform_list" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for revision in "$base" "$head"; do
|
||||
if ! git rev-parse --verify --quiet "$revision^{commit}" >/dev/null; then
|
||||
printf 'error: cannot read revision %s; the macOS check cannot decide\n' "$revision" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if ! changed_files="$(git diff --name-only --no-renames "$base" "$head")"; then
|
||||
printf 'error: git diff %s %s failed; the macOS check cannot decide\n' "$base" "$head" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
is_platform_test() {
|
||||
jq -e --arg file "$1" 'index($file) != null' "$platform_list" >/dev/null
|
||||
}
|
||||
@@ -55,10 +76,11 @@ if [ "${SDK_NATIVE_REQUESTED:-false}" = true ]; then
|
||||
fi
|
||||
|
||||
while IFS= read -r path; do
|
||||
[ -n "$path" ] || continue
|
||||
case "$path" in
|
||||
.github/workflows/macos.yml)
|
||||
require_sdk_native "$path: the macOS checks changed" ;;
|
||||
scripts/detect-macos-need.sh | scripts/smoke-binary.sh | tests/e2e/ci-run-files.sh | "$platform_list")
|
||||
scripts/detect-macos-need.sh | scripts/smoke-binary.sh | tests/e2e/ci-run-files.sh | tests/e2e/ci-shards.ts | "$platform_list")
|
||||
require "$path: the macOS checks changed" ;;
|
||||
build.zig | build.zig.zon)
|
||||
require "$path: build configuration changed" ;;
|
||||
@@ -67,8 +89,8 @@ while IFS= read -r path; do
|
||||
src/napi_*.zig | sdk/node.js | sdk/tests/test-node-napi.mjs)
|
||||
require_sdk_native "$path: native SDK addon changed" ;;
|
||||
*.zig)
|
||||
if has_macos_code "$path"; then
|
||||
require "$path: contains macOS-specific code"
|
||||
if has_platform_code "$path"; then
|
||||
require "$path: has a macOS or Linux code path"
|
||||
fi ;;
|
||||
tests/e2e/*.test.ts)
|
||||
name="${path#tests/e2e/}"
|
||||
@@ -76,7 +98,7 @@ while IFS= read -r path; do
|
||||
require "$path: macOS platform E2E file changed"
|
||||
fi ;;
|
||||
esac
|
||||
done < <(git diff --name-only --no-renames "$base" "$head")
|
||||
done <<<"$changed_files"
|
||||
|
||||
if [ "$needed" = true ]; then
|
||||
printf 'macOS arm64 checks needed:\n'
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
||||
SCRIPT_PATH = REPO_ROOT / "scripts" / "detect-macos-need.sh"
|
||||
PLATFORM_LIST = "tests/e2e/macos-platform-tests.json"
|
||||
|
||||
|
||||
class DetectMacosNeedTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = pathlib.Path(self.temp.name)
|
||||
self.env = {
|
||||
"PATH": os.environ["PATH"],
|
||||
"HOME": self.temp.name,
|
||||
"GIT_CONFIG_GLOBAL": os.devnull,
|
||||
"GIT_CONFIG_NOSYSTEM": "1",
|
||||
"GIT_AUTHOR_NAME": "Test",
|
||||
"GIT_AUTHOR_EMAIL": "test@example.com",
|
||||
"GIT_COMMITTER_NAME": "Test",
|
||||
"GIT_COMMITTER_EMAIL": "test@example.com",
|
||||
}
|
||||
self.git("init", "-q")
|
||||
self.write(PLATFORM_LIST, '["listed.test.ts"]\n')
|
||||
self.write("src/plain.zig", "pub fn plain() void {}\n")
|
||||
self.write("src/mac.zig", "if (builtin.os.tag == .macos) {}\n")
|
||||
self.write("README.md", "fx\n")
|
||||
self.base = self.commit("base")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.temp.cleanup()
|
||||
|
||||
def git(self, *args: str) -> str:
|
||||
result = subprocess.run(
|
||||
["git", *args], cwd=self.root, env=self.env, check=True,
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
def write(self, path: str, content: str) -> None:
|
||||
target = self.root / path
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
target.write_text(content, encoding="utf-8")
|
||||
|
||||
def commit(self, message: str) -> str:
|
||||
self.git("add", "-A")
|
||||
self.git("commit", "-q", "--allow-empty", "-m", message)
|
||||
return self.git("rev-parse", "HEAD")
|
||||
|
||||
def detect(
|
||||
self, base: str | None = None, head: str = "HEAD", **env: str,
|
||||
) -> tuple[int, dict[str, str], str]:
|
||||
output_path = self.root.parent / f"{self.root.name}-output"
|
||||
output_path.write_text("", encoding="utf-8")
|
||||
result = subprocess.run(
|
||||
["bash", str(SCRIPT_PATH), base or self.base, head],
|
||||
cwd=self.root,
|
||||
env={**self.env, "GITHUB_OUTPUT": str(output_path), **env},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
outputs = dict(
|
||||
line.split("=", 1)
|
||||
for line in output_path.read_text(encoding="utf-8").splitlines()
|
||||
)
|
||||
output_path.unlink()
|
||||
return result.returncode, outputs, result.stdout + result.stderr
|
||||
|
||||
def assert_needed(self, *, sdk_native: bool = False, **env: str) -> str:
|
||||
code, outputs, text = self.detect(**env)
|
||||
self.assertEqual(0, code, text)
|
||||
self.assertEqual("true", outputs.get("needed"), text)
|
||||
self.assertEqual("true" if sdk_native else "false", outputs.get("sdk_native"), text)
|
||||
return text
|
||||
|
||||
def assert_not_needed(self) -> None:
|
||||
code, outputs, text = self.detect()
|
||||
self.assertEqual(0, code, text)
|
||||
self.assertEqual({"needed": "false", "sdk_native": "false"}, outputs, text)
|
||||
|
||||
def test_change_without_platform_behavior_skips_macos(self) -> None:
|
||||
self.write("README.md", "fx docs\n")
|
||||
self.write("src/plain.zig", "pub fn plain() u8 { return 1; }\n")
|
||||
self.write("src/windows.zig", "if (builtin.os.tag == .windows) {} else {}\n")
|
||||
self.write("src/linux_target.zig", "const name = .linux_x64;\n")
|
||||
self.write("tests/e2e/unlisted.test.ts", "test\n")
|
||||
self.commit("head")
|
||||
|
||||
self.assert_not_needed()
|
||||
|
||||
def test_zig_file_with_macos_code_needs_macos(self) -> None:
|
||||
self.write("src/mac.zig", "if (builtin.os.tag == .macos) { run(); }\n")
|
||||
self.commit("head")
|
||||
|
||||
self.assertIn("src/mac.zig", self.assert_needed())
|
||||
|
||||
def test_linux_branch_needs_macos_because_macos_takes_the_else_path(self) -> None:
|
||||
self.write("src/self_exe.zig", "if (comptime builtin.os.tag == .linux) {} else {}\n")
|
||||
self.commit("head")
|
||||
|
||||
self.assertIn("src/self_exe.zig", self.assert_needed())
|
||||
|
||||
def test_linux_namespace_and_bsd_checks_need_macos(self) -> None:
|
||||
self.write("src/syscall.zig", "const pid = std.os.linux.getpid();\n")
|
||||
self.write("src/bsd.zig", "if (builtin.os.tag.isBSD()) {}\n")
|
||||
self.commit("head")
|
||||
|
||||
text = self.assert_needed()
|
||||
self.assertIn("src/syscall.zig", text)
|
||||
self.assertIn("src/bsd.zig", text)
|
||||
|
||||
def test_removing_macos_code_needs_macos(self) -> None:
|
||||
self.write("src/mac.zig", "pub fn now_plain() void {}\n")
|
||||
self.commit("head")
|
||||
|
||||
self.assertIn("src/mac.zig", self.assert_needed())
|
||||
|
||||
def test_deleted_and_renamed_macos_files_need_macos(self) -> None:
|
||||
self.git("mv", "src/mac.zig", "src/renamed.zig")
|
||||
self.commit("head")
|
||||
|
||||
text = self.assert_needed()
|
||||
self.assertIn("src/mac.zig", text)
|
||||
self.assertIn("src/renamed.zig", text)
|
||||
|
||||
def test_build_and_signing_changes_need_macos(self) -> None:
|
||||
for path in ("build.zig", "build.zig.zon", "scripts/sign-and-notarize-macos.sh"):
|
||||
with self.subTest(path=path):
|
||||
self.write(path, f"{path}\n")
|
||||
self.commit(path)
|
||||
self.assertIn(path, self.assert_needed())
|
||||
self.base = self.git("rev-parse", "HEAD")
|
||||
|
||||
def test_native_sdk_changes_also_check_the_addon(self) -> None:
|
||||
for path in ("src/napi_core_main.zig", "sdk/node.js", "sdk/tests/test-node-napi.mjs"):
|
||||
with self.subTest(path=path):
|
||||
self.write(path, f"// {path}\n")
|
||||
self.commit(path)
|
||||
self.assertIn(path, self.assert_needed(sdk_native=True))
|
||||
self.base = self.git("rev-parse", "HEAD")
|
||||
|
||||
def test_macos_check_inputs_need_macos(self) -> None:
|
||||
self.write(".github/workflows/macos.yml", "name: macOS\n")
|
||||
self.commit("workflow")
|
||||
self.assert_needed(sdk_native=True)
|
||||
self.base = self.git("rev-parse", "HEAD")
|
||||
|
||||
for path in (
|
||||
"scripts/detect-macos-need.sh",
|
||||
"scripts/smoke-binary.sh",
|
||||
"tests/e2e/ci-run-files.sh",
|
||||
"tests/e2e/ci-shards.ts",
|
||||
):
|
||||
with self.subTest(path=path):
|
||||
self.write(path, f"# {path}\n")
|
||||
self.commit(path)
|
||||
self.assertIn(path, self.assert_needed())
|
||||
self.base = self.git("rev-parse", "HEAD")
|
||||
|
||||
def test_listed_platform_test_needs_macos_and_nested_paths_do_not(self) -> None:
|
||||
self.write("tests/e2e/listed.test.ts", "test\n")
|
||||
self.commit("listed")
|
||||
self.assertIn("listed.test.ts", self.assert_needed())
|
||||
self.base = self.git("rev-parse", "HEAD")
|
||||
|
||||
self.write("tests/e2e/fixtures/listed.test.ts", "fixture\n")
|
||||
self.commit("nested")
|
||||
self.assert_not_needed()
|
||||
|
||||
def test_requests_force_the_checks(self) -> None:
|
||||
self.assertIn("ci:macos label", self.assert_needed(MACOS_REQUESTED="ci:macos label"))
|
||||
self.assert_needed(sdk_native=True, SDK_NATIVE_REQUESTED="true")
|
||||
|
||||
def test_unreadable_revision_fails_instead_of_skipping(self) -> None:
|
||||
code, outputs, text = self.detect(base="does-not-exist")
|
||||
|
||||
self.assertNotEqual(0, code, text)
|
||||
self.assertEqual({}, outputs)
|
||||
self.assertIn("cannot read revision", text)
|
||||
|
||||
def test_missing_platform_list_fails_instead_of_skipping(self) -> None:
|
||||
(self.root / PLATFORM_LIST).unlink()
|
||||
|
||||
code, outputs, text = self.detect()
|
||||
|
||||
self.assertNotEqual(0, code, text)
|
||||
self.assertEqual({}, outputs)
|
||||
self.assertIn("not a JSON array", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test";
|
||||
import { mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { buildShardPlan, selectShard } from "./ci-shards";
|
||||
import { buildShardPlan, restrictToFiles, selectShard } from "./ci-shards";
|
||||
|
||||
const files = ["a.test.ts", "b.test.ts", "c.test.ts", "d.test.ts"];
|
||||
|
||||
@@ -146,12 +146,37 @@ describe("macOS platform E2E list", () => {
|
||||
expect(names).toEqual([...new Set(names)].sort());
|
||||
});
|
||||
|
||||
test("includes every E2E file that branches on macOS", () => {
|
||||
const macosBranch = /(process\.platform|platform\(\))\s*[!=]==\s*["']darwin["']/;
|
||||
test("includes every E2E file that branches on the platform", () => {
|
||||
const platformBranch = /\bplatform(\(\))?\s*[!=]==\s*["'](darwin|linux)["']/;
|
||||
const missing = rootTests.filter((name) =>
|
||||
macosBranch.test(readFileSync(join(import.meta.dir, name), "utf8")) &&
|
||||
platformBranch.test(readFileSync(join(import.meta.dir, name), "utf8")) &&
|
||||
!(listed as string[]).includes(name)
|
||||
);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
test("plans the listed files with their checked-in weights", () => {
|
||||
const manifest = [
|
||||
{ file: "a.test.ts", weight: 9 },
|
||||
{ file: "b.test.ts", weight: 7 },
|
||||
{ file: "c.test.ts", weight: 5 },
|
||||
{ file: "d.test.ts", weight: 3 },
|
||||
];
|
||||
|
||||
const restricted = restrictToFiles(files, manifest, ["d.test.ts", "b.test.ts", "c.test.ts"]);
|
||||
const plan = buildShardPlan(restricted.files, restricted.manifest, 2);
|
||||
|
||||
expect(plan).toEqual({
|
||||
shards: [["b.test.ts"], ["c.test.ts", "d.test.ts"]],
|
||||
totals: [7, 8],
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects empty, unknown, and duplicate listed files", () => {
|
||||
const manifest = files.map((file) => ({ file, weight: 1 }));
|
||||
|
||||
expect(() => restrictToFiles(files, manifest, [])).toThrow("non-empty array");
|
||||
expect(() => restrictToFiles(files, manifest, ["z.test.ts"])).toThrow("not a discovered test");
|
||||
expect(() => restrictToFiles(files, manifest, ["a.test.ts", "a.test.ts"])).toThrow("duplicate");
|
||||
});
|
||||
});
|
||||
+40
-3
@@ -1,5 +1,5 @@
|
||||
import { readFileSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
export type ShardPlan = {
|
||||
shards: string[][];
|
||||
@@ -92,6 +92,29 @@ export function buildShardPlan(
|
||||
return { shards, totals };
|
||||
}
|
||||
|
||||
// Narrows a shard plan to a listed subset of the discovered files, such as the
|
||||
// macOS platform list, while keeping each file's checked-in weight.
|
||||
export function restrictToFiles(
|
||||
discoveredFiles: string[],
|
||||
manifestValue: unknown,
|
||||
listValue: unknown,
|
||||
): { files: string[]; manifest: WeightEntry[] } {
|
||||
if (!Array.isArray(listValue) || listValue.length === 0) {
|
||||
throw new Error("file list must be a non-empty array");
|
||||
}
|
||||
const discovered = new Set(discoveredFiles);
|
||||
const listed = new Set<string>();
|
||||
for (const file of listValue) {
|
||||
if (typeof file !== "string" || !discovered.has(file)) {
|
||||
throw new Error(`listed file is not a discovered test: ${String(file)}`);
|
||||
}
|
||||
if (listed.has(file)) throw new Error(`duplicate listed file: ${file}`);
|
||||
listed.add(file);
|
||||
}
|
||||
const manifest = parseManifest(manifestValue).filter((entry) => listed.has(entry.file));
|
||||
return { files: [...listed], manifest };
|
||||
}
|
||||
|
||||
export function selectShard(plan: ShardPlan, shardIndex: number): string[] {
|
||||
if (!Number.isSafeInteger(shardIndex) ||
|
||||
shardIndex < 0 || shardIndex >= plan.shards.length) {
|
||||
@@ -108,15 +131,29 @@ function integerArgument(args: string[], name: string): number {
|
||||
return value;
|
||||
}
|
||||
|
||||
function optionalArgument(args: string[], name: string): string | undefined {
|
||||
const index = args.indexOf(name);
|
||||
if (index < 0) return undefined;
|
||||
if (index + 1 >= args.length) throw new Error(`missing ${name} value`);
|
||||
return args[index + 1];
|
||||
}
|
||||
|
||||
function run(args: string[]): void {
|
||||
const shardCount = integerArgument(args, "--shard-count");
|
||||
const shardIndex = integerArgument(args, "--shard-index");
|
||||
const manifest = JSON.parse(
|
||||
let manifest: unknown = JSON.parse(
|
||||
readFileSync(join(import.meta.dir, "ci-shard-weights.json"), "utf8"),
|
||||
);
|
||||
const discovered = readdirSync(import.meta.dir, { withFileTypes: true })
|
||||
let discovered = readdirSync(import.meta.dir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".test.ts"))
|
||||
.map((entry) => entry.name);
|
||||
const listPath = optionalArgument(args, "--only");
|
||||
if (listPath !== undefined) {
|
||||
const list = JSON.parse(readFileSync(resolve(import.meta.dir, listPath), "utf8"));
|
||||
const restricted = restrictToFiles(discovered, manifest, list);
|
||||
discovered = restricted.files;
|
||||
manifest = restricted.manifest;
|
||||
}
|
||||
const plan = buildShardPlan(discovered, manifest, shardCount);
|
||||
const selected = selectShard(plan, shardIndex);
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
"tui-command-permissions.test.ts",
|
||||
"tui-full-transcript-brutal.test.ts",
|
||||
"tui-interrupt-recovery.test.ts",
|
||||
"tui-performance.test.ts",
|
||||
"tui-resume-brutal.test.ts",
|
||||
"tui-resume.test.ts",
|
||||
"tui-slash-extra.test.ts",
|
||||
"tui-terminal-tool.test.ts",
|
||||
"vision-route-fake-gateway.test.ts",
|
||||
|
||||
Reference in new issue
Block a user