feat(companion): emit release manifest metadata in bundles

This commit is contained in:
William Valentin
2026-02-26 19:39:11 -08:00
parent be8b1f29a4
commit 90b6d94a81
10 changed files with 71 additions and 7 deletions
+1
View File
@@ -100,6 +100,7 @@ const {
readmePath: `${input.outputDir}/README.md`,
checksumsPath: `${input.outputDir}/CHECKSUMS.sha256`,
signaturePath: input.signingKeyPem ? `${input.outputDir}/CHECKSUMS.sha256.sig` : undefined,
releaseManifestPath: `${input.outputDir}/RELEASE_MANIFEST.json`,
}));
const writeCompanionShellTemplate = vi.fn(async (input: {
outputDir: string;
+1
View File
@@ -398,6 +398,7 @@ export async function runCompanionSession(options: CompanionCommandOptions): Pro
console.log(`- Launcher: ${result.launcherPath}`);
console.log(`- README: ${result.readmePath}`);
console.log(`- Checksums: ${result.checksumsPath}`);
console.log(`- Release manifest: ${result.releaseManifestPath}`);
if (result.signaturePath) {
console.log(`- Signature: ${result.signaturePath}`);
}
+16
View File
@@ -50,6 +50,7 @@ describe('writeCompanionReleaseBundle', () => {
const launcherRaw = await readFile(result.launcherPath, 'utf8');
const readmeRaw = await readFile(result.readmePath, 'utf8');
const checksumsRaw = await readFile(result.checksumsPath, 'utf8');
const bundleManifestRaw = await readFile(result.releaseManifestPath, 'utf8');
const launcherStat = await stat(result.launcherPath);
expect(JSON.parse(manifestRaw)).toMatchObject({
@@ -64,6 +65,13 @@ describe('writeCompanionReleaseBundle', () => {
expect(checksumsRaw).toContain('companion.bootstrap.json');
expect(checksumsRaw).toContain('run-companion.sh');
expect(checksumsRaw).toContain('README.md');
expect(JSON.parse(bundleManifestRaw)).toMatchObject({
schemaVersion: 1,
bundle: { nodeId: 'ios-node', platform: 'ios' },
files: expect.arrayContaining([
expect.objectContaining({ path: 'CHECKSUMS.sha256' }),
]),
});
expect((launcherStat.mode & 0o111) !== 0).toBe(true);
await rm(tempDir, { recursive: true, force: true });
@@ -99,12 +107,20 @@ describe('writeCompanionReleaseBundle', () => {
});
expect(result.signaturePath).toBe(`${outputDir}/CHECKSUMS.sha256.sig`);
expect(result.releaseManifestPath).toBe(`${outputDir}/RELEASE_MANIFEST.json`);
const checksumsRaw = await readFile(result.checksumsPath, 'utf8');
const signatureRaw = await readFile(result.signaturePath!, 'utf8');
const bundleManifestRaw = await readFile(result.releaseManifestPath, 'utf8');
const signatureLine = signatureRaw.split('\n').find((line) => line.startsWith('signature='));
expect(signatureLine).toBeTruthy();
expect(signatureRaw).toContain('key_id=test-key');
expect(JSON.parse(bundleManifestRaw)).toMatchObject({
signature: {
path: 'CHECKSUMS.sha256.sig',
keyId: 'test-key',
},
});
const signature = Buffer.from(String(signatureLine).replace('signature=', ''), 'base64');
const verified = verify(
+26
View File
@@ -16,6 +16,7 @@ export interface WriteCompanionReleaseBundleResult {
readmePath: string;
checksumsPath: string;
signaturePath?: string;
releaseManifestPath: string;
}
function shSingleQuote(value: string): string {
@@ -171,6 +172,7 @@ export async function writeCompanionReleaseBundle(
const launcherPath = `${input.outputDir}/run-companion.sh`;
const readmePath = `${input.outputDir}/README.md`;
const checksumsPath = `${input.outputDir}/CHECKSUMS.sha256`;
const releaseManifestPath = `${input.outputDir}/RELEASE_MANIFEST.json`;
const manifestBody = `${JSON.stringify(input.manifest, null, 2)}\n`;
const launcherBody = createLauncherScript(input.manifest);
const readmeBody = createReadme(input.manifest);
@@ -201,6 +203,29 @@ export async function writeCompanionReleaseBundle(
].filter(Boolean).join('\n');
await writeFile(signaturePath, `${signatureBody}\n`, 'utf8');
}
const releaseManifest = {
schemaVersion: 1,
generatedAt: input.manifest.generatedAt,
bundle: {
nodeId: input.manifest.node.nodeId,
platform: input.manifest.node.platform,
role: input.manifest.node.role,
gatewayUrl: input.manifest.gateway.url,
},
files: [
{ path: 'companion.bootstrap.json', sha256: createHash('sha256').update(manifestBody, 'utf8').digest('hex') },
{ path: 'run-companion.sh', sha256: createHash('sha256').update(launcherBody, 'utf8').digest('hex') },
{ path: 'README.md', sha256: createHash('sha256').update(readmeBody, 'utf8').digest('hex') },
{ path: 'CHECKSUMS.sha256', sha256: createHash('sha256').update(checksumsPayload, 'utf8').digest('hex') },
],
signature: signaturePath ? {
path: 'CHECKSUMS.sha256.sig',
algorithm: 'sha256',
encoding: 'base64',
keyId: input.signingKeyId ?? undefined,
} : undefined,
};
await writeFile(releaseManifestPath, `${JSON.stringify(releaseManifest, null, 2)}\n`, 'utf8');
return {
outputDir: input.outputDir,
@@ -209,5 +234,6 @@ export async function writeCompanionReleaseBundle(
readmePath,
checksumsPath,
signaturePath,
releaseManifestPath,
};
}