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
19 changes: 18 additions & 1 deletion .github/workflows/android.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,27 @@ jobs:
- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm

- name: Fetch the PR base for Android smoke selection
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: git fetch origin "$BASE_SHA" --depth=1 || echo "Base fetch failed; Android smoke selection will fail open."

- name: Select Android smoke tests
id: impact
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node --experimental-strip-types scripts/device-lane-impact.ts replay-android android-helpers

- name: Setup Android replay host
if: steps.impact.outputs.run != 'false'
id: android-replay-host
uses: ./.github/actions/setup-android-replay-host
with:
package-helpers: 'true'

- name: Restore fixture APK
if: steps.impact.outputs.run != 'false'
id: fixture-app
uses: ./.github/actions/setup-fixture-app
with:
Expand All @@ -56,12 +70,15 @@ jobs:
wait-for-artifact-seconds: '1800'

- name: Report fixture cache source
if: steps.impact.outputs.run != 'false'
run: echo "Android fixture APK source=${{ steps.fixture-app.outputs.source }}"

- name: Run Android emulator catalog coverage contract
if: steps.impact.outputs.run != 'false'
run: node --experimental-strip-types scripts/node-test-tmpdir.ts --test test/integration/smoke-android-emulator-coverage.test.ts

- name: Run Android smoke checks
if: steps.impact.outputs.run != 'false'
uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b # v2.35.0
with:
api-level: 36
Expand All @@ -79,7 +96,7 @@ jobs:
node --experimental-strip-types src/bin.ts test test/integration/replays/android/emulator/01-settings.ad --retries 2 --report-junit test/artifacts/replays-android-smoke.junit.xml

- name: Upload Android artifacts
if: always()
if: always() && steps.impact.outputs.run != 'false'
uses: ./.github/actions/upload-agent-device-artifacts
with:
artifact-name: android-artifacts
Expand Down
33 changes: 24 additions & 9 deletions .github/workflows/apple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,20 @@ jobs:
shell: bash
run: |
git fetch origin "$BASE_SHA" --depth=1 || echo "Base fetch failed; Apple selection will fail open."
- name: Select iOS smoke tests
id: ios-impact
shell: bash
run: node --experimental-strip-types scripts/apple-ci-impact.ts smoke
- name: Select macOS smoke tests
id: macos-impact
shell: bash
run: node --experimental-strip-types scripts/apple-ci-impact.ts macos
- name: Build the shared CLI
id: build
if: steps.ios-impact.outputs.run != 'false' || steps.macos-impact.outputs.run != 'false'
uses: ./.github/actions/run-gate
with:
gate: build
- name: Select iOS smoke tests
id: ios-impact
shell: bash
run: node --experimental-strip-types scripts/apple-ci-impact.ts smoke
# The macOS clean-install proof may raise permission UI. Run the iOS focus proof first.
- name: iOS smoke tests
id: ios
Expand Down Expand Up @@ -101,7 +106,7 @@ jobs:
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
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.ios-cleanup.outcome == 'success' }}
if: ${{ !cancelled() && steps.build.outcome == 'success' && steps.ios-cleanup.outcome == 'success' && steps.macos-impact.outputs.run != 'false' }}
- name: Diagnose the Apple host after a phase failure
if: ${{ always() && (steps.ios.outcome == 'failure' || steps.macos.outcome == 'failure' || steps.ios-cleanup.outcome == 'failure') }}
continue-on-error: true
Expand All @@ -119,11 +124,12 @@ jobs:
shell: bash
run: |
pnpm clean:daemon
- name: Require both Apple phases to succeed
- name: Require the selected Apple phases to succeed
if: always()
env:
IOS_SELECTED: ${{ steps.ios-impact.outputs.run }}
IOS_OUTCOME: ${{ steps.ios.outcome }}
MACOS_SELECTED: ${{ steps.macos-impact.outputs.run }}
MACOS_OUTCOME: ${{ steps.macos.outcome }}
IOS_CLEANUP_OUTCOME: ${{ steps.ios-cleanup.outcome }}
MACOS_CLEANUP_OUTCOME: ${{ steps.macos-cleanup.outcome }}
Expand All @@ -136,9 +142,18 @@ jobs:
else
test "$IOS_OUTCOME" = success
fi
test "$MACOS_OUTCOME" = success
test "$IOS_CLEANUP_OUTCOME" = success
test "$MACOS_CLEANUP_OUTCOME" = success
if [ "$MACOS_SELECTED" = false ]; then
test "$MACOS_OUTCOME" = skipped
else
test "$MACOS_OUTCOME" = success
fi
if [ "$IOS_SELECTED" = false ] && [ "$MACOS_SELECTED" = false ]; then
test "$IOS_CLEANUP_OUTCOME" = skipped
test "$MACOS_CLEANUP_OUTCOME" = skipped
else
test "$IOS_CLEANUP_OUTCOME" = success
test "$MACOS_CLEANUP_OUTCOME" = success
fi
- name: Upload Apple artifacts
if: always()
uses: ./.github/actions/upload-agent-device-artifacts
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -395,21 +395,35 @@ jobs:
uses: ./.github/actions/run-gate
with: { gate: fixture-cache }

- name: Fetch the PR base for web smoke selection
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: git fetch origin "$BASE_SHA" --depth=1 || echo "Base fetch failed; web smoke selection will fail open."

- name: Select web smoke tests
id: web-impact
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node --experimental-strip-types scripts/device-lane-impact.ts web-smoke

# Shares this job's ubuntu toolchain. AGENT_DEVICE_WEB_E2E is step-scoped
# so it cannot leak into the node/provider suites above.
- name: Run live web smoke
if: steps.web-impact.outputs.run != 'false'
env:
AGENT_DEVICE_WEB_E2E: '1'
run: pnpm clean:daemon

- name: Execute live web smoke
if: steps.web-impact.outputs.run != 'false'
env:
AGENT_DEVICE_WEB_E2E: '1'
uses: ./.github/actions/run-gate
with: { gate: web-smoke }

- name: Upload web smoke artifacts
if: always()
if: always() && steps.web-impact.outputs.run != 'false'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: web-smoke-artifacts
Expand Down
28 changes: 24 additions & 4 deletions .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ jobs:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm

- name: Fetch the PR base for Linux smoke selection
if: github.event_name == 'pull_request'
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: git fetch origin "$BASE_SHA" --depth=1 || echo "Base fetch failed; Linux smoke selection will fail open."

- name: Select Linux smoke tests
id: impact
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: node --experimental-strip-types scripts/device-lane-impact.ts replay-linux linux-command-evidence

# Unbounded, this step could not fail — it could only stall. On 2026-08-19 a slow mirror
# held apt past the job's 30-minute budget on four main-branch runs and several unrelated
# PRs, which reported as a red check on branches that never ran a line of project code.
Expand All @@ -54,6 +69,7 @@ jobs:
# that drips bytes slowly enough to defeat the socket timeout. A healthy install takes
# about a minute, so six is generous.
- name: Install Linux desktop dependencies
if: steps.impact.outputs.run != 'false'
timeout-minutes: 6
run: |
set -euo pipefail
Expand All @@ -77,10 +93,8 @@ jobs:
gnome-calculator \
wmctrl

- name: Setup toolchain
uses: ./.github/actions/setup-node-pnpm

- name: Start Xvfb and D-Bus
if: steps.impact.outputs.run != 'false'
run: |
# Start virtual framebuffer (1280x1024, 24-bit color)
Xvfb :99 -screen 0 1280x1024x24 &
Expand All @@ -93,6 +107,7 @@ jobs:
echo "DBUS_SESSION_BUS_PID=$DBUS_SESSION_BUS_PID" >> "$GITHUB_ENV"

- name: Start AT-SPI2 registry
if: steps.impact.outputs.run != 'false'
run: |
# The registry must start AFTER DBUS_SESSION_BUS_ADDRESS is available
# (it was written to GITHUB_ENV in the previous step).
Expand All @@ -112,6 +127,7 @@ jobs:
fi

- name: Verify environment
if: steps.impact.outputs.run != 'false'
run: |
echo "=== Display ==="
xdotool getdisplaygeometry
Expand All @@ -125,9 +141,11 @@ jobs:
xdotool version

- name: Run Linux replay smoke test
if: steps.impact.outputs.run != 'false'
run: pnpm clean:daemon

- name: Execute Linux replay smoke test
if: steps.impact.outputs.run != 'false'
uses: ./.github/actions/run-gate
with:
gate: replay-linux
Expand All @@ -138,15 +156,17 @@ jobs:
test/artifacts/replays-linux.junit.xml

- name: Reset daemon before Linux command evidence
if: steps.impact.outputs.run != 'false'
run: pnpm clean:daemon

- name: Execute Linux command evidence
if: steps.impact.outputs.run != 'false'
uses: ./.github/actions/run-gate
with:
gate: linux-command-evidence

- name: Upload Linux artifacts
if: always()
if: always() && steps.impact.outputs.run != 'false'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: linux-artifacts
Expand Down
59 changes: 57 additions & 2 deletions scripts/__tests__/apple-ci-impact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import path from 'node:path';
import { expect, test } from 'vitest';
import { parse } from 'yaml';
import { mkdtempForTestSync } from '@agent-device/test-kit';
import { selectAppleBridgeProof, selectIosSmoke, selectIosXctests } from '../apple-ci-impact.ts';
import {
selectAppleBridgeProof,
selectIosSmoke,
selectIosXctests,
selectMacosSmoke,
} from '../apple-ci-impact.ts';
import { selectChecks } from '../check-affected/model.ts';

const repoRoot = path.resolve(import.meta.dirname, '../..');
Expand Down Expand Up @@ -226,12 +231,32 @@ test.each([
['macOS silently skipped', { MACOS_OUTCOME: 'skipped' }, 1],
['iOS cleanup fails', { IOS_CLEANUP_OUTCOME: 'failure' }, 1],
['macOS cleanup fails', { MACOS_CLEANUP_OUTCOME: 'failure' }, 1],
['macOS not selected', { MACOS_SELECTED: 'false', MACOS_OUTCOME: 'skipped' }, 0],
['unselected macOS unexpectedly ran', { MACOS_SELECTED: 'false' }, 1],
[
'no phase selected',
{
IOS_SELECTED: 'false',
IOS_OUTCOME: 'skipped',
MACOS_SELECTED: 'false',
MACOS_OUTCOME: 'skipped',
IOS_CLEANUP_OUTCOME: 'skipped',
MACOS_CLEANUP_OUTCOME: 'skipped',
},
0,
],
[
'cleanup skipped while a phase ran',
{ IOS_SELECTED: 'false', IOS_OUTCOME: 'skipped', MACOS_CLEANUP_OUTCOME: 'skipped' },
1,
],
] as const)('the final Apple status guard preserves failure: %s', (_label, overrides, expected) => {
const guard = appleJob().steps.find((step) => step.env?.IOS_OUTCOME)!;
expect(guard.if).toBe('always()');
expect(guard.env).toEqual({
IOS_SELECTED: '${{ steps.ios-impact.outputs.run }}',
IOS_OUTCOME: '${{ steps.ios.outcome }}',
MACOS_SELECTED: '${{ steps.macos-impact.outputs.run }}',
MACOS_OUTCOME: '${{ steps.macos.outcome }}',
IOS_CLEANUP_OUTCOME: '${{ steps.ios-cleanup.outcome }}',
MACOS_CLEANUP_OUTCOME: '${{ steps.macos-cleanup.outcome }}',
Expand All @@ -241,6 +266,7 @@ test.each([
...process.env,
IOS_SELECTED: 'true',
IOS_OUTCOME: 'success',
MACOS_SELECTED: 'true',
MACOS_OUTCOME: 'success',
IOS_CLEANUP_OUTCOME: 'success',
MACOS_CLEANUP_OUTCOME: 'success',
Expand Down Expand Up @@ -280,6 +306,35 @@ test('iOS smoke uses affected-check ownership and fails open on uncertain diffs'
expect(selectIosSmoke('push', ['src/foo.test.ts']).run).toBe(true);
});

test('macOS smoke uses affected-check ownership and keeps the bridge proof inputs', () => {
const steps = appleJob().steps;
const select = steps.find((step) => step.id === 'macos-impact')!;
const build = steps.find((step) => step.id === 'build')!;
const macos = steps.find((step) => step.id === 'macos')!;
expect(select.run).toContain('scripts/apple-ci-impact.ts macos');
expect(steps.indexOf(select)).toBeLessThan(steps.indexOf(build));
expect(build.if).toContain("steps.macos-impact.outputs.run != 'false'");
expect(macos.if).toContain("steps.macos-impact.outputs.run != 'false'");
for (const file of [
'src/foo.test.ts',
'packages/daemon/src/sessions/__tests__/session.test.ts',
'packages/platform-android/src/index.ts',
'test/integration/replays/ios/basic.ad',
]) {
expect(selectMacosSmoke('pull_request', [file]).run, file).toBe(false);
}
for (const file of [
'packages/platform-apple/src/foldable/fold-helper-cache.test.ts',
'packages/platform-apple/src/index.ts',
'src/index.ts',
'.github/actions/smoke-macos/action.yml',
]) {
expect(selectMacosSmoke('pull_request', [file]).run, file).toBe(true);
}
expect(selectMacosSmoke('pull_request', null).run).toBe(true);
expect(selectMacosSmoke('push', ['src/foo.test.ts']).run).toBe(true);
});

function cacheInputs(action: string, stepId = 'source-hash'): string[] {
const doc = parse(action) as { runs?: { steps?: Array<{ id?: string; run?: string }> } };
const hashStep = doc.runs?.steps?.find((step) => step.id === stepId)?.run ?? '';
Expand Down Expand Up @@ -542,7 +597,7 @@ test('a shallow PR merge still yields a known change set for all Apple selectors
);
expect(select('xctest')).toContain('skip;');
expect(select('bridge')).toContain('skip;');
expect(select('smoke')).toContain('run; iOS device-lane input changed: src/feature.ts');
expect(select('smoke')).toContain('run; replay-ios input changed: src/feature.ts');
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
Expand Down
Loading
Loading