mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
390 lines
12 KiB
JavaScript
390 lines
12 KiB
JavaScript
import { describe, expect, it, vi } from 'vitest'
|
|
|
|
import {
|
|
dispatchReleaseMacBuildWorkflow,
|
|
executeReleaseMacBuildMode,
|
|
expectedReleaseMacBuildRunTitle,
|
|
readReleaseMacBuildWorkflowOptions,
|
|
runReleaseMacBuildWorkflow
|
|
} from './run-release-mac-build-workflow.mjs'
|
|
|
|
const baseOptions = {
|
|
apiBaseUrl: 'https://api.github.test',
|
|
pollSeconds: 1,
|
|
ref: 'main',
|
|
releaseRunId: '777',
|
|
repo: 'stablyai/orca',
|
|
tag: 'v1.2.3-rc.4',
|
|
timeoutMinutes: 2,
|
|
token: 'token',
|
|
workflow: 'release-mac-build.yml'
|
|
}
|
|
|
|
describe('release mac build workflow dispatch', () => {
|
|
it('passes the shared JavaScript commit only when the parent produced a bundle', async () => {
|
|
const request = vi.fn(async () => ({}))
|
|
const api = { owner: 'stablyai', repo: 'orca', request }
|
|
await dispatchReleaseMacBuildWorkflow(api, {
|
|
...baseOptions,
|
|
javascriptSourceSha: 'a'.repeat(40)
|
|
})
|
|
expect(request.mock.calls[0][2].inputs).toEqual({
|
|
release_run_id: '777',
|
|
tag: 'v1.2.3-rc.4',
|
|
javascript_source_sha: 'a'.repeat(40)
|
|
})
|
|
})
|
|
it('dispatches the mac workflow and waits for the returned run id', async () => {
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(200, {
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/123',
|
|
workflow_run_id: 123
|
|
}),
|
|
jsonResponse(200, {
|
|
conclusion: 'success',
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/123',
|
|
id: 123,
|
|
status: 'completed'
|
|
})
|
|
])
|
|
|
|
const completedRun = await runReleaseMacBuildWorkflow(baseOptions, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T12:00:00Z'),
|
|
sleep: vi.fn()
|
|
})
|
|
|
|
expect(completedRun.conclusion).toBe('success')
|
|
expect(requests[0].method).toBe('POST')
|
|
expect(requests[0].path).toBe(
|
|
'/repos/stablyai/orca/actions/workflows/release-mac-build.yml/dispatches'
|
|
)
|
|
expect(requests[0].body).toEqual({
|
|
inputs: {
|
|
release_run_id: '777',
|
|
tag: 'v1.2.3-rc.4'
|
|
},
|
|
ref: 'main'
|
|
})
|
|
expect(requests[1].path).toBe('/repos/stablyai/orca/actions/runs/123')
|
|
})
|
|
|
|
it('finds the dispatched run by release tag and parent run id when GitHub returns no body', async () => {
|
|
const runTitle = expectedReleaseMacBuildRunTitle(baseOptions)
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(204, null),
|
|
jsonResponse(200, {
|
|
workflow_runs: [
|
|
{
|
|
created_at: '2026-06-30T11:59:00Z',
|
|
display_title: runTitle,
|
|
id: 122
|
|
},
|
|
{
|
|
created_at: '2026-06-30T12:00:01Z',
|
|
display_title: runTitle,
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/124',
|
|
id: 124
|
|
}
|
|
]
|
|
}),
|
|
jsonResponse(200, {
|
|
conclusion: 'success',
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/124',
|
|
id: 124,
|
|
status: 'completed'
|
|
})
|
|
])
|
|
|
|
const completedRun = await runReleaseMacBuildWorkflow(baseOptions, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T12:00:05Z'),
|
|
sleep: vi.fn()
|
|
})
|
|
|
|
expect(completedRun.id).toBe(124)
|
|
expect(requests[1].path).toBe(
|
|
'/repos/stablyai/orca/actions/workflows/release-mac-build.yml/runs'
|
|
)
|
|
expect(requests[1].query.get('event')).toBe('workflow_dispatch')
|
|
})
|
|
|
|
it('fails when the isolated mac workflow does not succeed', async () => {
|
|
const { fetch } = createGitHubFetch([
|
|
jsonResponse(200, {
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/125',
|
|
workflow_run_id: 125
|
|
}),
|
|
jsonResponse(200, {
|
|
conclusion: 'failure',
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/125',
|
|
id: 125,
|
|
status: 'completed'
|
|
})
|
|
])
|
|
|
|
await expect(
|
|
runReleaseMacBuildWorkflow(baseOptions, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T12:00:00Z'),
|
|
sleep: vi.fn()
|
|
})
|
|
).rejects.toThrow(/concluded failure/)
|
|
})
|
|
|
|
it('does not accept a combined-mode success first observed after its polling deadline', async () => {
|
|
let currentTime = Date.parse('2026-06-30T12:00:00Z')
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(200, { workflow_run_id: 123 }),
|
|
jsonResponse(200, { id: 123, status: 'in_progress', conclusion: null }),
|
|
jsonResponse(200, { id: 123, status: 'completed', conclusion: 'success' })
|
|
])
|
|
await expect(
|
|
runReleaseMacBuildWorkflow(baseOptions, {
|
|
fetch,
|
|
now: () => currentTime,
|
|
sleep: async () => {
|
|
currentTime += 3 * 60 * 1000
|
|
}
|
|
})
|
|
).rejects.toThrow(/Timed out after 2m/)
|
|
expect(requests.map((request) => request.method)).toEqual(['POST', 'GET'])
|
|
})
|
|
|
|
it('reads required workflow settings from the GitHub Actions environment', () => {
|
|
const options = readReleaseMacBuildWorkflowOptions({
|
|
GITHUB_REPOSITORY: 'stablyai/orca',
|
|
GITHUB_RUN_ID: '987',
|
|
GITHUB_TOKEN: 'token',
|
|
RELEASE_MAC_BUILD_REF: 'main',
|
|
RELEASE_MAC_BUILD_TAG: 'v1.2.3'
|
|
})
|
|
|
|
expect(options.releaseRunId).toBe('987')
|
|
expect(options.workflow).toBe('release-mac-build.yml')
|
|
expect(options.pollSeconds).toBe(30)
|
|
expect(options.timeoutMinutes).toBe(90)
|
|
})
|
|
})
|
|
|
|
const splitEnvironment = {
|
|
GITHUB_REPOSITORY: 'stablyai/orca',
|
|
GITHUB_RUN_ID: '777',
|
|
GITHUB_TOKEN: 'token',
|
|
GITHUB_OUTPUT: '/tmp/mac-build-output',
|
|
RELEASE_MAC_BUILD_REF: 'main',
|
|
RELEASE_MAC_BUILD_TAG: 'v1.2.3-rc.4',
|
|
RELEASE_MAC_BUILD_RUN_ID: '123',
|
|
RELEASE_MAC_BUILD_WORKFLOW_SHA: 'a'.repeat(40),
|
|
RELEASE_MAC_BUILD_RUN_ATTEMPT: '1',
|
|
RELEASE_MAC_BUILD_WAIT_DEADLINE_MS: String(Date.parse('2026-06-30T13:30:00Z'))
|
|
}
|
|
const splitRun = {
|
|
id: 123,
|
|
run_attempt: 1,
|
|
head_sha: 'a'.repeat(40),
|
|
head_branch: 'main',
|
|
path: '.github/workflows/release-mac-build.yml',
|
|
event: 'workflow_dispatch',
|
|
display_title: 'Mac release build v1.2.3-rc.4 (777)',
|
|
html_url: 'https://github.test/stablyai/orca/actions/runs/123',
|
|
updated_at: '2026-06-30T12:10:00Z',
|
|
status: 'completed',
|
|
conclusion: 'success'
|
|
}
|
|
|
|
describe('release mac build split dispatch and verification', () => {
|
|
it('returns a live run identity without holding a runner while macOS builds', async () => {
|
|
const live = { ...splitRun, status: 'in_progress', conclusion: null }
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(200, { workflow_run_id: 123, html_url: live.html_url }),
|
|
jsonResponse(200, live)
|
|
])
|
|
const appendOutput = vi.fn()
|
|
const result = await executeReleaseMacBuildMode('dispatch', splitEnvironment, {
|
|
fetch,
|
|
appendOutput,
|
|
now: () => Date.parse('2026-06-30T12:00:00Z')
|
|
})
|
|
expect(result.status).toBe('in_progress')
|
|
expect(requests.map((request) => request.method)).toEqual(['POST', 'GET'])
|
|
expect(requests[0].headers['X-GitHub-Api-Version']).toBe('2026-03-10')
|
|
expect(appendOutput).toHaveBeenCalledWith(
|
|
splitEnvironment.GITHUB_OUTPUT,
|
|
`run_id=123\nworkflow_sha=${splitRun.head_sha}\nrun_attempt=1\nwait_deadline_ms=${splitEnvironment.RELEASE_MAC_BUILD_WAIT_DEADLINE_MS}\n`
|
|
)
|
|
})
|
|
|
|
it.each([null, {}, { workflow_run_id: 0 }, { workflow_run_id: -1 }, { workflow_run_id: 1.5 }])(
|
|
'fails closed when a dispatch response cannot identify its exact child: %j',
|
|
async (response) => {
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(response == null ? 204 : 200, response),
|
|
jsonResponse(200, { workflow_runs: [splitRun] })
|
|
])
|
|
const appendOutput = vi.fn()
|
|
await expect(
|
|
executeReleaseMacBuildMode('dispatch', splitEnvironment, {
|
|
fetch,
|
|
appendOutput,
|
|
now: () => Date.parse('2026-06-30T12:00:00Z')
|
|
})
|
|
).rejects.toThrow(/refusing ambiguous discovery/)
|
|
expect(requests.map((request) => request.method)).toEqual(['POST'])
|
|
expect(appendOutput).not.toHaveBeenCalled()
|
|
}
|
|
)
|
|
|
|
it('verifies the recorded run without issuing another dispatch', async () => {
|
|
const { fetch, requests } = createGitHubFetch([jsonResponse(200, splitRun)])
|
|
const result = await executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T12:11:00Z')
|
|
})
|
|
expect(result.id).toBe(123)
|
|
expect(requests.map((request) => request.method)).toEqual(['GET'])
|
|
})
|
|
|
|
it.each(['failure', 'cancelled', 'timed_out', 'action_required', null])(
|
|
'blocks publication for the terminal conclusion %s',
|
|
async (conclusion) => {
|
|
const { fetch } = createGitHubFetch([jsonResponse(200, { ...splitRun, conclusion })])
|
|
await expect(
|
|
executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T12:11:00Z')
|
|
})
|
|
).rejects.toThrow(/concluded/)
|
|
}
|
|
)
|
|
|
|
it.each([
|
|
['id', 124],
|
|
['head_sha', 'b'.repeat(40)],
|
|
['head_branch', 'other'],
|
|
['run_attempt', 2],
|
|
['event', 'pull_request'],
|
|
['path', '.github/workflows/other.yml'],
|
|
['display_title', 'Mac release build v1.2.4 (777)']
|
|
])('rejects a changed %s before inspecting success', async (field, value) => {
|
|
const { fetch } = createGitHubFetch([jsonResponse(200, { ...splitRun, [field]: value })])
|
|
await expect(
|
|
executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T12:11:00Z')
|
|
})
|
|
).rejects.toThrow(/does not match/)
|
|
})
|
|
|
|
it('accepts a run completed within its original deadline after a slow Windows build', async () => {
|
|
const { fetch } = createGitHubFetch([jsonResponse(200, splitRun)])
|
|
await expect(
|
|
executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T17:00:00Z')
|
|
})
|
|
).resolves.toMatchObject({ conclusion: 'success' })
|
|
})
|
|
|
|
it('rejects a successful run that exceeded its original deadline', async () => {
|
|
const { fetch } = createGitHubFetch([
|
|
jsonResponse(200, { ...splitRun, updated_at: '2026-06-30T13:31:00Z' })
|
|
])
|
|
await expect(
|
|
executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
now: () => Date.parse('2026-06-30T17:00:00Z')
|
|
})
|
|
).rejects.toThrow(/exceeded its original deadline/)
|
|
})
|
|
|
|
it('permits only the first late wait-only observation', async () => {
|
|
let currentTime = Date.parse('2026-06-30T13:29:59Z')
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(200, { ...splitRun, status: 'in_progress', conclusion: null }),
|
|
jsonResponse(200, splitRun)
|
|
])
|
|
await expect(
|
|
executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
now: () => currentTime,
|
|
sleep: async () => {
|
|
currentTime += 2_000
|
|
}
|
|
})
|
|
).rejects.toThrow(/Timed out/)
|
|
expect(requests).toHaveLength(1)
|
|
})
|
|
|
|
it('does not reset the polling deadline after the Windows build finishes', async () => {
|
|
const { fetch, requests } = createGitHubFetch([
|
|
jsonResponse(200, { ...splitRun, status: 'in_progress', conclusion: null })
|
|
])
|
|
const sleep = vi.fn()
|
|
await expect(
|
|
executeReleaseMacBuildMode('wait', splitEnvironment, {
|
|
fetch,
|
|
sleep,
|
|
now: () => Date.parse('2026-06-30T17:00:00Z')
|
|
})
|
|
).rejects.toThrow(/Timed out/)
|
|
expect(requests).toHaveLength(1)
|
|
expect(sleep).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it.each([
|
|
['RELEASE_MAC_BUILD_RUN_ID', ''],
|
|
['RELEASE_MAC_BUILD_RUN_ID', '../456'],
|
|
['RELEASE_MAC_BUILD_WORKFLOW_SHA', ''],
|
|
['RELEASE_MAC_BUILD_RUN_ATTEMPT', ''],
|
|
['RELEASE_MAC_BUILD_WAIT_DEADLINE_MS', '']
|
|
])('fails closed without valid %s', async (field, value) => {
|
|
const fetch = vi.fn()
|
|
await expect(
|
|
executeReleaseMacBuildMode(
|
|
'wait',
|
|
{
|
|
...splitEnvironment,
|
|
[field]: value
|
|
},
|
|
{ fetch }
|
|
)
|
|
).rejects.toThrow()
|
|
expect(fetch).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
function createGitHubFetch(responses) {
|
|
const requests = []
|
|
const fetch = vi.fn(async (rawUrl, init) => {
|
|
const url = new URL(rawUrl)
|
|
const response = responses.shift()
|
|
|
|
if (response == null) {
|
|
throw new Error(`Unexpected request: ${init.method} ${rawUrl}`)
|
|
}
|
|
|
|
requests.push({
|
|
body: init.body == null ? undefined : JSON.parse(init.body),
|
|
method: init.method,
|
|
headers: init.headers,
|
|
path: url.pathname,
|
|
query: url.searchParams
|
|
})
|
|
|
|
return response
|
|
})
|
|
|
|
return { fetch, requests }
|
|
}
|
|
|
|
function jsonResponse(status, body) {
|
|
return {
|
|
ok: status >= 200 && status < 300,
|
|
status,
|
|
async text() {
|
|
return body == null ? '' : JSON.stringify(body)
|
|
}
|
|
}
|
|
}
|