mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
* ci(release): publish after a skipped orcad template #24872 skips orcad-template for tags that predate it, but a skipped ancestor skips every job that keeps the implicit success(), so publish-release and the post-release jobs never ran for v1.4.219. * test: brace-free filter in the orcad downstream contract
199 lines
8.9 KiB
JavaScript
199 lines
8.9 KiB
JavaScript
import { readFileSync } from 'node:fs'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { parse } from 'yaml'
|
|
|
|
function readWorkflow(name) {
|
|
return parse(readFileSync(new URL(`../../.github/workflows/${name}`, import.meta.url), 'utf8'))
|
|
}
|
|
|
|
const LANES = ['persistence', 'linux_glibc_floor', 'linux_glibc217_compat', 'linux_musl']
|
|
|
|
function stepIndex(steps, predicate) {
|
|
const index = steps.findIndex(predicate)
|
|
expect(index).toBeGreaterThanOrEqual(0)
|
|
return index
|
|
}
|
|
|
|
describe('orcad template release wiring (design D2)', () => {
|
|
const nodeServer = readWorkflow('node-server-tests.yml')
|
|
const releaseCut = readWorkflow('release-cut.yml')
|
|
const releaseMac = readWorkflow('release-mac-build.yml')
|
|
|
|
it('builds the template from the slots the node-server lanes qualified at the release ref', () => {
|
|
expect(nodeServer.on.workflow_call.inputs).toMatchObject({
|
|
ref: { type: 'string' },
|
|
build_template: { type: 'boolean', default: false }
|
|
})
|
|
// A release call shares github.ref with main's push runs; neither may cancel the other.
|
|
expect(nodeServer.concurrency['cancel-in-progress']).toBe(
|
|
"${{ !inputs.build_template && github.event_name != 'push' }}"
|
|
)
|
|
expect(nodeServer.concurrency.group).toContain('github.run_id')
|
|
for (const lane of LANES) {
|
|
const steps = nodeServer.jobs[lane].steps
|
|
const checkout = steps.find((step) => step.uses === 'actions/checkout@v6')
|
|
expect(checkout.with.ref).toBe('${{ inputs.ref }}')
|
|
const upload = steps.find(
|
|
(step) =>
|
|
step.uses === 'actions/upload-artifact@v7' &&
|
|
String(step.with.name).startsWith('orcad-prebuild-')
|
|
)
|
|
expect(upload.if).toContain('inputs.build_template')
|
|
expect(upload.with.path).toBe('out/orcad-prebuilds/')
|
|
// A rerun of a flaky lane must be able to replace its earlier attempt's slot.
|
|
expect(upload.with.overwrite).toBe(true)
|
|
// Only qualified slots: the upload follows the lane's own gates and tests.
|
|
const gates = steps.filter((step) => /require-slots|test:node-server/.test(step.run ?? ''))
|
|
expect(gates.length).toBeGreaterThan(0)
|
|
for (const gate of gates) {
|
|
expect(steps.indexOf(upload)).toBeGreaterThan(steps.indexOf(gate))
|
|
}
|
|
}
|
|
|
|
// The addon build script imports TypeScript, which the lane's later Node 18 check cannot load.
|
|
const persistence = nodeServer.jobs.persistence.steps
|
|
const addons = stepIndex(
|
|
persistence,
|
|
(step) => step.name === 'Build the Windows process-table addons for the desktop template'
|
|
)
|
|
const node18 = stepIndex(persistence, (step) => step.with?.['node-version'] === '18')
|
|
expect(addons).toBeLessThan(node18)
|
|
|
|
const template = nodeServer.jobs.desktop_template
|
|
expect(template.needs).toEqual(LANES)
|
|
for (const lane of LANES) {
|
|
expect(template.if).toContain(`needs.${lane}.result == 'success'`)
|
|
}
|
|
const run = template.steps.map((step) => step.run ?? '').join('\n')
|
|
expect(run).toContain('merge-orcad-prebuilds.mjs "$RUNNER_TEMP"/orcad-prebuild-lanes/*')
|
|
expect(run).toContain('pnpm build:orcad-prebuilds --require-slots\n')
|
|
expect(run).toContain('pnpm build:orcad-prebuilds --require-slots linux-x64-glibc217')
|
|
expect(run).toContain('pnpm build:orcad-template')
|
|
const upload = template.steps.find((step) => step.uses === 'actions/upload-artifact@v7')
|
|
expect(upload.with).toMatchObject({
|
|
name: 'orcad-template',
|
|
path: 'out/orcad-template/',
|
|
'include-hidden-files': true,
|
|
overwrite: true
|
|
})
|
|
})
|
|
|
|
it('makes every desktop release package wait for, download and require the template', () => {
|
|
const job = releaseCut.jobs['orcad-template']
|
|
expect(job.uses).toBe('./.github/workflows/node-server-tests.yml')
|
|
expect(job.with).toEqual({
|
|
ref: 'refs/tags/${{ needs.cut.outputs.tag }}',
|
|
build_template: true
|
|
})
|
|
for (const name of ['build', 'build-mac']) {
|
|
expect(releaseCut.jobs[name].needs).toContain('orcad-template')
|
|
}
|
|
const build = releaseCut.jobs.build
|
|
expect(build.env.ORCA_REQUIRE_ORCAD_TEMPLATE).toBe('1')
|
|
const download = stepIndex(
|
|
build.steps,
|
|
(step) => step.name === 'Download the orcad deployment template'
|
|
)
|
|
expect(build.steps[download].with).toEqual({
|
|
name: 'orcad-template',
|
|
path: 'out/orcad-template'
|
|
})
|
|
const packaging = build.steps.filter((step) =>
|
|
/electron-builder|release_command/.test(`${step.run ?? ''}${step.with?.command ?? ''}`)
|
|
)
|
|
expect(packaging.length).toBeGreaterThan(0)
|
|
for (const step of packaging) {
|
|
expect(build.steps.indexOf(step)).toBeGreaterThan(download)
|
|
}
|
|
|
|
const macSteps = releaseMac.jobs['build-mac'].steps
|
|
// Why by name: the mac job also downloads the relay Windows process-tree addons.
|
|
const macDownload = stepIndex(
|
|
macSteps,
|
|
(step) => step.uses === 'actions/download-artifact@v8' && step.with?.name === 'orcad-template'
|
|
)
|
|
expect(macSteps[macDownload].with).toMatchObject({
|
|
name: 'orcad-template',
|
|
path: 'out/orcad-template',
|
|
'run-id': '${{ inputs.release_run_id }}'
|
|
})
|
|
const publish = stepIndex(macSteps, (step) => step.name === 'Publish release artifacts (macOS)')
|
|
expect(publish).toBeGreaterThan(macDownload)
|
|
expect(macSteps[publish].env.ORCA_REQUIRE_ORCAD_TEMPLATE).toBe('1')
|
|
expect(releaseMac.permissions.actions).toBe('read')
|
|
})
|
|
|
|
it('skips the template only for a tag that predates it', () => {
|
|
const cutSteps = releaseCut.jobs.cut.steps
|
|
const push = stepIndex(cutSteps, (step) => step.name === 'Push tag')
|
|
const detect = stepIndex(cutSteps, (step) => step.id === 'orcad-template-support')
|
|
expect(detect).toBeGreaterThan(push)
|
|
expect(cutSteps[detect].run).toContain(':config/scripts/packaged-orcad-template.cjs"')
|
|
expect(releaseCut.jobs.cut.outputs.ships_orcad_template).toBe(
|
|
'${{ steps.orcad-template-support.outputs.ships }}'
|
|
)
|
|
expect(releaseCut.jobs['orcad-template'].if).toContain(
|
|
"needs.cut.outputs.ships_orcad_template == 'true'"
|
|
)
|
|
|
|
for (const name of ['build', 'build-mac']) {
|
|
const condition = releaseCut.jobs[name].if
|
|
// Every other dependency still has to succeed, as under the implicit success().
|
|
for (const need of releaseCut.jobs[name].needs.filter((need) => need !== 'orcad-template')) {
|
|
expect(condition).toContain(`needs.${need}.result == 'success'`)
|
|
}
|
|
expect(condition).toContain("needs.orcad-template.result == 'success'")
|
|
expect(condition).toContain(
|
|
"(needs.orcad-template.result == 'skipped' && needs.cut.outputs.ships_orcad_template == 'false')"
|
|
)
|
|
}
|
|
|
|
const buildSteps = releaseCut.jobs.build.steps
|
|
for (const step of [
|
|
buildSteps.find((step) => step.name === 'Download the orcad deployment template'),
|
|
buildSteps.find((step) => step.id === 'reseal-orcad-template')
|
|
]) {
|
|
expect(step.if).toContain("needs.cut.outputs.ships_orcad_template == 'true'")
|
|
}
|
|
const macDownload = releaseMac.jobs['build-mac'].steps.find(
|
|
(step) => step.with?.name === 'orcad-template'
|
|
)
|
|
expect(macDownload.if).toBe("hashFiles('config/scripts/packaged-orcad-template.cjs') != ''")
|
|
})
|
|
|
|
it('keeps every job downstream of the template from inheriting its skip', () => {
|
|
const needsOf = (name) => [releaseCut.jobs[name].needs ?? []].flat()
|
|
const dependsOnTemplate = (name) =>
|
|
needsOf(name).some((need) => need === 'orcad-template' || dependsOnTemplate(need))
|
|
const downstream = Object.keys(releaseCut.jobs).filter(dependsOnTemplate)
|
|
expect(downstream).toEqual(
|
|
expect.arrayContaining(['build', 'build-mac', 'publish-release', 'homebrew-bump'])
|
|
)
|
|
for (const name of downstream) {
|
|
// A skipped ancestor skips the job under the implicit success() that any `if` without a
|
|
// status function gets, so each one must override it and check its own needs instead.
|
|
const condition = releaseCut.jobs[name].if
|
|
expect(condition, name).toContain('!cancelled()')
|
|
for (const need of needsOf(name).filter(
|
|
(need) => need !== 'orcad-template' && need !== 'cut'
|
|
)) {
|
|
expect(condition, `${name} -> ${need}`).toContain(`needs.${need}.result == 'success'`)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('signs only Windows template binaries and reseals the manifest before the installer rebuild', () => {
|
|
const steps = releaseCut.jobs.build.steps
|
|
const stage = steps.find((step) => step.id === 'stage-inner')
|
|
expect(stage.run).toContain("orcad-template[\\\\/]targets[\\\\/](?!win32-)')")
|
|
const restore = stepIndex(steps, (step) => step.id === 'restore-signed-inner')
|
|
const reseal = stepIndex(steps, (step) => step.id === 'reseal-orcad-template')
|
|
const rebuild = stepIndex(steps, (step) => step.id === 'rebuild-nsis-signed')
|
|
expect(restore).toBeLessThan(reseal)
|
|
expect(reseal).toBeLessThan(rebuild)
|
|
expect(steps[reseal].run).toBe(
|
|
'node config/scripts/packaged-orcad-template.cjs --reseal-signed dist/win-unpacked inner-signing-list.txt'
|
|
)
|
|
})
|
|
})
|