ci: avoid mobile runs for desktop test-only changes (#29138)

This commit is contained in:
Neil
2026-10-15 04:35:45 -07:00
committed by GitHub
parent da8de743eb
commit deeadb1ab6
3 changed files with 122 additions and 4 deletions
+22
View File
@@ -24,6 +24,7 @@ on:
- 'src/main/runtime/orca-runtime.ts'
- 'src/main/runtime/agent-session-*.ts'
- 'src/main/native-chat/agent-session-wire/**'
- 'src/main/runtime/structured-attention-original-delivery.test-fixture.ts'
# Why: this job holds the only checks that load the Fastfile, so edits to
# it or to the release workflow it guards must re-run them.
- '.github/workflows/mobile.yml'
@@ -38,6 +39,16 @@ on:
- 'config/scripts/pr-code-change-scope.mjs'
- 'config/scripts/pr-package-test-paths.mjs'
- 'config/scripts/pr-layout-oracle-route.mjs'
# Desktop suites do not feed mobile; keep fixtures and test/toolchain config inputs.
- '!src/shared/**/*.test.[jt]s'
- '!src/shared/**/*.test.[jt]sx'
- '!src/shared/**/*.test.[cm][jt]s'
- '!src/packages/process-host/**/*.test.[jt]s'
- '!src/packages/process-host/**/*.test.[jt]sx'
- '!src/packages/process-host/**/*.test.[cm][jt]s'
- '!src/main/**/*.test.[jt]s'
- '!src/main/**/*.test.[jt]sx'
- '!src/main/**/*.test.[cm][jt]s'
# Why main too: two pull requests can each pass against their own base and disagree once both
# land, and `verify` never runs on main. The same source paths as above, less the CI inputs only
# `verify` reads.
@@ -57,10 +68,21 @@ on:
- 'src/main/runtime/orca-runtime.ts'
- 'src/main/runtime/agent-session-*.ts'
- 'src/main/native-chat/agent-session-wire/**'
- 'src/main/runtime/structured-attention-original-delivery.test-fixture.ts'
- '.github/workflows/mobile.yml'
- 'config/mobile-ci.vitest.config.mjs'
- 'config/scripts/run-mobile-ci-tests.mjs'
- 'config/.bun-version'
# Desktop suites do not feed mobile; keep fixtures and test/toolchain config inputs.
- '!src/shared/**/*.test.[jt]s'
- '!src/shared/**/*.test.[jt]sx'
- '!src/shared/**/*.test.[cm][jt]s'
- '!src/packages/process-host/**/*.test.[jt]s'
- '!src/packages/process-host/**/*.test.[jt]sx'
- '!src/packages/process-host/**/*.test.[cm][jt]s'
- '!src/main/**/*.test.[jt]s'
- '!src/main/**/*.test.[jt]sx'
- '!src/main/**/*.test.[cm][jt]s'
concurrency:
# Per commit on main, not per branch. GitHub cancels any PENDING run in a group when a new one
@@ -15,6 +15,23 @@ const NON_TEST_FILES = new Set([
'config/scripts/mobile-release-check-scope.test.mjs'
])
const DESKTOP_SUITE_PREFIXES = [
'src/shared/',
'src/packages/process-host/',
'src/main/agent-launch/',
'src/main/runtime/rpc/',
'src/main/runtime/runtime-rpc/',
'src/main/runtime/agent-session-',
'src/main/native-chat/agent-session-wire/'
]
function isDesktopSuite(file) {
return (
DESKTOP_SUITE_PREFIXES.some((prefix) => file.startsWith(prefix)) &&
/\.test\.(?:[cm]?[jt]s|[jt]sx)$/.test(file)
)
}
export function shouldRunMobileTests(files) {
return (
files.length === 0 ||
@@ -23,6 +40,7 @@ export function shouldRunMobileTests(files) {
file.split('/').includes('..') ||
(!isDocsOnlyPath(file) &&
!NON_TEST_FILES.has(file) &&
!isDesktopSuite(file) &&
!file.startsWith('mobile/docs/') &&
!file.startsWith('mobile/fastlane/'))
)
@@ -4,6 +4,15 @@ import { parse } from 'yaml'
import { expect, it } from 'vitest'
import { shouldRunMobileTests } from './mobile-test-change-scope.mjs'
function workflowIncludes(paths, files) {
return files.some((file) =>
paths.reduce((included, pattern) => {
const excluded = pattern.startsWith('!')
return matchesGlob(file, excluded ? pattern.slice(1) : pattern) ? !excluded : included
}, false)
)
}
it.each([
'docs/ci.md',
'mobile/docs/release.md',
@@ -54,16 +63,85 @@ it('runs Mobile Checks for shared modules, process-host tooling and the root loc
'src/shared/any/module.ts',
'src/packages/process-host/src/run-process.ts',
'src/packages/process-host/package.json',
'src/main/runtime/structured-attention-original-delivery.test-fixture.ts',
'pnpm-lock.yaml'
]) {
expect(paths.some((pattern) => matchesGlob(file, pattern))).toBe(true)
expect(workflowIncludes(paths, [file])).toBe(true)
}
expect(
paths.some((pattern) => matchesGlob('src/packages/unrelated/src/index.ts', pattern))
).toBe(false)
expect(workflowIncludes(paths, ['src/packages/unrelated/src/index.ts'])).toBe(false)
}
})
it('honors ordered exclusions and later re-inclusion when matching workflow paths', () => {
const paths = ['src/**', '!src/**/*.test.ts', 'src/retained.test.ts']
expect(workflowIncludes(paths, ['src/removed.test.ts'])).toBe(false)
expect(workflowIncludes(paths, ['src/retained.test.ts'])).toBe(true)
})
it.each([
'src/shared/model-id-label.test.ts',
'src/shared/nested/view.test.tsx',
'src/shared/nested/module.test.js',
'src/shared/nested/view.test.jsx',
'src/shared/nested/module.test.cjs',
'src/shared/nested/module.test.mjs',
'src/shared/nested/module.test.cts',
'src/shared/nested/module.test.mts',
'src/packages/process-host/src/run-process.test.ts',
'src/main/agent-launch/dispatch.test.ts',
'src/main/runtime/rpc/dispatcher.test.ts',
'src/main/runtime/runtime-rpc/server.test.ts',
'src/main/runtime/agent-session-store.test.ts',
'src/main/native-chat/agent-session-wire/history.test.ts'
])('skips desktop suites on PRs and main, including removal: %s', (file) => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8')
)
expect(shouldRunMobileTests([file])).toBe(false)
expect(shouldRunMobileTests([file, 'mobile/docs/ci.md'])).toBe(false)
for (const paths of [workflow.on.pull_request.paths, workflow.on.push.paths]) {
expect(workflowIncludes(paths, [file])).toBe(false)
expect(workflowIncludes(paths, [file, 'src/shared/changed.ts'])).toBe(true)
expect(workflowIncludes(paths, [file, 'mobile/src/changed.test.ts'])).toBe(true)
// A rename is represented by the removed and added names, so moving a suite into source runs.
expect(workflowIncludes(paths, [file, file.replace('.test.', '.')])).toBe(true)
}
expect(shouldRunMobileTests([file, 'src/shared/changed.ts'])).toBe(true)
expect(shouldRunMobileTests([file, file.replace('.test.', '.')])).toBe(true)
})
it.each([
'mobile/src/changed.test.ts',
'mobile/src/changed.test.tsx',
'src/shared/project-section-parity.test-fixture.ts',
'src/shared/fixtures/recording.json',
'src/main/runtime/rpc/methods/agent-launch.test-fixture.ts',
'src/main/runtime/agent-session-record-store-test-harness.ts',
'src/packages/process-host/tsconfig.test.json',
'src/packages/process-host/vitest.config.ts',
'src/packages/process-host/package.json',
'pnpm-lock.yaml',
'mobile/pnpm-lock.yaml',
'config/mobile-ci.vitest.config.mjs',
'config/scripts/run-mobile-ci-tests.mjs',
'config/.bun-version',
'.github/workflows/mobile.yml'
])('retains mobile suites, shared fixtures and toolchain inputs: %s', (file) => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8')
)
expect(shouldRunMobileTests([file])).toBe(true)
for (const paths of [workflow.on.pull_request.paths, workflow.on.push.paths]) {
expect(workflowIncludes(paths, [file])).toBe(true)
}
})
it('retains unknown suite paths and noncanonical traversal evidence', () => {
expect(shouldRunMobileTests(['unknown/module.test.ts'])).toBe(true)
expect(shouldRunMobileTests(['src/unknown/module.test.ts'])).toBe(true)
expect(shouldRunMobileTests(['src/shared/../main/module.test.ts'])).toBe(true)
})
it('skips only tests, after successful detection, and retains all other mobile gates', () => {
const workflow = parse(
readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8')