File size: 9,147 Bytes
08c07af | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 | import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { runInNewContext } from 'node:vm'
import * as yaml from 'js-yaml'
import { describe, expect, it } from 'vitest'
interface Step {
name?: string
uses?: string
if?: string
run?: string
env?: Record<string, string>
with?: Record<string, unknown>
}
interface CompatibilityJob {
'runs-on': string
if: string
env: Record<string, string>
strategy: { 'fail-fast': boolean; matrix: { include: Array<{ node: string | number; name: string; runner: string; gate_concurrency: string }> } }
steps: Step[]
}
const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci.yml'), 'utf8')) as {
jobs: { 'node-compat': CompatibilityJob; 'python-sdk': { 'runs-on': string } }
}
const job = workflow.jobs['node-compat']
const labels = ['self-hosted', 'linux', 'x64', 'vm-backup']
// This wiring check uses equal-typed, canonical-case fixtures. Actions compares
// strings case-insensitively; JavaScript does not. This is not an Actions evaluator.
function evaluate(expression: string, context: Record<string, unknown>): unknown {
const body = expression.trim().slice(3, -2)
return runInNewContext(body, {
...context, fromJSON: JSON.parse,
}, { timeout: 1000 }) as unknown
}
function route(options: { mode?: string; author?: string; repository?: string; fork?: boolean; actor?: string } = {}): unknown {
return evaluate(job['runs-on'], {
vars: { DSH_CI_FAILOVER_LINUX: options.mode ?? 'selfhosted' },
github: {
repository: 'deepseek-harness/deepseek-harness',
actor: options.actor ?? 'maintainer',
event: { pull_request: {
user: { login: options.author ?? 'maintainer' },
head: { repo: { full_name: options.repository ?? 'deepseek-harness/deepseek-harness', fork: options.fork ?? false } },
} },
},
matrix: { runner: 'ubuntu-latest' },
})
}
describe('Node compatibility self-hosted routing', () => {
it('uses the Linux pool only for opted-in repository-owned PRs', () => {
expect(route()).toEqual(labels)
for (const mode of ['', 'hosted', 'unexpected']) expect(route({ mode })).toBe('ubuntu-latest')
// The blacksmith value routes the compatibility legs onto Blacksmith's
// standard Linux runner regardless of PR ownership (ephemeral runners).
expect(route({ mode: 'blacksmith' })).toBe('blacksmith-4vcpu-ubuntu-2404')
expect(route({ author: 'dependabot[bot]', actor: 'maintainer' })).toBe('ubuntu-latest')
expect(route({ repository: 'outsider/fork', fork: true })).toBe('ubuntu-latest')
expect(route({ repository: 'outsider/fork', fork: false })).toBe('ubuntu-latest')
expect(route({ fork: true })).toBe('ubuntu-latest')
expect(route({ repository: '' })).toBe('ubuntu-latest')
})
it('preserves all three required version jobs and their concurrency', () => {
expect(job.if).toBe("github.event_name == 'pull_request'")
expect(job.strategy['fail-fast']).toBe(false)
expect(job.strategy.matrix.include).toEqual([
{ node: '22.19', name: 'node 22.19', runner: 'ubuntu-latest', gate_concurrency: '1' },
{ node: '24.9', name: 'node 24.9', runner: 'ubuntu-latest', gate_concurrency: '1' },
{ node: 26, name: 'node 26', runner: 'ubuntu-latest', gate_concurrency: '1' },
])
expect(job.env.DSH_GATE_CONCURRENCY).toBe('${{ matrix.gate_concurrency }}')
expect(job.steps.map(step => step.run)).toContain('pnpm run check:node-compat')
expect(job.steps.map(step => step.run)).toContain('pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts')
expect(workflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
})
it('isolates version installs and enables hosted package caching only on hosted runners', () => {
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
expect(setup.env).toEqual({
NODE_OPTIONS: "${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}",
})
expect(setup.with?.['node-version']).toBe('${{ matrix.node }}')
expect(setup.with?.['package-manager-cache']).toBe(false)
for (const [environment, cache] of [['github-hosted', 'pnpm'], ['self-hosted', '']]) {
const context = { runner: { environment, temp: '/runner/temp', tool_cache: '/runner/toolcache' } }
expect(evaluate(setup.with?.cache as string, context)).toBe(cache)
expect(evaluate(setup.env!.NODE_OPTIONS!, context)).toBe(
environment === 'self-hosted' ? '--import=./scripts/ci-compatible-toolcache.mjs' : '',
)
}
expect(job.steps[0]?.with).toEqual({ 'persist-credentials': false })
expect(job.steps.some(step => step.uses?.startsWith('actions/cache/'))).toBe(false)
})
it('overrides runner exports inside setup-node without affecting later Node processes', () => {
const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
const nodeOptions = evaluate(setup.env!.NODE_OPTIONS!, { runner: { environment: 'self-hosted' } }) as string
const root = mkdtempSync(join(tmpdir(), 'ci-compatible preload-'))
try {
const env = { PATH: process.env.PATH, RUNNER_TEMP: root, RUNNER_TOOL_CACHE: join(root, 'persistent') }
const probe = (options: Record<string, string | undefined>) => {
const child = spawnSync(process.execPath, ['-p', 'process.env.RUNNER_TOOL_CACHE'], {
cwd: resolve(import.meta.dirname, '..'), env: options, encoding: 'utf8', timeout: 10_000,
})
expect(child.error).toBeUndefined()
expect(child.signal).toBeNull()
return child
}
const setupChild = probe({ ...env, NODE_OPTIONS: nodeOptions })
expect(setupChild.status, setupChild.stderr).toBe(0)
expect(setupChild.stdout.trim()).toBe(join(root, 'node-compat-toolcache'))
const normalChild = probe(env)
expect(normalChild.status, normalChild.stderr).toBe(0)
expect(normalChild.stdout.trim()).toBe(env.RUNNER_TOOL_CACHE)
const missingTemp = probe({ ...env, RUNNER_TEMP: undefined, NODE_OPTIONS: nodeOptions })
expect(missingTemp.status).not.toBe(0)
expect(missingTemp.stderr).toContain('requires an absolute RUNNER_TEMP')
expect(job.env).not.toHaveProperty('NODE_OPTIONS')
expect(job.steps.filter(step => step.env?.NODE_OPTIONS)).toEqual([setup])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
it.skipIf(process.platform === 'win32')('rejects a Node executable outside its runner temporary installation', () => {
const step = job.steps.find(candidate => candidate.name === 'Verify isolated Node installation')!
expect(step.if).toBe("runner.environment == 'self-hosted'")
for (const [executable, status] of [
['/runner temp/node-compat-toolcache/node/24.9.0/x64/bin/node', 0],
['/shared/toolcache/node/24.9.0/x64/bin/node', 1],
['/runner temp/node-compat-toolcache-other/node', 1],
] as const) {
const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', 'node() { printf "%s" "$TEST_EXECUTABLE"; }; ' + step.run!], {
env: { PATH: process.env.PATH, RUNNER_TEMP: '/runner temp', TEST_EXECUTABLE: executable }, encoding: 'utf8', timeout: 10_000,
})
expect(child.error).toBeUndefined()
expect(child.signal).toBeNull()
expect(child.status, child.stderr).toBe(status)
}
})
it.skipIf(process.platform === 'win32')('configures generated caches before pnpm without changing HOME or global links', () => {
const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches')
const step = job.steps[index]!
expect(index).toBeGreaterThan(0)
expect(index).toBeLessThan(job.steps.findIndex(candidate => candidate.uses === 'pnpm/action-setup@v4'))
expect(step.if).toBe("runner.environment == 'self-hosted'")
const root = mkdtempSync(join(tmpdir(), 'ci-compatible-selfhosted-'))
try {
const outputs = ['runner-a', 'runner-b'].map((runner) => {
const envFile = join(root, runner + '.env')
const temp = join(root, runner)
const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', step.run!], {
env: { PATH: process.env.PATH, HOME: join(root, 'shared home'), RUNNER_TEMP: temp, GITHUB_ENV: envFile },
encoding: 'utf8', timeout: 10_000,
})
expect(child.error).toBeUndefined()
expect(child.signal).toBeNull()
expect(child.status, child.stderr).toBe(0)
const output = readFileSync(envFile, 'utf8')
expect(output).toBe([
'NODE_COMPILE_CACHE=' + temp + '/node-compile-cache',
'npm_config_devdir=' + temp + '/node-gyp',
'PNPM_CONFIG_STORE_DIR=' + join(root, 'shared home') + '/.local/share/pnpm/store',
'',
].join('\n'))
return output
})
expect(outputs[0]).not.toBe(outputs[1])
} finally {
rmSync(root, { recursive: true, force: true })
}
})
})
|