Skip to content

Commit 82edebb

Browse files
committed
chore(release): harden publish preflight and verification
1 parent 78ddc1c commit 82edebb

8 files changed

Lines changed: 375 additions & 150 deletions

File tree

.github/workflows/release.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Setup Bun
3434
uses: oven-sh/setup-bun@v2
3535
with:
36-
bun-version: latest
36+
bun-version: "1.3.14"
3737

3838
- name: Verify release toolchain
3939
run: |
@@ -44,10 +44,18 @@ jobs:
4444
- name: Install dependencies
4545
run: bun install --frozen-lockfile
4646

47+
- name: Release preflight
48+
run: bun run release:preflight --skip-install
49+
4750
- name: Create version PR or publish
51+
id: changesets
4852
uses: changesets/action@v1
4953
with:
5054
version: bun run version-packages
5155
publish: bun run release-packages
5256
env:
5357
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
59+
- name: Verify published npm bundles
60+
if: steps.changesets.outputs.published == 'true'
61+
run: bun run release:verify-published

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "agenter-workspace",
33
"private": true,
4+
"packageManager": "bun@1.3.14",
45
"workspaces": [
56
"demo",
67
"apps/*",
@@ -27,8 +28,10 @@
2728
"format:check": "prettier --check .",
2829
"openspec:vision": "bun run scripts/openspec/vision-driven.ts",
2930
"release:build-bundles": "bun run scripts/release/build-bundles.ts",
31+
"release:preflight": "bun run scripts/release/preflight.ts",
3032
"release:publish-bundles": "bun run scripts/release/publish-bundles.ts",
3133
"release:publish-bundles:dry-run": "bun run scripts/release/publish-bundles.ts --dry-run",
34+
"release:verify-published": "bun run scripts/release/verify-published.ts",
3235
"release-packages": "bun run release:build-bundles && bun run release:publish-bundles",
3336
"typecheck": "bun run --filter '*' typecheck",
3437
"test": "bun run --filter '*' test",

scripts/release/build-bundles.ts

Lines changed: 31 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,24 @@
11
import { existsSync } from "node:fs";
22
import { chmod, cp, mkdir, rm, writeFile } from "node:fs/promises";
33
import { dirname, join, resolve } from "node:path";
4-
import { suffix } from "bun:ffi";
54

6-
interface PackageJson {
7-
name: string;
8-
version: string;
9-
description?: string;
10-
license?: string;
11-
type?: string;
12-
bin?: Record<string, string>;
13-
exports?: Record<string, string>;
14-
dependencies?: Record<string, string>;
15-
optionalDependencies?: Record<string, string>;
16-
peerDependencies?: Record<string, string>;
17-
engines?: Record<string, string>;
18-
publishConfig?: Record<string, string>;
19-
repository?: {
20-
type: string;
21-
url: string;
22-
directory?: string;
23-
};
24-
}
25-
26-
interface BundleAssetSpec {
27-
from: string;
28-
to: string;
29-
optional?: boolean;
30-
}
31-
32-
interface BundlePackageSpec {
33-
sourcePackageDir: string;
34-
bundlePackageDir: string;
35-
entry?: string;
36-
bin?: Record<string, string>;
37-
exports?: Record<string, string>;
38-
dependencies?: Record<string, string>;
39-
optionalDependencies?: Record<string, string>;
40-
peerDependencies?: Record<string, string>;
41-
external?: string[];
42-
assets?: BundleAssetSpec[];
43-
bundledAssetsRoot?: boolean;
44-
}
5+
import {
6+
createReleaseBundlePackageSpecs,
7+
releaseBundleManifestFiles,
8+
releaseNativeLibrarySuffix,
9+
releasePublishablePackageJsonPaths,
10+
releaseRepositoryUrl,
11+
releaseToolchain,
12+
type ReleaseBundleAssetSpec,
13+
type ReleaseBundlePackageSpec,
14+
type ReleasePackageJson,
15+
} from "./release-manifest";
4516

4617
const repoRoot = resolve(import.meta.dir, "../..");
4718
const bundleRoot = join(repoRoot, "bundle");
48-
const releaseRepositoryUrl = "git+https://github.com/jixoai/agenter.git";
49-
const expectedZigVersion = "0.15.2";
50-
const opentuiNativePackageVersion = "0.3.0";
19+
const expectedZigVersion = releaseToolchain.zigVersion;
5120
const releaseZigRoot = `/tmp/zig-${expectedZigVersion}`;
5221
const releaseZigBin = join(releaseZigRoot, "zig");
53-
const bundleManifestFiles = [
54-
"bin",
55-
"dist",
56-
"assets",
57-
"build",
58-
"native",
59-
"vendor",
60-
"src",
61-
"README.md",
62-
"termless-ghostty-native.node",
63-
] as const;
64-
const publishablePackageJsonPaths = [
65-
"packages/agenter/package.json",
66-
"apps/shell/package.json",
67-
"apps/studio/package.json",
68-
"packages/ghostty-native/package.json",
69-
] as const;
7022

7123
const readJson = async <T>(path: string): Promise<T> => (await Bun.file(path).json()) as T;
7224

@@ -75,7 +27,7 @@ const writeJson = async (path: string, value: unknown): Promise<void> => {
7527
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`);
7628
};
7729

78-
const copyAsset = async (asset: BundleAssetSpec, bundlePackageDir: string): Promise<void> => {
30+
const copyAsset = async (asset: ReleaseBundleAssetSpec, bundlePackageDir: string): Promise<void> => {
7931
const from = join(repoRoot, asset.from);
8032
const to = join(bundlePackageDir, asset.to);
8133
if (existsSync(from)) {
@@ -125,7 +77,7 @@ const createBundledBinWrapper = async (input: {
12577
'import { dirname, resolve } from "node:path";',
12678
'import { fileURLToPath } from "node:url";',
12779
"",
128-
"const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");",
80+
'const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");',
12981
assetRootLine,
13082
`await import(${JSON.stringify(`../${distImportPath.replace(/^\.\//u, "")}`)});`,
13183
"",
@@ -152,10 +104,10 @@ const toWrapperBin = (
152104
};
153105
};
154106

155-
const buildBundle = async (input: BundlePackageSpec): Promise<void> => {
107+
const buildBundle = async (input: ReleaseBundlePackageSpec): Promise<void> => {
156108
const sourcePackageDir = join(repoRoot, input.sourcePackageDir);
157109
const bundlePackageDir = join(repoRoot, input.bundlePackageDir);
158-
const sourcePkg = await readJson<PackageJson>(join(sourcePackageDir, "package.json"));
110+
const sourcePkg = await readJson<ReleasePackageJson>(join(sourcePackageDir, "package.json"));
159111
await rm(bundlePackageDir, { recursive: true, force: true });
160112
await mkdir(bundlePackageDir, { recursive: true });
161113

@@ -194,9 +146,7 @@ const buildBundle = async (input: BundlePackageSpec): Promise<void> => {
194146
type: sourcePkg.type ?? "module",
195147
bin: packageBin,
196148
exports: input.exports,
197-
files: bundleManifestFiles.filter((entry) =>
198-
existsSync(join(bundlePackageDir, entry)),
199-
),
149+
files: releaseBundleManifestFiles.filter((entry) => existsSync(join(bundlePackageDir, entry))),
200150
dependencies: input.dependencies ? normalizeWorkspaceDependencies(input.dependencies) : undefined,
201151
optionalDependencies: input.optionalDependencies
202152
? normalizeWorkspaceDependencies(input.optionalDependencies)
@@ -219,8 +169,8 @@ const readWorkspacePackageVersion = async (packageName: string): Promise<string>
219169
if (cached) {
220170
return cached;
221171
}
222-
for (const relativePath of publishablePackageJsonPaths) {
223-
const pkg = await readJson<PackageJson>(join(repoRoot, relativePath));
172+
for (const relativePath of releasePublishablePackageJsonPaths) {
173+
const pkg = await readJson<ReleasePackageJson>(join(repoRoot, relativePath));
224174
packageVersions.set(pkg.name, pkg.version);
225175
}
226176
const version = packageVersions.get(packageName);
@@ -263,7 +213,9 @@ const resolveZigArchiveName = (): string => {
263213
if (process.platform === "linux" && process.arch === "arm64") {
264214
return `zig-aarch64-linux-${expectedZigVersion}`;
265215
}
266-
throw new Error(`automatic Zig ${expectedZigVersion} bootstrap is not configured for ${process.platform}/${process.arch}`);
216+
throw new Error(
217+
`automatic Zig ${expectedZigVersion} bootstrap is not configured for ${process.platform}/${process.arch}`,
218+
);
267219
};
268220

269221
const ensureReleaseZig = async (): Promise<string> => {
@@ -272,7 +224,13 @@ const ensureReleaseZig = async (): Promise<string> => {
272224
}
273225
const archiveName = resolveZigArchiveName();
274226
const archivePath = `/tmp/${archiveName}.tar.xz`;
275-
await run(["curl", "-L", `https://ziglang.org/download/${expectedZigVersion}/${archiveName}.tar.xz`, "-o", archivePath]);
227+
await run([
228+
"curl",
229+
"-L",
230+
`https://ziglang.org/download/${expectedZigVersion}/${archiveName}.tar.xz`,
231+
"-o",
232+
archivePath,
233+
]);
276234
await rm(releaseZigRoot, { recursive: true, force: true });
277235
await run(["tar", "-xf", archivePath, "-C", "/tmp"]);
278236
await run(["mv", `/tmp/${archiveName}`, releaseZigRoot]);
@@ -286,67 +244,7 @@ const ensureNativeAssets = async (): Promise<void> => {
286244
await run(["bun", "run", "--filter", "@agenter/auth-service", "build:webauthn-ui"]);
287245
};
288246

289-
export const createBundlePackageSpecs = (): BundlePackageSpec[] => [
290-
{
291-
sourcePackageDir: "packages/agenter",
292-
bundlePackageDir: "bundle/agenter",
293-
entry: "src/bin/agenter.ts",
294-
bin: { agenter: "./dist/agenter.js" },
295-
bundledAssetsRoot: true,
296-
dependencies: {
297-
"@duckdb/node-api": "^1.5.1-r.1",
298-
"@jixo/ghostty-native": "workspace:*",
299-
"@termless/core": "^0.6.0",
300-
},
301-
external: ["@duckdb/node-api", "@jixo/ghostty-native"],
302-
assets: [
303-
{
304-
from: `packages/auth-service/native/resvg_bridge/target/release/libprofile_resvg_bridge.${suffix}`,
305-
to: `assets/auth-service/native/resvg_bridge/target/release/libprofile_resvg_bridge.${suffix}`,
306-
},
307-
{ from: "packages/auth-service/src/server/webauthn-ui", to: "assets/auth-service/webauthn-ui" },
308-
{ from: "packages/i18n-en/prompts", to: "assets/i18n-en/prompts" },
309-
{ from: "packages/i18n-en/prompts.json", to: "assets/i18n-en/prompts.json" },
310-
{ from: "packages/i18n-en/runtime.json", to: "assets/i18n-en/runtime.json" },
311-
{ from: "packages/i18n-zh-Hans/prompts", to: "assets/i18n-zh-Hans/prompts" },
312-
{ from: "packages/i18n-zh-Hans/prompts.json", to: "assets/i18n-zh-Hans/prompts.json" },
313-
{ from: "packages/i18n-zh-Hans/runtime.json", to: "assets/i18n-zh-Hans/runtime.json" },
314-
],
315-
},
316-
{
317-
sourcePackageDir: "apps/shell",
318-
bundlePackageDir: "bundle/agenter-app-shell",
319-
entry: "src/bin/agenter-shell.ts",
320-
bin: { "agenter-shell": "./dist/agenter-shell.js" },
321-
optionalDependencies: {
322-
"@opentui/core-darwin-arm64": opentuiNativePackageVersion,
323-
"@opentui/core-darwin-x64": opentuiNativePackageVersion,
324-
"@opentui/core-linux-arm64": opentuiNativePackageVersion,
325-
"@opentui/core-linux-x64": opentuiNativePackageVersion,
326-
"@opentui/core-win32-arm64": opentuiNativePackageVersion,
327-
"@opentui/core-win32-x64": opentuiNativePackageVersion,
328-
},
329-
},
330-
{
331-
sourcePackageDir: "apps/studio",
332-
bundlePackageDir: "bundle/agenter-app-studio",
333-
entry: "src/bin/agenter-studio.ts",
334-
bin: { "agenter-studio": "./dist/agenter-studio.js" },
335-
bundledAssetsRoot: true,
336-
assets: [{ from: "apps/studio/build", to: "assets/studio/build" }],
337-
},
338-
{
339-
sourcePackageDir: "packages/ghostty-native",
340-
bundlePackageDir: "bundle/@jixo/ghostty-native",
341-
exports: { ".": "./src/index.ts" },
342-
peerDependencies: { "@termless/core": "*" },
343-
assets: [
344-
{ from: "packages/ghostty-native/README.md", to: "README.md" },
345-
{ from: "packages/ghostty-native/src", to: "src" },
346-
{ from: "packages/ghostty-native/termless-ghostty-native.node", to: "termless-ghostty-native.node" },
347-
],
348-
},
349-
];
247+
export const createBundlePackageSpecs = createReleaseBundlePackageSpecs;
350248

351249
export const buildReleaseBundles = async (): Promise<void> => {
352250
await rm(bundleRoot, { recursive: true, force: true });
@@ -362,7 +260,7 @@ export const buildReleaseBundles = async (): Promise<void> => {
362260

363261
await writeFile(join(bundleRoot, ".gitignore"), "*\n!.gitignore\n");
364262
console.log("Release bundles written to ./bundle");
365-
console.log(`Auth resvg suffix: ${suffix}`);
263+
console.log(`Auth resvg suffix: ${releaseNativeLibrarySuffix}`);
366264
};
367265

368266
if (import.meta.main) {

scripts/release/preflight.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { releaseToolchain } from "./release-manifest";
2+
3+
const run = async (cmd: string[]): Promise<void> => {
4+
const proc = Bun.spawn({
5+
cmd,
6+
stdin: "inherit",
7+
stdout: "inherit",
8+
stderr: "inherit",
9+
});
10+
const exitCode = await proc.exited;
11+
if (exitCode !== 0) {
12+
throw new Error(`${cmd.join(" ")} failed with exit code ${exitCode}`);
13+
}
14+
};
15+
16+
const readStdout = async (cmd: string[]): Promise<string> => {
17+
const proc = Bun.spawn({
18+
cmd,
19+
stdout: "pipe",
20+
stderr: "inherit",
21+
});
22+
const [stdout, exitCode] = await Promise.all([new Response(proc.stdout).text(), proc.exited]);
23+
if (exitCode !== 0) {
24+
throw new Error(`${cmd.join(" ")} failed with exit code ${exitCode}`);
25+
}
26+
return stdout.trim();
27+
};
28+
29+
const assertBunVersion = async (): Promise<void> => {
30+
const actual = await readStdout(["bun", "--version"]);
31+
if (actual !== releaseToolchain.bunVersion) {
32+
throw new Error(`release requires Bun ${releaseToolchain.bunVersion}, got ${actual}`);
33+
}
34+
};
35+
36+
const main = async (): Promise<void> => {
37+
const skipInstall = process.argv.includes("--skip-install");
38+
await assertBunVersion();
39+
if (!skipInstall) {
40+
await run(["bun", "install", "--frozen-lockfile"]);
41+
}
42+
await run(["bun", "test", "scripts/release/release-bundles.test.ts"]);
43+
await run(["bun", "run", "release:build-bundles"]);
44+
};
45+
46+
await main();

scripts/release/publish-bundles.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
import { existsSync } from "node:fs";
22
import { join, resolve } from "node:path";
33

4+
import { releaseBundlePublishOrder } from "./release-manifest";
5+
46
interface PackageJson {
57
name: string;
68
version: string;
79
}
810

9-
export const bundlePublishOrder = [
10-
"bundle/@jixo/ghostty-native",
11-
"bundle/agenter-app-shell",
12-
"bundle/agenter-app-studio",
13-
"bundle/agenter",
14-
] as const;
11+
export const bundlePublishOrder = releaseBundlePublishOrder;
1512

1613
const repoRoot = resolve(import.meta.dir, "../..");
1714

0 commit comments

Comments
 (0)