mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Prepare server template assembly alongside qualification (#31422)
* Prepare template assembly during qualification and cache Intel downloads * Limit early template qualification polling to fifteen seconds * Verify Windows retains its download cache policy * Drop Intel store restoration after measuring its overhead
This commit is contained in:
@@ -563,12 +563,11 @@ jobs:
|
||||
|
||||
# Design D2: the desktop ships every target's addons, merged from the lanes that qualified them.
|
||||
desktop_template:
|
||||
needs: [persistence, mobile_web_bundle, linux_glibc_floor, linux_glibc217_compat, linux_musl]
|
||||
if: >-
|
||||
${{ !cancelled() && inputs.build_template &&
|
||||
needs.persistence.result == 'success' && needs.mobile_web_bundle.result == 'success' &&
|
||||
needs.linux_glibc_floor.result == 'success' &&
|
||||
needs.linux_glibc217_compat.result == 'success' && needs.linux_musl.result == 'success' }}
|
||||
needs: changes
|
||||
if: ${{ !cancelled() && inputs.build_template }}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
runs-on: ${{ inputs.fast_runners && 'blacksmith-4vcpu-ubuntu-2404' || 'ubuntu-latest' }}
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
@@ -577,6 +576,45 @@ jobs:
|
||||
ref: ${{ inputs.ref }}
|
||||
persist-credentials: false
|
||||
- uses: ./.github/actions/install-node-dependencies
|
||||
- name: Wait for every template qualification lane
|
||||
uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const deadline = Date.now() + 25 * 60_000
|
||||
const expected = {
|
||||
persistence: 6,
|
||||
mobile_web_bundle: 1,
|
||||
linux_glibc_floor: 2,
|
||||
linux_glibc217_compat: 1,
|
||||
linux_musl: 2
|
||||
}
|
||||
while (Date.now() < deadline) {
|
||||
const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, {
|
||||
...context.repo, run_id: context.runId, filter: 'latest', per_page: 100
|
||||
})
|
||||
const assemblers = jobs.filter(job =>
|
||||
job.name === 'desktop_template' || job.name.endsWith(' / desktop_template'))
|
||||
if (assemblers.length > 1) throw new Error('Ambiguous desktop template qualification')
|
||||
if (assemblers.length === 1) {
|
||||
const prefix = assemblers[0].name.slice(0, -'desktop_template'.length)
|
||||
let ready = true
|
||||
for (const [name, count] of Object.entries(expected)) {
|
||||
const lanes = jobs.filter(job => job.name === `${prefix}${name}` ||
|
||||
job.name.startsWith(`${prefix}${name} (`))
|
||||
if (lanes.length > count) throw new Error(`Unexpected qualification lanes: ${name}`)
|
||||
for (const lane of lanes) {
|
||||
if (lane.status === 'completed' && lane.conclusion !== 'success') {
|
||||
throw new Error(`Template qualification did not succeed: ${lane.name} (${lane.conclusion})`)
|
||||
}
|
||||
}
|
||||
if (lanes.length !== count || lanes.some(lane => lane.status !== 'completed')) ready = false
|
||||
}
|
||||
if (ready) return
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 15_000))
|
||||
}
|
||||
throw new Error('Timed out waiting for every template qualification lane')
|
||||
|
||||
- name: Collect every lane's slot
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
|
||||
@@ -73,28 +73,23 @@ it('starts independent Linux qualification without waiting for another platform'
|
||||
}
|
||||
})
|
||||
|
||||
it('requires every platform and compatibility result before publishing a desktop template', () => {
|
||||
it('prepares template dependencies before waiting for every qualification lane', () => {
|
||||
const template = workflow.jobs.desktop_template
|
||||
const required = [
|
||||
'persistence',
|
||||
'mobile_web_bundle',
|
||||
'linux_glibc_floor',
|
||||
'linux_glibc217_compat',
|
||||
'linux_musl'
|
||||
]
|
||||
expect(template.needs).toEqual(required)
|
||||
const evaluate = compileAggregateExpression(template.if, ['needs', 'inputs'])
|
||||
const needs = Object.fromEntries(required.map((name) => [name, { result: 'success' }]))
|
||||
const ctx = { needs, inputs: { build_template: true } }
|
||||
expect(evaluate(ctx).coerceString()).toBe('true')
|
||||
expect(evaluate(ctx, true).coerceString()).toBe('false')
|
||||
for (const name of required) {
|
||||
for (const result of ['failure', 'cancelled', 'skipped']) {
|
||||
expect(evaluate({ ...ctx, needs: { ...needs, [name]: { result } } }).coerceString()).toBe(
|
||||
'false'
|
||||
)
|
||||
}
|
||||
}
|
||||
expect(template.needs).toBe('changes')
|
||||
expect(template.permissions).toEqual({ contents: 'read', actions: 'read' })
|
||||
const evaluate = compileAggregateExpression(template.if, ['inputs'])
|
||||
expect(evaluate({ inputs: { build_template: true } }).coerceString()).toBe('true')
|
||||
expect(evaluate({ inputs: { build_template: false } }).coerceString()).toBe('false')
|
||||
expect(evaluate({ inputs: { build_template: true } }, true).coerceString()).toBe('false')
|
||||
const wait = template.steps.findIndex(
|
||||
(step) => step.name === 'Wait for every template qualification lane'
|
||||
)
|
||||
expect(
|
||||
template.steps.findIndex((step) => step.uses?.includes('install-node-dependencies'))
|
||||
).toBeLessThan(wait)
|
||||
expect(
|
||||
template.steps.findIndex((step) => step.name === "Collect every lane's slot")
|
||||
).toBeGreaterThan(wait)
|
||||
})
|
||||
|
||||
it('avoids template download-cache transfers while retaining native and verification caches', () => {
|
||||
@@ -106,7 +101,11 @@ it('avoids template download-cache transfers while retaining native and verifica
|
||||
for (const os of ['Linux', 'macOS', 'Windows']) {
|
||||
for (const buildTemplate of [true, false]) {
|
||||
expect(
|
||||
runInNewContext(source, { inputs: { build_template: buildTemplate }, runner: { os } })
|
||||
runInNewContext(source, {
|
||||
inputs: { build_template: buildTemplate, fast_runners: false },
|
||||
matrix: { os: '' },
|
||||
runner: { os }
|
||||
})
|
||||
).toBe(!buildTemplate && os !== 'Windows')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,8 +65,13 @@ it('ships the page the qualification lanes tested in the release template', () =
|
||||
readFileSync(new URL('../../.github/workflows/node-server-tests.yml', import.meta.url), 'utf8')
|
||||
)
|
||||
const template = workflow.jobs.desktop_template
|
||||
expect(template.needs).toContain('mobile_web_bundle')
|
||||
expect(template.if).toContain("needs.mobile_web_bundle.result == 'success'")
|
||||
const wait = template.steps.findIndex(
|
||||
(step) => step.name === 'Wait for every template qualification lane'
|
||||
)
|
||||
expect(template.steps[wait].with.script).toContain('mobile_web_bundle: 1')
|
||||
expect(wait).toBeLessThan(
|
||||
template.steps.findIndex((step) => step.with?.name === 'orcad-mobile-web-bundle')
|
||||
)
|
||||
expect(template.steps.some((step) => step.with?.name === 'orcad-mobile-web-bundle')).toBe(true)
|
||||
expect(template.steps.some((step) => step.uses?.includes('build-mobile-web-bundle'))).toBe(false)
|
||||
})
|
||||
|
||||
@@ -32,7 +32,7 @@ const EXPECTED_MATRIX = {
|
||||
'persistence'
|
||||
].map((job) => [
|
||||
`.github/workflows/release-mac-build.yml#qualification-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#${job}`,
|
||||
{ contents: 'read' }
|
||||
job === 'desktop_template' ? { actions: 'read', contents: 'read' } : { contents: 'read' }
|
||||
])
|
||||
),
|
||||
...Object.fromEntries(
|
||||
@@ -68,6 +68,7 @@ const EXPECTED_MATRIX = {
|
||||
},
|
||||
[`${RELEASE_WORKFLOW}#orcad-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#desktop_template`]:
|
||||
{
|
||||
actions: 'read',
|
||||
contents: 'read'
|
||||
},
|
||||
[`${RELEASE_WORKFLOW}#orcad-template -> .github/workflows/qualified-orcad-template.yml#qualify -> .github/workflows/node-server-tests.yml#linux_glibc217_compat`]:
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
|
||||
const { jobs } = parse(readFileSync('.github/workflows/node-server-tests.yml', 'utf8'))
|
||||
const step = jobs.desktop_template.steps.find(
|
||||
(item) => item.name === 'Wait for every template qualification lane'
|
||||
)
|
||||
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor
|
||||
function lanes(prefix = 'orcad-template / qualify / ') {
|
||||
return [
|
||||
{ name: `${prefix}desktop_template`, status: 'in_progress' },
|
||||
...Object.entries({
|
||||
persistence: 6,
|
||||
mobile_web_bundle: 1,
|
||||
linux_glibc_floor: 2,
|
||||
linux_glibc217_compat: 1,
|
||||
linux_musl: 2
|
||||
}).flatMap(([name, count]) =>
|
||||
Array.from({ length: count }, (_, i) => ({
|
||||
name: `${prefix}${name}${count > 1 ? ` (platform-${i})` : ''}`,
|
||||
status: 'completed',
|
||||
conclusion: 'success'
|
||||
}))
|
||||
)
|
||||
]
|
||||
}
|
||||
function execute(snapshots) {
|
||||
let now = 0
|
||||
let index = 0
|
||||
const calls = []
|
||||
const github = {
|
||||
rest: { actions: { listJobsForWorkflowRun: 'jobs' } },
|
||||
async paginate(endpoint, input) {
|
||||
calls.push({ endpoint, input })
|
||||
return snapshots[Math.min(index, snapshots.length - 1)]
|
||||
}
|
||||
}
|
||||
return {
|
||||
calls,
|
||||
result: new AsyncFunction('github', 'context', 'Date', 'setTimeout', step.with.script)(
|
||||
github,
|
||||
{ repo: { owner: 'owner', repo: 'repo' }, runId: 42 },
|
||||
{ now: () => now },
|
||||
(resolve) => {
|
||||
now += 5000
|
||||
index++
|
||||
resolve()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('early template assembly qualification gate', () => {
|
||||
it.each(['', 'orcad-template / qualify / '])(
|
||||
'waits for all current-attempt jobs with prefix %s',
|
||||
async (prefix) => {
|
||||
const incomplete = lanes(prefix)
|
||||
incomplete[1] = { ...incomplete[1], status: 'in_progress', conclusion: null }
|
||||
const { calls, result } = execute([incomplete, lanes(prefix)])
|
||||
await result
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls.every(({ input }) => input.filter === 'latest' && input.run_id === 42)).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
)
|
||||
it.each(['failure', 'cancelled', 'skipped', 'timed_out', 'action_required', 'startup_failure'])(
|
||||
'rejects %s from any lane',
|
||||
async (conclusion) => {
|
||||
for (let i = 1; i < lanes().length; i++) {
|
||||
const input = lanes()
|
||||
input[i].conclusion = conclusion
|
||||
await expect(execute([input]).result).rejects.toThrow('qualification did not succeed')
|
||||
}
|
||||
}
|
||||
)
|
||||
it('fails closed for missing, extra, ambiguous and unrelated jobs', async () => {
|
||||
await expect(execute([lanes().slice(0, -1)]).result).rejects.toThrow('Timed out')
|
||||
await expect(
|
||||
execute([[...lanes(), { ...lanes()[1], name: `${lanes()[1].name}-extra` }]]).result
|
||||
).rejects.toThrow('Unexpected')
|
||||
await expect(execute([[...lanes(), ...lanes('other / ')]]).result).rejects.toThrow('Ambiguous')
|
||||
await expect(
|
||||
execute([
|
||||
[{ name: 'desktop_template', status: 'in_progress' }, ...lanes('unrelated / ').slice(1)]
|
||||
]).result
|
||||
).rejects.toThrow('Timed out')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user