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
45 changes: 21 additions & 24 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,13 @@ concurrency:
cancel-in-progress: true

jobs:
ios-runner-swift-compat:
name: iOS Runner Swift Compatibility
# Two single-`rg`-assertion jobs (formerly `ios-runner-swift-compat`,
# `no-test-di-seams`, added independently in b79bd8601 / 9eb060406) folded
# into steps here: each was checkout + one grep, paying full job
# scheduling/checkout overhead and its own PR status-check line for what is
# a single assertion. Each step keeps its own failure message. See #1462.
static-checks:
name: Static Checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand All @@ -37,6 +42,13 @@ jobs:
exit 1
fi

- name: Fail if test-only DI seams reappear in production code
run: |
if rg '\?\s*:\s*typeof\s+' src/ --glob '!**/__tests__/**' --glob '!*.test.ts'; then
echo "Found test-only DI seams (optional typeof params) in production code."
exit 1
fi

swift-runner-unit-compile:
name: Swift Runner Unit Compile
runs-on: macos-26
Expand All @@ -57,21 +69,6 @@ jobs:
xcuitest-platform: macos
xcuitest-destination: platform=macOS,arch=arm64

no-test-di-seams:
name: No test-only DI seams
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Fail if test-only DI seams reappear in production code
run: |
if rg '\?\s*:\s*typeof\s+' src/ --glob '!**/__tests__/**' --glob '!*.test.ts'; then
echo "Found test-only DI seams (optional typeof params) in production code."
exit 1
fi

lint:
name: Lint & Format
runs-on: ubuntu-latest
Expand Down Expand Up @@ -110,16 +107,14 @@ jobs:
# Generalizes the former inline commands/-import grep into a structured
# import-direction lint over the resolved graph. See scripts/layering/check.ts
# and CONTEXT.md (Architecture: folder DAG + layering lint).
run: |
node --experimental-strip-types --test scripts/layering/model.test.ts scripts/layering/zone-policy.test.ts
node --experimental-strip-types scripts/layering/check.ts
run: pnpm check:layering

- name: Check the depgraph report agrees with the gate
# scripts/depgraph reads the same model as the gate, so its inversion count must
# reproduce TYPE_INVERSION_BASELINE. Free two-sources check: if the tree changes
# and only one side is updated, this fails and names the difference. Runs here
# rather than in its own job so the two can never be green independently.
run: node --experimental-strip-types --test scripts/depgraph/model.test.ts
run: pnpm depgraph:test

affected-selector:
name: Affected-check Selector
Expand All @@ -135,10 +130,12 @@ jobs:
install-deps: false

# The selector is fail-open and advisory (GitHub CI stays authoritative),
# so the gate only guards the derivation model. Invoked directly with no
# deps, mirroring the layering guard.
# so the gate only guards the derivation model. Invoked with no deps,
# mirroring the layering guard. R8 (scripts/layering/zero-dep-jobs.ts)
# resolves this bare pnpm script name back to package.json's own command
# to find its entry scripts, so this is not a `pnpm install` for the job.
- name: Check affected-selector model
run: node --experimental-strip-types --test scripts/check-affected/model.test.ts scripts/check-affected/run.test.ts
run: pnpm check:affected:test

maestro-conformance:
name: Maestro Conformance Oracle
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/conformance-differential.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ jobs:
# burning its budget) — outcome parity alone cannot see that.
- name: Run differential
run: |
node --experimental-strip-types scripts/maestro-conformance/differential/run.ts \
pnpm maestro:conformance:differential \
--platform ios \
--out-dir "${{ github.workspace }}/.tmp/conformance-differential" \
--trace-root "${{ github.workspace }}/.agent-device" \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/conformance-regenerate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
# Verifies the pinned jar SHA-256s against pinned-upstream.json, runs the
# harness over the corpus, and rewrites fixtures/ + corpus/manifest.json.
- name: Regenerate fixtures from pinned upstream
run: node scripts/maestro-conformance/regenerate.mjs
run: pnpm maestro:conformance:regenerate

- name: Fail if regeneration changed anything
run: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ jobs:
- name: Run Linux replay smoke test
run: |
pnpm clean:daemon
node --experimental-strip-types src/bin.ts test test/integration/replays/linux \
pnpm test:replay:linux \
--retries 2 \
--report-junit test/artifacts/replays-linux.junit.xml

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
run: pnpm build:macos-helper

- name: Run macOS integration test
run: node --experimental-strip-types src/bin.ts test test/integration/replays/macos --retries 2 --report-junit test/artifacts/replays-macos.junit.xml
run: pnpm test:replay:macos --retries 2 --report-junit test/artifacts/replays-macos.junit.xml

- name: Upload macOS artifacts
if: always()
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/perf-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ jobs:

- name: Run iOS command perf benchmark
run: |
node --experimental-strip-types scripts/perf/run.ts \
pnpm perf \
--platform ios \
--device "iPhone 17 Pro" \
--n "$PERF_ROUNDS" --warmup 1 \
Expand Down Expand Up @@ -112,7 +112,7 @@ jobs:
adb -s emulator-5554 shell settings put global window_animation_scale 0 || true;
adb -s emulator-5554 shell settings put global transition_animation_scale 0 || true;
adb -s emulator-5554 shell settings put global animator_duration_scale 0 || true;
node --experimental-strip-types scripts/perf/run.ts --platform android --serial emulator-5554 --n "$PERF_ROUNDS" --warmup 1 --out-dir "$GITHUB_WORKSPACE/perf-results"
pnpm perf:android --serial emulator-5554 --n "$PERF_ROUNDS" --warmup 1 --out-dir "$GITHUB_WORKSPACE/perf-results"

- name: Upload Android perf report
if: always()
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish-mcp-registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ jobs:
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
shell: bash

# This job provisions pnpm via plain actions/setup-node, not the repo's
# ./.github/actions/setup-node-pnpm — so pnpm itself is never installed here,
# and `pnpm check:mcp-metadata` would fail before publishing. Keep the direct
# node invocation; it duplicates check:mcp-metadata's command deliberately.
- name: Check MCP metadata sync
run: node scripts/sync-mcp-metadata.mjs --check

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/replays-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ jobs:
profile: pixel_7
target: google_apis_playstore
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -no-metrics
script: node --experimental-strip-types src/bin.ts test test/integration/replays/android --retries 2 --report-junit test/artifacts/replays-android.junit.xml
script: pnpm test:replay:android --retries 2 --report-junit test/artifacts/replays-android.junit.xml

- name: Upload Android artifacts
if: always()
Expand Down Expand Up @@ -168,7 +168,7 @@ jobs:
pnpm clean:daemon

- name: Run iOS simulator replay suite
run: node --experimental-strip-types src/bin.ts test test/integration/replays/ios/simulator --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --retries 2 --artifacts-dir test/artifacts/replays-ios-simulator --report-junit test/artifacts/replays-ios-simulator.junit.xml
run: pnpm test:replay:ios --udid "${{ steps.ios-simulator.outputs.simulator-udid }}" --retries 2 --artifacts-dir test/artifacts/replays-ios-simulator --report-junit test/artifacts/replays-ios-simulator.junit.xml

- name: Fetch current fixture app
id: fixture-app
Expand All @@ -195,7 +195,7 @@ jobs:
if: env.IOS_UDID != ''
env:
IOS_UDID: ${{ vars.IOS_UDID }}
run: node --experimental-strip-types src/bin.ts test test/integration/replays/ios/device --udid "$IOS_UDID" --retries 2 --artifacts-dir test/artifacts/replays-ios-device --report-junit test/artifacts/replays-ios-device.junit.xml
run: pnpm test:replay:ios-device --udid "$IOS_UDID" --retries 2 --artifacts-dir test/artifacts/replays-ios-device --report-junit test/artifacts/replays-ios-device.junit.xml

- name: Upload iOS artifacts
if: always()
Expand Down Expand Up @@ -232,7 +232,7 @@ jobs:
run: pnpm build:macos-helper

- name: Run macOS replay suite
run: node --experimental-strip-types src/bin.ts test test/integration/replays/macos --retries 2 --report-junit test/artifacts/replays-macos.junit.xml
run: pnpm test:replay:macos --retries 2 --report-junit test/artifacts/replays-macos.junit.xml

- name: Upload macOS artifacts
if: always()
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
git checkout --detach "${{ github.event.pull_request.head.sha }}"
pnpm install --frozen-lockfile
pnpm build
node scripts/size-report.mjs \
pnpm size \
--compare /tmp/agent-device-size-base.json \
--startup-runs 7 \
--json .tmp/size-report.json \
Expand All @@ -84,4 +84,4 @@ jobs:
env:
GITHUB_TOKEN: ${{ github.token }}
GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
run: node scripts/size-report.mjs --post-comment .tmp/size-report.md
run: pnpm size --post-comment .tmp/size-report.md
6 changes: 5 additions & 1 deletion scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,9 +400,13 @@ function checkZeroDepJobs(): Violation[] {
.split('\n')
.filter(Boolean),
);
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')) as {
scripts?: Record<string, string>;
};
const packageScripts = new Map(Object.entries(packageJson.scripts ?? {}));

const violations: Violation[] = [];
for (const job of zeroDepJobs(workflows, fileExists)) {
for (const job of zeroDepJobs(workflows, fileExists, packageScripts)) {
// Fail closed: a zero-dep job whose commands the entry scan cannot recognize would
// otherwise be silently exempt from the rule it is the whole reason for.
if (job.entries.length === 0) {
Expand Down
129 changes: 129 additions & 0 deletions scripts/layering/model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,132 @@ test('a job with no recognizable entry script is reported rather than exempted',
assert.deepEqual(jobs, [{ workflow: 'w.yml', job: 'zero-dep', entries: [] }]);
});

test('a bare pnpm script name resolves through package.json to its entry scripts', () => {
// The workflow names no path at all — only the pnpm script name package.json maps to the
// real command. A zero-dep job may call its script this way (#1462) without R8 losing the
// entries it needs to check: the resolution reads the same paths out of the mapped command.
const workflow = `
name: CI
jobs:
zero-dep:
steps:
- uses: ./.github/actions/setup-node-pnpm
with:
install-deps: false
- run: pnpm check:affected:test
`;
const present = new Set([
'scripts/check-affected/model.test.ts',
'scripts/check-affected/run.test.ts',
]);
const packageScripts = new Map([
[
'check:affected:test',
'node --experimental-strip-types --test scripts/check-affected/model.test.ts scripts/check-affected/run.test.ts',
],
]);
const jobs = zeroDepJobs(
new Map([['w.yml', workflow]]),
(file) => present.has(file),
packageScripts,
);
assert.deepEqual(jobs, [
{
workflow: 'w.yml',
job: 'zero-dep',
entries: ['scripts/check-affected/model.test.ts', 'scripts/check-affected/run.test.ts'],
},
]);
});

test('a resolved script that itself runs a named script is expanded too', () => {
// A chained alias (`outer` runs `pnpm inner`) is one hop further from the workflow text
// than the direct case above. If resolution stopped at one level, inner's entry would be
// invisible to R8 even though the job genuinely depends on it at runtime.
const workflow = `
name: CI
jobs:
zero-dep:
steps:
- uses: ./.github/actions/setup-node-pnpm
with:
install-deps: false
- run: pnpm outer
`;
const present = new Set(['scripts/outer/entry.ts', 'scripts/inner/entry.ts']);
const packageScripts = new Map([
['outer', 'node scripts/outer/entry.ts && pnpm inner'],
['inner', 'node scripts/inner/entry.ts'],
]);
const jobs = zeroDepJobs(
new Map([['w.yml', workflow]]),
(file) => present.has(file),
packageScripts,
);
assert.deepEqual(jobs, [
{
workflow: 'w.yml',
job: 'zero-dep',
entries: ['scripts/inner/entry.ts', 'scripts/outer/entry.ts'],
},
]);
});

test('an alias cycle does not hang, and still collects every non-cyclic entry', () => {
// `a` runs `pnpm b`, `b` runs `pnpm a` back — resolution must stop re-expanding a name it
// has already walked on this chain, not recurse until the stack overflows. Each script's
// own direct entry is still found before the cycle closes.
const workflow = `
name: CI
jobs:
zero-dep:
steps:
- uses: ./.github/actions/setup-node-pnpm
with:
install-deps: false
- run: pnpm a
`;
const present = new Set(['scripts/a/entry.ts', 'scripts/b/entry.ts']);
const packageScripts = new Map([
['a', 'node scripts/a/entry.ts && pnpm b'],
['b', 'node scripts/b/entry.ts && pnpm a'],
]);
const jobs = zeroDepJobs(
new Map([['w.yml', workflow]]),
(file) => present.has(file),
packageScripts,
);
assert.deepEqual(jobs, [
{
workflow: 'w.yml',
job: 'zero-dep',
entries: ['scripts/a/entry.ts', 'scripts/b/entry.ts'],
},
]);
});

test('a pnpm word that names no real package.json script resolves to nothing', () => {
// `pnpm install` (or any other non-script pnpm subcommand) must not be treated as a script
// name just because it follows `pnpm` — it is absent from packageScripts, same as a shell
// word that merely looks like a path is absent from the tree.
const workflow = `
name: CI
jobs:
zero-dep:
steps:
- uses: ./.github/actions/setup-node-pnpm
with:
install-deps: false
- run: pnpm install --frozen-lockfile
`;
const jobs = zeroDepJobs(
new Map([['w.yml', workflow]]),
() => true,
new Map([['check:affected:test', 'node scripts/check-affected/run.test.ts']]),
);
assert.deepEqual(jobs, [{ workflow: 'w.yml', job: 'zero-dep', entries: [] }]);
});

test('a package import anywhere in a zero-dep closure is rejected, builtins are not', () => {
const tree = new Map([
[
Expand Down Expand Up @@ -394,10 +520,13 @@ test("the repo's own zero-dep jobs resolve without node_modules", () => {
: null;
};
const exists = (file: string): boolean => read(file) !== null;
const packageJson = JSON.parse(read('package.json')!) as { scripts?: Record<string, string> };
const packageScripts = new Map(Object.entries(packageJson.scripts ?? {}));

const jobs = zeroDepJobs(
new Map([['.github/workflows/ci.yml', read('.github/workflows/ci.yml')!]]),
exists,
packageScripts,
);
assert.ok(jobs.length > 0, 'expected ci.yml to still declare at least one zero-dep job');
for (const job of jobs) {
Expand Down
Loading
Loading