Files
bsky-social-app/.github/scripts/find-native-receipt.mjs
2026-09-05 18:56:37 +03:00

134 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs/promises'
import process from 'node:process'
import {pathToFileURL} from 'node:url'
export function parsePositiveInteger(value, label) {
if (!/^[1-9][0-9]*$/.test(value ?? '')) {
throw new Error(`${label} must be a positive integer`)
}
return value
}
export function expectedArtifactName({platform, buildNumber, runId, attempt}) {
if (!['ios', 'android'].includes(platform)) {
throw new Error('platform must be ios or android')
}
if (!/^[0-9]+$/.test(buildNumber ?? '')) {
throw new Error('build-number must be numeric')
}
parsePositiveInteger(runId, 'run-id')
parsePositiveInteger(attempt, 'run-attempt')
return `native-build-${platform}-production-${buildNumber}-${runId}-${attempt}`
}
export function validateTrustedRun(run, {repository, workflow}) {
if (
run?.conclusion !== 'success' ||
run?.head_repository?.full_name !== repository ||
run?.path !== workflow ||
!/^[0-9a-f]{40}$/.test(run?.head_sha ?? '')
) {
throw new Error('Receipt artifact came from an untrusted native build run')
}
return run.head_sha
}
async function githubJson(url, token) {
const response = await fetch(url, {
signal: AbortSignal.timeout(15_000),
headers: {
accept: 'application/vnd.github+json',
authorization: `Bearer ${token}`,
'x-github-api-version': '2022-11-28',
},
})
if (!response.ok)
throw new Error(`GitHub API request failed (${response.status})`)
return response.json()
}
async function findArtifact({apiUrl, repository, runId, name, token}) {
for (let page = 1; page <= 10; page++) {
const result = await githubJson(
`${apiUrl}/repos/${repository}/actions/runs/${runId}/artifacts?per_page=100&page=${page}`,
token,
)
const artifact = result.artifacts?.find(
item => item.name === name && !item.expired,
)
if (artifact) return artifact
if (!result.artifacts || result.artifacts.length < 100) return undefined
}
throw new Error('Receipt artifact lookup exceeded 1000 artifacts')
}
async function main(argv = process.argv.slice(2), env = process.env) {
const args = Object.fromEntries(
Array.from({length: argv.length / 2}, (_, index) => [
argv[index * 2]?.replace(/^--/, ''),
argv[index * 2 + 1],
]),
)
const runId = parsePositiveInteger(args['run-id'], 'run-id')
const attempt = parsePositiveInteger(args['run-attempt'], 'run-attempt')
const name = expectedArtifactName({
platform: args.platform,
buildNumber: args['build-number'],
runId,
attempt,
})
const repository = env.GITHUB_REPOSITORY
const token = env.GH_TOKEN
const output = env.GITHUB_OUTPUT
if (!repository || !token || !output)
throw new Error('GitHub Actions environment is incomplete')
let artifact
try {
artifact = await findArtifact({
apiUrl: env.GITHUB_API_URL ?? 'https://api.github.com',
repository,
runId,
name,
token,
})
} catch (error) {
console.error(`Warning: receipt artifact lookup was unavailable: ${error}`)
await fs.appendFile(output, `available=false\nartifact-name=${name}\n`)
return
}
if (!artifact) {
console.error(
`Warning: ${name} is missing or expired; target could not be verified`,
)
await fs.appendFile(output, `available=false\nartifact-name=${name}\n`)
return
}
const workflow = `.github/workflows/build-submit-${args.platform}.yml`
let run
try {
run = await githubJson(
`${env.GITHUB_API_URL ?? 'https://api.github.com'}/repos/${repository}/actions/runs/${runId}`,
token,
)
} catch (error) {
console.error(`Warning: receipt build-run lookup was unavailable: ${error}`)
await fs.appendFile(output, `available=false\nartifact-name=${name}\n`)
return
}
const headSha = validateTrustedRun(run, {repository, workflow})
await fs.appendFile(
output,
`available=true\nartifact-name=${name}\nhead-sha=${headSha}\n`,
)
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(error => {
console.error(error instanceof Error ? error.message : error)
process.exitCode = 1
})
}