feat(HAI-108): remove binary builds and configure npm publishing
- Remove binary build steps from CI and delete release/test-release workflows - Replace release workflow with npm publish via version.yml and changesets - Configure all packages (cli, core, dashboard, engine) for npm publishing - Add package-config tests to verify publishConfig and package metadata - Update README and documentation to reflect npm-based distribution
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
"bin": {
|
||||
"hai": "./dist/bin.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md"
|
||||
],
|
||||
"scripts": {
|
||||
"dev": "tsx src/bin.ts",
|
||||
"build": "tsc",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, beforeAll } from "vitest";
|
||||
import { readFileSync, accessSync, constants } from "node:fs";
|
||||
import { readFileSync, accessSync, constants, existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parse } from "yaml";
|
||||
|
||||
@@ -42,8 +42,12 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
|
||||
expect(content).toContain("pnpm build");
|
||||
});
|
||||
|
||||
it("includes pnpm build:exe step", () => {
|
||||
expect(content).toContain("pnpm build:exe");
|
||||
it("does not include binary build step", () => {
|
||||
expect(content).not.toContain("pnpm build:exe");
|
||||
});
|
||||
|
||||
it("does not include Bun setup", () => {
|
||||
expect(content).not.toContain("oven-sh/setup-bun");
|
||||
});
|
||||
|
||||
it("includes pnpm test step", () => {
|
||||
@@ -51,12 +55,12 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Release workflow (.github/workflows/release.yml)", () => {
|
||||
describe("Version & Release workflow (.github/workflows/version.yml)", () => {
|
||||
let workflow: any;
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
const result = loadWorkflow("release.yml");
|
||||
const result = loadWorkflow("version.yml");
|
||||
workflow = result.parsed;
|
||||
content = result.content;
|
||||
});
|
||||
@@ -66,18 +70,8 @@ describe("Release workflow (.github/workflows/release.yml)", () => {
|
||||
expect(typeof workflow).toBe("object");
|
||||
});
|
||||
|
||||
it("has tag-based trigger matching v*", () => {
|
||||
expect(workflow.on.push.tags).toBeDefined();
|
||||
const tags = workflow.on.push.tags;
|
||||
expect(tags.some((t: string) => t.includes("v"))).toBe(true);
|
||||
});
|
||||
|
||||
it("includes softprops/action-gh-release action", () => {
|
||||
expect(content).toContain("softprops/action-gh-release");
|
||||
});
|
||||
|
||||
it("includes permissions contents write", () => {
|
||||
expect(workflow.permissions?.contents).toBe("write");
|
||||
it("has push trigger on main", () => {
|
||||
expect(workflow.on.push.branches).toContain("main");
|
||||
});
|
||||
|
||||
it("includes pnpm install step", () => {
|
||||
@@ -88,189 +82,39 @@ describe("Release workflow (.github/workflows/release.yml)", () => {
|
||||
expect(content).toContain("pnpm build");
|
||||
});
|
||||
|
||||
it("includes build:exe step", () => {
|
||||
expect(content).toContain("build:exe");
|
||||
it("uses changesets/action", () => {
|
||||
expect(content).toContain("changesets/action");
|
||||
});
|
||||
|
||||
it("generates SHA256 checksums", () => {
|
||||
expect(content).toContain("sha256sum");
|
||||
it("has publish command for npm", () => {
|
||||
expect(content).toContain("pnpm -r publish");
|
||||
});
|
||||
|
||||
describe("matrix build strategy", () => {
|
||||
it("has a build job with strategy.matrix including at least 4 entries", () => {
|
||||
const buildJob = workflow.jobs.build;
|
||||
expect(buildJob).toBeDefined();
|
||||
expect(buildJob.strategy?.matrix?.include?.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
it("references NPM_TOKEN secret", () => {
|
||||
expect(content).toContain("secrets.NPM_TOKEN");
|
||||
});
|
||||
|
||||
it("includes all required OS runners", () => {
|
||||
const runners = workflow.jobs.build.strategy.matrix.include.map((e: any) => e.os);
|
||||
expect(runners).toContain("ubuntu-latest");
|
||||
expect(runners).toContain("macos-latest");
|
||||
expect(runners).toContain("macos-13");
|
||||
expect(runners).toContain("windows-latest");
|
||||
});
|
||||
it("has required permissions", () => {
|
||||
expect(workflow.permissions.contents).toBe("write");
|
||||
expect(workflow.permissions["pull-requests"]).toBe("write");
|
||||
});
|
||||
|
||||
it("includes all required Bun targets", () => {
|
||||
const targets = workflow.jobs.build.strategy.matrix.include.map((e: any) => e.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 a release job that needs the build job", () => {
|
||||
const releaseJob = workflow.jobs.release;
|
||||
expect(releaseJob).toBeDefined();
|
||||
const needs = Array.isArray(releaseJob.needs) ? releaseJob.needs : [releaseJob.needs];
|
||||
expect(needs).toContain("build");
|
||||
});
|
||||
|
||||
it("generates checksums on all platforms", () => {
|
||||
expect(content).toContain("sha256sum");
|
||||
expect(content).toContain("shasum -a 256");
|
||||
expect(content).toContain("Get-FileHash");
|
||||
});
|
||||
it("configures npm registry-url", () => {
|
||||
const steps = workflow.jobs.release.steps;
|
||||
const nodeStep = steps.find((s: any) => s.uses?.includes("actions/setup-node"));
|
||||
expect(nodeStep?.with?.["registry-url"]).toBe("https://registry.npmjs.org");
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
describe("Deleted binary workflows", () => {
|
||||
it("release.yml no longer exists", () => {
|
||||
const path = join(workspaceRoot, ".github", "workflows", "release.yml");
|
||||
expect(existsSync(path)).toBe(false);
|
||||
});
|
||||
|
||||
it("is valid YAML", () => {
|
||||
expect(workflow).toBeDefined();
|
||||
expect(typeof workflow).toBe("object");
|
||||
});
|
||||
|
||||
it("has workflow_dispatch trigger", () => {
|
||||
expect(workflow.on).toHaveProperty("workflow_dispatch");
|
||||
});
|
||||
|
||||
it("includes pnpm install step", () => {
|
||||
expect(content).toContain("pnpm install");
|
||||
});
|
||||
|
||||
it("includes pnpm build step", () => {
|
||||
expect(content).toContain("pnpm build");
|
||||
});
|
||||
|
||||
it("includes build:exe step", () => {
|
||||
expect(content).toContain("build:exe");
|
||||
});
|
||||
|
||||
it("includes smoke test with --help", () => {
|
||||
expect(content).toContain("--help");
|
||||
});
|
||||
|
||||
it("uploads artifact", () => {
|
||||
expect(content).toContain("actions/upload-artifact");
|
||||
});
|
||||
|
||||
describe("matrix build strategy", () => {
|
||||
it("has a build job with strategy.matrix including at least 4 entries", () => {
|
||||
const buildJob = workflow.jobs.build;
|
||||
expect(buildJob).toBeDefined();
|
||||
expect(buildJob.strategy?.matrix?.include?.length).toBeGreaterThanOrEqual(4);
|
||||
});
|
||||
|
||||
it("includes all required OS runners", () => {
|
||||
const runners = workflow.jobs.build.strategy.matrix.include.map((e: any) => e.os);
|
||||
expect(runners).toContain("ubuntu-latest");
|
||||
expect(runners).toContain("macos-latest");
|
||||
expect(runners).toContain("macos-13");
|
||||
expect(runners).toContain("windows-latest");
|
||||
});
|
||||
|
||||
it("includes all required Bun targets", () => {
|
||||
const targets = workflow.jobs.build.strategy.matrix.include.map((e: any) => e.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("generates checksums on all platforms", () => {
|
||||
expect(content).toContain("sha256sum");
|
||||
expect(content).toContain("shasum -a 256");
|
||||
expect(content).toContain("Get-FileHash");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Code signing — Release workflow", () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
const result = loadWorkflow("release.yml");
|
||||
content = result.content;
|
||||
});
|
||||
|
||||
it("contains macOS signing step referencing sign-macos.sh", () => {
|
||||
expect(content).toContain("sign-macos.sh");
|
||||
});
|
||||
|
||||
it("contains Windows signing step referencing sign-windows.ps1", () => {
|
||||
expect(content).toContain("sign-windows.ps1");
|
||||
});
|
||||
|
||||
it("macOS signing step is conditioned on runner.os", () => {
|
||||
expect(content).toMatch(/if:.*runner\.os\s*==\s*'macOS'/);
|
||||
});
|
||||
|
||||
it("Windows signing step is conditioned on runner.os", () => {
|
||||
expect(content).toMatch(/if:.*runner\.os\s*==\s*'Windows'/);
|
||||
});
|
||||
|
||||
it("references all required Apple secrets", () => {
|
||||
const requiredSecrets = [
|
||||
"APPLE_CERTIFICATE_BASE64",
|
||||
"APPLE_CERTIFICATE_PASSWORD",
|
||||
"APPLE_ID",
|
||||
"APPLE_TEAM_ID",
|
||||
"APPLE_APP_PASSWORD",
|
||||
];
|
||||
for (const secret of requiredSecrets) {
|
||||
expect(content).toContain(`secrets.${secret}`);
|
||||
}
|
||||
});
|
||||
|
||||
it("references Windows signing secrets", () => {
|
||||
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_BASE64");
|
||||
expect(content).toContain("secrets.WINDOWS_CERTIFICATE_PASSWORD");
|
||||
});
|
||||
|
||||
it("checksums step comes after signing steps", () => {
|
||||
const signMacosIndex = content.indexOf("sign-macos.sh");
|
||||
const signWindowsIndex = content.indexOf("sign-windows.ps1");
|
||||
const checksumIndex = content.indexOf("Generate checksum");
|
||||
expect(signMacosIndex).toBeLessThan(checksumIndex);
|
||||
expect(signWindowsIndex).toBeLessThan(checksumIndex);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Code signing — Test-release workflow", () => {
|
||||
let content: string;
|
||||
|
||||
beforeAll(() => {
|
||||
const result = loadWorkflow("test-release.yml");
|
||||
content = result.content;
|
||||
});
|
||||
|
||||
it("has macOS signing step with secret-availability guard", () => {
|
||||
expect(content).toContain("sign-macos.sh");
|
||||
expect(content).toMatch(/if:.*APPLE_CERTIFICATE_BASE64\s*!=\s*''/);
|
||||
});
|
||||
|
||||
it("has Windows signing step with secret-availability guard", () => {
|
||||
expect(content).toContain("sign-windows.ps1");
|
||||
expect(content).toMatch(/if:.*WINDOWS_CERTIFICATE_BASE64\s*!=\s*''/);
|
||||
it("test-release.yml no longer exists", () => {
|
||||
const path = join(workspaceRoot, ".github", "workflows", "test-release.yml");
|
||||
expect(existsSync(path)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
81
packages/cli/src/__tests__/package-config.test.ts
Normal file
81
packages/cli/src/__tests__/package-config.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { parse } from "yaml";
|
||||
|
||||
const workspaceRoot = join(__dirname, "..", "..", "..", "..");
|
||||
|
||||
function loadPackageJson(packageDir: string): any {
|
||||
const path = join(workspaceRoot, "packages", packageDir, "package.json");
|
||||
return JSON.parse(readFileSync(path, "utf-8"));
|
||||
}
|
||||
|
||||
function loadWorkflowYaml(name: string): any {
|
||||
const path = join(workspaceRoot, ".github", "workflows", name);
|
||||
const content = readFileSync(path, "utf-8");
|
||||
return parse(content);
|
||||
}
|
||||
|
||||
describe("CLI package.json publishing config", () => {
|
||||
const pkg = loadPackageJson("cli");
|
||||
|
||||
it('has "bin" field with hai pointing to ./dist/bin.js', () => {
|
||||
expect(pkg.bin).toBeDefined();
|
||||
expect(pkg.bin.hai).toBe("./dist/bin.js");
|
||||
});
|
||||
|
||||
it('has "files" array that includes "dist"', () => {
|
||||
expect(pkg.files).toBeDefined();
|
||||
expect(Array.isArray(pkg.files)).toBe(true);
|
||||
expect(pkg.files).toContain("dist");
|
||||
});
|
||||
|
||||
it("is not private", () => {
|
||||
expect(pkg.private).not.toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Scoped @hai/* packages publishing config", () => {
|
||||
const scopedPackages = ["core", "engine", "dashboard"];
|
||||
|
||||
for (const name of scopedPackages) {
|
||||
describe(`@hai/${name}`, () => {
|
||||
const pkg = loadPackageJson(name);
|
||||
|
||||
it('has publishConfig with access "public"', () => {
|
||||
expect(pkg.publishConfig).toBeDefined();
|
||||
expect(pkg.publishConfig.access).toBe("public");
|
||||
});
|
||||
|
||||
it('has "files" array', () => {
|
||||
expect(pkg.files).toBeDefined();
|
||||
expect(Array.isArray(pkg.files)).toBe(true);
|
||||
expect(pkg.files).toContain("dist");
|
||||
});
|
||||
|
||||
it("exports point to compiled dist output", () => {
|
||||
const exports = pkg.exports?.["."];
|
||||
expect(exports).toBeDefined();
|
||||
if (typeof exports === "object") {
|
||||
expect(exports.import).toMatch(/^\.\/dist\//);
|
||||
} else {
|
||||
expect(exports).toMatch(/^\.\/dist\//);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe("Workflow YAML validity", () => {
|
||||
it("ci.yml is valid YAML", () => {
|
||||
const parsed = loadWorkflowYaml("ci.yml");
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed.name).toBe("CI");
|
||||
});
|
||||
|
||||
it("version.yml is valid YAML", () => {
|
||||
const parsed = loadWorkflowYaml("version.yml");
|
||||
expect(parsed).toBeDefined();
|
||||
expect(parsed.name).toBe("Version & Release");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user