mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
ci: move PR analysis and Linux unit tests to Blacksmith (#29362)
* ci: report final PR checks outside the shared runner queue * ci: fix inherited mobile test file size failure * ci: move PR analysis and ARM test shards to Blacksmith * Qualify x64 Blacksmith capacity and reject incompatible runner images early * Keep extracted mobile test adapters within the test-only lint scope * Identify the runner profile overriding shell fixture homes * Preserve fixture HOME through the Blacksmith login profile * Make cache invalidation fixtures observable within one filesystem clock tick * Scope Blacksmith system-profile preparation to Linux runners * Wait for observable ctime in daemon hash cache fixtures * Use the mobile test lint scope already merged on main * Stabilize metadata and timer fixtures on fast CI runners --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -7,12 +7,16 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
warm-linux-arm:
|
||||
warm-linux-runtime:
|
||||
if: github.event_name != 'pull_request'
|
||||
concurrency:
|
||||
group: ci-cache-warmup-linux-arm-${{ github.event.pull_request.number || github.ref }}
|
||||
group: ci-cache-warmup-linux-runtime-${{ matrix.runner }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name != 'schedule' }}
|
||||
runs-on: ubuntu-24.04-arm
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runner: [ubuntu-24.04-arm, blacksmith-4vcpu-ubuntu-2404]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
ORCA_BACKGROUND_LAUNCH: '1'
|
||||
|
||||
@@ -25,8 +25,8 @@ jobs:
|
||||
# inputs are unchanged; empty diffs fail closed and run everything.
|
||||
code_paths:
|
||||
name: detect changes and check repository guards
|
||||
# Reuse one lightweight checkout for detection and the always-required guards.
|
||||
runs-on: ubuntu-slim
|
||||
# Start routing without waiting behind the GitHub-hosted test queue.
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -175,11 +175,8 @@ jobs:
|
||||
name: static analysis and typecheck
|
||||
needs: [code_paths]
|
||||
if: needs.code_paths.outputs.static_analysis == 'true' || needs.code_paths.outputs.typecheck == 'true'
|
||||
# Why ARM: measured 128s against 172s on ubuntu-latest, with every compute step faster --
|
||||
# type-aware 24s->15s, anti-slop 28->19s, localization extraction 67->46s, the orcad smoke
|
||||
# 39->14s. Both lint engines ship linux-arm64 and the Bun target follows process.arch, so
|
||||
# the whole toolchain resolves. Free for public repositories.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
# Leave headroom for typechecking and overlapping checks.
|
||||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
outputs:
|
||||
shards: ${{ steps.unit-plan.outputs.shards }}
|
||||
managed_hook_node18: ${{ steps.managed_hook_node18.outcome }}
|
||||
@@ -896,7 +893,7 @@ jobs:
|
||||
uses: ./.github/workflows/unit-tests.yml
|
||||
with:
|
||||
node_versions: '["24"]'
|
||||
runner: ubuntu-24.04-arm
|
||||
runner: blacksmith-4vcpu-ubuntu-2404
|
||||
shards: ${{ needs.preflight.outputs.shards }}
|
||||
run_shards: ${{ needs.code_paths.outputs.mobile_ci_tools != 'true' }}
|
||||
browser_contract: ${{ needs.code_paths.outputs.orcad_browser == 'true' }}
|
||||
|
||||
@@ -91,6 +91,36 @@ jobs:
|
||||
- name: Install Electron package binary for tests
|
||||
run: node config/scripts/install-electron-package-binary.mjs
|
||||
|
||||
# The image resets HOME during login; shell fixtures intentionally use temporary homes.
|
||||
- name: Preserve explicit login-shell homes on Blacksmith
|
||||
if: runner.os == 'Linux' && startsWith(inputs.runner, 'blacksmith-')
|
||||
run: |
|
||||
sudo python3 - <<'PYTHON'
|
||||
from pathlib import Path
|
||||
profile = Path('/etc/profile.d/blacksmith.sh')
|
||||
if profile.exists():
|
||||
original = profile.read_text()
|
||||
updated = original.replace('export HOME="$RUNNER_HOME"', 'export HOME="${HOME:-$RUNNER_HOME}"')
|
||||
if updated != original:
|
||||
profile.write_text(updated)
|
||||
PYTHON
|
||||
|
||||
- name: Verify Blacksmith unit runner environment
|
||||
if: runner.os == 'Linux' && startsWith(inputs.runner, 'blacksmith-')
|
||||
run: |
|
||||
ELECTRON_RUN_AS_NODE=1 "$(node -p "require('electron')")" --version
|
||||
fixture_home="$(mktemp -d)"
|
||||
trap 'rm -rf "$fixture_home"' EXIT
|
||||
printf 'export ORCA_CI_PROFILE_LOADED=1\n' > "$fixture_home/.bash_profile"
|
||||
HOME="$fixture_home" ORCA_CI_FIXTURE_HOME="$fixture_home" /bin/bash -lc '
|
||||
test "$HOME" = "$ORCA_CI_FIXTURE_HOME" && test "$ORCA_CI_PROFILE_LOADED" = 1
|
||||
' || {
|
||||
echo '::error::Runner login profile overrides fixture HOME'
|
||||
grep -nH 'HOME' /etc/profile /etc/profile.d/*.sh || true
|
||||
HOME="$fixture_home" /bin/bash -lc 'printf "Observed HOME=%s\n" "$HOME"'
|
||||
exit 1
|
||||
}
|
||||
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
|
||||
with:
|
||||
bun-version-file: config/.bun-version
|
||||
@@ -145,7 +175,7 @@ jobs:
|
||||
export ORCA_SHARD_SOURCE_SHA
|
||||
node config/scripts/run-vitest.mjs run --config config/vitest.config.ts \
|
||||
--shard=${{ matrix.shard.index }}/${{ matrix.shard.count }} \
|
||||
${{ inputs.runner == 'ubuntu-24.04-arm' && '--maxWorkers=4' || '' }}
|
||||
${{ (runner.arch == 'ARM64' || inputs.runner == 'blacksmith-4vcpu-ubuntu-2404') && '--maxWorkers=4' || '' }}
|
||||
|
||||
- name: Seal unit module cache
|
||||
id: module-cache-seal
|
||||
|
||||
@@ -295,7 +295,7 @@ describe('CI dependency download caches', () => {
|
||||
expect(warmer.with['cache-pnpm-store']).toBe('false')
|
||||
expect(warmer.with['native-runtime']).toBe('node')
|
||||
expect(persistence.with['cache-pnpm-store-lookup-only']).toBe('true')
|
||||
for (const job of [workflow('ci-cache-warmup').jobs.warm, platformJobs['warm-linux-arm']]) {
|
||||
for (const job of [workflow('ci-cache-warmup').jobs.warm, platformJobs['warm-linux-runtime']]) {
|
||||
const install = job.steps.find((step) => step.uses?.includes('install-node-dependencies'))
|
||||
expect(install.with['cache-pnpm-store-lookup-only']).toBe('true')
|
||||
}
|
||||
@@ -346,7 +346,7 @@ describe('CI dependency download caches', () => {
|
||||
const shared = workflow('ci-cache-warmup')
|
||||
const platforms = read(workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses)
|
||||
const context = { github: { event_name: event } }
|
||||
expect(runInNewContext(platforms.jobs['warm-linux-arm'].if, context)).toBe(armRuns)
|
||||
expect(runInNewContext(platforms.jobs['warm-linux-runtime'].if, context)).toBe(armRuns)
|
||||
expect(runInNewContext(shared.jobs['warm-platforms'].if, context)).toBe(sharedPlatforms)
|
||||
expect(shared.jobs.warm.if).toBeUndefined()
|
||||
expect(platforms.jobs['warm-windows'].if).toBeUndefined()
|
||||
@@ -356,17 +356,18 @@ describe('CI dependency download caches', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('validates the omitted ARM warmer preparation in the real PR consumers', () => {
|
||||
it('validates the omitted Linux warmer preparation in the real PR consumers', () => {
|
||||
const platforms = read(workflow('ci-cache-warmup-platforms').jobs['warm-platforms'].uses)
|
||||
const warm = platforms.jobs['warm-linux-arm']
|
||||
const warm = platforms.jobs['warm-linux-runtime']
|
||||
const pr = workflow('pr')
|
||||
const units = workflow('unit-tests').jobs.test
|
||||
const installer = './.github/actions/install-node-dependencies'
|
||||
const warmInstall = warm.steps.find((step) => step.uses === installer)
|
||||
const unitInstall = units.steps.find((step) => step.uses === installer)
|
||||
expect(warm['runs-on']).toBe('ubuntu-24.04-arm')
|
||||
expect(pr.jobs.preflight['runs-on']).toBe(warm['runs-on'])
|
||||
expect(pr.jobs.test.with.runner).toBe(warm['runs-on'])
|
||||
expect(warm['runs-on']).toBe('${{ matrix.runner }}')
|
||||
expect(warm.strategy.matrix.runner).toEqual(['ubuntu-24.04-arm', pr.jobs.test.with.runner])
|
||||
expect(pr.jobs.preflight['runs-on']).toBe('blacksmith-8vcpu-ubuntu-2404')
|
||||
expect(pr.jobs.test.with.runner).toBe('blacksmith-4vcpu-ubuntu-2404')
|
||||
expect(units['runs-on']).toBe('${{ inputs.runner }}')
|
||||
expect(JSON.parse(pr.jobs.test.with.node_versions)).toEqual(['24'])
|
||||
expect(warmInstall.if).toBeUndefined()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import {
|
||||
@@ -146,6 +147,26 @@ it('uses an optional exact restore after install and saves successful scoped pro
|
||||
expect(workflow.on.workflow_call.inputs.run_relay_integration.default).toBe(true)
|
||||
})
|
||||
|
||||
it('retains four workers on ARM and the four-core Blacksmith unit runner', () => {
|
||||
const workflow = parse(readFileSync('.github/workflows/unit-tests.yml', 'utf8'))
|
||||
const step = workflow.jobs.test.steps.find((step) => step.name === 'Test shard')
|
||||
const expression = [...step.run.matchAll(/\$\{\{(.+?)\}\}/g)].find((match) =>
|
||||
match[1].includes('--maxWorkers')
|
||||
)[1]
|
||||
for (const [arch, runner] of [
|
||||
['ARM64', 'ubuntu-24.04-arm'],
|
||||
['ARM64', 'blacksmith-4vcpu-ubuntu-2404-arm'],
|
||||
['X64', 'blacksmith-4vcpu-ubuntu-2404']
|
||||
]) {
|
||||
expect(runInNewContext(expression, { runner: { arch }, inputs: { runner } })).toBe(
|
||||
'--maxWorkers=4'
|
||||
)
|
||||
}
|
||||
expect(
|
||||
runInNewContext(expression, { runner: { arch: 'X64' }, inputs: { runner: 'ubuntu-latest' } })
|
||||
).toBe('')
|
||||
})
|
||||
|
||||
it('runs full same-source controls serially with five four-worker shards and included producer costs', () => {
|
||||
const workflow = parse(readFileSync('.github/workflows/unit-module-cache-benchmark.yml', 'utf8'))
|
||||
const { baseline, producer, warm } = workflow.jobs
|
||||
|
||||
@@ -16,7 +16,7 @@ it('shares one setup and overlaps the compiler only after type-aware checks fini
|
||||
expect(workflow.jobs.static_analysis).toBeUndefined()
|
||||
expect(workflow.jobs.typecheck).toBeUndefined()
|
||||
expect(preflight.needs).toEqual(['code_paths'])
|
||||
expect(preflight['runs-on']).toBe('ubuntu-24.04-arm')
|
||||
expect(preflight['runs-on']).toBe('blacksmith-8vcpu-ubuntu-2404')
|
||||
const installs = steps.filter(
|
||||
(step) => step.uses === './.github/actions/install-node-dependencies'
|
||||
)
|
||||
|
||||
@@ -185,12 +185,16 @@ describe('cursor chat meta', () => {
|
||||
it('re-indexes after a chat appears under an already indexed workspace', async () => {
|
||||
const cursorHome = await createCursorHome()
|
||||
await writeChatMeta(cursorHome, 'workspace-hash', 'chat-first')
|
||||
const workspacePath = join(cursorHome, 'chats', 'workspace-hash')
|
||||
// The directory update must be visible even within one filesystem clock tick.
|
||||
await utimes(workspacePath, 1, 1)
|
||||
const firstTranscript = await writeTranscript(cursorHome, 'slug', 'chat-first', [])
|
||||
expect(await cursorChatMetaPath(firstTranscript)).toBeDefined()
|
||||
|
||||
const laterMetaPath = await writeChatMeta(cursorHome, 'workspace-hash', 'chat-later')
|
||||
const laterTranscript = await writeTranscript(cursorHome, 'slug', 'chat-later', [])
|
||||
|
||||
expect((await stat(workspacePath)).mtimeMs).not.toBe(1_000)
|
||||
expect(await cursorChatMetaPath(laterTranscript)).toBe(laterMetaPath)
|
||||
})
|
||||
|
||||
|
||||
@@ -129,7 +129,15 @@ vi.mock('../usage/jsonl-line-offsets', async () => {
|
||||
}
|
||||
})
|
||||
|
||||
import { mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync, appendFileSync } from 'node:fs'
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
utimesSync,
|
||||
writeFileSync,
|
||||
appendFileSync
|
||||
} from 'node:fs'
|
||||
import { scanCodexUsageFiles } from './scanner'
|
||||
import type { CodexUsagePersistedFile } from './types'
|
||||
|
||||
@@ -526,6 +534,8 @@ describe('scanCodexUsageFiles incremental append', () => {
|
||||
const original = `${sessionMeta('session-a')}${usageRecordRange(0, RESUMABLE_RECORDS)}`
|
||||
writeFileSync(rolloutPath, original, 'utf-8')
|
||||
|
||||
// Keep the rewrite observable on filesystems whose clock has not ticked yet.
|
||||
utimesSync(rolloutPath, 1, 1)
|
||||
const first = await scanCodexUsageFiles([], [])
|
||||
expect(totalTokens(first.dailyAggregates)).toBe(RESUMABLE_RECORDS)
|
||||
expect(recordedResumeOffset(first.processedFiles, rolloutPath)).toBeGreaterThan(0)
|
||||
@@ -536,6 +546,7 @@ describe('scanCodexUsageFiles incremental append', () => {
|
||||
writeFileSync(rolloutPath, replacement, 'utf-8')
|
||||
expect(statSync(rolloutPath).size).toBe(original.length)
|
||||
|
||||
expect(statSync(rolloutPath).mtimeMs).not.toBe(first.processedFiles[0]?.mtimeMs)
|
||||
const second = await scanCodexUsageFiles([], first.processedFiles)
|
||||
const fromScratch = await scanCodexUsageFiles([], [])
|
||||
expect(second.dailyAggregates).toEqual(fromScratch.dailyAggregates)
|
||||
@@ -608,12 +619,15 @@ describe('scanCodexUsageFiles incremental append', () => {
|
||||
expect(original.length - sharedHead.length).toBeLessThan(BOUNDARY_WINDOW_BYTES)
|
||||
writeFileSync(rolloutPath, original, 'utf-8')
|
||||
|
||||
// Keep the rewrite observable on filesystems whose clock has not ticked yet.
|
||||
utimesSync(rolloutPath, 1, 1)
|
||||
const first = await scanCodexUsageFiles([], [])
|
||||
expect(totalTokens(first.dailyAggregates)).toBe(swappedTo)
|
||||
expect(recordedResumeOffset(first.processedFiles, rolloutPath)).toBeGreaterThan(0)
|
||||
|
||||
writeFileSync(rolloutPath, replacement, 'utf-8')
|
||||
|
||||
expect(statSync(rolloutPath).mtimeMs).not.toBe(first.processedFiles[0]?.mtimeMs)
|
||||
const second = await scanCodexUsageFiles([], first.processedFiles)
|
||||
const fromScratch = await scanCodexUsageFiles([], [])
|
||||
expect(second.dailyAggregates).toEqual(fromScratch.dailyAggregates)
|
||||
@@ -628,12 +642,15 @@ describe('scanCodexUsageFiles incremental append', () => {
|
||||
const { original, replacement } = prefixSwapPair('session-prefix-rotated')
|
||||
writeFileSync(rolloutPath, original, 'utf-8')
|
||||
|
||||
// Keep the rewrite observable on filesystems whose clock has not ticked yet.
|
||||
utimesSync(rolloutPath, 1, 1)
|
||||
const first = await scanCodexUsageFiles([], [])
|
||||
expect(totalTokens(first.dailyAggregates)).toBe(40)
|
||||
|
||||
rmSync(rolloutPath)
|
||||
writeFileSync(rolloutPath, replacement, 'utf-8')
|
||||
|
||||
expect(statSync(rolloutPath).mtimeMs).not.toBe(first.processedFiles[0]?.mtimeMs)
|
||||
const second = await scanCodexUsageFiles([], first.processedFiles)
|
||||
const fromScratch = await scanCodexUsageFiles([], [])
|
||||
expect(second.dailyAggregates).toEqual(fromScratch.dailyAggregates)
|
||||
@@ -648,6 +665,8 @@ describe('scanCodexUsageFiles incremental append', () => {
|
||||
const { original, replacement } = prefixSwapPair('session-prefix-in-place')
|
||||
writeFileSync(rolloutPath, original, 'utf-8')
|
||||
|
||||
// Keep the rewrite observable on filesystems whose clock has not ticked yet.
|
||||
utimesSync(rolloutPath, 1, 1)
|
||||
const first = await scanCodexUsageFiles([], [])
|
||||
expect(totalTokens(first.dailyAggregates)).toBe(40)
|
||||
|
||||
@@ -656,6 +675,7 @@ describe('scanCodexUsageFiles incremental append', () => {
|
||||
// Pins why this test is not a duplicate of the rotation case above.
|
||||
expect(statSync(rolloutPath).ino).toBe(inodeBefore)
|
||||
|
||||
expect(statSync(rolloutPath).mtimeMs).not.toBe(first.processedFiles[0]?.mtimeMs)
|
||||
const second = await scanCodexUsageFiles([], first.processedFiles)
|
||||
const fromScratch = await scanCodexUsageFiles([], [])
|
||||
expect(second.dailyAggregates).toEqual(fromScratch.dailyAggregates)
|
||||
|
||||
@@ -514,8 +514,8 @@ describe('while the lookup is still asking Codex', () => {
|
||||
it('writes nothing while an entry is already in place', async () => {
|
||||
await start()
|
||||
const before = snapshot(codexHome())
|
||||
// Why new bytes: a Codex update the lookup has not asked about yet.
|
||||
writeFileSync(mocks.codexPath, 'codex 0.161.0')
|
||||
// A different size invalidates the binary fingerprint even within one filesystem clock tick.
|
||||
writeFileSync(mocks.codexPath, 'codex 0.161.0 updated')
|
||||
const release = holdDerivation()
|
||||
|
||||
await finishHeldLookupWait(reconcileCodexHooksForLaunch, true)
|
||||
|
||||
@@ -51,12 +51,16 @@ function pinMtime(path: string): void {
|
||||
utimesSync(path, PINNED_MTIME_SECONDS, PINNED_MTIME_SECONDS)
|
||||
}
|
||||
|
||||
function rewriteKeepingSizeAndMtime(path: string, contents: string): void {
|
||||
async function rewriteKeepingSizeAndMtime(path: string, contents: string): Promise<void> {
|
||||
const before = statSync(path)
|
||||
expect(contents.length).toBe(before.size)
|
||||
// Allow ctime to tick before restoring mtime on the same-size rewrite.
|
||||
await new Promise((resolve) => setTimeout(resolve, 20))
|
||||
writeFileSync(path, contents)
|
||||
pinMtime(path)
|
||||
expect(statSync(path).mtimeMs).toBe(before.mtimeMs)
|
||||
const after = statSync(path)
|
||||
expect(after.mtimeMs).toBe(before.mtimeMs)
|
||||
expect(after.ctimeMs).not.toBe(before.ctimeMs)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -103,10 +107,10 @@ describe('daemon-host code hash cache', () => {
|
||||
expect(codeReads()).toEqual([])
|
||||
})
|
||||
|
||||
it('detects a same-size source edit that restores mtime', () => {
|
||||
it('detects a same-size source edit that restores mtime', async () => {
|
||||
pinMtime(join(installDir, chunkRel))
|
||||
const root = materializedRoot()
|
||||
rewriteKeepingSizeAndMtime(join(installDir, chunkRel), 'chunX')
|
||||
await rewriteKeepingSizeAndMtime(join(installDir, chunkRel), 'chunX')
|
||||
|
||||
expect(getRelocatedDaemonHost()).toBeNull()
|
||||
expect(codeReads()).toContain(join(installDir, chunkRel))
|
||||
@@ -115,11 +119,11 @@ describe('daemon-host code hash cache', () => {
|
||||
expect(readFileSync(join(rebuilt, chunkRel), 'utf8')).toBe('chunX')
|
||||
})
|
||||
|
||||
it('detects a same-size mirror edit that restores mtime', () => {
|
||||
it('detects a same-size mirror edit that restores mtime', async () => {
|
||||
const root = materializedRoot()
|
||||
pinMtime(join(root, chunkRel))
|
||||
expect(getRelocatedDaemonHost()).not.toBeNull()
|
||||
rewriteKeepingSizeAndMtime(join(root, chunkRel), 'chunX')
|
||||
await rewriteKeepingSizeAndMtime(join(root, chunkRel), 'chunX')
|
||||
|
||||
expect(getRelocatedDaemonHost()).toBeNull()
|
||||
})
|
||||
|
||||
@@ -509,6 +509,8 @@ describe('Linear workspace file cross-process visibility', () => {
|
||||
[{ id: 'ws-alpha', token: 'token-alpha', credentialRevision: 3 }],
|
||||
'ws-alpha'
|
||||
)
|
||||
// A revision update must change metadata even within one filesystem clock tick.
|
||||
utimesSync(join(tempHome, '.orca', 'linear-workspaces.json'), 1, 1)
|
||||
const linear = await loadClientModule()
|
||||
const signal = new AbortController().signal
|
||||
const captured = linear.getClients('ws-alpha', signal)[0]
|
||||
|
||||
@@ -141,7 +141,8 @@ async function until(predicate: () => boolean, what: string): Promise<void> {
|
||||
async function untilWithTimers(predicate: () => boolean, what: string): Promise<void> {
|
||||
const deadline = Date.now() + 10_000
|
||||
while (!predicate() && Date.now() < deadline) {
|
||||
await vi.advanceTimersByTimeAsync(QUIET_MS)
|
||||
// Leave real I/O time to settle before consuming the attempt deadline.
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
await yieldToIo()
|
||||
}
|
||||
if (!predicate()) {
|
||||
|
||||
Reference in New Issue
Block a user