Files
orca/config/scripts/orcad-template-release-workflow.test.mjs
T
Jinwoo Hong d3a406fcbe ci(release): publish after a skipped orcad template (#24882)
* 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
2026-10-02 17:32:33 -04:00

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'
)
})
})