fix(ci): run macOS Swift checks on a required macOS lane

This commit is contained in:
程序员阿江(Relakkes)
2026-09-07 23:23:32 +08:00
parent 2d2fc1b432
commit bd5689dfad
5 changed files with 159 additions and 2 deletions
+17
View File
@@ -245,6 +245,21 @@ jobs:
- name: Run desktop native checks
run: bun run check:native
macos-swift-checks:
name: macos-swift-checks
needs: scope-plan
if: needs.scope-plan.outputs.desktop_native_checks == 'true'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json
- name: Run macOS Swift checks
run: bun run check:swift
persistence-checks:
name: persistence-checks
needs: scope-plan
@@ -336,6 +351,7 @@ jobs:
- agent-flow-checks
- adapter-checks
- desktop-native-checks
- macos-swift-checks
- persistence-checks
- docs-checks
- coverage-checks
@@ -380,6 +396,7 @@ jobs:
require_selected "agent-flow-checks" "${{ needs.scope-plan.outputs.agent_flow_checks }}" "${{ needs.agent-flow-checks.result }}"
require_selected "adapter-checks" "${{ needs.scope-plan.outputs.adapter_checks }}" "${{ needs.adapter-checks.result }}"
require_selected "desktop-native-checks" "${{ needs.scope-plan.outputs.desktop_native_checks }}" "${{ needs.desktop-native-checks.result }}"
require_selected "macos-swift-checks" "${{ needs.scope-plan.outputs.desktop_native_checks }}" "${{ needs.macos-swift-checks.result }}"
require_selected "persistence-checks" "${{ needs.scope-plan.outputs.persistence_checks }}" "${{ needs.persistence-checks.result }}"
require_selected "docs-checks" "${{ needs.scope-plan.outputs.docs_checks }}" "${{ needs.docs-checks.result }}"
require_selected "coverage-checks" "${{ needs.scope-plan.outputs.coverage_checks }}" "${{ needs.coverage-checks.result }}"
+2 -2
View File
@@ -14,7 +14,7 @@
"perf:local-index:10k": "bun run scripts/perf/local-index-benchmark.ts --sessions 10000 --runs 20",
"check:pr": "bun run scripts/pr/check-pr.ts",
"check:impact": "bun run scripts/pr/impact-report.ts",
"check:policy": "bun test ./scripts/pr/bun-test-filter.test.ts ./scripts/pr/change-policy.test.ts ./scripts/pr/changed-files.test.ts ./scripts/pr/dead-imports.test.ts ./scripts/pr/module-graph.test.ts ./scripts/pr/pr-triage-workflow.test.ts ./scripts/pr/pr-quality-workflow.test.ts ./scripts/pr/release-workflow.test.ts ./scripts/pr/quality-contract.test.ts ./scripts/pr/test-environment.test.ts ./scripts/release-update-metadata.test.ts ./scripts/git-hooks/install.test.ts ./scripts/quality-gate/quarantine.test.ts ./scripts/quality-gate/coverage.test.ts ./scripts/quality-gate/provider-smoke/execute.test.ts ./scripts/quality-gate/desktop-smoke/execute.test.ts ./scripts/quality-gate/providerTargets.test.ts ./scripts/quality-gate/runner.test.ts ./scripts/quality-gate/sandbox.test.ts ./scripts/quality-gate/agent-flow/scenarios.test.ts ./scripts/quality-gate/agent-flow/live.test.ts ./scripts/quality-gate/desktop-smoke/deterministic.test.ts ./scripts/quality-gate/computer-use-live-smoke.test.ts",
"check:policy": "bun test ./scripts/pr/bun-test-filter.test.ts ./scripts/pr/change-policy.test.ts ./scripts/pr/changed-files.test.ts ./scripts/pr/dead-imports.test.ts ./scripts/pr/module-graph.test.ts ./scripts/pr/pr-triage-workflow.test.ts ./scripts/pr/pr-quality-workflow.test.ts ./scripts/pr/release-workflow.test.ts ./scripts/pr/quality-contract.test.ts ./scripts/pr/test-environment.test.ts ./scripts/pr/run-swift-checks.test.ts ./scripts/release-update-metadata.test.ts ./scripts/git-hooks/install.test.ts ./scripts/quality-gate/quarantine.test.ts ./scripts/quality-gate/coverage.test.ts ./scripts/quality-gate/provider-smoke/execute.test.ts ./scripts/quality-gate/desktop-smoke/execute.test.ts ./scripts/quality-gate/providerTargets.test.ts ./scripts/quality-gate/runner.test.ts ./scripts/quality-gate/sandbox.test.ts ./scripts/quality-gate/agent-flow/scenarios.test.ts ./scripts/quality-gate/agent-flow/live.test.ts ./scripts/quality-gate/desktop-smoke/deterministic.test.ts ./scripts/quality-gate/computer-use-live-smoke.test.ts",
"check:server": "bun run scripts/pr/run-server-tests.ts",
"check:provider-contract": "bun run scripts/pr/run-provider-contract-tests.ts",
"check:chat-contract": "bun run scripts/pr/run-chat-contract-tests.ts",
@@ -44,7 +44,7 @@
"docs:build": "npm --prefix site run build",
"docs:preview": "npm --prefix site run preview",
"check:agent-flow:live": "bun run scripts/quality-gate/agent-flow/live-cli.ts",
"check:swift": "swift test --package-path native/cu-helper --enable-xctest",
"check:swift": "bun run scripts/pr/run-swift-checks.ts",
"check:computer-use-live-smoke": "bun run scripts/quality-gate/computer-use-live-smoke.ts"
},
"dependencies": {
+53
View File
@@ -3,6 +3,9 @@ import { readFileSync } from 'node:fs'
import { parse } from 'yaml'
type WorkflowJob = {
if?: string
'runs-on'?: string
'continue-on-error'?: boolean
needs?: string | string[]
steps?: Array<{ name?: string; run?: string; 'working-directory'?: string }>
}
@@ -62,6 +65,7 @@ describe('PR quality workflow', () => {
'agent-flow-checks',
'adapter-checks',
'desktop-native-checks',
'macos-swift-checks',
'persistence-checks',
'docs-checks',
'coverage-checks',
@@ -92,6 +96,55 @@ describe('PR quality workflow', () => {
}
})
test('requires macOS Swift checks alongside the selected Linux native packaging lane', () => {
const jobs = workflowJobs(readFileSync('.github/workflows/pr-quality.yml', 'utf8'))
const packageJson = JSON.parse(readFileSync('package.json', 'utf8')) as { scripts: Record<string, string> }
const macos = jobs['macos-swift-checks']!
const linux = jobs['desktop-native-checks']!
const gate = jobs['pr-quality-gate']!
expect(macos['runs-on']).toBe('macos-latest')
expect(linux['runs-on']).toBe('ubuntu-22.04')
for (const job of [macos, linux]) {
expect(job.needs).toBe('scope-plan')
expect(job.if).toBe("needs.scope-plan.outputs.desktop_native_checks == 'true'")
expect(job['continue-on-error']).not.toBe(true)
}
expect(macos.steps?.some(step => step.run === 'bun run check:swift')).toBe(true)
expect(linux.steps?.some(step => step.run === 'bun run check:native')).toBe(true)
expect(gate.needs).toContain('macos-swift-checks')
expect(gate.needs).toContain('desktop-native-checks')
expect(packageJson.scripts['check:swift']).toBe('bun run scripts/pr/run-swift-checks.ts')
expect(packageJson.scripts['check:policy']).toContain('scripts/pr/run-swift-checks.test.ts')
for (const command of ['check:swift', 'build:sidecars', 'test:compiled-sidecar-smoke', 'check:electron', 'electron:package:dir', 'test:package-smoke:current']) {
expect(packageJson.scripts['check:native']).toContain(`bun run ${command}`)
}
})
test.each([
{ selected: true, macos: 'success', linux: 'success', exit: 0 },
{ selected: true, macos: 'failure', linux: 'success', exit: 1 },
{ selected: true, macos: 'skipped', linux: 'success', exit: 1 },
{ selected: true, macos: 'success', linux: 'failure', exit: 1 },
{ selected: false, macos: 'skipped', linux: 'skipped', exit: 0 },
{ selected: false, macos: 'success', linux: 'skipped', exit: 1 },
])('enforces both native results in the stable gate: %j', scenario => {
const jobs = workflowJobs(readFileSync('.github/workflows/pr-quality.yml', 'utf8'))
const gateScript = jobs['pr-quality-gate']!.steps!.find(step => step.run?.includes('require_selected'))!.run!
const script = gateScript.replace(/\$\{\{\s*([^}]+?)\s*\}\}/g, (_match, expression: string) => {
if (expression === 'needs.scope-plan.outputs.desktop_native_checks') return String(scenario.selected)
if (expression.startsWith('needs.scope-plan.outputs.')) return 'false'
if (expression === 'needs.macos-swift-checks.result') return scenario.macos
if (expression === 'needs.desktop-native-checks.result') return scenario.linux
if (expression === 'needs.scope-plan.result' || expression === 'needs.policy-enforcement.result') return 'success'
return 'skipped'
})
const result = Bun.spawnSync(['bash', '-c', script], {
env: { PATH: process.env.PATH ?? '' },
stdout: 'pipe', stderr: 'pipe',
})
expect(result.exitCode, new TextDecoder().decode(result.stdout)).toBe(scenario.exit)
})
test('keeps coverage artifacts observable in CI', () => {
const workflow = readFileSync('.github/workflows/pr-quality.yml', 'utf8')
+46
View File
@@ -0,0 +1,46 @@
import { describe, expect, mock, test } from 'bun:test'
import { existsSync, writeFileSync } from 'node:fs'
import { isAbsolute, join } from 'node:path'
import { runSwiftChecks } from './run-swift-checks'
describe('Swift platform checks', () => {
test.each(['linux', 'win32'] as const)('does not invoke the macOS-only package on %s', async platform => {
const run = mock(async () => { throw new Error('Swift cannot run on this fixture platform') })
expect(await runSwiftChecks({ platform, run })).toBe(0)
expect(run).not.toHaveBeenCalled()
})
test.each([0, 7])('runs the full macOS package in isolation and propagates exit %s', async exitCode => {
let home = ''
const run = mock(async (command: string[], options: { cwd: string; env: Record<string, string> }) => {
home = options.env.HOME!
expect(command.slice(0, 2)).toEqual(['swift', 'test'])
expect(command).toContain('--enable-xctest')
expect(command[command.indexOf('--package-path') + 1]).toBe(join(options.cwd, 'native/cu-helper'))
expect(command[command.indexOf('--scratch-path') + 1]).toBe(join(home, 'build'))
expect(isAbsolute(options.cwd)).toBe(true)
expect(home).not.toBe(process.env.HOME)
expect(options.env.CLAUDE_CONFIG_DIR).toBe(join(home, '.claude'))
expect(options.env.ANTHROPIC_API_KEY).toBeUndefined()
expect(options.env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
expect(options.env.GH_TOKEN).toBeUndefined()
writeFileSync(join(home, 'cleanup-fixture'), 'disposable Swift test output')
return exitCode
})
expect(await runSwiftChecks({ platform: 'darwin', run })).toBe(exitCode)
expect(run).toHaveBeenCalledTimes(1)
expect(existsSync(home)).toBe(false)
})
test('cleans the sandbox and reports a Swift launch failure', async () => {
let home = ''
await expect(runSwiftChecks({
platform: 'darwin',
run: async (_command, options) => {
home = options.env.HOME!
throw new Error('swift executable unavailable')
},
})).rejects.toThrow('swift executable unavailable')
expect(existsSync(home)).toBe(false)
})
})
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env bun
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { createSandboxedTestEnvironment } from './test-environment'
type SwiftCheckRunner = (
command: string[],
options: { cwd: string; env: Record<string, string> },
) => Promise<number>
export async function runSwiftChecks(options: {
platform?: NodeJS.Platform
run?: SwiftCheckRunner
} = {}): Promise<number> {
const platform = options.platform ?? process.platform
if (platform !== 'darwin') {
console.log(`[swift-checks] not applicable on ${platform}: cu-helper targets macOS; PR CI requires the macOS Swift job separately`)
return 0
}
const root = resolve(import.meta.dir, '../..')
const sandboxHome = mkdtempSync(join(tmpdir(), 'cc-haha-swift-checks-'))
const run = options.run ?? (async (command, spawnOptions) => {
const child = Bun.spawn(command, { ...spawnOptions, stdout: 'inherit', stderr: 'inherit' })
return await child.exited
})
try {
return await run([
'swift', 'test',
'--package-path', join(root, 'native/cu-helper'),
'--scratch-path', join(sandboxHome, 'build'),
'--enable-xctest',
], { cwd: root, env: createSandboxedTestEnvironment(sandboxHome) })
} finally {
rmSync(sandboxHome, { recursive: true, force: true })
}
}
if (import.meta.main) process.exit(await runSwiftChecks())