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`)