Files
zeekay 937adac37a Publish only on green: the release runs CI first, and reads its tokens from KMS, as 1.1.8
The release calls ci.yml before anything is built: every platform's
tests, Windows included, must pass before the binaries, the wheels or
the crate are made. The crate is published by this workflow with the
KMS CARGO_REGISTRY_TOKEN, and npm with the KMS NPM_TOKEN, both granted
to hanzo-ci, as PyPI already was; crates.io trusted publishing was never
configured for the repository, and the repository's npm secret could
not publish the package. The npm installer fetches the Windows binary.

Claude-Session: https://claude.ai/code/session_01M8RoubcPN71KgJuZ8LdBUL
2026-10-15 04:50:39 -07:00

117 lines
4.6 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
// Downloads the canonical zap-proto/zapd binary for this platform/arch from the
// matching GitHub release. Native binaries only — no QEMU, no build on install.
//
// SUPPLY CHAIN: the release publishes `<asset>.tar.gz.sha256` next to each
// tarball (`shasum -a 256 <asset>.tar.gz`). We download that digest first and
// verify the tarball against it with SHA-256 BEFORE extracting, chmod, exec, or
// the atomic rename. Fail closed on any download error, malformed digest, or
// mismatch — a tampered or truncated asset never reaches disk as an executable.
const https = require('https');
const fs = require('fs');
const path = require('path');
const { createHash } = require('crypto');
const { execFileSync } = require('child_process');
const VERSION = require('./package.json').version;
const REPO = 'zap-proto/zapd';
const TARGETS = {
'darwin-arm64': 'aarch64-apple-darwin',
'darwin-x64': 'x86_64-apple-darwin',
'linux-x64': 'x86_64-unknown-linux-musl',
'linux-arm64': 'aarch64-unknown-linux-musl',
'win32-x64': 'x86_64-pc-windows-msvc',
};
const EXE = process.platform === 'win32' ? 'zapd.exe' : 'zapd';
const key = `${process.platform}-${process.arch}`;
const target = TARGETS[key];
if (!target) {
console.error(`[zapd] unsupported platform ${key} (darwin/linux × x64/arm64, win32 x64)`);
process.exit(1);
}
const binDir = path.join(__dirname, 'bin');
fs.mkdirSync(binDir, { recursive: true });
const asset = `zapd-${target}.tar.gz`;
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${asset}`;
const sumUrl = `${url}.sha256`;
const tarPath = path.join(binDir, 'zapd.tar.gz');
function fail(msg) {
console.error(`[zapd] ${msg}`);
process.exit(1);
}
// Follow redirects, buffer the whole body. Used for the small .sha256 file.
function fetchText(u, cb, redirects = 0) {
if (redirects > 5) return fail(`too many redirects fetching ${u}`);
https.get(u, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
res.resume();
return fetchText(res.headers.location, cb, redirects + 1);
}
if (res.statusCode !== 200) return fail(`download failed ${res.statusCode}: ${u}`);
const chunks = [];
res.on('data', (c) => chunks.push(c));
res.on('end', () => cb(Buffer.concat(chunks).toString('utf8')));
}).on('error', (e) => fail(e.message));
}
// Follow redirects, stream to dest. Used for the (large) tarball.
function download(u, dest, cb, redirects = 0) {
if (redirects > 5) return fail(`too many redirects fetching ${u}`);
https.get(u, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
res.resume();
return download(res.headers.location, dest, cb, redirects + 1);
}
if (res.statusCode !== 200) return fail(`download failed ${res.statusCode}: ${u}`);
const f = fs.createWriteStream(dest);
res.pipe(f);
f.on('finish', () => f.close(cb));
}).on('error', (e) => fail(e.message));
}
// `shasum -a 256 file` → "<64-hex> file". Take the first token, validate shape.
function parseSha256(text) {
const hex = text.trim().split(/\s+/)[0]?.toLowerCase() ?? '';
if (!/^[0-9a-f]{64}$/.test(hex)) fail(`malformed .sha256 digest: ${JSON.stringify(text)}`);
return hex;
}
function sha256File(file) {
return createHash('sha256').update(fs.readFileSync(file)).digest('hex');
}
console.log(`[zapd] downloading ${target} v${VERSION}`);
fetchText(sumUrl, (sumText) => {
const expected = parseSha256(sumText);
download(url, tarPath, () => {
const actual = sha256File(tarPath);
if (actual !== expected) {
fs.unlinkSync(tarPath);
fail(`checksum mismatch for ${asset}\n expected ${expected}\n actual ${actual}`);
}
// ATOMIC install: never overwrite the live binary in place. A running zapd
// holds the inode; extracting over it mid-exec yields a corrupt or partial
// binary (ETXTBSY). Stage to a
// temp dir, chmod, verify it runs, then rename() over the path — atomic on
// macOS/Linux; existing processes keep the old inode, new launches get the new.
const stageDir = fs.mkdtempSync(path.join(binDir, '.stage-'));
execFileSync('tar', ['-xzf', tarPath, '-C', stageDir]);
fs.unlinkSync(tarPath);
const staged = path.join(stageDir, EXE);
fs.chmodSync(staged, 0o755);
try {
execFileSync(staged, ['--version'], { stdio: 'ignore' });
} catch (e) {
fs.rmSync(stageDir, { recursive: true, force: true });
fail('staged binary failed --version; aborting install (not swapping in a bad binary)');
}
fs.renameSync(staged, path.join(binDir, EXE));
fs.rmSync(stageDir, { recursive: true, force: true });
console.log(`[zapd] installed bin/${EXE} (sha256 verified, atomic)`);
});
});