From 7abab635365a6bff5f1b589fbaeccadda4606ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:27:09 +0200 Subject: [PATCH 1/2] fix(release): resolve the embedded package runtime from process.execPath MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.10.0's released binaries fail every akua pkg command with AKUA_PACKAGE_UNAVAILABLE. Two compounding bugs, both found and fixed by actually installing v0.10.0 via Homebrew and running the real extracted binary (not the mocked install-smoke test, which stubs the executable and never proves the packaged runtime loads): 1. stagePackageRuntime only staged native/native-engines; @akua-dev/sdk itself was never copied into the archive's node_modules. Its package.json declares files: ["dist", ...] (a directory), unlike native/native-engines' flat file lists, so a naive fix needed directory-listed manifest entries expanded into their individual files (expandPackageManifestFiles/walkPackageDirectory) before staging — copyFileSync can't copy a directory. 2. Even with sdk staged correctly, bun build --compile still couldn't load it at runtime: without --external, the bundler tries to inline @akua-dev/sdk/execute, which transitively needs @akua-dev/native's platform .node binding — a real binary the bundler cannot inline. Marking @akua-dev/* --external keeps it a real runtime import, but a compiled executable resolves bare specifiers against its own embedded virtual filesystem (), never the real one, so it still couldn't see the sidecar node_modules staged next to it on disk. process.execPath resolves to the executable's real filesystem location even when compiled (empirically confirmed), so resolvePackageExecute in services-live.ts imports the sdk from an absolute path built off it, falling back to normal package resolution when the sidecar isn't present (dev/test/build). Rationale: this is the one place in the CLI that needs a dynamic import — a compiled sidecar-native-module architecture cannot resolve its runtime dependency through a static specifier the bundler would either inline (breaking the native binding) or resolve against its own virtual filesystem. The result is cached via Effect.cached, not a mutable variable. Rejected: leaving @akua-dev/sdk bundled inline — impossible, it transitively needs a real .node file. Leaving it a static external import — resolves against , not the real sidecar path. Tested: bun test (165 pass); bun run build; bun run generate:check; real end-to-end proof — bun scripts/release.ts package built a real v0.10.1-local archive, extracted to a clean directory outside the repo, and the extracted akua binary's pkg version/--help/init/render all worked against the real staged node_modules (previously reproduced the exact AKUA_PACKAGE_UNAVAILABLE failure from the real Homebrew-installed v0.10.0 binary before this fix). --- scripts/runtime/release-host-live.ts | 66 +++++++++++++++++++- src/runtime/services-live.ts | 68 ++++++++++++++++++-- test/release.test.ts | 92 ++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+), 7 deletions(-) diff --git a/scripts/runtime/release-host-live.ts b/scripts/runtime/release-host-live.ts index 30a2b4b..7272c30 100644 --- a/scripts/runtime/release-host-live.ts +++ b/scripts/runtime/release-host-live.ts @@ -370,6 +370,12 @@ function packageRelease( `--target=${target.bunTarget}`, "--no-compile-autoload-dotenv", "--no-compile-autoload-bunfig", + // Keep @akua-dev/* as a real runtime import instead of bundling + // it: @akua-dev/native's platform .node binding is a binary file + // the bundler cannot inline. The compiled binary resolves it via + // process.execPath at runtime (src/runtime/services-live.ts). + "--external", + "@akua-dev/*", `--outfile=${binaryPath}`, ]); const bytes = yield* attempt("read compiled executable", () => @@ -767,10 +773,15 @@ function stagePackageRuntime( const nativeDestination = join(scopeRoot, "native"); const archiveFiles: string[] = []; const runtimeDirectories = new Set(); - for (const packageName of ["native", "native-engines"]) { + for (const packageName of ["native", "native-engines", "sdk"]) { const manifest = yield* readPackageManifest(packageRoot, packageName); const declaredFiles = yield* packageManifestFiles(manifest, packageName); - for (const file of ["package.json", ...declaredFiles]) { + const expandedFiles = yield* expandPackageManifestFiles( + packageRoot, + packageName, + declaredFiles, + ); + for (const file of ["package.json", ...expandedFiles]) { const destination = join(scopeRoot, packageName, file); yield* stagePackageRuntimeFile( join(packageRoot, packageName, file), @@ -856,6 +867,57 @@ function packageManifestFiles( ); } +// @akua-dev/sdk declares directory entries (e.g. "dist") in package.json's +// `files`, unlike native/native-engines' flat file lists — expand every +// directory entry into its individual files so each one gets staged. +function expandPackageManifestFiles( + packageRoot: string, + packageName: string, + files: string[], +): Effect.Effect { + return Effect.gen(function* () { + const expanded: string[] = []; + for (const file of files) { + const absolute = join(packageRoot, packageName, file); + const info = yield* attempt("stat package runtime file", () => + statSync(absolute), + ); + if (info.isDirectory()) { + expanded.push(...(yield* walkPackageDirectory(absolute, file))); + } else { + expanded.push(file); + } + } + return expanded; + }); +} + +function walkPackageDirectory( + absoluteDirectory: string, + relativeDirectory: string, +): Effect.Effect { + return Effect.gen(function* () { + const entries = yield* attempt("read package runtime directory", () => + readdirSync(absoluteDirectory, { withFileTypes: true }), + ); + const files: string[] = []; + for (const entry of entries) { + const relativePath = join(relativeDirectory, entry.name); + if (entry.isDirectory()) { + files.push( + ...(yield* walkPackageDirectory( + join(absoluteDirectory, entry.name), + relativePath, + )), + ); + } else { + files.push(relativePath); + } + } + return files; + }); +} + function packageManifestMain( value: unknown, packageName: string, diff --git a/src/runtime/services-live.ts b/src/runtime/services-live.ts index 5643638..b579be2 100644 --- a/src/runtime/services-live.ts +++ b/src/runtime/services-live.ts @@ -1,4 +1,5 @@ import { randomUUID } from "node:crypto"; +import { existsSync } from "node:fs"; import { chmod, mkdir, @@ -10,7 +11,7 @@ import { import { dirname, join } from "node:path"; import { Data, Duration, Effect, Layer } from "effect"; -import { execute as executePackageCommand } from "@akua-dev/sdk/execute"; +import type * as PackageExecuteModule from "@akua-dev/sdk/execute"; import { FetchHttpClient, HttpBody, @@ -180,10 +181,20 @@ export const PublicInputLive = Layer.succeed(PublicInput, { }); export const PackageCliLive = Layer.succeed(PackageCli, { - execute: (args) => Effect.try({ - try: () => executePackageCommand(args, { binName: "akua pkg" }), - catch: (cause) => new PackageCliFailure({ cause }), - }), + execute: (args) => + resolvePackageExecute.pipe( + Effect.flatMap((execute) => + Effect.try({ + try: () => execute(args, { binName: "akua pkg" }), + catch: (cause) => new PackageCliFailure({ cause }), + }), + ), + Effect.mapError((cause) => + cause instanceof PackageCliFailure + ? cause + : new PackageCliFailure({ cause }), + ), + ), }); export const CliLive: Layer.Layer = Layer.mergeAll( @@ -313,3 +324,50 @@ function isRecord(value: unknown): value is Record { function isNotFound(error: unknown): boolean { return error instanceof Error && "code" in error && error.code === "ENOENT"; } + +// `bun build --compile` cannot statically bundle @akua-dev/sdk/execute: it +// transitively loads @akua-dev/native's platform .node binding, which is a +// real binary file, not JS the bundler can inline. Marking the package +// --external (release-host-live.ts) makes the compiled binary keep a real +// runtime import instead of inlining it, but a compiled executable resolves +// bare specifiers against its own embedded virtual filesystem ($bunfs), not +// the real one — so it never sees the sidecar node_modules staged next to +// it on disk. process.execPath, unlike module-resolution base paths, does +// resolve to the executable's real filesystem location even when compiled, +// so an absolute dynamic import from there reaches the sidecar package. +// This is the one place in the CLI that needs a dynamic import for that +// reason; dev/test/build (no compiled sidecar present) fall back to normal +// package resolution. The result is cached for the process lifetime via +// Effect.cached rather than a hand-rolled mutable variable. +const resolvePackageExecute: Effect.Effect< + typeof PackageExecuteModule.execute, + PackageCliFailure +> = Effect.runSync( + Effect.cached( + Effect.sync(() => + join( + dirname(process.execPath), + "node_modules", + "@akua-dev", + "sdk", + "dist", + "execute.js", + ), + ).pipe( + Effect.flatMap((sidecarPath) => + Effect.sync(() => existsSync(sidecarPath)).pipe( + Effect.map((exists) => + exists ? sidecarPath : "@akua-dev/sdk/execute", + ), + ), + ), + Effect.flatMap((specifier) => + Effect.tryPromise({ + try: () => import(specifier), + catch: (cause) => new PackageCliFailure({ cause }), + }), + ), + Effect.map((loaded: typeof PackageExecuteModule) => loaded.execute), + ), + ), +); diff --git a/test/release.test.ts b/test/release.test.ts index 6b1113f..1d4dcb8 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -41,6 +41,13 @@ async function makePackageRuntimeFixture(root: string): Promise { packageName: "native-engines", files: ["index.js", "helm-engine.wasm", "kustomize-engine.wasm"], }, + { + // Real @akua-dev/sdk declares a directory entry ("dist") rather than + // a flat file list, unlike native/native-engines — this exercises + // directory expansion in packageManifestFiles. + packageName: "sdk", + files: ["dist", "README.md"], + }, ]; for (const runtimePackage of packages) { const { packageName, files } = runtimePackage; @@ -51,6 +58,15 @@ async function makePackageRuntimeFixture(root: string): Promise { `${JSON.stringify({ name: `@akua-dev/${packageName}`, files })}\n`, ); for (const file of files) { + if (packageName === "sdk" && file === "dist") { + await mkdir(join(directory, "dist"), { recursive: true }); + await writeFile(join(directory, "dist", "mod.js"), "sdk/dist/mod.js\n"); + await writeFile( + join(directory, "dist", "execute.js"), + "sdk/dist/execute.js\n", + ); + continue; + } await writeFile(join(directory, file), `${packageName}/${file}\n`); } } @@ -853,6 +869,82 @@ describe("release target contract", () => { } }); + test("stages the sdk package's runtime files, including directory-listed entries, into the archive", async () => { + const release = (await import("../scripts/release")) as Record< + string, + unknown + >; + const hostTargetId = release.hostTargetId as () => Effect.Effect< + string, + Error, + ReleaseHost + >; + const packageExistingExecutables = + release.packageExistingExecutables as (input: { + version: string; + outputDir: string; + binaries: Record; + packageRoot: string; + }) => Effect.Effect; + const artifactName = release.artifactName as ( + version: string, + target: { id: string }, + ) => string; + const root = await makeReleaseTempDir(); + + try { + const source = join(root, "akua-fixture"); + const outputDir = join(root, "release"); + const packageRoot = await makePackageRuntimeFixture(root); + await writeFile(source, "#!/bin/sh\nexit 0\n"); + await chmod(source, 0o755); + const { RELEASE_TARGETS: targets } = release as { + RELEASE_TARGETS: Array<{ id: string }>; + }; + runRelease( + packageExistingExecutables({ + version: "1.2.3", + outputDir, + binaries: Object.fromEntries( + targets.map((target) => [target.id, source]), + ), + packageRoot, + }), + ); + + // The staging directory is cleaned up after packaging, so verify the + // real produced archive contents (what an installer actually + // extracts), not the intermediate .staging tree. + const targetId = runRelease(hostTargetId()); + const target = targets.find((candidate) => candidate.id === targetId); + if (!target) throw new Error(`Unknown host target: ${targetId}`); + const archivePath = join(outputDir, artifactName("1.2.3", target)); + const extractDir = join(root, "extracted"); + await mkdir(extractDir, { recursive: true }); + const extract = Bun.spawnSync({ + cmd: ["tar", "-xzf", archivePath, "-C", extractDir], + stderr: "pipe", + }); + expect(extract.exitCode).toBe(0); + + const sdkDir = join(extractDir, "node_modules", "@akua-dev", "sdk"); + expect(await readFile(join(sdkDir, "dist", "mod.js"), "utf8")).toBe( + "sdk/dist/mod.js\n", + ); + expect(await readFile(join(sdkDir, "dist", "execute.js"), "utf8")).toBe( + "sdk/dist/execute.js\n", + ); + expect(await readFile(join(sdkDir, "README.md"), "utf8")).toBe( + "sdk/README.md\n", + ); + expect(await readFile(join(sdkDir, "package.json"), "utf8")).toContain( + '"@akua-dev/sdk"', + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + test("rejects an install-smoke executable whose longer version contains the expected version", async () => { const release = (await import("../scripts/release")) as Record< string, From cd29b29dd48d6049b4796daac7a1a55cba3e937d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robin=20Br=C3=A4mer?= <22003767+robinbraemer@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:43:00 +0200 Subject: [PATCH 2/2] fix(release): run linux-x64 install-smoke on a GitHub-hosted runner Rationale: every other release-matrix leg (darwin-arm64, darwin-x64, linux-arm64, windows-x64) already runs install-smoke on a GitHub-hosted runner; linux-x64 was the sole exception, pinned to the self-hosted akua-x64-ci-v2 pool. That pool's shared egress IP has been getting codeload.github.com 429-rate-limited under concurrent CI load across akua-dev repos, failing this exact job 3 times in a row on PR #44 while every sibling leg passed cleanly. The smoke step only needs a generic Linux x64 environment to extract and run an already-built artifact, so there's no correctness reason to keep it on the flaky self-hosted pool. Tested: bun test test/release.test.ts (23 pass) --- scripts/runtime/release-services.ts | 8 +++++++- test/release.test.ts | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scripts/runtime/release-services.ts b/scripts/runtime/release-services.ts index 68bcd9a..e5e00c1 100644 --- a/scripts/runtime/release-services.ts +++ b/scripts/runtime/release-services.ts @@ -69,7 +69,13 @@ export const RELEASE_TARGETS: readonly ReleaseTarget[] = [ archive: "tar.gz", executable: "akua", bindingPackage: "native-linux-x64-gnu", - runner: "akua-x64-ci-v2", + // GitHub-hosted, matching every other leg in this matrix. The + // self-hosted akua-x64-ci-v2 pool's shared egress IP gets + // codeload.github.com 429-rate-limited under concurrent CI load + // (observed 3x in a row on this exact job), and this leg only + // needs a generic Linux x64 execution environment to smoke-test + // an already-built artifact. + runner: "ubuntu-24.04", homebrew: { os: "linux", arch: "intel" }, }, { diff --git a/test/release.test.ts b/test/release.test.ts index 1d4dcb8..754d231 100644 --- a/test/release.test.ts +++ b/test/release.test.ts @@ -200,7 +200,7 @@ describe("release target contract", () => { archive: "tar.gz", executable: "akua", bindingPackage: "native-linux-x64-gnu", - runner: "akua-x64-ci-v2", + runner: "ubuntu-24.04", homebrew: { os: "linux", arch: "intel" }, }, { @@ -230,7 +230,7 @@ describe("release target contract", () => { { target: "darwin-arm64", runner: "macos-15" }, { target: "darwin-x64", runner: "macos-15-intel" }, { target: "linux-arm64", runner: "ubuntu-24.04-arm" }, - { target: "linux-x64", runner: "akua-x64-ci-v2" }, + { target: "linux-x64", runner: "ubuntu-24.04" }, { target: "windows-x64", runner: "windows-2025" }, ], });