feat(HAI-109): add binary release infrastructure and dual-channel release workflow

- Add binary build verification step to CI workflow
- Add binary release job to release.yml for platform-specific builds
- Restore test-release workflow for pre-release validation
- Update CI workflow tests to cover binary release infrastructure
- Update RELEASING.md with dual-channel (npm + binary) release process
This commit is contained in:
Dustin Byrne
2026-03-26 21:50:50 -04:00
parent 323ce09c1b
commit e4f6582c59
5 changed files with 487 additions and 17 deletions

View File

@@ -42,12 +42,16 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
expect(content).toContain("pnpm build");
});
it("does not include binary build step", () => {
expect(content).not.toContain("pnpm build:exe");
it("includes binary build step", () => {
expect(content).toContain("build:exe");
});
it("does not include Bun setup", () => {
expect(content).not.toContain("oven-sh/setup-bun");
it("includes Bun setup", () => {
expect(content).toContain("oven-sh/setup-bun");
});
it("verifies binary exists after build", () => {
expect(content).toContain("test -f packages/cli/dist/hai");
});
it("includes pnpm test step", () => {
@@ -114,15 +118,153 @@ describe("Version & Release workflow (.github/workflows/version.yml)", () => {
});
});
describe("Deleted binary workflows", () => {
it("release.yml no longer exists", () => {
const path = join(workspaceRoot, ".github", "workflows", "release.yml");
expect(existsSync(path)).toBe(false);
describe("Binary release workflow (.github/workflows/release.yml)", () => {
let workflow: any;
let content: string;
beforeAll(() => {
const result = loadWorkflow("release.yml");
workflow = result.parsed;
content = result.content;
});
it("test-release.yml no longer exists", () => {
const path = join(workspaceRoot, ".github", "workflows", "test-release.yml");
expect(existsSync(path)).toBe(false);
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("triggers on version tags", () => {
expect(workflow.on.push.tags).toBeDefined();
expect(workflow.on.push.tags.some((t: string) => t.includes("v"))).toBe(true);
});
it("has build-binaries job with 4-target matrix", () => {
const matrix = workflow.jobs["build-binaries"].strategy.matrix.include;
expect(matrix).toHaveLength(4);
const targets = matrix.map((m: any) => m.target);
expect(targets).toContain("bun-linux-x64");
expect(targets).toContain("bun-darwin-arm64");
expect(targets).toContain("bun-darwin-x64");
expect(targets).toContain("bun-windows-x64");
});
it("has correct OS runners for each target", () => {
const matrix = workflow.jobs["build-binaries"].strategy.matrix.include;
const osMap: Record<string, string> = {};
matrix.forEach((m: any) => { osMap[m.target] = m.os; });
expect(osMap["bun-linux-x64"]).toBe("ubuntu-latest");
expect(osMap["bun-darwin-arm64"]).toBe("macos-latest");
expect(osMap["bun-darwin-x64"]).toBe("macos-13");
expect(osMap["bun-windows-x64"]).toBe("windows-latest");
});
it("uses softprops/action-gh-release", () => {
expect(content).toContain("softprops/action-gh-release");
});
it("references signing scripts", () => {
expect(content).toContain("scripts/sign-macos.sh");
expect(content).toContain("scripts/sign-windows.ps1");
});
it("generates checksums on all platforms", () => {
expect(content).toContain("sha256sum");
expect(content).toContain("shasum -a 256");
expect(content).toContain("Get-FileHash");
});
it("has contents: write permission", () => {
expect(workflow.permissions.contents).toBe("write");
});
it("has github-release job that depends on build-binaries", () => {
expect(workflow.jobs["github-release"].needs).toContain("build-binaries");
});
});
describe("Test-release workflow (.github/workflows/test-release.yml)", () => {
let workflow: any;
let content: string;
beforeAll(() => {
const result = loadWorkflow("test-release.yml");
workflow = result.parsed;
content = result.content;
});
it("is valid YAML", () => {
expect(workflow).toBeDefined();
expect(typeof workflow).toBe("object");
});
it("has workflow_dispatch trigger", () => {
expect(workflow.on).toHaveProperty("workflow_dispatch");
});
it("has 4-target build matrix", () => {
const matrix = workflow.jobs["build-binaries"].strategy.matrix.include;
expect(matrix).toHaveLength(4);
const targets = matrix.map((m: any) => m.target);
expect(targets).toContain("bun-linux-x64");
expect(targets).toContain("bun-darwin-arm64");
expect(targets).toContain("bun-darwin-x64");
expect(targets).toContain("bun-windows-x64");
});
it("includes smoke tests with --help", () => {
expect(content).toContain("--help");
});
it("has signing steps with secret-availability guards", () => {
expect(content).toContain("APPLE_CERTIFICATE_BASE64 != ''");
expect(content).toContain("WINDOWS_CERTIFICATE_BASE64 != ''");
});
it("uploads artifacts", () => {
expect(content).toContain("actions/upload-artifact");
});
it("has a collect job that combines artifacts", () => {
expect(workflow.jobs.collect).toBeDefined();
expect(workflow.jobs.collect.needs).toContain("build-binaries");
expect(content).toContain("all-binaries");
});
});
describe("Code signing — Release workflow secrets", () => {
let content: string;
beforeAll(() => {
const result = loadWorkflow("release.yml");
content = result.content;
});
it("references macOS signing secrets", () => {
expect(content).toContain("secrets.APPLE_CERTIFICATE_BASE64");
expect(content).toContain("secrets.APPLE_CERTIFICATE_PASSWORD");
expect(content).toContain("secrets.APPLE_IDENTITY");
expect(content).toContain("secrets.APPLE_ID");
expect(content).toContain("secrets.APPLE_TEAM_ID");
expect(content).toContain("secrets.APPLE_APP_PASSWORD");
});
it("references Windows signing secrets", () => {
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_BASE64");
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_PASSWORD");
});
it("generates checksums after signing", () => {
const signMacIdx = content.indexOf("Sign macOS binary");
const signWinIdx = content.indexOf("Sign Windows binary");
const checksumLinuxIdx = content.indexOf("Generate checksum (Linux)");
const checksumMacIdx = content.indexOf("Generate checksum (macOS)");
const checksumWinIdx = content.indexOf("Generate checksum (Windows)");
// All checksum steps come after all signing steps
expect(checksumLinuxIdx).toBeGreaterThan(signMacIdx);
expect(checksumLinuxIdx).toBeGreaterThan(signWinIdx);
expect(checksumMacIdx).toBeGreaterThan(signMacIdx);
expect(checksumWinIdx).toBeGreaterThan(signWinIdx);
});
});