#!/usr/bin/env node import crypto from 'node:crypto' import fs from 'node:fs' import path from 'node:path' import { pathToFileURL } from 'node:url' import { execFileSync } from 'node:child_process' const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex') const required = (value, name) => { if (!value) throw new Error(`${name}_REQUIRED`) return value } const write = (file, value) => fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' }) export function buildControllerBundle(options) { const version = required(options.version, 'VERSION') const sourceCommit = required(options.sourceCommit, 'SOURCE_COMMIT') if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error('VERSION_INVALID') if (!/^[a-f0-9]{40}$/.test(sourceCommit)) throw new Error('SOURCE_COMMIT_INVALID') const repository = path.resolve(options.repository || path.join(import.meta.dirname, '../../..')) const head = execFileSync('git', ['-C', repository, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() if (head !== sourceCommit) throw new Error(`SOURCE_COMMIT_NOT_CURRENT_HEAD:${head}`) execFileSync('git', ['-C', repository, 'cat-file', '-e', `${sourceCommit}^{commit}`]) const output = path.resolve(required(options.output, 'OUTPUT')) if (fs.existsSync(output)) throw new Error('OUTPUT_ALREADY_EXISTS') const artifact = path.resolve(required(options.artifact, 'ARTIFACT')) const signatureFile = path.resolve(required(options.signatureFile, 'SIGNATURE')) const bytes = fs.readFileSync(artifact) const signature = fs.readFileSync(signatureFile, 'utf8').trim() if (signature.length < 32) throw new Error('SIGNATURE_INVALID') const base = new URL(required(options.baseUrl, 'BASE_URL')) if (base.protocol !== 'https:' || base.username || base.password || base.search || base.hash) throw new Error('BASE_URL_INVALID') const artifactName = path.basename(artifact) const releaseId = `GH-HOLOLAKE-RELEASE-${version}` const codesignId = `GH-HOLOLAKE-CODESIGN-${version}` const notarizationId = `GH-HOLOLAKE-NOTARIZATION-${version}` const approvalId = `GH-HOLOLAKE-APPROVAL-${version}-ICE-GL-INFINITY` const latest = { schema: 'hololake.release-broadcast/v1', releaseId, version, notes: options.notes || 'HoloLake signed notarized release', pub_date: required(options.publishedAt, 'PUBLISHED_AT'), hololake: { restart: { required: true, automaticAllowed: false }, rollback: { supported: true, healthReceiptRequired: true }, }, platforms: { 'darwin-aarch64': { signature, url: new URL(artifactName, base.href.endsWith('/') ? base : new URL(`${base.href}/`)).href, size: bytes.length, sha256: sha256(bytes), platformCodeSignatureReceipt: codesignId, notarizationReceipt: notarizationId, }, }, } const latestBytes = Buffer.from(`${JSON.stringify(latest, null, 2)}\n`) const broadcastSha256 = sha256(latestBytes) fs.mkdirSync(output, { recursive: false, mode: 0o700 }) fs.copyFileSync(artifact, path.join(output, artifactName), fs.constants.COPYFILE_EXCL) fs.chmodSync(path.join(output, artifactName), 0o600) fs.writeFileSync(path.join(output, 'latest.json'), latestBytes, { mode: 0o600, flag: 'wx' }) write(path.join(output, `${codesignId}.json`), { schema: 'hololake.platform-code-signature-receipt/v1', state: 'DEVELOPER_ID_SIGNATURE_STRICT_AND_GATEKEEPER_ACCEPTED', sourceCommit, version, artifactSha256: sha256(bytes), signingIdentity: 'Developer ID Application: bei sun (825A9L3G7Q)', }) write(path.join(output, `${notarizationId}.json`), { schema: 'hololake.apple-notarization-receipt/v1', state: 'APPLE_NOTARIZATION_ACCEPTED_AND_STAPLED', sourceCommit, version, appSubmissionId: required(options.appSubmissionId, 'APP_SUBMISSION_ID'), dmgSubmissionId: required(options.dmgSubmissionId, 'DMG_SUBMISSION_ID'), staplerValidation: 'PASS', gatekeeper: 'NOTARIZED_DEVELOPER_ID_ACCEPTED', }) write(path.join(output, 'pipeline-receipt.json'), { schema: 'hololake.signed-release-pipeline-receipt/v1', state: 'SIGNED_NOTARIZED_RELEASE_BROADCAST_READY_FOR_JD_CONTROLLER_UPLOAD', sourceCommit, version, broadcastSha256, automaticUpload: false, automaticActivation: false, }) write(path.join(output, 'human-approval.json'), { schema: 'hololake.release-broadcast-human-approval/v1', state: 'HUMAN_APPROVED_EXACT_SIGNED_NOTARIZED_RELEASE', approvalId, releaseId, version, broadcastSha256, humanAnchor: 'ICE-GL∞', sourceMessageId: required(options.approvalMessageId, 'APPROVAL_MESSAGE_ID'), sourceMessageSha256: required(options.approvalMessageSha256, 'APPROVAL_MESSAGE_SHA256'), }) return { output, releaseId, version, sourceCommit, broadcastSha256, approvalId } } function args(values) { const result = {} for (let index = 0; index < values.length; index += 2) result[values[index].slice(2)] = values[index + 1] return result } if (import.meta.url === pathToFileURL(process.argv[1]).href) { const value = args(process.argv.slice(2)) const result = buildControllerBundle({ version: value.version, sourceCommit: value['source-commit'], artifact: value.artifact, signatureFile: value.signature, baseUrl: value['base-url'], notes: value.notes, publishedAt: value['published-at'], appSubmissionId: value['app-submission-id'], dmgSubmissionId: value['dmg-submission-id'], approvalMessageId: value['approval-message-id'], approvalMessageSha256: value['approval-message-sha256'], output: value.output, repository: value.repository, }) process.stdout.write(`${JSON.stringify({ outcome: 'CONTROLLER_BUNDLE_WRITTEN', ...result })}\n`) }