Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions .github/workflows/release-prebuilt-npm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: Release prebuilt npm packages

on:
workflow_dispatch:
inputs:
publish:
description: Publish the staged prebuilt packages to npm
required: true
default: false
type: boolean
push:
tags:
- "v*"

concurrency:
group: release-prebuilt-${{ github.ref }}
cancel-in-progress: false

jobs:
build-binaries:
name: Build ${{ matrix.package_name }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- package_name: hunkdiff-linux-x64
runner: ubuntu-latest
- package_name: hunkdiff-darwin-x64
runner: macos-13
- package_name: hunkdiff-darwin-arm64
runner: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Build host artifact
run: |
bun run build:bin
bun run ./scripts/build-prebuilt-artifact.ts --expect-package "${{ matrix.package_name }}"

- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.package_name }}
path: dist/release/artifacts/${{ matrix.package_name }}
if-no-files-found: error

stage-release:
name: Stage prebuilt npm release
runs-on: ubuntu-latest
needs:
- build-binaries
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Verify tag matches package version
if: github.event_name == 'push'
run: bun run ./scripts/check-release-version.ts "${{ github.ref_name }}"

- name: Download platform artifacts
uses: actions/download-artifact@v4
with:
path: dist/release/artifacts

- name: Show downloaded artifacts
run: find dist/release/artifacts -maxdepth 3 -type f | sort

- name: Stage npm release directories
run: bun run stage:prebuilt:release

- name: Verify staged packages
run: bun run check:prebuilt-pack

- name: Dry-run npm publish order
run: bun run publish:prebuilt:npm -- --dry-run

- name: Upload staged npm release
uses: actions/upload-artifact@v4
with:
name: staged-prebuilt-npm-release
path: dist/release/npm
if-no-files-found: error

publish:
name: Publish prebuilt npm release
runs-on: ubuntu-latest
needs:
- stage-release
if: github.event_name == 'push' || inputs.publish == true
environment: npm
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: 22
registry-url: https://registry.npmjs.org

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Download staged npm release
uses: actions/download-artifact@v4
with:
name: staged-prebuilt-npm-release
path: dist/release/npm

- name: Show staged packages
run: find dist/release/npm -maxdepth 3 -type f | sort

- name: Verify npm auth
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: npm whoami

- name: Publish packages
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bun run publish:prebuilt:npm
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,14 +242,25 @@ bun run build:npm
bun run check:pack
```

Stage the prototype prebuilt npm packages for the current host and smoke test the install path without Bun on `PATH`:
Stage the prebuilt npm packages for the current host and smoke test the install path without Bun on `PATH`:

```bash
bun run build:prebuilt:npm
bun run check:prebuilt-pack
bun run smoke:prebuilt-install
```

Prepare the multi-platform release directories from downloaded build artifacts and dry-run the publish order:

```bash
bun run build:prebuilt:artifact
bun run stage:prebuilt:release
bun run check:prebuilt-pack
bun run publish:prebuilt:npm -- --dry-run
```

The automated tag/manual release workflow lives in `.github/workflows/release-prebuilt-npm.yml`.

## License

[MIT](LICENSE)
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@
"build:npm": "bash ./scripts/build-npm.sh",
"build:bin": "bash ./scripts/build-bin.sh",
"build:prebuilt:npm": "bun run build:bin && bun run ./scripts/stage-prebuilt-npm.ts",
"build:prebuilt:artifact": "bun run build:bin && bun run ./scripts/build-prebuilt-artifact.ts",
"stage:prebuilt:release": "bun run ./scripts/stage-prebuilt-npm.ts --artifact-root ./dist/release/artifacts",
"install:bin": "bash ./scripts/install-bin.sh",
"typecheck": "tsc --noEmit",
"test": "bun test",
"test:tty-smoke": "bun test test/tty-render-smoke.test.ts",
"check:pack": "bun run ./scripts/check-pack.ts",
"check:prebuilt-pack": "bun run ./scripts/check-prebuilt-pack.ts",
"smoke:prebuilt-install": "bun run ./scripts/smoke-prebuilt-install.ts",
"publish:prebuilt:npm": "bun run ./scripts/publish-prebuilt-npm.ts",
"prepack": "bun run build:npm",
"bench:bootstrap-load": "bun run test/bootstrap-load-benchmark.ts",
"bench:highlight-prefetch": "bun run test/adjacent-highlight-prefetch-benchmark.ts",
Expand Down
64 changes: 64 additions & 0 deletions scripts/build-prebuilt-artifact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#!/usr/bin/env bun

import { cpSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { binaryFilenameForSpec, getHostPlatformPackageSpec, releaseArtifactsDir } from "./prebuilt-package-helpers";

function parseArgs(argv: string[]) {
let outputRoot: string | undefined;
let expectedPackage: string | undefined;

for (let index = 0; index < argv.length; index += 1) {
const argument = argv[index];
if (argument === "--output-root") {
outputRoot = argv[index + 1];
index += 1;
continue;
}

if (argument === "--expect-package") {
expectedPackage = argv[index + 1];
index += 1;
continue;
}

throw new Error(`Unknown argument: ${argument}`);
}

return { outputRoot, expectedPackage };
}

const repoRoot = path.resolve(import.meta.dir, "..");
const options = parseArgs(process.argv.slice(2));
const spec = getHostPlatformPackageSpec();
const binaryName = binaryFilenameForSpec(spec);
const compiledBinary = path.join(repoRoot, "dist", "hunk");
const outputRoot = path.resolve(options.outputRoot ?? releaseArtifactsDir(repoRoot));
const outputDir = path.join(outputRoot, spec.packageName);

if (options.expectedPackage && options.expectedPackage !== spec.packageName) {
throw new Error(`Host build resolved to ${spec.packageName}, but the workflow expected ${options.expectedPackage}.`);
}

if (!existsSync(compiledBinary)) {
throw new Error(`Missing compiled binary at ${compiledBinary}. Run \`bun run build:bin\` first.`);
}

rmSync(outputDir, { recursive: true, force: true });
mkdirSync(outputDir, { recursive: true });
cpSync(compiledBinary, path.join(outputDir, binaryName));
writeFileSync(
path.join(outputDir, "metadata.json"),
`${JSON.stringify(
{
packageName: spec.packageName,
os: spec.os,
cpu: spec.cpu,
binaryName,
},
null,
2,
)}\n`,
);

console.log(`Prepared prebuilt artifact in ${outputDir}`);
43 changes: 30 additions & 13 deletions scripts/check-prebuilt-pack.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/usr/bin/env bun

import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { getHostPlatformPackageSpec, releaseNpmDir } from "./prebuilt-package-helpers";
import { releaseNpmDir } from "./prebuilt-package-helpers";

interface PackedFile {
path: string;
Expand Down Expand Up @@ -43,29 +44,45 @@ function runPackDryRun(cwd: string) {
return pack;
}

function assertPaths(pack: PackResult, requiredPaths: string[], forbiddenPrefixes: string[] = []) {
function assertPaths(pack: PackResult, requiredPaths: string[]) {
const publishedPaths = new Set(pack.files.map((file) => file.path));

for (const requiredPath of requiredPaths) {
if (!publishedPaths.has(requiredPath)) {
throw new Error(`Expected ${pack.name} to include ${requiredPath}.`);
}
}

for (const file of pack.files) {
if (forbiddenPrefixes.some((prefix) => file.path.startsWith(prefix))) {
throw new Error(`Unexpected file in ${pack.name}: ${file.path}`);
}
}
}

const repoRoot = path.resolve(import.meta.dir, "..");
const releaseRoot = releaseNpmDir(repoRoot);
const hostSpec = getHostPlatformPackageSpec();
const metaPack = runPackDryRun(path.join(releaseRoot, "hunkdiff"));
const hostPack = runPackDryRun(path.join(releaseRoot, hostSpec.packageName));
const metaDir = path.join(releaseRoot, "hunkdiff");

if (!existsSync(metaDir)) {
throw new Error(`Missing staged top-level package at ${metaDir}`);
}

const metaPack = runPackDryRun(metaDir);
assertPaths(metaPack, ["bin/hunk.cjs", "README.md", "LICENSE", "package.json"]);
assertPaths(hostPack, ["bin/hunk", "LICENSE", "package.json"]);

console.log(`Verified prebuilt npm packages for ${metaPack.version}: ${metaPack.name} + ${hostPack.name}`);
const packageDirectories = readdirSync(releaseRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory() && entry.name !== "hunkdiff")
.map((entry) => path.join(releaseRoot, entry.name))
.sort();

if (packageDirectories.length === 0) {
throw new Error(`No staged platform packages found in ${releaseRoot}`);
}

const verifiedNames = [metaPack.name];
for (const packageDirectory of packageDirectories) {
const pack = runPackDryRun(packageDirectory);
assertPaths(pack, ["LICENSE", "package.json"]);
const binaryPath = pack.files.find((file) => file.path.startsWith("bin/"))?.path;
if (!binaryPath) {
throw new Error(`Expected ${pack.name} to publish one binary under bin/.`);
}
verifiedNames.push(pack.name);
}

console.log(`Verified prebuilt npm packages for ${metaPack.version}: ${verifiedNames.join(", ")}`);
18 changes: 18 additions & 0 deletions scripts/check-release-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bun

import path from "node:path";

const repoRoot = path.resolve(import.meta.dir, "..");
const packageJson = JSON.parse(await Bun.file(path.join(repoRoot, "package.json")).text()) as { version: string };
const refName = process.argv[2];

if (!refName) {
throw new Error("Usage: bun run ./scripts/check-release-version.ts <tag>");
}

const expectedTag = `v${packageJson.version}`;
if (refName !== expectedTag) {
throw new Error(`Tag ${refName} does not match package.json version ${packageJson.version} (${expectedTag}).`);
}

console.log(`Verified release tag ${refName} matches package.json version ${packageJson.version}.`);
Loading
Loading