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:
Dustin Byrne
2026-03-26 21:33:55 -04:00
parent d616273a73
commit a7a9e985e2
12 changed files with 181 additions and 552 deletions

View File

@@ -4,7 +4,7 @@
"commit": false, "commit": false,
"fixed": [], "fixed": [],
"linked": [], "linked": [],
"access": "restricted", "access": "public",
"baseBranch": "main", "baseBranch": "main",
"updateInternalDependencies": "patch", "updateInternalDependencies": "patch",
"ignore": [] "ignore": []

View File

@@ -24,9 +24,6 @@ jobs:
node-version: "22" node-version: "22"
cache: pnpm cache: pnpm
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
@@ -35,9 +32,3 @@ jobs:
- name: Test - name: Test
run: pnpm test run: pnpm test
- name: Build standalone binary
run: pnpm build:exe
- name: Verify binary exists
run: test -f packages/cli/dist/hai

View File

@@ -1,159 +0,0 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
build:
name: Build (${{ matrix.binary }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: bun-linux-x64
binary: hai-linux-x64
- os: macos-latest
target: bun-darwin-arm64
binary: hai-darwin-arm64
- os: macos-13
target: bun-darwin-x64
binary: hai-darwin-x64
- os: windows-latest
target: bun-windows-x64
binary: hai-windows-x64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Extract version from tag
run: |
VERSION="${GITHUB_REF#refs/tags/v}"
echo "RELEASE_VERSION=${VERSION}" >> $GITHUB_ENV
echo "Releasing version: ${VERSION}"
- name: Verify tag matches package version
run: |
PKG_VERSION=$(node -p "require('./packages/cli/package.json').version")
if [ "$RELEASE_VERSION" != "$PKG_VERSION" ]; then
echo "ERROR: Tag version ($RELEASE_VERSION) does not match packages/cli/package.json version ($PKG_VERSION)"
exit 1
fi
echo "Version check passed: $RELEASE_VERSION"
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Build standalone binary
run: pnpm --filter hai build:exe -- --target ${{ matrix.target }}
- name: Rename binary with platform and arch
run: |
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
esac
cp packages/cli/dist/hai packages/cli/dist/hai-${PLATFORM}-${ARCH}
# Code signing steps — activate when cross-platform matrix is in place (HAI-079)
- name: Sign macOS binaries
if: runner.os == 'macOS'
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
for binary in packages/cli/dist/hai-darwin-*; do
[ -f "$binary" ] && bash scripts/sign-macos.sh "$binary"
done
- name: Sign Windows binaries
if: runner.os == 'Windows'
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
Get-ChildItem packages/cli/dist/hai-windows-*.exe | ForEach-Object {
& .\scripts\sign-windows.ps1 $_.FullName
}
shell: pwsh
- name: Generate checksum (Linux)
if: runner.os == 'Linux'
run: |
cd packages/cli/dist
sha256sum ${{ matrix.binary }} > ${{ matrix.binary }}.sha256
- name: Generate checksum (macOS)
if: runner.os == 'macOS'
run: |
cd packages/cli/dist
shasum -a 256 ${{ matrix.binary }} > ${{ matrix.binary }}.sha256
- name: Generate checksum (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
cd packages/cli/dist
$hash = (Get-FileHash -Algorithm SHA256 "${{ matrix.binary }}").Hash.ToLower()
"$hash ${{ matrix.binary }}" | Out-File -Encoding ascii "${{ matrix.binary }}.sha256"
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary }}
path: |
packages/cli/dist/${{ matrix.binary }}
packages/cli/dist/${{ matrix.binary }}.sha256
release:
name: Create Release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Collect release files
run: |
mkdir -p release
find artifacts -type f -exec cp {} release/ \;
ls -la release/
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
files: release/*

View File

@@ -1,142 +0,0 @@
name: Test Release
on:
workflow_dispatch:
jobs:
build:
name: Test Build (${{ matrix.binary }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: bun-linux-x64
binary: hai-linux-x64
- os: macos-latest
target: bun-darwin-arm64
binary: hai-darwin-arm64
- os: macos-13
target: bun-darwin-x64
binary: hai-darwin-x64
- os: windows-latest
target: bun-windows-x64
binary: hai-windows-x64.exe
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: pnpm
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Install dependencies
run: pnpm install
- name: Build
run: pnpm build
- name: Build standalone binary
run: pnpm --filter hai build:exe -- --target ${{ matrix.target }}
- name: Rename binary with platform and arch
run: |
PLATFORM=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)
case "$ARCH" in
x86_64) ARCH="x64" ;;
aarch64) ARCH="arm64" ;;
esac
cp packages/cli/dist/hai packages/cli/dist/hai-${PLATFORM}-${ARCH}
# Code signing steps — activate when cross-platform matrix is in place (HAI-079)
# Signing is skipped gracefully when secrets are not configured
- name: Sign macOS binaries
if: runner.os == 'macOS' && env.APPLE_CERTIFICATE_BASE64 != ''
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
run: |
for binary in packages/cli/dist/hai-darwin-*; do
[ -f "$binary" ] && bash scripts/sign-macos.sh "$binary"
done
- name: Sign Windows binaries
if: runner.os == 'Windows' && env.WINDOWS_CERTIFICATE_BASE64 != ''
env:
WINDOWS_CERTIFICATE_BASE64: ${{ secrets.WINDOWS_CERTIFICATE_BASE64 }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
Get-ChildItem packages/cli/dist/hai-windows-*.exe | ForEach-Object {
& .\scripts\sign-windows.ps1 $_.FullName
}
shell: pwsh
- name: Generate checksum (Linux)
if: runner.os == 'Linux'
run: |
cd packages/cli/dist
sha256sum ${{ matrix.binary }} > ${{ matrix.binary }}.sha256
- name: Generate checksum (macOS)
if: runner.os == 'macOS'
run: |
cd packages/cli/dist
shasum -a 256 ${{ matrix.binary }} > ${{ matrix.binary }}.sha256
- name: Generate checksum (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
cd packages/cli/dist
$hash = (Get-FileHash -Algorithm SHA256 "${{ matrix.binary }}").Hash.ToLower()
"$hash ${{ matrix.binary }}" | Out-File -Encoding ascii "${{ matrix.binary }}.sha256"
- name: Smoke test (Unix)
if: runner.os != 'Windows'
run: |
chmod +x packages/cli/dist/${{ matrix.binary }}
packages/cli/dist/${{ matrix.binary }} --help
- name: Smoke test (Windows)
if: runner.os == 'Windows'
run: .\packages\cli\dist\${{ matrix.binary }} --help
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.binary }}
path: |
packages/cli/dist/${{ matrix.binary }}
packages/cli/dist/${{ matrix.binary }}.sha256
collect:
name: Collect Artifacts
needs: build
runs-on: ubuntu-latest
steps:
- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
- name: Upload combined archive
uses: actions/upload-artifact@v4
with:
name: hai-all-platforms
path: artifacts/**/*

View File

@@ -1,4 +1,14 @@
name: Version Packages # Release workflow: npm publishing via changesets
#
# This workflow runs on every push to main and does one of two things:
# 1. If there are pending changesets: creates/updates a "Version Packages" PR
# that bumps versions and updates changelogs.
# 2. If there are no pending changesets (i.e., a version PR was just merged):
# builds all packages and publishes them to npm.
#
# Requires NPM_TOKEN secret for npm authentication.
name: Version & Release
on: on:
push: push:
@@ -8,10 +18,11 @@ on:
permissions: permissions:
contents: write contents: write
pull-requests: write pull-requests: write
id-token: write
jobs: jobs:
version: release:
name: Version Packages name: Version or Publish
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@@ -26,31 +37,19 @@ jobs:
with: with:
node-version: "22" node-version: "22"
cache: pnpm cache: pnpm
registry-url: "https://registry.npmjs.org"
- name: Install dependencies - name: Install dependencies
run: pnpm install --no-frozen-lockfile run: pnpm install --no-frozen-lockfile
- name: Create Release Pull Request or Tag - name: Build
id: changesets run: pnpm build
- name: Create Release Pull Request or Publish to npm
uses: changesets/action@v1 uses: changesets/action@v1
with: with:
version: pnpm release:version version: pnpm release:version
publish: pnpm -r publish
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Push version tag
if: steps.changesets.outputs.hasChangesets == 'false'
run: |
VERSION=$(node -p "require('./packages/cli/package.json').version")
TAG="v${VERSION}"
# Check if tag already exists
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "Tag $TAG already exists, skipping"
exit 0
fi
git tag "$TAG"
git push origin "$TAG"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -205,43 +205,24 @@ HAI_CLIENT_DIR=/path/to/client ./hai dashboard
## Releases ## Releases
Pre-built standalone binaries are published automatically via GitHub Actions. Packages are published to npm automatically via GitHub Actions and [changesets](https://github.com/changesets/changesets).
### Downloading binaries ### Installing from npm
Download the latest binary from the [GitHub Releases](../../releases) page. Each release includes platform-specific binaries and SHA256 checksum files for verification. ```bash
npm install -g hai
#### Supported platforms ```
| Platform | Binary | Runner |
|----------|--------|--------|
| Linux x64 | `hai-linux-x64` | `ubuntu-latest` |
| macOS arm64 (Apple Silicon) | `hai-darwin-arm64` | `macos-latest` |
| macOS x64 (Intel) | `hai-darwin-x64` | `macos-13` |
| Windows x64 | `hai-windows-x64.exe` | `windows-latest` |
macOS and Windows binaries are **code-signed** to avoid OS security warnings (Gatekeeper/SmartScreen). See [docs/CODE_SIGNING.md](docs/CODE_SIGNING.md) for setup details.
### Triggering a release ### Triggering a release
Releases are automated via [changesets](https://github.com/changesets/changesets). See [RELEASING.md](./RELEASING.md) for the full workflow. Releases are automated via [changesets](https://github.com/changesets/changesets). See [RELEASING.md](./RELEASING.md) for the full workflow.
In short: add a changeset with `pnpm changeset`, merge to main, then merge the auto-generated Version Packages PR to trigger a release. In short: add a changeset with `pnpm changeset`, merge to main, then merge the auto-generated "Version Packages" PR. Once merged, the workflow automatically publishes all updated packages to npm.
Manual fallback — tag a version and push:
```bash
git tag v0.1.0
git push origin v0.1.0
```
The release workflow will automatically build native binaries for all supported platforms and create a GitHub Release with all artifacts attached.
### CI pipeline ### CI pipeline
- **Pull requests & pushes to main** — runs tests, build, and verifies the standalone binary can be compiled (`.github/workflows/ci.yml`) - **Pull requests & pushes to main** — runs tests and build (`.github/workflows/ci.yml`)
- **Version tags (`v*`)** — builds the binary and publishes it as a GitHub Release (`.github/workflows/release.yml`) - **Push to main** — creates a version PR (if changesets exist) or publishes to npm (`.github/workflows/version.yml`)
- **Manual testing** — maintainers can trigger `.github/workflows/test-release.yml` via the Actions tab to test the build pipeline without publishing
## License ## License

View File

@@ -5,6 +5,10 @@
"bin": { "bin": {
"hai": "./dist/bin.js" "hai": "./dist/bin.js"
}, },
"files": [
"dist",
"README.md"
],
"scripts": { "scripts": {
"dev": "tsx src/bin.ts", "dev": "tsx src/bin.ts",
"build": "tsc", "build": "tsc",

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll } from "vitest"; 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 { join } from "node:path";
import { parse } from "yaml"; import { parse } from "yaml";
@@ -42,8 +42,12 @@ describe("CI workflow (.github/workflows/ci.yml)", () => {
expect(content).toContain("pnpm build"); expect(content).toContain("pnpm build");
}); });
it("includes pnpm build:exe step", () => { it("does not include binary build step", () => {
expect(content).toContain("pnpm build:exe"); 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", () => { 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 workflow: any;
let content: string; let content: string;
beforeAll(() => { beforeAll(() => {
const result = loadWorkflow("release.yml"); const result = loadWorkflow("version.yml");
workflow = result.parsed; workflow = result.parsed;
content = result.content; content = result.content;
}); });
@@ -66,18 +70,8 @@ describe("Release workflow (.github/workflows/release.yml)", () => {
expect(typeof workflow).toBe("object"); expect(typeof workflow).toBe("object");
}); });
it("has tag-based trigger matching v*", () => { it("has push trigger on main", () => {
expect(workflow.on.push.tags).toBeDefined(); expect(workflow.on.push.branches).toContain("main");
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("includes pnpm install step", () => { it("includes pnpm install step", () => {
@@ -88,189 +82,39 @@ describe("Release workflow (.github/workflows/release.yml)", () => {
expect(content).toContain("pnpm build"); expect(content).toContain("pnpm build");
}); });
it("includes build:exe step", () => { it("uses changesets/action", () => {
expect(content).toContain("build:exe"); expect(content).toContain("changesets/action");
}); });
it("generates SHA256 checksums", () => { it("has publish command for npm", () => {
expect(content).toContain("sha256sum"); expect(content).toContain("pnpm -r publish");
}); });
describe("matrix build strategy", () => { it("references NPM_TOKEN secret", () => {
it("has a build job with strategy.matrix including at least 4 entries", () => { expect(content).toContain("secrets.NPM_TOKEN");
const buildJob = workflow.jobs.build; });
expect(buildJob).toBeDefined();
expect(buildJob.strategy?.matrix?.include?.length).toBeGreaterThanOrEqual(4);
});
it("includes all required OS runners", () => { it("has required permissions", () => {
const runners = workflow.jobs.build.strategy.matrix.include.map((e: any) => e.os); expect(workflow.permissions.contents).toBe("write");
expect(runners).toContain("ubuntu-latest"); expect(workflow.permissions["pull-requests"]).toBe("write");
expect(runners).toContain("macos-latest"); });
expect(runners).toContain("macos-13");
expect(runners).toContain("windows-latest");
});
it("includes all required Bun targets", () => { it("configures npm registry-url", () => {
const targets = workflow.jobs.build.strategy.matrix.include.map((e: any) => e.target); const steps = workflow.jobs.release.steps;
expect(targets).toContain("bun-linux-x64"); const nodeStep = steps.find((s: any) => s.uses?.includes("actions/setup-node"));
expect(targets).toContain("bun-darwin-arm64"); expect(nodeStep?.with?.["registry-url"]).toBe("https://registry.npmjs.org");
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");
});
}); });
}); });
describe("Test Release workflow (.github/workflows/test-release.yml)", () => { describe("Deleted binary workflows", () => {
let workflow: any; it("release.yml no longer exists", () => {
let content: string; const path = join(workspaceRoot, ".github", "workflows", "release.yml");
expect(existsSync(path)).toBe(false);
beforeAll(() => {
const result = loadWorkflow("test-release.yml");
workflow = result.parsed;
content = result.content;
}); });
it("is valid YAML", () => { it("test-release.yml no longer exists", () => {
expect(workflow).toBeDefined(); const path = join(workspaceRoot, ".github", "workflows", "test-release.yml");
expect(typeof workflow).toBe("object"); expect(existsSync(path)).toBe(false);
});
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*''/);
}); });
}); });

View 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");
});
});

View File

@@ -3,8 +3,18 @@
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts" ".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}, },
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"README.md"
],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",

View File

@@ -3,8 +3,18 @@
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts" ".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}, },
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"README.md"
],
"scripts": { "scripts": {
"build": "vite build && tsc", "build": "vite build && tsc",
"build:client": "vite build", "build:client": "vite build",

View File

@@ -3,8 +3,18 @@
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.ts" ".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
}, },
"publishConfig": {
"access": "public"
},
"files": [
"dist",
"README.md"
],
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",