Files
stablyai--orca/config/scripts/run-release-mac-build-workflow.mjs
2026-07-13 13:05:33 +08:00

232 lines
6.5 KiB
JavaScript

const DEFAULT_API_VERSION = '2026-03-10'
const DEFAULT_TIMEOUT_MINUTES = 90
const DEFAULT_POLL_SECONDS = 30
const RUN_DISCOVERY_TIMEOUT_SECONDS = 120
export function readReleaseMacBuildWorkflowOptions(env = process.env) {
return {
apiBaseUrl: env.GITHUB_API_URL ?? 'https://api.github.com',
pollSeconds: readPositiveInteger(env.RELEASE_MAC_BUILD_POLL_SECONDS, DEFAULT_POLL_SECONDS),
ref: requiredEnv(env.RELEASE_MAC_BUILD_REF, 'RELEASE_MAC_BUILD_REF'),
releaseRunId: requiredEnv(
env.RELEASE_MAC_BUILD_RELEASE_RUN_ID ?? env.GITHUB_RUN_ID,
'RELEASE_MAC_BUILD_RELEASE_RUN_ID'
),
repo: requiredEnv(env.GITHUB_REPOSITORY, 'GITHUB_REPOSITORY'),
tag: requiredEnv(env.RELEASE_MAC_BUILD_TAG, 'RELEASE_MAC_BUILD_TAG'),
timeoutMinutes: readPositiveInteger(
env.RELEASE_MAC_BUILD_TIMEOUT_MINUTES,
DEFAULT_TIMEOUT_MINUTES
),
token: requiredEnv(env.GITHUB_TOKEN ?? env.GH_TOKEN, 'GITHUB_TOKEN'),
workflow: env.RELEASE_MAC_BUILD_WORKFLOW ?? 'release-mac-build.yml'
}
}
export function expectedReleaseMacBuildRunTitle({ releaseRunId, tag }) {
return `Mac release build ${tag} (${releaseRunId})`
}
export async function runReleaseMacBuildWorkflow(options, deps = {}) {
const api = createGitHubApiClient(options, deps)
const now = deps.now ?? Date.now
const sleep = deps.sleep ?? sleepMilliseconds
const dispatchStartedAtMs = now() - 10_000
const dispatchResult = await dispatchReleaseMacBuildWorkflow(api, options)
const workflowRun =
readWorkflowRunFromDispatchResult(dispatchResult) ??
(await findDispatchedReleaseMacBuildRun(api, options, {
dispatchStartedAtMs,
now,
sleep
}))
console.log(
`Waiting for mac release build workflow run ${workflowRun.id}: ${workflowRun.html_url}`
)
const completedRun = await waitForReleaseMacBuildRun(api, workflowRun.id, options, {
now,
sleep
})
if (completedRun.conclusion !== 'success') {
throw new Error(
`Mac release build workflow ${completedRun.html_url ?? completedRun.id} concluded ${
completedRun.conclusion ?? 'without a conclusion'
}.`
)
}
console.log(`Mac release build workflow succeeded: ${completedRun.html_url}`)
return completedRun
}
export async function dispatchReleaseMacBuildWorkflow(api, options) {
const body = {
inputs: {
release_run_id: options.releaseRunId,
tag: options.tag
},
ref: options.ref
}
return await api.request(
'POST',
`/repos/${api.owner}/${api.repo}/actions/workflows/${encodeURIComponent(
options.workflow
)}/dispatches`,
body
)
}
export async function findDispatchedReleaseMacBuildRun(api, options, deps = {}) {
const now = deps.now ?? Date.now
const sleep = deps.sleep ?? sleepMilliseconds
const deadlineMs = now() + RUN_DISCOVERY_TIMEOUT_SECONDS * 1000
const expectedTitle = expectedReleaseMacBuildRunTitle(options)
while (now() <= deadlineMs) {
const response = await api.request(
'GET',
`/repos/${api.owner}/${api.repo}/actions/workflows/${encodeURIComponent(
options.workflow
)}/runs?event=workflow_dispatch&per_page=20`
)
const workflowRuns = Array.isArray(response?.workflow_runs) ? response.workflow_runs : []
const match = workflowRuns.find((run) => {
const createdAtMs = Date.parse(run.created_at ?? '')
return createdAtMs >= deps.dispatchStartedAtMs && run.display_title === expectedTitle
})
if (match) {
return match
}
await sleep(options.pollSeconds * 1000)
}
throw new Error(
`Timed out finding dispatched mac release build workflow run named "${expectedTitle}".`
)
}
export async function waitForReleaseMacBuildRun(api, workflowRunId, options, deps = {}) {
const now = deps.now ?? Date.now
const sleep = deps.sleep ?? sleepMilliseconds
const deadlineMs = now() + options.timeoutMinutes * 60 * 1000
while (now() <= deadlineMs) {
const run = await api.request(
'GET',
`/repos/${api.owner}/${api.repo}/actions/runs/${workflowRunId}`
)
if (run.status === 'completed') {
return run
}
console.log(
`Mac release build workflow is ${run.status}; polling again in ${options.pollSeconds}s`
)
await sleep(options.pollSeconds * 1000)
}
throw new Error(
`Timed out after ${options.timeoutMinutes}m waiting for mac release build workflow ${workflowRunId}.`
)
}
export function createGitHubApiClient(options, deps = {}) {
const fetchImpl = deps.fetch ?? globalThis.fetch
if (typeof fetchImpl !== 'function') {
throw new Error('A fetch implementation is required.')
}
const [owner, repo] = options.repo.split('/')
if (!owner || !repo) {
throw new Error(`GITHUB_REPOSITORY must be in owner/repo form, got "${options.repo}".`)
}
return {
owner,
repo,
async request(method, path, body) {
const response = await fetchImpl(`${options.apiBaseUrl}${path}`, {
body: body == null ? undefined : JSON.stringify(body),
headers: {
Accept: 'application/vnd.github+json',
Authorization: `Bearer ${options.token}`,
'Content-Type': 'application/json',
'X-GitHub-Api-Version': DEFAULT_API_VERSION
},
method
})
const text = await response.text()
const data = text.length > 0 ? JSON.parse(text) : null
if (!response.ok) {
throw new Error(
`GitHub API ${method} ${path} failed with ${response.status}: ${formatApiError(data)}`
)
}
return data
}
}
}
function readWorkflowRunFromDispatchResult(result) {
if (Number.isInteger(result?.workflow_run_id)) {
return {
html_url: result.html_url,
id: result.workflow_run_id
}
}
return null
}
function formatApiError(data) {
if (typeof data?.message === 'string') {
return data.message
}
return JSON.stringify(data)
}
function readPositiveInteger(rawValue, defaultValue) {
if (rawValue == null || rawValue === '') {
return defaultValue
}
const value = Number(rawValue)
if (!Number.isInteger(value) || value <= 0) {
throw new Error(`Expected a positive integer, got "${rawValue}".`)
}
return value
}
function requiredEnv(value, name) {
if (value == null || value === '') {
throw new Error(`${name} is required.`)
}
return value
}
function sleepMilliseconds(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds)
})
}
if (import.meta.url === `file://${process.argv[1]}`) {
runReleaseMacBuildWorkflow(readReleaseMacBuildWorkflowOptions()).catch((error) => {
console.error(error)
process.exitCode = 1
})
}