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
66 changes: 64 additions & 2 deletions scripts/runtime/release-host-live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () =>
Expand Down Expand Up @@ -767,10 +773,15 @@ function stagePackageRuntime(
const nativeDestination = join(scopeRoot, "native");
const archiveFiles: string[] = [];
const runtimeDirectories = new Set<string>();
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),
Expand Down Expand Up @@ -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<string[], ReleaseFailure> {
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<string[], ReleaseFailure> {
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,
Expand Down
8 changes: 7 additions & 1 deletion scripts/runtime/release-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
},
{
Expand Down
68 changes: 63 additions & 5 deletions src/runtime/services-live.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import {
chmod,
mkdir,
Expand All @@ -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,
Expand Down Expand Up @@ -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<CliServices> = Layer.mergeAll(
Expand Down Expand Up @@ -313,3 +324,50 @@ function isRecord(value: unknown): value is Record<string, unknown> {
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),
),
),
);
96 changes: 94 additions & 2 deletions test/release.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ async function makePackageRuntimeFixture(root: string): Promise<string> {
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;
Expand All @@ -51,6 +58,15 @@ async function makePackageRuntimeFixture(root: string): Promise<string> {
`${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`);
}
}
Expand Down Expand Up @@ -184,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" },
},
{
Expand Down Expand Up @@ -214,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" },
],
});
Expand Down Expand Up @@ -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<string, string>;
packageRoot: string;
}) => Effect.Effect<void, Error>;
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,
Expand Down
Loading