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
7 changes: 7 additions & 0 deletions .changeset/sdk-node-engines-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@openagentpack/sdk": patch
---

Lower the SDK `engines` floor from Node `>=22` to `>=18.17.0`.

The SDK runtime only relies on APIs available since Node 18.17 (`node:fs`/`path`/`crypto`/`os`, global `fetch`/`FormData`, `structuredClone`, web streams), so the previous floor was a repository baseline rather than a runtime requirement. The tsup build target is pinned to `es2022` so emitted syntax stays parseable on the new floor, and CI now runs an SDK-only packed-install smoke on Node 18 and 20 (`smoke-packed.ts --sdk-only`) to enforce the contract. The `@openagentpack/cli` and `@openagentpack/playground` packages keep their Node `>=22` requirement.
25 changes: 21 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,23 @@ jobs:
run: bun scripts/verify.ts full --step "${{ matrix.step }}"

package-compatibility:
name: Package compatibility / Node ${{ matrix.node }}
name: Package compatibility / Node ${{ matrix.node }}${{ matrix.scope == 'sdk' && ' (sdk-only)' || '' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node: [22, 24]
include:
# Full consumer smoke (sdk + playground + cli) on the repo baseline.
- node: 22
scope: all
- node: 24
scope: all
# SDK-only smoke on the sdk engines floor (>=18.17.0): cli/playground
# require Node >=22, so only the sdk tarball is installed and imported.
- node: 18.17.0
scope: sdk
- node: 20
scope: sdk
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
Expand All @@ -68,8 +79,14 @@ jobs:
node-version: ${{ matrix.node }}
package-manager-cache: false
- run: bun install --frozen-lockfile
- run: bun scripts/verify.ts release --step build-packages
- run: bun scripts/verify.ts release --step package-smoke
- if: matrix.scope == 'all'
run: |
bun scripts/verify.ts release --step build-packages
bun scripts/verify.ts release --step package-smoke
- if: matrix.scope == 'sdk'
run: |
bun run build:sdk
bun scripts/release/smoke-packed.ts --sdk-only

gate:
name: Gate
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"url": "https://github.com/modelstudioai/OpenAgentPack/issues"
},
"engines": {
"node": ">=22"
"node": ">=18.17.0"
},
"type": "module",
"main": "./dist/index.js",
Expand Down
4 changes: 3 additions & 1 deletion packages/sdk/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@ export default defineConfig({
},
},
clean: true,
target: "esnext",
// Keep the emitted syntax parseable by Node 18.17 (the package's engines
// floor): esnext would let future syntax (e.g. `using`) leak into dist.
target: "es2022",
});
9 changes: 7 additions & 2 deletions scripts/open-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,16 @@ describe("open-source repository invariants", () => {
});

test("public packages require a maintained Node.js baseline", () => {
for (const pkg of ["sdk", "playground", "cli"]) {
const nodeBaselines: Record<string, string> = {
sdk: ">=18.17.0",
playground: ">=22",
cli: ">=22",
};
for (const [pkg, expectedNodeRange] of Object.entries(nodeBaselines)) {
const manifest = JSON.parse(readFileSync(resolve(root, `packages/${pkg}/package.json`), "utf8")) as {
engines?: { node?: string };
};
expect(manifest.engines?.node).toBe(">=22");
expect(manifest.engines?.node).toBe(expectedNodeRange);
}
});

Expand Down
18 changes: 17 additions & 1 deletion scripts/release/smoke-packed.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { packedFilename } from "./smoke-packed.ts";
import { isSdkOnly, packedFilename, smokePackages } from "./smoke-packed.ts";

describe("npm pack output", () => {
test("accepts the npm 10 and 11 array format", () => {
Expand All @@ -14,3 +14,19 @@ describe("npm pack output", () => {
expect(() => packedFilename("[]", "sdk")).toThrow("Unexpected npm pack output for sdk");
});
});

describe("--sdk-only mode", () => {
test("detects the flag among CLI arguments", () => {
expect(isSdkOnly(["--sdk-only"])).toBe(true);
expect(isSdkOnly([])).toBe(false);
expect(isSdkOnly(["--verbose"])).toBe(false);
});

test("restricts the package set to sdk when enabled", () => {
expect(smokePackages(true)).toEqual(["sdk"]);
});

test("keeps the full package set when disabled", () => {
expect(smokePackages(false)).toEqual(["sdk", "playground", "cli"]);
});
});
51 changes: 34 additions & 17 deletions scripts/release/smoke-packed.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/**
* Pack the exact publish manifests, install them as an external npm consumer,
* then exercise every public SDK entry point plus the CLI and Playground bins.
*
* `--sdk-only` restricts the run to the SDK package: pack/install only the sdk
* tarball (still with --engine-strict) and execute the SDK entry-point imports.
* This is the mode CI uses on Node versions below the cli/playground engines
* floor (>=22) to enforce the SDK's own >=18.17.0 engines contract.
*/

import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
Expand All @@ -18,6 +23,14 @@ import {

const root = resolve(import.meta.dirname, "../..");

export function isSdkOnly(argv: readonly string[]): boolean {
return argv.includes("--sdk-only");
}

export function smokePackages(sdkOnly: boolean): readonly (typeof PACKAGES)[number][] {
return sdkOnly ? PACKAGES.filter((pkg) => pkg === "sdk") : PACKAGES;
}

type PackedPackage = { filename: string };

export function packedFilename(raw: string, pkg: string): string {
Expand All @@ -33,9 +46,9 @@ function run(command: string[], cwd: string, stdout: "inherit" | "pipe" = "inher
return stdout === "pipe" ? (result.stdout?.toString().trim() ?? "") : "";
}

function packPackages(destination: string): string[] {
function packPackages(destination: string, packages: readonly (typeof PACKAGES)[number][]): string[] {
const versions = workspaceVersions();
return PACKAGES.map((pkg) => {
return packages.map((pkg) => {
const pkgDir = join(root, "packages", pkg);
let originalLicense: string | undefined;
let originalManifest: string | undefined;
Expand Down Expand Up @@ -104,6 +117,8 @@ async function smokePlayground(consumer: string): Promise<void> {
}

async function main(): Promise<void> {
const sdkOnly = isSdkOnly(process.argv.slice(2));
const packages = smokePackages(sdkOnly);
const temporaryRoot = mkdtempSync(join(tmpdir(), "openagentpack-consumer-"));
try {
const tarballsDir = join(temporaryRoot, "tarballs");
Expand All @@ -112,7 +127,7 @@ async function main(): Promise<void> {
mkdirSync(consumer);
writeFileSync(join(consumer, "package.json"), '{"name":"agents-package-smoke","private":true,"type":"module"}\n');

const tarballs = packPackages(tarballsDir);
const tarballs = packPackages(tarballsDir, packages);
run(
[
"npm",
Expand All @@ -126,7 +141,7 @@ async function main(): Promise<void> {
],
consumer,
);
for (const pkg of PACKAGES) {
for (const pkg of packages) {
const license = readFileSync(join(consumer, `node_modules/@openagentpack/${pkg}/LICENSE`), "utf8");
if (license !== readFileSync(join(root, "LICENSE"), "utf8")) {
throw new Error(`${pkg} package is missing the repository license`);
Expand All @@ -143,21 +158,23 @@ async function main(): Promise<void> {
consumer,
);

const expectedVersion = JSON.parse(readFileSync(join(root, "packages/cli/package.json"), "utf8")) as {
version: string;
};
const cliVersion = run(
["node", join(consumer, "node_modules/@openagentpack/cli/dist/bin/agents.js"), "--version"],
consumer,
"pipe",
);
if (cliVersion !== expectedVersion.version) {
throw new Error(`Packed CLI version mismatch: expected ${expectedVersion.version}, received ${cliVersion}`);
}
if (!sdkOnly) {
const expectedVersion = JSON.parse(readFileSync(join(root, "packages/cli/package.json"), "utf8")) as {
version: string;
};
const cliVersion = run(
["node", join(consumer, "node_modules/@openagentpack/cli/dist/bin/agents.js"), "--version"],
consumer,
"pipe",
);
if (cliVersion !== expectedVersion.version) {
throw new Error(`Packed CLI version mismatch: expected ${expectedVersion.version}, received ${cliVersion}`);
}

await smokePlayground(consumer);
await smokePlayground(consumer);
}
const nodeVersion = run(["node", "--version"], consumer, "pipe");
console.log(`✓ Packed packages install and run under ${nodeVersion}`);
console.log(`✓ Packed ${sdkOnly ? "SDK package installs" : "packages install"} and run under ${nodeVersion}`);
} finally {
rmSync(temporaryRoot, { recursive: true, force: true });
}
Expand Down