mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
* Parallelize adhoc Mac architecture packaging * Include parallel Mac jobs in release timing reports * Cover parallel Mac publication and failure gates * Explicitly scope archive targets to each Mac architecture
161 lines
6.5 KiB
JavaScript
161 lines
6.5 KiB
JavaScript
import { createRequire } from 'node:module'
|
|
import { readFileSync } from 'node:fs'
|
|
import { setImmediate } from 'node:timers/promises'
|
|
import { describe, expect, it } from 'vitest'
|
|
import { parse } from 'yaml'
|
|
|
|
const builderRequire = createRequire(createRequire(import.meta.url).resolve('electron-builder'))
|
|
const require = createRequire(builderRequire.resolve('app-builder-lib'))
|
|
// Initialize the package root before submodules to preserve its CJS dependency order.
|
|
const { Platform } = require('app-builder-lib')
|
|
const { PlatformPackager } = require('app-builder-lib/out/platformPackager')
|
|
const { ArchiveTarget } = require('app-builder-lib/out/targets/ArchiveTarget')
|
|
const { DmgTarget } = require('dmg-builder/out/dmg')
|
|
const { computeArchToTargetNamesMap } = require('app-builder-lib/out/targets/targetFactory')
|
|
const { AsyncTaskManager } = require('builder-util')
|
|
const { CancellationToken } = require('builder-util-runtime')
|
|
const workflow = (name) => parse(readFileSync(`.github/workflows/${name}-mac-build.yml`, 'utf8'))
|
|
|
|
it('the pinned builder reaches both signed slices before either real archive target passes its gate', async () => {
|
|
let release
|
|
const barrier = new Promise((resolve) => {
|
|
release = resolve
|
|
})
|
|
const events = []
|
|
const packager = {
|
|
platform: Platform.MAC,
|
|
config: {},
|
|
appInfo: { version: '1.0.0' },
|
|
platformSpecificBuildOptions: {},
|
|
info: {
|
|
cancellationToken: new CancellationToken(),
|
|
emitArtifactBuildStarted: async ({ arch }) => {
|
|
events.push(`wait:${arch}`)
|
|
await barrier
|
|
events.push(`pass:${arch}`)
|
|
throw new Error('STOP_BEFORE_ARTIFACT_IO')
|
|
}
|
|
},
|
|
expandArtifactNamePattern: (_options, extension, arch) => `fixture-${arch}.${extension}`
|
|
}
|
|
const archive = new ArchiveTarget('zip', '/unused', packager)
|
|
const targets = [archive, new DmgTarget(packager, '/unused')]
|
|
const manager = new AsyncTaskManager(packager.info.cancellationToken)
|
|
for (const arch of [1, 3]) {
|
|
events.push(`signed:${arch}`)
|
|
PlatformPackager.prototype.packageInDistributableFormat.call(
|
|
packager,
|
|
'/unused.app',
|
|
arch,
|
|
targets,
|
|
manager
|
|
)
|
|
await setImmediate()
|
|
}
|
|
expect(events.filter((event) => event.startsWith('signed:'))).toEqual(['signed:1', 'signed:3'])
|
|
expect(events.filter((event) => event.startsWith('wait:'))).toHaveLength(4)
|
|
expect(events.some((event) => event.startsWith('pass:'))).toBe(false)
|
|
release()
|
|
manager.addTask(archive.finishBuild())
|
|
await expect(manager.awaitTasks()).rejects.toThrow('STOP_BEFORE_ARTIFACT_IO')
|
|
expect(events.filter((event) => event.startsWith('pass:'))).toHaveLength(4)
|
|
})
|
|
|
|
it('explicit archive targets keep the configured dual-architecture defaults out of each matrix job', () => {
|
|
const packager = {
|
|
platformSpecificBuildOptions: {
|
|
target: [
|
|
{ target: 'dmg', arch: ['x64', 'arm64'] },
|
|
{ target: 'zip', arch: ['x64', 'arm64'] }
|
|
]
|
|
}
|
|
}
|
|
for (const arch of [1, 3]) {
|
|
const targets = computeArchToTargetNamesMap(
|
|
new Map([[arch, ['dmg', 'zip']]]),
|
|
packager,
|
|
Platform.MAC
|
|
)
|
|
expect([...targets]).toEqual([[arch, ['dmg', 'zip']]])
|
|
}
|
|
})
|
|
|
|
describe('macOS build workflow policy', () => {
|
|
it.each(['release', 'adhoc', 'daily', 'hourly'])(
|
|
'%s uses the guarded builder, preserving the serial fallback for older source refs',
|
|
(channel) => {
|
|
const job = Object.values(workflow(channel).jobs).find((job) =>
|
|
job.steps?.some((step) => step.with?.command?.includes('electron-builder --config'))
|
|
)
|
|
expect(job['runs-on']).toBe(
|
|
channel === 'adhoc' ? 'blacksmith-12vcpu-macos-15' : 'blacksmith-6vcpu-macos-15'
|
|
)
|
|
const step = job.steps.find((step) =>
|
|
step.with?.command?.includes('electron-builder --config')
|
|
)
|
|
const command = step.with.command
|
|
if (channel === 'adhoc') {
|
|
expect(job.strategy).toEqual({ 'fail-fast': false, matrix: { arch: ['x64', 'arm64'] } })
|
|
expect(command).toContain('--mac dmg zip --${{ matrix.arch }} --publish never')
|
|
expect(command).toContain('ORCA_MAC_ADHOC=1 pnpm exec electron-builder')
|
|
expect(command).not.toContain('build-mac-notarization-overlap.mjs')
|
|
} else {
|
|
expect(command).toContain('if [ -f config/scripts/build-mac-notarization-overlap.mjs ]')
|
|
expect(command).toContain(
|
|
`ORCA_MAC_${channel.toUpperCase()}=1 node config/scripts/build-mac-notarization-overlap.mjs`
|
|
)
|
|
expect(command).toContain(
|
|
`else ORCA_MAC_${channel.toUpperCase()}=1 pnpm exec electron-builder`
|
|
)
|
|
}
|
|
expect(step.with.max_attempts).toBeGreaterThan(1)
|
|
for (const key of [
|
|
'CSC_LINK',
|
|
'CSC_KEY_PASSWORD',
|
|
'APPLE_ID',
|
|
'APPLE_APP_SPECIFIC_PASSWORD',
|
|
'APPLE_TEAM_ID'
|
|
]) {
|
|
expect(step.env[key]).toContain('secrets.')
|
|
}
|
|
}
|
|
)
|
|
|
|
it('qualification signs the workflow commit, gates its complete template, and cannot publish', () => {
|
|
const source = workflow('release')
|
|
expect(source.on.workflow_dispatch.inputs.qualification_only.default).toBe(false)
|
|
expect(source.concurrency).toEqual({
|
|
group:
|
|
"release-mac-build-${{ inputs.tag }}${{ inputs.qualification_only && '-qualification' || '' }}",
|
|
'cancel-in-progress': false
|
|
})
|
|
expect(source.jobs['qualification-template'].if).toBe('inputs.qualification_only')
|
|
expect(source.jobs['qualification-template'].with).toEqual({
|
|
ref: '${{ github.sha }}'
|
|
})
|
|
expect(source.jobs['qualification-template'].uses).toBe(
|
|
'./.github/workflows/qualified-orcad-template.yml'
|
|
)
|
|
const job = source.jobs['build-mac']
|
|
expect(job.needs).toBe('qualification-template')
|
|
expect(job.if).toContain('!cancelled()')
|
|
expect(job.if).toContain("needs.qualification-template.result == 'success'")
|
|
const publish = job.steps.find((step) => step.name === 'Publish release artifacts (macOS)')
|
|
expect(publish.env.ORCA_MAC_PUBLISH).toBe(
|
|
"${{ inputs.qualification_only && 'never' || 'always' }}"
|
|
)
|
|
expect(publish.with.command.match(/--publish "\$ORCA_MAC_PUBLISH"/g)).toHaveLength(2)
|
|
for (const name of [
|
|
'Abort if the parent release-cut run was cancelled',
|
|
'Verify release remains draft after artifact upload'
|
|
]) {
|
|
expect(job.steps.find((step) => step.name === name).if).toBe(
|
|
'${{ !inputs.qualification_only }}'
|
|
)
|
|
}
|
|
const upload = job.steps.find((step) => step.name === 'Upload qualification artifacts')
|
|
expect(upload.if).toBe('inputs.qualification_only')
|
|
expect(upload.with['if-no-files-found']).toBe('error')
|
|
})
|
|
})
|