build(client): make @openclaw/gateway-client publishable to npm (#111707)

This commit is contained in:
Peter Steinberger
2026-07-20 00:23:36 -07:00
committed by GitHub
parent 3e14caa2bd
commit a971188a6d
14 changed files with 374 additions and 25 deletions
+53 -2
View File
@@ -204,7 +204,7 @@ jobs:
- name: Pack publishable core packages
id: core_package_tarballs
env:
CORE_PACKAGE_DIRS: packages/ai packages/gateway-protocol
CORE_PACKAGE_DIRS: packages/ai packages/gateway-protocol packages/gateway-client
run: |
set -euo pipefail
PACKAGE_VERSION="$(node -p "require('./package.json').version")"
@@ -386,6 +386,7 @@ jobs:
set -euo pipefail
AI_TARBALL_PATH=""
GATEWAY_PROTOCOL_TARBALL_PATH=""
GATEWAY_CLIENT_TARBALL_PATH=""
if [[ -n "$CORE_PACKAGE_TARBALL_DIR" ]]; then
mapfile -t AI_TARBALLS < <(find "$CORE_PACKAGE_TARBALL_DIR" -maxdepth 1 -type f -name 'openclaw-ai-*.tgz' -print | sort)
if node -e 'const pkg = require("./package.json"); process.exit(pkg.dependencies?.["@openclaw/ai"] ? 0 : 1)'; then
@@ -410,6 +411,17 @@ jobs:
echo "Frozen target without a publishable @openclaw/gateway-protocol package contains unexpected artifacts." >&2
exit 1
fi
mapfile -t GATEWAY_CLIENT_TARBALLS < <(find "$CORE_PACKAGE_TARBALL_DIR" -maxdepth 1 -type f -name 'openclaw-gateway-client-*.tgz' -print | sort)
if [[ -f packages/gateway-client/package.json ]] && node -e 'const pkg = require("./packages/gateway-client/package.json"); process.exit(pkg.openclaw?.release?.publishToNpm === true ? 0 : 1)'; then
if [[ "${#GATEWAY_CLIENT_TARBALLS[@]}" -ne 1 ]]; then
echo "Expected exactly one prepared @openclaw/gateway-client tarball, found ${#GATEWAY_CLIENT_TARBALLS[@]}." >&2
exit 1
fi
GATEWAY_CLIENT_TARBALL_PATH="${GATEWAY_CLIENT_TARBALLS[0]}"
elif [[ "${#GATEWAY_CLIENT_TARBALLS[@]}" -ne 0 ]]; then
echo "Frozen target without a publishable @openclaw/gateway-client package contains unexpected artifacts." >&2
exit 1
fi
fi
PACK_OUTPUT="$RUNNER_TEMP/npm-pack-output.txt"
pnpm pack --json 2>&1 | tee "$PACK_OUTPUT"
@@ -508,6 +520,9 @@ jobs:
GATEWAY_PROTOCOL_TARBALL_NAME=""
GATEWAY_PROTOCOL_TARBALL_SHA256=""
GATEWAY_PROTOCOL_PACKAGE_VERSION=""
GATEWAY_CLIENT_TARBALL_NAME=""
GATEWAY_CLIENT_TARBALL_SHA256=""
GATEWAY_CLIENT_PACKAGE_VERSION=""
ARTIFACT_DIR="$RUNNER_TEMP/openclaw-npm-preflight"
rm -rf "$ARTIFACT_DIR"
mkdir -p "$ARTIFACT_DIR"
@@ -536,6 +551,18 @@ jobs:
exit 1
fi
fi
if [[ -n "$GATEWAY_CLIENT_TARBALL_PATH" ]]; then
GATEWAY_CLIENT_TARBALL_NAME="$(basename "$GATEWAY_CLIENT_TARBALL_PATH")"
GATEWAY_CLIENT_TARBALL_SHA256="$(sha256sum "$GATEWAY_CLIENT_TARBALL_PATH" | awk '{print $1}')"
GATEWAY_CLIENT_PACKAGE_VERSION="$(
tar -xOf "$GATEWAY_CLIENT_TARBALL_PATH" package/package.json |
node -e 'let raw = ""; process.stdin.on("data", (chunk) => (raw += chunk)); process.stdin.on("end", () => process.stdout.write(JSON.parse(raw).version));'
)"
if [[ "$GATEWAY_CLIENT_PACKAGE_VERSION" != "$PACKAGE_VERSION" ]]; then
echo "Prepared @openclaw/gateway-client version ${GATEWAY_CLIENT_PACKAGE_VERSION} does not match openclaw ${PACKAGE_VERSION}." >&2
exit 1
fi
fi
if [[ -n "$CORE_PACKAGE_TARBALL_DIR" ]]; then
cp "$CORE_PACKAGE_TARBALL_DIR"/*.tgz "$ARTIFACT_DIR/"
cp "$CORE_PACKAGE_TARBALL_DIR/SHA256SUMS" "$ARTIFACT_DIR/core-packages-SHA256SUMS"
@@ -544,7 +571,7 @@ jobs:
printf '%s\n' "$RELEASE_TAG" > "$ARTIFACT_DIR/release-tag.txt"
printf '%s\n' "$RELEASE_SHA" > "$ARTIFACT_DIR/release-sha.txt"
printf '%s\n' "$RELEASE_NPM_DIST_TAG" > "$ARTIFACT_DIR/release-npm-dist-tag.txt"
ARTIFACT_DIR="$ARTIFACT_DIR" RELEASE_TAG="$RELEASE_TAG" RELEASE_SHA="$RELEASE_SHA" RELEASE_NPM_DIST_TAG="$RELEASE_NPM_DIST_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" TARBALL_NAME="$TARBALL_NAME" TARBALL_SHA256="$TARBALL_SHA256" AI_PACKAGE_VERSION="$AI_PACKAGE_VERSION" AI_TARBALL_NAME="$AI_TARBALL_NAME" AI_TARBALL_SHA256="$AI_TARBALL_SHA256" GATEWAY_PROTOCOL_PACKAGE_VERSION="$GATEWAY_PROTOCOL_PACKAGE_VERSION" GATEWAY_PROTOCOL_TARBALL_NAME="$GATEWAY_PROTOCOL_TARBALL_NAME" GATEWAY_PROTOCOL_TARBALL_SHA256="$GATEWAY_PROTOCOL_TARBALL_SHA256" node <<'NODE'
ARTIFACT_DIR="$ARTIFACT_DIR" RELEASE_TAG="$RELEASE_TAG" RELEASE_SHA="$RELEASE_SHA" RELEASE_NPM_DIST_TAG="$RELEASE_NPM_DIST_TAG" PACKAGE_VERSION="$PACKAGE_VERSION" TARBALL_NAME="$TARBALL_NAME" TARBALL_SHA256="$TARBALL_SHA256" AI_PACKAGE_VERSION="$AI_PACKAGE_VERSION" AI_TARBALL_NAME="$AI_TARBALL_NAME" AI_TARBALL_SHA256="$AI_TARBALL_SHA256" GATEWAY_PROTOCOL_PACKAGE_VERSION="$GATEWAY_PROTOCOL_PACKAGE_VERSION" GATEWAY_PROTOCOL_TARBALL_NAME="$GATEWAY_PROTOCOL_TARBALL_NAME" GATEWAY_PROTOCOL_TARBALL_SHA256="$GATEWAY_PROTOCOL_TARBALL_SHA256" GATEWAY_CLIENT_PACKAGE_VERSION="$GATEWAY_CLIENT_PACKAGE_VERSION" GATEWAY_CLIENT_TARBALL_NAME="$GATEWAY_CLIENT_TARBALL_NAME" GATEWAY_CLIENT_TARBALL_SHA256="$GATEWAY_CLIENT_TARBALL_SHA256" node <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const manifest = {
@@ -577,6 +604,16 @@ jobs:
},
]
: []),
...(process.env.GATEWAY_CLIENT_TARBALL_NAME
? [
{
packageName: "@openclaw/gateway-client",
packageVersion: process.env.GATEWAY_CLIENT_PACKAGE_VERSION,
tarballName: process.env.GATEWAY_CLIENT_TARBALL_NAME,
tarballSha256: process.env.GATEWAY_CLIENT_TARBALL_SHA256,
},
]
: []),
],
dependencyTarballs: process.env.AI_TARBALL_NAME
? [
@@ -1046,6 +1083,7 @@ jobs:
EXPECTED_PACKAGE_VERSION="$(node -p "require('./package.json').version")"
SEEN_AI_TARBALL="false"
SEEN_GATEWAY_PROTOCOL_TARBALL="false"
SEEN_GATEWAY_CLIENT_TARBALL="false"
while IFS=$'\t' read -r package_name package_version tarball_name tarball_sha256; do
if [[ -z "$tarball_name" || "$tarball_name" != "$(basename -- "$tarball_name")" || "$tarball_name" != *.tgz ]]; then
echo "Prepared core package tarball name is unsafe: $tarball_name" >&2
@@ -1060,6 +1098,10 @@ jobs:
[[ "$SEEN_GATEWAY_PROTOCOL_TARBALL" == "false" ]] || { echo "Prepared core package manifest contains duplicate @openclaw/gateway-protocol metadata." >&2; exit 1; }
SEEN_GATEWAY_PROTOCOL_TARBALL="true"
;;
"@openclaw/gateway-client")
[[ "$SEEN_GATEWAY_CLIENT_TARBALL" == "false" ]] || { echo "Prepared core package manifest contains duplicate @openclaw/gateway-client metadata." >&2; exit 1; }
SEEN_GATEWAY_CLIENT_TARBALL="true"
;;
*)
echo "Prepared core package manifest contains unsupported package: $package_name" >&2
exit 1
@@ -1093,6 +1135,15 @@ jobs:
echo "Frozen target without a publishable @openclaw/gateway-protocol package contains unexpected artifacts." >&2
exit 1
fi
if [[ -f packages/gateway-client/package.json ]] && node -e 'const pkg = require("./packages/gateway-client/package.json"); process.exit(pkg.openclaw?.release?.publishToNpm === true ? 0 : 1)'; then
if [[ "$SEEN_GATEWAY_CLIENT_TARBALL" != "true" ]]; then
echo "Prepared @openclaw/gateway-client tarball is missing from the manifest." >&2
exit 1
fi
elif jq -e '.corePackageTarballs[] | select(.packageName == "@openclaw/gateway-client")' "$MANIFEST_FILE" >/dev/null || find preflight-tarball -maxdepth 1 -type f -name 'openclaw-gateway-client-*.tgz' -print -quit | grep -q .; then
echo "Frozen target without a publishable @openclaw/gateway-client package contains unexpected artifacts." >&2
exit 1
fi
echo "tarball_name=$ARTIFACT_TARBALL_NAME" >> "$GITHUB_OUTPUT"
echo "tarball_sha256=$ARTIFACT_TARBALL_SHA256" >> "$GITHUB_OUTPUT"
echo "tarball_path=$ARTIFACT_TARBALL_PATH" >> "$GITHUB_OUTPUT"
+7
View File
@@ -0,0 +1,7 @@
# Changelog
## Unreleased
- Publish the reference Gateway WebSocket client for the first time.
- Record the client extraction from the Gateway protocol implementation in #87797.
- Support the current Gateway wire protocol, protocol v4.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 OpenClaw Foundation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+134
View File
@@ -0,0 +1,134 @@
# `@openclaw/gateway-client`
Reference WebSocket client for the OpenClaw Gateway protocol. It provides the
connection state machine used by OpenClaw's own Node and browser clients:
challenge-based authentication, typed protocol frames, request correlation,
timeouts, reconnect backoff, device-token handling, and event delivery.
The current wire protocol is version 4. General clients must negotiate v4 with
`minProtocol: 4` and `maxProtocol: 4`. See the
[Gateway protocol specification](https://docs.openclaw.ai/gateway/protocol) for
the complete handshake, authentication, role, scope, and method contracts.
## Versioning
Package versions follow the OpenClaw calendar release train: `YYYY.M.PATCH`,
including the OpenClaw prerelease suffix when applicable. The package version is
separate from the negotiated wire protocol number.
## Install
```bash
npm install @openclaw/gateway-client @openclaw/gateway-protocol
```
Node consumers use the `ws` transport included as a runtime dependency. Browser
consumers provide their platform WebSocket through the browser-safe protocol
client surface.
## Entry points
- `@openclaw/gateway-client` exports the Node `GatewayClient`, device-auth
helpers, readiness helpers, and timeout utilities.
- `@openclaw/gateway-client/browser` exports the browser-safe protocol client,
browser device-auth lifecycle, reconnect policy, and lightweight protocol
constants. Its module graph does not import Node built-ins or `ws`.
- `@openclaw/gateway-client/readiness` exports helpers that delay client startup
until the event loop can process Gateway IO.
- `@openclaw/gateway-client/timeouts` exports timeout constants and safe timer
resolution helpers.
## Node quickstart
```ts
import { GatewayClient } from "@openclaw/gateway-client";
import { PROTOCOL_VERSION } from "@openclaw/gateway-protocol/version";
const connected = Promise.withResolvers<void>();
const client = new GatewayClient({
url: "ws://127.0.0.1:18789",
token: process.env.OPENCLAW_GATEWAY_TOKEN,
minProtocol: PROTOCOL_VERSION, // v4
maxProtocol: PROTOCOL_VERSION, // v4
onHelloOk: () => connected.resolve(),
onConnectError: (error) => connected.reject(error),
onEvent: (event) => {
console.log(event.event, event.payload);
},
});
client.start();
await connected.promise;
const status = await client.request("status", {});
console.log(status);
client.stop();
```
The client waits for the Gateway's `connect.challenge` event before sending its
`connect` request. It includes the challenge nonce in device authentication and
does not fall back to a pre-challenge handshake. `onHelloOk` fires only after the
Gateway accepts the v4 connection, so requests should wait for that callback.
For remote connections, use `wss://`. Plaintext `ws://` is allowed by default
only for loopback addresses. Authentication material and Gateway traffic must
not cross an untrusted network without transport security.
## Browser clients
Import `@openclaw/gateway-client/browser` when the host owns the WebSocket
adapter and device-key storage. The browser entry includes
`GatewayProtocolClient` and `GatewayBrowserDeviceAuthLifecycle`; it deliberately
omits the Node transport, TLS fingerprint handling, and private-network address
policy.
The host is responsible for:
- creating a `GatewayProtocolSocket` adapter around the browser WebSocket;
- loading and storing browser device identity and issued device tokens;
- signing the challenge-bound device payload;
- supplying the client identity, role, scopes, and authentication selection;
- choosing close and reconnect behavior for product-specific errors.
The shared protocol client still owns frame parsing, request correlation,
challenge ordering, timeout cleanup, sequence-gap detection, and reconnect
scheduling.
## Defaults and reconnect behavior
The Node client starts with a 30 second request timeout, a 15 second
connect-challenge timeout, and exponential reconnect delays from 1 second to 30
seconds with a multiplier of 2. Server-provided startup retry hints may override
the next delay.
The canonical defaults table and the server policy fields that can replace
pre-handshake values are documented in the
[Gateway protocol specification](https://docs.openclaw.ai/gateway/protocol#client-constants).
Use the `./timeouts` entry point when a host must align readiness or watchdog
budgets with these defaults. Use the `./readiness` entry point when startup must
wait for an event-loop probe before opening the socket.
## Bundled internals
The retry supervisor and the small `@openclaw/net-policy/ip` implementation are
inlined into the published JavaScript and declarations. They are implementation
details, not public exports or supported API surfaces. `ipaddr.js` remains an
external dependency because the inlined IP helpers use its public runtime and
types.
`ws`, `@openclaw/gateway-protocol`, and `ipaddr.js` remain external in the
published distribution. Consumers should import protocol types and constants
from `@openclaw/gateway-protocol`, not from bundled implementation paths.
## Contract notes
- The client is inert at module import and construction time. `start()` opens
the socket; `stop()` closes it and rejects pending requests.
- A request uses `request(method, params)` after `hello-ok`. Passing
`timeoutMs: null` creates an intentionally unbounded request.
- Device identity persistence, signing, proxy routing, TLS formatting, and
logging stay host-owned through `GatewayClientHostDeps`.
- Protocol changes are additive first. Incompatible changes require an explicit
wire-version decision and coordinated server/client follow-through.
+50
View File
@@ -0,0 +1,50 @@
{
"name": "@openclaw/gateway-client",
"version": "2026.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@openclaw/gateway-client",
"version": "2026.7.2",
"license": "MIT",
"dependencies": {
"ipaddr.js": "2.4.0",
"ws": "8.21.0"
},
"engines": {
"node": ">=22.19.0"
}
},
"node_modules/ipaddr.js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.4.0.tgz",
"integrity": "sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==",
"license": "MIT",
"engines": {
"node": ">= 10"
}
},
"node_modules/ws": {
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
+37 -6
View File
@@ -1,10 +1,31 @@
{
"name": "@openclaw/gateway-client",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
"version": "2026.7.2",
"description": "Reference WebSocket client for the OpenClaw Gateway protocol",
"keywords": [
"client",
"gateway",
"openclaw",
"websocket"
],
"homepage": "https://github.com/openclaw/openclaw#readme",
"bugs": {
"url": "https://github.com/openclaw/openclaw/issues"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/openclaw/openclaw.git",
"directory": "packages/gateway-client"
},
"files": [
"dist",
"npm-shrinkwrap.json",
"LICENSE",
"README.md",
"CHANGELOG.md"
],
"sideEffects": false,
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
@@ -35,8 +56,18 @@
},
"dependencies": {
"@openclaw/gateway-protocol": "workspace:*",
"@openclaw/net-policy": "workspace:*",
"@openclaw/retry": "workspace:*",
"ipaddr.js": "2.4.0",
"ws": "8.21.0"
},
"engines": {
"node": ">=22.19.0"
},
"publishConfig": {
"access": "public"
},
"openclaw": {
"release": {
"publishToNpm": true
}
}
}
+3 -6
View File
@@ -2090,12 +2090,9 @@ importers:
'@openclaw/gateway-protocol':
specifier: workspace:*
version: link:../gateway-protocol
'@openclaw/net-policy':
specifier: workspace:*
version: link:../net-policy
'@openclaw/retry':
specifier: workspace:*
version: link:../retry
ipaddr.js:
specifier: 2.4.0
version: 2.4.0
ws:
specifier: 8.21.0
version: 8.21.0
+14 -3
View File
@@ -477,7 +477,11 @@ export function resolveReleaseCheckLocalPackageTarballs(
const gatewayProtocolTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/gateway-protocol",
);
const recognizedTarballs = aiTarballs.length + gatewayProtocolTarballs.length;
const gatewayClientTarballs = tarballs.filter(
(tarballPath) => localPackageNameForTarball(tarballPath) === "@openclaw/gateway-client",
);
const recognizedTarballs =
aiTarballs.length + gatewayProtocolTarballs.length + gatewayClientTarballs.length;
if (recognizedTarballs !== tarballs.length) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} contains an unsupported package tarball.`,
@@ -487,9 +491,13 @@ export function resolveReleaseCheckLocalPackageTarballs(
const aiTarballRequirement = requiresAi
? "exactly one @openclaw/ai tarball"
: "no @openclaw/ai tarballs";
if (aiTarballs.length !== expectedAiTarballs || gatewayProtocolTarballs.length > 1) {
if (
aiTarballs.length !== expectedAiTarballs ||
gatewayProtocolTarballs.length > 1 ||
gatewayClientTarballs.length > 1
) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} must contain ${aiTarballRequirement} and at most one @openclaw/gateway-protocol tarball; found ${aiTarballs.length} and ${gatewayProtocolTarballs.length}.`,
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} must contain ${aiTarballRequirement}, at most one @openclaw/gateway-protocol tarball, and at most one @openclaw/gateway-client tarball; found ${aiTarballs.length}, ${gatewayProtocolTarballs.length}, and ${gatewayClientTarballs.length}.`,
);
}
return tarballs;
@@ -510,6 +518,9 @@ function localPackageNameForTarball(tarballPath: string): string | undefined {
if (/^openclaw-gateway-protocol(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/gateway-protocol";
}
if (/^openclaw-gateway-client(?:-.+)?\.tgz$/.test(filename)) {
return "@openclaw/gateway-client";
}
return undefined;
}
+5 -1
View File
@@ -25,7 +25,11 @@ type SyncPluginVersionsOptions = {
};
const OPENCLAW_VERSION_RANGE_RE = /^>=\d{4}\.\d{1,2}\.\d{1,2}(?:[-.][^"\s]+)?$/u;
const VERSION_ALIGNED_PACKAGE_DIRS = ["packages/ai", "packages/gateway-protocol"] as const;
const VERSION_ALIGNED_PACKAGE_DIRS = [
"packages/ai",
"packages/gateway-client",
"packages/gateway-protocol",
] as const;
function syncOpenClawDependencyRange(
deps: Record<string, string> | undefined,
@@ -497,6 +497,15 @@ describe("generate-npm-shrinkwrap", () => {
).toEqual(["packages/gateway-protocol"]);
});
it("targets the changed publishable gateway client shrinkwrap", () => {
expect(
shrinkwrapPackageDirsForChangedPaths([
"packages/gateway-client/package.json",
"packages/gateway-client/npm-shrinkwrap.json",
]).map(repoRelativePath),
).toEqual(["packages/gateway-client"]);
});
it("targets changed tracked shrinkwraps for private packages", () => {
expect(
shrinkwrapPackageDirsForChangedPaths(["extensions/vault/package.json"]).map(repoRelativePath),
@@ -509,6 +518,7 @@ describe("generate-npm-shrinkwrap", () => {
);
expect(packageDirs).toContain("");
expect(packageDirs).toContain("packages/gateway-client");
expect(packageDirs).toContain("packages/gateway-protocol");
expect(packageDirs).toContain("extensions/acpx");
expect(packageDirs).toContain("extensions/vault");
@@ -257,4 +257,21 @@ describe("minimal npm extended-stable workflow", () => {
"find preflight-tarball -type f -name '*.tgz'",
);
});
it("publishes gateway packages in manifest order before the root package", () => {
const parsed = workflow();
const preflightPack = step(
parsed.jobs?.preflight_openclaw_npm,
"Pack publishable core packages",
);
const publish = step(parsed.jobs?.publish_openclaw_npm, "Publish");
expect(preflightPack.env?.CORE_PACKAGE_DIRS).toBe(
"packages/ai packages/gateway-protocol packages/gateway-client",
);
expect(readFileSync(workflowPath, "utf8")).toContain('packageName: "@openclaw/gateway-client"');
expect(publish.run).toContain(".corePackageTarballs[] | [.packageName, .tarballName] | @tsv");
expect(publish.run).toContain(
'bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"',
);
});
});
@@ -3679,9 +3679,13 @@ describe("package artifact reuse", () => {
expect(npmWorkflow).toContain("dependencyTarballs: process.env.AI_TARBALL_NAME");
expect(npmWorkflow).toContain('packageName: "@openclaw/ai"');
expect(npmWorkflow).toContain('packageName: "@openclaw/gateway-protocol"');
expect(npmWorkflow).toContain("CORE_PACKAGE_DIRS: packages/ai packages/gateway-protocol");
expect(npmWorkflow).toContain('packageName: "@openclaw/gateway-client"');
expect(npmWorkflow).toContain(
"CORE_PACKAGE_DIRS: packages/ai packages/gateway-protocol packages/gateway-client",
);
expect(npmWorkflow).toContain("AI_TARBALL_SHA256");
expect(npmWorkflow).toContain("GATEWAY_PROTOCOL_TARBALL_SHA256");
expect(npmWorkflow).toContain("GATEWAY_CLIENT_TARBALL_SHA256");
expect(npmWorkflow).toContain("does not match openclaw");
expect(npmWorkflow).toContain("Frozen target does not depend on @openclaw/ai");
expect(npmWorkflow).toContain("core-packages-SHA256SUMS");
+17 -5
View File
@@ -35,10 +35,12 @@ describe("release-check", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
writeFileSync(join(root, "openclaw-ai-2026.6.33.tgz"), "fixture");
writeFileSync(join(root, "openclaw-gateway-client-2026.6.33.tgz"), "fixture");
writeFileSync(join(root, "openclaw-gateway-protocol-2026.6.33.tgz"), "fixture");
writeFileSync(join(root, "SHA256SUMS"), "fixture");
expect(resolveReleaseCheckLocalPackageTarballs(root)).toEqual([
join(root, "openclaw-ai-2026.6.33.tgz"),
join(root, "openclaw-gateway-client-2026.6.33.tgz"),
join(root, "openclaw-gateway-protocol-2026.6.33.tgz"),
]);
expect(resolveReleaseCheckLocalPackageTarballs(undefined)).toEqual([]);
@@ -47,12 +49,17 @@ describe("release-check", () => {
}
});
it("accepts a gateway-only core package directory when the root does not require AI", () => {
it("accepts gateway core packages when the root does not require AI", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
const gatewayTarball = join(root, "openclaw-gateway-protocol-2026.7.2.tgz");
const gatewayClientTarball = join(root, "openclaw-gateway-client-2026.7.2.tgz");
writeFileSync(gatewayTarball, "fixture");
expect(resolveReleaseCheckLocalPackageTarballs(root, false)).toEqual([gatewayTarball]);
writeFileSync(gatewayClientTarball, "fixture");
expect(resolveReleaseCheckLocalPackageTarballs(root, false)).toEqual([
gatewayClientTarball,
gatewayTarball,
]);
} finally {
rmSync(root, { recursive: true, force: true });
}
@@ -63,6 +70,7 @@ describe("release-check", () => {
try {
writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", [
"/tmp/openclaw-ai.tgz",
"/tmp/openclaw-gateway-client.tgz",
"/tmp/openclaw-gateway-protocol.tgz",
]);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
@@ -72,6 +80,7 @@ describe("release-check", () => {
expect(manifest.private).toBe(true);
expect(manifest.dependencies).toEqual({
"@openclaw/ai": "file:///tmp/openclaw-ai.tgz",
"@openclaw/gateway-client": "file:///tmp/openclaw-gateway-client.tgz",
"@openclaw/gateway-protocol": "file:///tmp/openclaw-gateway-protocol.tgz",
openclaw: "file:///tmp/openclaw.tgz",
});
@@ -80,19 +89,20 @@ describe("release-check", () => {
}
});
it("writes a gateway-only local project when the root does not require AI", () => {
it("writes a gateway-packages-only local project when the root does not require AI", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-install-test-"));
try {
writePackedTarballInstallManifest(
root,
"/tmp/openclaw.tgz",
["/tmp/openclaw-gateway-protocol.tgz"],
["/tmp/openclaw-gateway-client.tgz", "/tmp/openclaw-gateway-protocol.tgz"],
false,
);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
};
expect(manifest.dependencies).toEqual({
"@openclaw/gateway-client": "file:///tmp/openclaw-gateway-client.tgz",
"@openclaw/gateway-protocol": "file:///tmp/openclaw-gateway-protocol.tgz",
openclaw: "file:///tmp/openclaw.tgz",
});
@@ -128,7 +138,9 @@ describe("release-check", () => {
preparedDir,
"openclaw-gateway-protocol-2026.7.1-beta.3.tgz",
);
const gatewayClientTarball = join(preparedDir, "openclaw-gateway-client-2026.7.1-beta.3.tgz");
writeFileSync(preparedTarball, "fixture");
writeFileSync(gatewayClientTarball, "fixture");
writeFileSync(gatewayProtocolTarball, "fixture");
const tarballs = prepareReleaseCheckLocalPackageTarballs({
tmpRoot: root,
@@ -137,7 +149,7 @@ describe("release-check", () => {
throw new Error("workspace pack should not run");
},
});
expect(tarballs).toEqual([preparedTarball, gatewayProtocolTarball]);
expect(tarballs).toEqual([preparedTarball, gatewayClientTarball, gatewayProtocolTarball]);
} finally {
rmSync(root, { recursive: true, force: true });
}
+1 -1
View File
@@ -435,7 +435,7 @@ function shouldExternalizeGatewayProtocolDependency(id: string): boolean {
}
function shouldExternalizeGatewayClientDependency(id: string): boolean {
return ["ws", "@openclaw/net-policy", "@openclaw/gateway-protocol"].some(
return ["ws", "@openclaw/gateway-protocol"].some(
(dependency) => id === dependency || id.startsWith(`${dependency}/`),
);
}