Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
446 changes: 155 additions & 291 deletions .github/workflows/ci.yml

Large diffs are not rendered by default.

70 changes: 70 additions & 0 deletions .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,37 @@ concurrency:
cancel-in-progress: true

jobs:
# One macOS lane for everything the macOS host can prove: command coverage,
# the runner's pure-decision XCTests, the helper, and the replay smoke. All
# of it consumes one runner build, compiled with the unit-test flag so the
# host XCTest run below has a bundle to execute (#1781 A7).
#
# Which tests that build reaches is not a list: it is everything the macOS
# build compiles, and the simulator-only tests keep themselves out with an
# `os(iOS)` guard (see the classification note in RunnerTests.swift). The
# `-skip-testing:` entry is validated by `pnpm check:xctest-selection` for
# the same reason the nightly's is: `RunnerTests/testCommand` is the
# runner's 24-hour server entry point, and a typo re-arms the hang. The
# lane's reporter asserts the executed count equals the reach the check
# derives from source, so a build without the compile flag or a guard that
# compiles a file out reads as red rather than as a smaller green.
#
# Kill criterion: if the host run proves flaky where the simulator run of the
# same tests is not (a platform-branch difference this classification
# missed), gate the offender `os(iOS)` and it returns to the simulator lanes;
# if that happens to more than a handful, the host lane is the wrong tool and
# goes.
smoke-macos:
name: Smoke Tests
runs-on: macos-26
timeout-minutes: 80
env:
AGENT_DEVICE_STATE_DIR: ${{ github.workspace }}/.tmp/agent-device-state
AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH: ${{ github.workspace }}/.tmp/macos-runner-derived
AGENT_DEVICE_XCUITEST_INCLUDE_UNIT_TESTS: '1'
RESULT_BUNDLE_PATH: ${{ github.workspace }}/.tmp/xctest-host/RunnerTests.xcresult
RESULT_SUMMARY_PATH: ${{ github.workspace }}/.tmp/xctest-host/test-results-summary.json
XCTEST_LANE: host
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand All @@ -55,6 +79,52 @@ jobs:
xcuitest-platform: macos
xcuitest-destination: platform=macOS,arch=arm64

- name: Run the host-runnable runner XCTests
run: |
set -euo pipefail
XCTESTRUN_PATH="$(find "$AGENT_DEVICE_IOS_RUNNER_DERIVED_PATH/Build/Products" -maxdepth 1 -name '*.xctestrun' -print -quit)"
test -n "$XCTESTRUN_PATH"
mkdir -p "$(dirname "$RESULT_BUNDLE_PATH")"
xcodebuild test-without-building \
-xctestrun "$XCTESTRUN_PATH" \
-destination 'platform=macOS,arch=arm64' \
-skip-testing:AgentDeviceRunnerUITests/RunnerTests/testCommand \
-resultBundlePath "$RESULT_BUNDLE_PATH"

# Same shape as xctest-nightly.yml: extract is best-effort, the report step asserts.
- name: Extract the test-results summary
if: always()
run: |
set -uo pipefail
[ -d "$RESULT_BUNDLE_PATH" ] || exit 0
xcrun xcresulttool get test-results summary --path "$RESULT_BUNDLE_PATH" --compact \
> "$RESULT_SUMMARY_PATH" 2>/dev/null ||
xcrun xcresulttool get test-results summary --path "$RESULT_BUNDLE_PATH" \
> "$RESULT_SUMMARY_PATH" 2>/dev/null ||
rm -f "$RESULT_SUMMARY_PATH"
exit 0

- name: Report the run and assert it executed the source-derived set
if: always()
run: |
set -euo pipefail
if [ ! -s "$RESULT_SUMMARY_PATH" ]; then
echo 'No usable test-results summary was produced, so nothing here can show a test ran.'
echo 'The suite step above failed before or during the run; read its log first.'
echo '### iOS runner host XCTest lane' >> "$GITHUB_STEP_SUMMARY"
echo 'No result-bundle summary; see the run log and the uploaded bundle.' >> "$GITHUB_STEP_SUMMARY"
exit 1
fi
node --experimental-strip-types scripts/xctest-run-summary.ts

- name: Upload result bundle
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: xctest-host-results-${{ github.run_id }}-${{ github.run_attempt }}
path: .tmp/xctest-host
if-no-files-found: warn

- name: Build macOS helper
uses: ./.github/actions/run-gate
with: { gate: macos-helper }
Expand Down
34 changes: 16 additions & 18 deletions .github/workflows/test-app-build-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,24 +77,22 @@ jobs:
--argjson iosBuild "$IOS_BUILD" \
--arg androidName "$ANDROID_NAME" \
--argjson androidBuild "$ANDROID_BUILD" \
'{
include: [
{
name: "iOS Release",
platform: "ios",
runsOn: (if $iosBuild then "macos-26" else "ubuntu-latest" end),
artifactName: $iosName,
build: $iosBuild
},
{
name: "Android Release",
platform: "android",
runsOn: "ubuntu-latest",
artifactName: $androidName,
build: $androidBuild
}
]
}')"
'[
{
name: "iOS Release",
platform: "ios",
runsOn: (if $iosBuild then "macos-26" else "ubuntu-latest" end),
artifactName: $iosName,
build: $iosBuild
},
{
name: "Android Release",
platform: "android",
runsOn: "ubuntu-latest",
artifactName: $androidName,
build: $androidBuild
}
] | { include: map(select(.build)) }')"
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"

release:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,4 @@ android/ime-helper/dist/
# Workspace package declaration output (tsc -b project references)
packages/*/dist-types/
*.tsbuildinfo
.vitest-reports/
10 changes: 5 additions & 5 deletions scripts/check-affected/checks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export const CHECK_CATALOG: readonly CheckSpec[] = [
gate('android-helpers', 'Android helper builds (snapshot + IME)', 'build:android', false),
gate('macos-helper', 'macOS helper build', 'build:macos-helper', false),
gate('web-smoke', 'Live web platform smoke', 'test:smoke:web', false),
// Needs full history and tags, so it runs in its own fetch-depth: 0 job rather
// than inside the shallow-clone-safe unit lane.
// Needs full history and tags, so it runs in the shared fetch-depth: 0 job
// rather than inside the shallow-clone-safe unit lane.
gate('replay-compat', 'Replay-compat corpus provenance (released blobs)', 'check:replay-compat'),
gate(
'daemon-wire-compat',
Expand All @@ -95,9 +95,9 @@ export const CHECK_CATALOG: readonly CheckSpec[] = [
gate('wire-compat-model', 'Wire-compat rules model', 'check:daemon-wire-compat:test'),
gate('production-exports', 'Production-unused exports', 'check:production-exports'),
gate('bundle-owner-files', 'Bundle owner-file manifest', 'check:bundle-owner-files'),
// Not locally runnable: `@chenglou/freerange`'s bin is `fr.ts`, so the CI job installs Bun
// for it and `pnpm check` never runs this gate. Left default, fail-open would have made
// every `scripts/**`/`.github/**`/`package.json` edit require Bun on the pre-push path.
// Not locally runnable: the audit takes ~90s, more than the pre-push affected
// path should pay on every scripts/** edit. It runs on plain Node; CI needs
// no runtime beyond the default toolchain.
gate('freerange', 'Numeric range audit', 'check:freerange', false),
gate('fixture-cache', 'Trusted fixture-artifact selection', 'test:fixture-cache'),
gate('fixture-fallback', 'Fixture-app cache-failure fallback', 'test:fixture-fallback'),
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-xctest-selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
//
// Three lanes run the `AgentDeviceRunnerUITests` bundle, and each reaches a different set:
//
// - host ci.yml, macOS host, every PR: the whole bundle as compiled for macOS, minus
// - host macos.yml, macOS host, every PR: the whole bundle as compiled for macOS, minus
// `-skip-testing:` — the pure runner-decision tests, whose guard is
// `#if AGENT_DEVICE_RUNNER_UNIT_TESTS` alone.
// - pr ios.yml, iOS Simulator, every PR: the hand-written `-only-testing:` list.
Expand Down Expand Up @@ -49,7 +49,7 @@ const repoRoot = path.resolve(import.meta.dirname, '..');
const packageAppleRunnerScript = path.join(repoRoot, 'scripts/package-apple-runner-source.mjs');

/** The macOS host lane, which runs the whole macOS-compiled bundle on every PR. */
export const HOST_WORKFLOW_FILE = '.github/workflows/ci.yml';
export const HOST_WORKFLOW_FILE = '.github/workflows/macos.yml';

/** The PR lane, whose `-only-testing:` list decides what every pull request runs on the simulator. */
export const PR_WORKFLOW_FILE = '.github/workflows/ios.yml';
Expand Down
25 changes: 20 additions & 5 deletions vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,22 @@ export const SETUP_FILES = [
'src/__tests__/process-memo-setup.ts',
];

// The CI Coverage lane shards the instrumented suite across runners and merges
// the results on one of them (see ci.yml). AGENT_DEVICE_COVERAGE_SHARD="<i>/<n>"
// turns an invocation into shard i of n writing a blob report; both unset means
// the ordinary full run. AGENT_DEVICE_COVERAGE_MERGE=1 aggregates previously
// written blobs instead of collecting tests — it still evaluates thresholds and
// writes every configured coverage report.
const COVERAGE_SHARD = process.env.AGENT_DEVICE_COVERAGE_SHARD;
const COVERAGE_MERGE = process.env.AGENT_DEVICE_COVERAGE_MERGE === '1';

export default defineConfig({
test: {
...(COVERAGE_SHARD ? { shard: COVERAGE_SHARD } : {}),
...(COVERAGE_MERGE ? { mergeReports: '.vitest-reports' } : {}),
outputFile: COVERAGE_SHARD
? { blob: `.vitest-reports/blob-${COVERAGE_SHARD.split('/')[0]}.json` }
: undefined,
// Redirects TMPDIR to one per-run directory for the whole invocation (all
// projects, every worker) and removes it once at the end — see the file
// for why a single global hook beats per-file cleanup here.
Expand All @@ -51,7 +65,9 @@ export default defineConfig({
// assumption without reducing ordinary file-level parallelism.
maxConcurrency: 1,
// Gate reporters for every lane; a `--reporter` flag would replace them, so no lane passes one.
reporters: ['default', slowTestGateReporter()],
// A coverage shard swaps in the blob reporter alongside the default one: its console output is
// only per-shard noise anyway, and the merge run below needs the blobs to exist.
reporters: COVERAGE_SHARD ? ['default', 'blob'] : ['default', slowTestGateReporter()],
projects: [
{
test: {
Expand Down Expand Up @@ -165,10 +181,9 @@ export default defineConfig({
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov', 'json-summary'],
thresholds: {
statements: 78,
lines: 80,
},
// A shard sees roughly half the suite, so its own numbers sit far below
// the gate; thresholds are enforced once on the merged full-suite run.
thresholds: COVERAGE_SHARD ? { statements: 0, lines: 0 } : { statements: 78, lines: 80 },
include: ['src/**/*.ts', 'packages/*/src/**/*.ts'],
exclude: [
'src/**/*.test.ts',
Expand Down
Loading