232 lines
6.5 KiB
JavaScript
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
|
|
})
|
|
}
|