145 lines
4.9 KiB
JavaScript
145 lines
4.9 KiB
JavaScript
import { spawnSync } from 'node:child_process'
|
|
import { dirname, join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import {
|
|
buildReleasePayload,
|
|
createImmutableVerificationSnapshot,
|
|
hashRegularFileStable,
|
|
readSmallFileStable,
|
|
releaseBoundaryFail,
|
|
signReleaseEvidence,
|
|
writeJsonCreateOnly,
|
|
} from './mobile-release-evidence-lib.mjs'
|
|
|
|
function parseArguments(argv) {
|
|
const allowed = new Set([
|
|
'aab',
|
|
'apk',
|
|
'bundletool',
|
|
'commit-sha',
|
|
'expected-admob-app-id',
|
|
'expected-upload-cert-sha256',
|
|
'expected-version-code',
|
|
'expected-version-name',
|
|
'git-ref',
|
|
'private-key',
|
|
'repository',
|
|
'run-attempt',
|
|
'run-id',
|
|
'runner-identity',
|
|
'snapshot-dir',
|
|
'tree-sha',
|
|
'workflow-identity',
|
|
])
|
|
const result = {}
|
|
for (let index = 0; index < argv.length; index += 2) {
|
|
const flag = argv[index]
|
|
const value = argv[index + 1]
|
|
if (!flag?.startsWith('--') || !value || value.startsWith('--')) {
|
|
releaseBoundaryFail('create_evidence_arguments_invalid')
|
|
}
|
|
const name = flag.slice(2)
|
|
if (!allowed.has(name) || Object.hasOwn(result, name)) {
|
|
releaseBoundaryFail(`create_evidence_argument_rejected_${name}`)
|
|
}
|
|
result[name] = value
|
|
}
|
|
for (const name of allowed) {
|
|
if (!result[name]) releaseBoundaryFail(`create_evidence_argument_missing_${name}`)
|
|
}
|
|
return result
|
|
}
|
|
|
|
const options = parseArguments(process.argv.slice(2))
|
|
const scriptDirectory = dirname(fileURLToPath(import.meta.url))
|
|
const verifier = resolve(scriptDirectory, 'verify-android-artifact.mjs')
|
|
const bundletool = resolve(options.bundletool)
|
|
const snapshot = createImmutableVerificationSnapshot({
|
|
apkPath: resolve(options.apk),
|
|
aabPath: resolve(options.aab),
|
|
destinationDirectory: resolve(options['snapshot-dir']),
|
|
verifierPath: verifier,
|
|
bundletoolPath: bundletool,
|
|
})
|
|
const verifierBefore = hashRegularFileStable(snapshot.verifierPath)
|
|
const bundletoolBefore = hashRegularFileStable(snapshot.bundletoolPath)
|
|
const verifierArguments = [
|
|
snapshot.verifierPath,
|
|
'--mode', 'release',
|
|
'--apk', snapshot.apkPath,
|
|
'--aab', snapshot.aabPath,
|
|
'--bundletool', snapshot.bundletoolPath,
|
|
'--expected-admob-app-id', options['expected-admob-app-id'],
|
|
'--expected-upload-cert-sha256', options['expected-upload-cert-sha256'],
|
|
'--expected-version-name', options['expected-version-name'],
|
|
'--expected-version-code', options['expected-version-code'],
|
|
]
|
|
const verificationRun = spawnSync(process.execPath, verifierArguments, {
|
|
encoding: 'utf8',
|
|
maxBuffer: 32 * 1024 * 1024,
|
|
windowsHide: true,
|
|
})
|
|
if (verificationRun.status !== 0) {
|
|
const detail = String(verificationRun.stderr || verificationRun.stdout)
|
|
.trim()
|
|
.replace(/\s+/g, '_')
|
|
.slice(0, 500)
|
|
releaseBoundaryFail(`release_artifact_verifier_failed_${detail}`)
|
|
}
|
|
const verifierAfter = hashRegularFileStable(snapshot.verifierPath)
|
|
const bundletoolAfter = hashRegularFileStable(snapshot.bundletoolPath)
|
|
if (verifierBefore.sha256 !== verifierAfter.sha256 || verifierBefore.bytes !== verifierAfter.bytes) {
|
|
releaseBoundaryFail('verifier_changed_during_verification')
|
|
}
|
|
if (bundletoolBefore.sha256 !== bundletoolAfter.sha256 || bundletoolBefore.bytes !== bundletoolAfter.bytes) {
|
|
releaseBoundaryFail('bundletool_changed_during_verification')
|
|
}
|
|
let verification
|
|
try {
|
|
verification = JSON.parse(verificationRun.stdout)
|
|
} catch {
|
|
releaseBoundaryFail('release_artifact_verifier_json_invalid')
|
|
}
|
|
const expected = {
|
|
versionName: options['expected-version-name'],
|
|
versionCode: options['expected-version-code'],
|
|
adMobAppId: options['expected-admob-app-id'],
|
|
signerSha256: options['expected-upload-cert-sha256'],
|
|
}
|
|
const provenance = {
|
|
repository: options.repository,
|
|
commitSha: options['commit-sha'],
|
|
treeSha: options['tree-sha'],
|
|
gitRef: options['git-ref'],
|
|
workflowIdentity: options['workflow-identity'],
|
|
runId: options['run-id'],
|
|
runAttempt: options['run-attempt'],
|
|
runnerIdentity: options['runner-identity'],
|
|
verifierSha256: verifierBefore.sha256,
|
|
bundletoolSha256: bundletoolBefore.sha256,
|
|
}
|
|
const payload = buildReleasePayload({
|
|
verification,
|
|
apkPath: snapshot.apkPath,
|
|
aabPath: snapshot.aabPath,
|
|
expected,
|
|
provenance,
|
|
})
|
|
const privateKeyPem = readSmallFileStable(options['private-key'], 64 * 1024)
|
|
const evidence = signReleaseEvidence(payload, privateKeyPem)
|
|
|
|
// Both files are create-only. A rerun must start from a clean build output,
|
|
// never overwrite evidence that may already have been consumed downstream.
|
|
writeJsonCreateOnly(join(snapshot.destination, 'release-artifact-verification.json'), verification)
|
|
writeJsonCreateOnly(join(snapshot.destination, 'release-artifact-evidence.json'), evidence)
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
mode: payload.mode,
|
|
packageName: payload.packageName,
|
|
versionName: payload.versionName,
|
|
versionCode: payload.versionCode,
|
|
apkSha256: payload.apk.sha256,
|
|
aabSha256: payload.aab.sha256,
|
|
evidenceKeyId: evidence.signature.keyId,
|
|
provenance: payload.provenance,
|
|
})}\n`)
|