diff --git a/.changeset/sdk-node-engines-floor.md b/.changeset/sdk-node-engines-floor.md new file mode 100644 index 0000000..ed1ad06 --- /dev/null +++ b/.changeset/sdk-node-engines-floor.md @@ -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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38a20d1..77b253e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 + scope: sdk + - node: 20 + scope: sdk steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 @@ -69,7 +80,10 @@ jobs: 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 package-smoke + - if: matrix.scope == 'sdk' + run: bun scripts/release/smoke-packed.ts --sdk-only gate: name: Gate diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 830f1e3..2f51f2d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: packages: read steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 - - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 + - uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 with: languages: javascript-typescript - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4 diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 50d3f53..dced960 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -20,7 +20,7 @@ "url": "https://github.com/modelstudioai/OpenAgentPack/issues" }, "engines": { - "node": ">=22" + "node": ">=18.17.0" }, "type": "module", "main": "./dist/index.js", diff --git a/packages/sdk/tsup.config.ts b/packages/sdk/tsup.config.ts index c84ba2f..a70449f 100644 --- a/packages/sdk/tsup.config.ts +++ b/packages/sdk/tsup.config.ts @@ -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", }); diff --git a/scripts/open-source.test.ts b/scripts/open-source.test.ts index 70eb45f..69efce5 100644 --- a/scripts/open-source.test.ts +++ b/scripts/open-source.test.ts @@ -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 = { + 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); } }); diff --git a/scripts/release/smoke-packed.test.ts b/scripts/release/smoke-packed.test.ts index f503a96..60ba157 100644 --- a/scripts/release/smoke-packed.test.ts +++ b/scripts/release/smoke-packed.test.ts @@ -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", () => { @@ -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"]); + }); +}); diff --git a/scripts/release/smoke-packed.ts b/scripts/release/smoke-packed.ts index f76dad0..3d592eb 100644 --- a/scripts/release/smoke-packed.ts +++ b/scripts/release/smoke-packed.ts @@ -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"; @@ -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 { @@ -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; @@ -104,6 +117,8 @@ async function smokePlayground(consumer: string): Promise { } async function main(): Promise { + const sdkOnly = isSdkOnly(process.argv.slice(2)); + const packages = smokePackages(sdkOnly); const temporaryRoot = mkdtempSync(join(tmpdir(), "openagentpack-consumer-")); try { const tarballsDir = join(temporaryRoot, "tarballs"); @@ -112,7 +127,7 @@ async function main(): Promise { 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", @@ -126,7 +141,7 @@ async function main(): Promise { ], 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`); @@ -143,21 +158,23 @@ async function main(): Promise { 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 }); }