feat(release): preflight all extended-stable npm packages (#101757)

* feat(release): preflight all extended-stable npm packages

* fix(release): install packed ai runtime in smoke

* fix(release): install sibling tarballs before core

* fix(release): verify unpublished tarballs locally

* fix(release): declare local preflight dependencies

* fix(release): pack workspace dependencies for smoke

* fix(release): accept pnpm pack artifact path

* fix(release): seed ai in sdk smoke

* fix(release): preserve prepublish verifier compatibility
This commit is contained in:
Kevin Lin 2026-07-08 08:01:00 -07:00 committed by GitHub
parent 9d7a6b5da6
commit 9eeebf7cb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 452 additions and 107 deletions

View file

@ -4,7 +4,7 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
tag: tag:
description: Release tag to publish, or a full 40-character workflow-branch commit SHA for validation-only preflight (for example v2026.3.22 or 0123456789abcdef0123456789abcdef01234567) description: Release tag to publish, or any full 40-character commit SHA for validation-only preflight (for example v2026.3.22 or 0123456789abcdef0123456789abcdef01234567)
required: true required: true
type: string type: string
preflight_only: preflight_only:
@ -208,19 +208,16 @@ jobs:
RELEASE_SHA=$(git rev-parse HEAD) RELEASE_SHA=$(git rev-parse HEAD)
RELEASE_BRANCH_REF="refs/remotes/origin/${WORKFLOW_REF_NAME}" RELEASE_BRANCH_REF="refs/remotes/origin/${WORKFLOW_REF_NAME}"
export RELEASE_SHA RELEASE_BRANCH_REF export RELEASE_SHA RELEASE_BRANCH_REF
# Fetch the workflow branch so merge-base ancestry checks keep working
# for older tagged commits contained in a release branch.
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then if [[ "${RELEASE_REF}" =~ ^[0-9a-fA-F]{40}$ ]]; then
BRANCH_SHA="$(git rev-parse "${RELEASE_BRANCH_REF}")"
if [[ "${RELEASE_SHA}" != "${BRANCH_SHA}" ]]; then
echo "Validation-only SHA mode only supports the current ${WORKFLOW_REF_NAME} HEAD." >&2
exit 1
fi
RELEASE_TAG="v$(node -p "require('./package.json').version")" RELEASE_TAG="v$(node -p "require('./package.json').version")"
export RELEASE_TAG export RELEASE_TAG
RELEASE_BRANCH_REF="${RELEASE_SHA}"
export RELEASE_BRANCH_REF
echo "Validation-only SHA mode: using synthetic release tag ${RELEASE_TAG} for package metadata checks." echo "Validation-only SHA mode: using synthetic release tag ${RELEASE_TAG} for package metadata checks."
else else
# Tagged release validation still proves the commit is contained in
# the workflow branch selected by the operator.
git fetch --no-tags origin "+refs/heads/${WORKFLOW_REF_NAME}:refs/remotes/origin/${WORKFLOW_REF_NAME}"
RELEASE_TAG="${RELEASE_REF}" RELEASE_TAG="${RELEASE_REF}"
export RELEASE_TAG export RELEASE_TAG
fi fi
@ -232,8 +229,58 @@ jobs:
# IF A CHECK CAN TAKE A LONG TIME, NEEDS LIVE CREDENTIALS, OR IS KNOWN TO BE FLAKY, # IF A CHECK CAN TAKE A LONG TIME, NEEDS LIVE CREDENTIALS, OR IS KNOWN TO BE FLAKY,
# IT BELONGS IN openclaw-release-checks.yml INSTEAD OF BLOCKING npm PUBLISH. # IT BELONGS IN openclaw-release-checks.yml INSTEAD OF BLOCKING npm PUBLISH.
- name: Verify release contents - name: Verify release contents
env:
OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR: ${{ steps.ai_runtime_tarballs.outputs.dir }}
run: pnpm release:check run: pnpm release:check
- name: Exercise all extended-stable plugin npm packages
id: plugin_npm_preflight
if: ${{ inputs.npm_dist_tag == 'extended-stable' }}
env:
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: extended-stable
run: |
set -euo pipefail
plan="$RUNNER_TEMP/plugin-npm-extended-stable-preflight.json"
artifact_dir="$RUNNER_TEMP/openclaw-plugin-npm-preflight"
rm -rf "$artifact_dir"
mkdir -p "$artifact_dir"
node --import tsx scripts/plugin-npm-release-plan.ts \
--selection-mode all-publishable \
--npm-dist-tag extended-stable > "$plan"
cp "$plan" "$artifact_dir/plugin-npm-release-plan.json"
mapfile -t package_dirs < <(jq -r '.all[].packageDir' "$plan")
if [[ "${#package_dirs[@]}" == "0" ]]; then
echo "Extended-stable plugin preflight resolved no publishable packages." >&2
exit 1
fi
runtime_args=()
for package_dir in "${package_dirs[@]}"; do
runtime_args+=(--package "$package_dir")
done
node scripts/check-plugin-npm-runtime-builds.mjs "${runtime_args[@]}"
for package_dir in "${package_dirs[@]}"; do
OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD=0 \
OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR="$artifact_dir" \
bash scripts/plugin-npm-publish.sh --pack "$package_dir" >/dev/null
done
(cd "$artifact_dir" && sha256sum ./*.tgz > SHA256SUMS)
package_count="$(jq -r '.all | length' "$plan")"
echo "dir=$artifact_dir" >> "$GITHUB_OUTPUT"
echo "Extended-stable plugin preflight exercised ${package_count} publishable packages."
echo "- Extended-stable plugin packages exercised: ${package_count}" >> "$GITHUB_STEP_SUMMARY"
- name: Upload extended-stable plugin npm packages
if: ${{ inputs.npm_dist_tag == 'extended-stable' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: openclaw-plugin-npm-preflight-${{ inputs.tag }}
path: ${{ steps.plugin_npm_preflight.outputs.dir }}
if-no-files-found: error
- name: Generate dependency release evidence - name: Generate dependency release evidence
id: dependency_evidence id: dependency_evidence
env: env:

View file

@ -74,12 +74,13 @@ export function validateExtendedStableNpmReleaseRequest(request) {
requireExtendedStableBypassTag(request.npmDistTag, bypassExtendedStableGuard); requireExtendedStableBypassTag(request.npmDistTag, bypassExtendedStableGuard);
const shaPreflight = const shaPreflight =
request.preflightOnly === true && /^[0-9a-f]{40}$/iu.test(request.releaseTag); request.preflightOnly === true && /^[0-9a-f]{40}$/iu.test(request.releaseTag);
if ( if (shaPreflight) {
shaPreflight && if (!/^[0-9a-f]{40}$/iu.test(request.checkoutSha)) {
request.checkoutSha && throw new Error("Validation-only SHA preflight requires the full checked-out commit SHA.");
request.releaseTag.toLowerCase() !== request.checkoutSha.toLowerCase() }
) { if (request.releaseTag.toLowerCase() !== request.checkoutSha.toLowerCase()) {
throw new Error("Validation-only SHA must match the checked-out commit exactly."); throw new Error("Validation-only SHA must match the checked-out commit exactly.");
}
} }
const effectiveReleaseTag = shaPreflight ? `v${request.packageVersion}` : request.releaseTag; const effectiveReleaseTag = shaPreflight ? `v${request.packageVersion}` : request.releaseTag;
const taggedVersion = effectiveReleaseTag.startsWith("v") const taggedVersion = effectiveReleaseTag.startsWith("v")
@ -112,24 +113,26 @@ export function validateExtendedStableNpmReleaseRequest(request) {
const releaseVersion = taggedVersion.version; const releaseVersion = taggedVersion.version;
const extendedStableBranch = `extended-stable/${taggedVersion.year}.${taggedVersion.month}.33`; const extendedStableBranch = `extended-stable/${taggedVersion.year}.${taggedVersion.month}.33`;
const expectedWorkflowRef = `refs/heads/${extendedStableBranch}`;
if (request.npmWorkflowRef !== expectedWorkflowRef) {
throw new Error(
`Extended-stable npm workflow ref mismatch: expected ${expectedWorkflowRef}, got ${request.npmWorkflowRef}.`,
);
}
if (request.packageVersion !== releaseVersion) { if (request.packageVersion !== releaseVersion) {
throw new Error( throw new Error(
`Extended-stable npm package version mismatch: expected ${releaseVersion}, got ${request.packageVersion}.`, `Extended-stable npm package version mismatch: expected ${releaseVersion}, got ${request.packageVersion}.`,
); );
} }
const shaValues = [request.checkoutSha, request.tagSha, request.extendedStableBranchSha]; if (!shaPreflight) {
if (shaValues.some((sha) => !/^[0-9a-f]{40}$/iu.test(sha))) { const expectedWorkflowRef = `refs/heads/${extendedStableBranch}`;
throw new Error("Extended-stable npm release identity requires full 40-character Git SHAs."); if (request.npmWorkflowRef !== expectedWorkflowRef) {
} throw new Error(
if (new Set(shaValues.map((sha) => sha.toLowerCase())).size !== 1) { `Extended-stable npm workflow ref mismatch: expected ${expectedWorkflowRef}, got ${request.npmWorkflowRef}.`,
throw new Error("Extended-stable npm checkout, tag, and branch tip SHAs must match exactly."); );
}
const shaValues = [request.checkoutSha, request.tagSha, request.extendedStableBranchSha];
if (shaValues.some((sha) => !/^[0-9a-f]{40}$/iu.test(sha))) {
throw new Error("Extended-stable npm release identity requires full 40-character Git SHAs.");
}
if (new Set(shaValues.map((sha) => sha.toLowerCase())).size !== 1) {
throw new Error("Extended-stable npm checkout, tag, and branch tip SHAs must match exactly.");
}
} }
if (bypassExtendedStableGuard) { if (bypassExtendedStableGuard) {
@ -344,17 +347,13 @@ function validateRequestFromRepository() {
} }
const extendedStableBranch = `extended-stable/${parsed.year}.${parsed.month}.33`; const extendedStableBranch = `extended-stable/${parsed.year}.${parsed.month}.33`;
if (shaPreflight) { if (shaPreflight) {
execFileSync( if (!bypassExtendedStableGuard) {
"git", execFileSync(
[ "git",
"fetch", ["fetch", "--no-tags", "origin", "+refs/heads/main:refs/remotes/origin/main"],
"--no-tags", { stdio: "inherit" },
"origin", );
`+refs/heads/${extendedStableBranch}:refs/remotes/origin/${extendedStableBranch}`, }
...(bypassExtendedStableGuard ? [] : ["+refs/heads/main:refs/remotes/origin/main"]),
],
{ stdio: "inherit" },
);
const checkoutSha = git(["rev-parse", "HEAD"]); const checkoutSha = git(["rev-parse", "HEAD"]);
return validateExtendedStableNpmReleaseRequest({ return validateExtendedStableNpmReleaseRequest({
npmDistTag, npmDistTag,
@ -364,7 +363,7 @@ function validateRequestFromRepository() {
npmWorkflowRef, npmWorkflowRef,
checkoutSha, checkoutSha,
tagSha: checkoutSha, tagSha: checkoutSha,
extendedStableBranchSha: git(["rev-parse", `refs/remotes/origin/${extendedStableBranch}`]), extendedStableBranchSha: "",
packageVersion, packageVersion,
mainPackageVersion: bypassExtendedStableGuard mainPackageVersion: bypassExtendedStableGuard
? "" ? ""

View file

@ -1,12 +1,12 @@
#!/usr/bin/env -S node --import tsx #!/usr/bin/env -S node --import tsx
// Openclaw Npm Prepublish Verify script supports OpenClaw repository automation. // Openclaw Npm Prepublish Verify script supports OpenClaw repository automation.
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs"; import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { formatErrorMessage } from "../src/infra/errors.ts"; import { formatErrorMessage } from "../src/infra/errors.ts";
import { runNpmVerifyCommand } from "./lib/npm-verify-exec.ts"; import { type NpmVerifyCommandInvocation, runNpmVerifyCommand } from "./lib/npm-verify-exec.ts";
import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs"; import { runInstalledWorkspaceBootstrapSmoke } from "./lib/workspace-bootstrap-smoke.mjs";
import { import {
collectInstalledPackageErrors, collectInstalledPackageErrors,
@ -14,6 +14,7 @@ import {
resolveInstalledBinaryCommandInvocation, resolveInstalledBinaryCommandInvocation,
} from "./openclaw-npm-postpublish-verify.ts"; } from "./openclaw-npm-postpublish-verify.ts";
import { resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts"; import { resolveNpmCommandInvocation } from "./openclaw-npm-release-check.ts";
import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
type InstalledPackageJson = { type InstalledPackageJson = {
version?: string; version?: string;
@ -69,6 +70,10 @@ export function parseOpenClawNpmPrepublishVerifyArgs(
: { dependencyTarballPaths, help: false, tarballPath }; : { dependencyTarballPaths, help: false, tarballPath };
} }
export function usesPreparedLocalDependencyInstall(dependencyTarballCount: number): boolean {
return dependencyTarballCount === 1;
}
function npmExec(args: string[], cwd: string): string { function npmExec(args: string[], cwd: string): string {
const invocation = resolveNpmCommandInvocation({ const invocation = resolveNpmCommandInvocation({
npmArgs: args, npmArgs: args,
@ -90,21 +95,58 @@ function main(argv = process.argv.slice(2)): void {
const workingDir = mkdtempSync(join(tmpdir(), "openclaw-prepublish-")); const workingDir = mkdtempSync(join(tmpdir(), "openclaw-prepublish-"));
const prefixDir = join(workingDir, "prefix"); const prefixDir = join(workingDir, "prefix");
try { try {
npmExec( let binaryInvocation: NpmVerifyCommandInvocation;
[ let packageRoot: string;
"install", if (usesPreparedLocalDependencyInstall(args.dependencyTarballPaths.length)) {
"-g", mkdirSync(prefixDir, { recursive: true });
"--prefix", writeFileSync(
join(prefixDir, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies: {
"@openclaw/ai": pathToFileURL(realpathSync(args.dependencyTarballPaths[0])).href,
openclaw: pathToFileURL(realpathSync(args.tarballPath)).href,
},
},
null,
2,
)}\n`,
);
npmExec(["install", "--prefix", prefixDir, "--no-fund", "--no-audit"], workingDir);
packageRoot = join(prefixDir, "node_modules", "openclaw");
const binaryPath = join(
prefixDir, prefixDir,
...args.dependencyTarballPaths.map((dependency) => realpathSync(dependency)), "node_modules",
realpathSync(args.tarballPath), ".bin",
"--no-fund", process.platform === "win32" ? "openclaw.cmd" : "openclaw",
"--no-audit", );
], binaryInvocation =
workingDir, process.platform === "win32"
); ? {
const globalRoot = npmExec(["root", "-g", "--prefix", prefixDir], workingDir); command: resolveWindowsCmdExePath(),
const packageRoot = join(globalRoot, "openclaw"); args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, ["--version"])],
windowsVerbatimArguments: true,
}
: { command: binaryPath, args: ["--version"] };
} else {
npmExec(
[
"install",
"-g",
"--prefix",
prefixDir,
...args.dependencyTarballPaths.map((dependency) => realpathSync(dependency)),
realpathSync(args.tarballPath),
"--no-fund",
"--no-audit",
],
workingDir,
);
const globalRoot = npmExec(["root", "-g", "--prefix", prefixDir], workingDir);
packageRoot = join(globalRoot, "openclaw");
binaryInvocation = resolveInstalledBinaryCommandInvocation(prefixDir, ["--version"]);
}
const pkg = JSON.parse( const pkg = JSON.parse(
readFileSync(join(packageRoot, "package.json"), "utf8"), readFileSync(join(packageRoot, "package.json"), "utf8"),
) as InstalledPackageJson; ) as InstalledPackageJson;
@ -114,7 +156,6 @@ function main(argv = process.argv.slice(2)): void {
installedVersion: pkg.version?.trim() ?? "", installedVersion: pkg.version?.trim() ?? "",
packageRoot, packageRoot,
}); });
const binaryInvocation = resolveInstalledBinaryCommandInvocation(prefixDir, ["--version"]);
const installedBinaryVersion = runNpmVerifyCommand(binaryInvocation, workingDir); const installedBinaryVersion = runNpmVerifyCommand(binaryInvocation, workingDir);
if (normalizeInstalledBinaryVersion(installedBinaryVersion) !== resolvedExpectedVersion) { if (normalizeInstalledBinaryVersion(installedBinaryVersion) !== resolvedExpectedVersion) {
errors.push( errors.push(

View file

@ -3,7 +3,7 @@
set -euo pipefail set -euo pipefail
usage() { usage() {
echo "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack-dry-run|--publish] <package-dir>" echo "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack|--pack-dry-run|--publish] <package-dir>"
} }
if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
@ -12,7 +12,7 @@ if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then
fi fi
mode="${1:-}" mode="${1:-}"
if [[ "${mode}" != "--dry-run" && "${mode}" != "--pack-dry-run" && "${mode}" != "--publish" ]]; then if [[ "${mode}" != "--dry-run" && "${mode}" != "--pack" && "${mode}" != "--pack-dry-run" && "${mode}" != "--publish" ]]; then
usage >&2 usage >&2
exit 2 exit 2
fi fi
@ -36,12 +36,16 @@ if [[ "$#" -gt 0 ]]; then
echo "unexpected plugin npm publish argument: $1" >&2 echo "unexpected plugin npm publish argument: $1" >&2
exit 2 exit 2
fi fi
if [[ "${mode}" == "--pack" && -z "${OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR:-}" ]]; then
echo "--pack requires OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR" >&2
exit 2
fi
package_name="$(node -e 'const pkg = require(require("node:path").resolve(process.argv[1], "package.json")); console.log(pkg.name)' "${package_dir}")" package_name="$(node -e 'const pkg = require(require("node:path").resolve(process.argv[1], "package.json")); console.log(pkg.name)' "${package_dir}")"
package_version="$(node -e 'const pkg = require(require("node:path").resolve(process.argv[1], "package.json")); console.log(pkg.version)' "${package_dir}")" package_version="$(node -e 'const pkg = require(require("node:path").resolve(process.argv[1], "package.json")); console.log(pkg.version)' "${package_dir}")"
current_beta_version="$(npm view "${package_name}" dist-tags.beta 2>/dev/null || true)" current_beta_version="$(npm view "${package_name}" dist-tags.beta 2>/dev/null || true)"
log() { log() {
if [[ "${mode}" == "--pack-dry-run" ]]; then if [[ "${mode}" == "--pack" || "${mode}" == "--pack-dry-run" ]]; then
printf '%s\n' "$*" >&2 printf '%s\n' "$*" >&2
else else
printf '%s\n' "$*" printf '%s\n' "$*"
@ -143,7 +147,7 @@ if [[ "${mirror_auth_requirement}" == "required" && -z "${mirror_auth_token}" ]]
exit 1 exit 1
fi fi
if [[ "${mode}" == "--pack-dry-run" ]]; then if [[ "${mode}" == "--pack" || "${mode}" == "--pack-dry-run" ]]; then
{ {
printf 'Publish command:' printf 'Publish command:'
printf ' %q' "${publish_cmd[@]}" printf ' %q' "${publish_cmd[@]}"
@ -162,10 +166,18 @@ fi
build_package_runtime build_package_runtime
check_package_shrinkwrap check_package_shrinkwrap
if [[ "${mode}" == "--pack-dry-run" ]]; then if [[ "${mode}" == "--pack" || "${mode}" == "--pack-dry-run" ]]; then
pack_args=(npm pack --json --ignore-scripts)
if [[ "${mode}" == "--pack-dry-run" ]]; then
pack_args+=(--dry-run)
else
mkdir -p "${OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR}"
pack_output_dir="$(cd "${OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR}" && pwd)"
pack_args+=(--pack-destination "${pack_output_dir}")
fi
OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES=1 \ OPENCLAW_PLUGIN_NPM_BUNDLE_DEPENDENCIES=1 \
node scripts/lib/plugin-npm-package-manifest.mjs --run "${package_dir}" -- \ node scripts/lib/plugin-npm-package-manifest.mjs --run "${package_dir}" -- \
npm pack --dry-run --json --ignore-scripts "${pack_args[@]}"
exit 0 exit 0
fi fi

View file

@ -50,16 +50,18 @@ import { resolveNpmRunner } from "./npm-runner.mjs";
import { import {
collectInstalledPackageErrors, collectInstalledPackageErrors,
normalizeInstalledBinaryVersion, normalizeInstalledBinaryVersion,
resolveInstalledBinaryCommandInvocation,
resolveInstalledBinaryPath,
} from "./openclaw-npm-postpublish-verify.ts"; } from "./openclaw-npm-postpublish-verify.ts";
import { resolvePnpmRunner } from "./pnpm-runner.mjs";
import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs"; import { listStaticExtensionAssetOutputs } from "./runtime-postbuild.mjs";
import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts"; import { sparkleBuildFloorsFromShortVersion, type SparkleBuildFloors } from "./sparkle-build.ts";
import { buildCmdExeCommandLine } from "./windows-cmd-helpers.mjs"; import { buildCmdExeCommandLine, resolveWindowsCmdExePath } from "./windows-cmd-helpers.mjs";
export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts"; export { collectBundledExtensionManifestErrors } from "./lib/bundled-extension-manifest.ts";
export { packageNameFromSpecifier } from "./lib/plugin-package-dependencies.mjs"; export { packageNameFromSpecifier } from "./lib/plugin-package-dependencies.mjs";
export const RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV =
"OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR";
type PackFile = { path: string }; type PackFile = { path: string };
type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number }; type PackResult = { files?: PackFile[]; filename?: string; unpackedSize?: number };
type ReleaseCheckCommandInvocation = { type ReleaseCheckCommandInvocation = {
@ -362,6 +364,19 @@ function execNpm(
return runReleaseCheckCommand(invocation, options); return runReleaseCheckCommand(invocation, options);
} }
function execPnpm(
args: string[],
options: {
cwd?: string;
encoding: BufferEncoding;
maxBuffer?: number;
stdio: "inherit" | ["ignore", "pipe", "pipe"];
},
): string {
const invocation = resolvePnpmRunner({ env: process.env, pnpmArgs: args });
return runReleaseCheckCommand(invocation, options);
}
function runPackDry(): PackResult[] { function runPackDry(): PackResult[] {
const raw = execNpm(["pack", "--dry-run", "--json", "--ignore-scripts"], { const raw = execNpm(["pack", "--dry-run", "--json", "--ignore-scripts"], {
encoding: "utf8", encoding: "utf8",
@ -372,15 +387,16 @@ function runPackDry(): PackResult[] {
} }
function runPack(packDestination: string): PackResult[] { function runPack(packDestination: string): PackResult[] {
const raw = execNpm( const raw = execPnpm(
["pack", "--json", "--ignore-scripts", "--pack-destination", packDestination], ["--config.ignore-scripts=true", "pack", "--json", "--pack-destination", packDestination],
{ {
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
maxBuffer: 1024 * 1024 * 100, maxBuffer: 1024 * 1024 * 100,
}, },
); );
return JSON.parse(raw) as PackResult[]; const parsed = JSON.parse(raw) as PackResult | PackResult[];
return Array.isArray(parsed) ? parsed : [parsed];
} }
export function resolvePackedTarballPath(packDestination: string, results: PackResult[]): string { export function resolvePackedTarballPath(packDestination: string, results: PackResult[]): string {
@ -393,45 +409,120 @@ export function resolvePackedTarballPath(packDestination: string, results: PackR
); );
} }
const filename = filenames[0]; const filename = filenames[0];
const filenameBasename = basename(filename);
const resolvedDestination = resolve(packDestination);
const resolvedTarball = resolve(resolvedDestination, filenameBasename);
const isLocalFilename = filename === filenameBasename;
const isAbsolutePathInDestination = resolve(filename) === resolvedTarball;
if ( if (
!filename.endsWith(".tgz") || !filenameBasename.endsWith(".tgz") ||
filename.includes("\0") || filename.includes("\0") ||
filename !== basename(filename) || filenameBasename !== win32.basename(filename) ||
filename !== win32.basename(filename) (!isLocalFilename && !isAbsolutePathInDestination)
) { ) {
throw new Error( throw new Error(
`release-check: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}.`, `release-check: npm pack reported unsafe tarball filename ${JSON.stringify(filename)}.`,
); );
} }
return resolve(packDestination, filename); return resolvedTarball;
} }
function installPackedTarball(prefixDir: string, tarballPath: string, cwd: string): void { export function resolveReleaseCheckLocalPackageTarballs(
execNpm( tarballDir: string | undefined = process.env[RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV],
[ ): string[] {
"install", if (!tarballDir) {
"-g", return [];
"--prefix", }
prefixDir, const resolvedDir = resolve(tarballDir);
"--ignore-scripts", if (!existsSync(resolvedDir) || !statSync(resolvedDir).isDirectory()) {
"--no-audit", throw new Error(
"--no-fund", `release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} must name a directory.`,
tarballPath, );
], }
{ const tarballs = readdirSync(resolvedDir, { withFileTypes: true })
cwd, .filter((entry) => entry.isFile() && entry.name.endsWith(".tgz"))
encoding: "utf8", .map((entry) => resolve(resolvedDir, entry.name))
stdio: "inherit", .toSorted((left, right) => left.localeCompare(right));
}, if (tarballs.length !== 1) {
throw new Error(
`release-check: ${RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV} contains ${tarballs.length} tarballs; expected exactly one.`,
);
}
return tarballs;
}
export function createPackedTarballInstallArgs(prefixDir: string): string[] {
return ["install", "--prefix", prefixDir, "--ignore-scripts", "--no-audit", "--no-fund"];
}
export function writePackedTarballInstallManifest(
prefixDir: string,
tarballPath: string,
localPackageTarballs: string[],
): void {
if (localPackageTarballs.length > 1) {
throw new Error(
`release-check: packed install accepts at most one @openclaw/ai tarball; found ${localPackageTarballs.length}.`,
);
}
const dependencies: Record<string, string> = {
openclaw: pathToFileURL(tarballPath).href,
};
if (localPackageTarballs[0]) {
dependencies["@openclaw/ai"] = pathToFileURL(localPackageTarballs[0]).href;
}
mkdirSync(prefixDir, { recursive: true });
writeFileSync(
join(prefixDir, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies,
},
null,
2,
)}\n`,
); );
} }
function resolveGlobalRoot(prefixDir: string, cwd: string): string { function installPackedTarball(
return execNpm(["root", "-g", "--prefix", prefixDir], { prefixDir: string,
tarballPath: string,
cwd: string,
localPackageTarballs: string[] = [],
): void {
writePackedTarballInstallManifest(prefixDir, tarballPath, localPackageTarballs);
execNpm(createPackedTarballInstallArgs(prefixDir), {
cwd, cwd,
encoding: "utf8", encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"], stdio: "inherit",
}).trim(); });
}
export function resolvePackedInstalledBinaryPath(
prefixDir: string,
platform: NodeJS.Platform = process.platform,
): string {
return join(
prefixDir,
"node_modules",
".bin",
platform === "win32" ? "openclaw.cmd" : "openclaw",
);
}
function resolvePackedInstalledBinaryCommandInvocation(
prefixDir: string,
args: string[],
): ReleaseCheckCommandInvocation {
const binaryPath = resolvePackedInstalledBinaryPath(prefixDir);
return process.platform === "win32"
? {
command: resolveWindowsCmdExePath(),
args: ["/d", "/s", "/c", buildCmdExeCommandLine(binaryPath, args)],
windowsVerbatimArguments: true,
}
: { command: binaryPath, args };
} }
export function createPackedBundledPluginPostinstallEnv( export function createPackedBundledPluginPostinstallEnv(
@ -556,7 +647,7 @@ function verifyPackedInstalledPackage(params: {
prefixDir: string; prefixDir: string;
tmpRoot: string; tmpRoot: string;
}): void { }): void {
const invocation = resolveInstalledBinaryCommandInvocation(params.prefixDir, ["--version"]); const invocation = resolvePackedInstalledBinaryCommandInvocation(params.prefixDir, ["--version"]);
const installedBinaryVersion = runReleaseCheckCommand( const installedBinaryVersion = runReleaseCheckCommand(
{ {
command: invocation.command, command: invocation.command,
@ -584,7 +675,14 @@ function verifyPackedInstalledPackage(params: {
export function createPackedPluginSdkTypescriptSmokeProject(params: { export function createPackedPluginSdkTypescriptSmokeProject(params: {
consumerDir: string; consumerDir: string;
packageSpec: string; packageSpec: string;
aiPackageSpec?: string;
}): void { }): void {
const dependencies: Record<string, string> = {
openclaw: params.packageSpec,
};
if (params.aiPackageSpec) {
dependencies["@openclaw/ai"] = params.aiPackageSpec;
}
mkdirSync(join(params.consumerDir, "src"), { recursive: true }); mkdirSync(join(params.consumerDir, "src"), { recursive: true });
writeFileSync( writeFileSync(
join(params.consumerDir, "package.json"), join(params.consumerDir, "package.json"),
@ -593,9 +691,7 @@ export function createPackedPluginSdkTypescriptSmokeProject(params: {
name: "openclaw-plugin-sdk-type-smoke", name: "openclaw-plugin-sdk-type-smoke",
private: true, private: true,
type: "module", type: "module",
dependencies: { dependencies,
openclaw: params.packageSpec,
},
}, },
null, null,
2, 2,
@ -627,11 +723,16 @@ export function createPackedPluginSdkTypescriptSmokeProject(params: {
); );
} }
function runPackedPluginSdkTypescriptSmoke(tarballPath: string, tmpRoot: string): void { function runPackedPluginSdkTypescriptSmoke(
tarballPath: string,
tmpRoot: string,
localPackageTarballs: string[],
): void {
const consumerDir = join(tmpRoot, "plugin-sdk-type-consumer"); const consumerDir = join(tmpRoot, "plugin-sdk-type-consumer");
createPackedPluginSdkTypescriptSmokeProject({ createPackedPluginSdkTypescriptSmokeProject({
consumerDir, consumerDir,
packageSpec: `file:${tarballPath}`, packageSpec: `file:${tarballPath}`,
aiPackageSpec: localPackageTarballs[0] ? `file:${localPackageTarballs[0]}` : undefined,
}); });
execNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund"], { execNpm(["install", "--ignore-scripts", "--no-audit", "--no-fund"], {
cwd: consumerDir, cwd: consumerDir,
@ -762,7 +863,7 @@ function runPackedCliSmoke(params: {
homeDir: string; homeDir: string;
stateDir: string; stateDir: string;
}): void { }): void {
const binaryPath = resolveInstalledBinaryPath(params.prefixDir); const binaryPath = resolvePackedInstalledBinaryPath(params.prefixDir);
const env = createPackedCliSmokeEnv(process.env, { const env = createPackedCliSmokeEnv(process.env, {
HOME: params.homeDir, HOME: params.homeDir,
OPENCLAW_STATE_DIR: params.stateDir, OPENCLAW_STATE_DIR: params.stateDir,
@ -816,9 +917,10 @@ function runPackedBundledChannelEntrySmoke(): void {
const packResults = runPack(packDir); const packResults = runPack(packDir);
const tarballPath = resolvePackedTarballPath(packDir, packResults); const tarballPath = resolvePackedTarballPath(packDir, packResults);
const prefixDir = join(tmpRoot, "prefix"); const prefixDir = join(tmpRoot, "prefix");
installPackedTarball(prefixDir, tarballPath, tmpRoot); const localPackageTarballs = resolveReleaseCheckLocalPackageTarballs();
installPackedTarball(prefixDir, tarballPath, tmpRoot, localPackageTarballs);
const packageRoot = join(resolveGlobalRoot(prefixDir, tmpRoot), "openclaw"); const packageRoot = join(prefixDir, "node_modules", "openclaw");
verifyPackedInstalledPackage({ verifyPackedInstalledPackage({
expectedVersion, expectedVersion,
packageRoot, packageRoot,
@ -837,7 +939,7 @@ function runPackedBundledChannelEntrySmoke(): void {
runPackedBundledPluginPostinstall(packageRoot); runPackedBundledPluginPostinstall(packageRoot);
runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot); runPackedBundledPluginActivationSmoke(packageRoot, tmpRoot);
runPackedTaskRegistryControlRuntimeSmoke(packageRoot); runPackedTaskRegistryControlRuntimeSmoke(packageRoot);
runPackedPluginSdkTypescriptSmoke(tarballPath, tmpRoot); runPackedPluginSdkTypescriptSmoke(tarballPath, tmpRoot, localPackageTarballs);
runReleaseCheckCommand( runReleaseCheckCommand(
{ {
command: process.execPath, command: process.execPath,

View file

@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
import { import {
openClawNpmPrepublishVerifyUsage, openClawNpmPrepublishVerifyUsage,
parseOpenClawNpmPrepublishVerifyArgs, parseOpenClawNpmPrepublishVerifyArgs,
usesPreparedLocalDependencyInstall,
} from "../scripts/openclaw-npm-prepublish-verify.ts"; } from "../scripts/openclaw-npm-prepublish-verify.ts";
describe("parseOpenClawNpmPrepublishVerifyArgs", () => { describe("parseOpenClawNpmPrepublishVerifyArgs", () => {
@ -47,3 +48,11 @@ describe("parseOpenClawNpmPrepublishVerifyArgs", () => {
).toThrow("Invalid dependency tarball path: --bad"); ).toThrow("Invalid dependency tarball path: --bad");
}); });
}); });
describe("usesPreparedLocalDependencyInstall", () => {
it("uses the prepared local project only for the single AI tarball release path", () => {
expect(usesPreparedLocalDependencyInstall(0)).toBe(false);
expect(usesPreparedLocalDependencyInstall(1)).toBe(true);
expect(usesPreparedLocalDependencyInstall(2)).toBe(false);
});
});

View file

@ -835,6 +835,7 @@ describe("createPackedPluginSdkTypescriptSmokeProject", () => {
createPackedPluginSdkTypescriptSmokeProject({ createPackedPluginSdkTypescriptSmokeProject({
consumerDir, consumerDir,
packageSpec: `file:${packageRoot}`, packageSpec: `file:${packageRoot}`,
aiPackageSpec: "file:/tmp/openclaw-ai.tgz",
}); });
const packageJson = JSON.parse(readFileSync(join(consumerDir, "package.json"), "utf8")) as { const packageJson = JSON.parse(readFileSync(join(consumerDir, "package.json"), "utf8")) as {
@ -850,6 +851,7 @@ describe("createPackedPluginSdkTypescriptSmokeProject", () => {
); );
expect(packageJson.dependencies?.openclaw).toBe(`file:${packageRoot}`); expect(packageJson.dependencies?.openclaw).toBe(`file:${packageRoot}`);
expect(packageJson.dependencies?.["@openclaw/ai"]).toBe("file:/tmp/openclaw-ai.tgz");
expect(tsconfig.compilerOptions?.skipLibCheck).toBe(true); expect(tsconfig.compilerOptions?.skipLibCheck).toBe(true);
expect(source).toBe(fixtureSource); expect(source).toBe(fixtureSource);
expect(source).toContain('"openclaw/plugin-sdk"'); expect(source).toContain('"openclaw/plugin-sdk"');
@ -898,6 +900,11 @@ describe("resolvePackedTarballPath", () => {
expect( expect(
resolvePackedTarballPath("/tmp/openclaw-pack", [{ filename: "openclaw-2026.6.17.tgz" }]), resolvePackedTarballPath("/tmp/openclaw-pack", [{ filename: "openclaw-2026.6.17.tgz" }]),
).toBe(resolvePath("/tmp/openclaw-pack", "openclaw-2026.6.17.tgz")); ).toBe(resolvePath("/tmp/openclaw-pack", "openclaw-2026.6.17.tgz"));
expect(
resolvePackedTarballPath("/tmp/openclaw-pack", [
{ filename: "/tmp/openclaw-pack/openclaw-2026.6.17.tgz" },
]),
).toBe(resolvePath("/tmp/openclaw-pack", "openclaw-2026.6.17.tgz"));
}); });
it("rejects path-like npm pack tarball filenames", () => { it("rejects path-like npm pack tarball filenames", () => {

View file

@ -209,12 +209,14 @@ describe("extended-stable npm release request", () => {
).toEqual({ extendedStable: false }); ).toEqual({ extendedStable: false });
}); });
it("accepts a SHA-only extended-stable preflight bound to the branch tip", () => { it("accepts a SHA-only extended-stable preflight from an arbitrary workflow ref", () => {
expect( expect(
validateExtendedStableNpmReleaseRequest({ validateExtendedStableNpmReleaseRequest({
...valid, ...valid,
preflightOnly: true, preflightOnly: true,
releaseTag: sha, releaseTag: sha,
npmWorkflowRef: "refs/heads/main",
extendedStableBranchSha: "",
}), }),
).toEqual({ ).toEqual({
extendedStable: true, extendedStable: true,
@ -226,8 +228,18 @@ describe("extended-stable npm release request", () => {
...valid, ...valid,
preflightOnly: true, preflightOnly: true,
releaseTag: "b".repeat(40), releaseTag: "b".repeat(40),
npmWorkflowRef: "refs/heads/dev/preflight-candidate",
extendedStableBranchSha: "",
}), }),
).toThrow(/must match the checked-out commit/u); ).toThrow(/must match the checked-out commit/u);
expect(() =>
validateExtendedStableNpmReleaseRequest({
...valid,
preflightOnly: true,
releaseTag: sha,
checkoutSha: "",
}),
).toThrow(/requires the full checked-out commit SHA/u);
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, releaseTag: sha })).toThrow( expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, releaseTag: sha })).toThrow(
/exact final vYYYY\.M\.P release tag/u, /exact final vYYYY\.M\.P release tag/u,
); );

View file

@ -111,6 +111,31 @@ describe("minimal npm extended-stable workflow", () => {
expect(summary.run).toContain("Extended-stable guard bypass: ${BYPASS_EXTENDED_STABLE_GUARD}"); expect(summary.run).toContain("Extended-stable guard bypass: ${BYPASS_EXTENDED_STABLE_GUARD}");
}); });
it("accepts arbitrary SHA preflight targets and exercises every publishable plugin package", () => {
const parsed = workflow();
const preflight = parsed.jobs?.preflight_openclaw_npm;
const metadata = step(preflight, "Validate release metadata");
expect(metadata.run).toContain('RELEASE_BRANCH_REF="${RELEASE_SHA}"');
expect(metadata.run).not.toContain("Validation-only SHA mode only supports");
const plugins = step(preflight, "Exercise all extended-stable plugin npm packages");
expect(step(preflight, "Verify release contents").env).toMatchObject({
OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR:
"${{ steps.ai_runtime_tarballs.outputs.dir }}",
});
expect(plugins.if).toBe("${{ inputs.npm_dist_tag == 'extended-stable' }}");
expect(plugins.env).toMatchObject({
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: "extended-stable",
});
expect(plugins.run).toContain("--selection-mode all-publishable");
expect(plugins.run).toContain("--npm-dist-tag extended-stable");
expect(plugins.run).toContain("scripts/check-plugin-npm-runtime-builds.mjs");
expect(plugins.run).toContain("scripts/plugin-npm-publish.sh --pack");
expect(plugins.run).toContain("OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR");
expect(plugins.run).not.toContain("--publish");
expect(step(preflight, "Upload extended-stable plugin npm packages")).toBeDefined();
});
it("authenticates exact extended-stable run and Full Validation identities", () => { it("authenticates exact extended-stable run and Full Validation identities", () => {
const raw = readFileSync(workflowPath, "utf8"); const raw = readFileSync(workflowPath, "utf8");
expect(raw).toContain("--json workflowName,headBranch,headSha,event,conclusion,url"); expect(raw).toContain("--json workflowName,headBranch,headSha,event,conclusion,url");

View file

@ -46,7 +46,7 @@ describe("plugin npm publish wrapper", () => {
expect(result.status).toBe(0); expect(result.status).toBe(0);
expect(result.stdout.trim()).toBe( expect(result.stdout.trim()).toBe(
"usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack-dry-run|--publish] <package-dir>", "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack|--pack-dry-run|--publish] <package-dir>",
); );
expect(result.stderr).toBe(""); expect(result.stderr).toBe("");
}); });
@ -57,10 +57,18 @@ describe("plugin npm publish wrapper", () => {
expect(result.status).toBe(2); expect(result.status).toBe(2);
expect(result.stdout).toBe(""); expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe( expect(result.stderr.trim()).toBe(
"usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack-dry-run|--publish] <package-dir>", "usage: bash scripts/plugin-npm-publish.sh [--dry-run|--pack|--pack-dry-run|--publish] <package-dir>",
); );
}); });
it("requires an explicit artifact directory for real pack mode", () => {
const result = runPluginPublishWrapper(["--pack", "extensions/telegram"]);
expect(result.status).toBe(2);
expect(result.stdout).toBe("");
expect(result.stderr.trim()).toBe("--pack requires OPENCLAW_PLUGIN_NPM_PACK_OUTPUT_DIR");
});
it("rejects option-like package dirs before package checks", () => { it("rejects option-like package dirs before package checks", () => {
const result = runPluginPublishWrapper(["--dry-run", "--wat"]); const result = runPluginPublishWrapper(["--dry-run", "--wat"]);

View file

@ -1,9 +1,15 @@
// Release Check tests cover release check script behavior. // Release Check tests cover release check script behavior.
import { mkdtempSync, readFileSync, rmSync } from "node:fs"; import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join } from "node:path"; import { join } from "node:path";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { writePackedBundledPluginActivationConfig } from "../../scripts/release-check.ts"; import {
createPackedTarballInstallArgs,
RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV,
resolveReleaseCheckLocalPackageTarballs,
writePackedTarballInstallManifest,
writePackedBundledPluginActivationConfig,
} from "../../scripts/release-check.ts";
function requirePluginEntries(config: { plugins?: { entries?: Record<string, unknown> } }) { function requirePluginEntries(config: { plugins?: { entries?: Record<string, unknown> } }) {
if (!config.plugins?.entries) { if (!config.plugins?.entries) {
@ -13,6 +19,83 @@ function requirePluginEntries(config: { plugins?: { entries?: Record<string, unk
} }
describe("release-check", () => { describe("release-check", () => {
it("installs the packed core and local sibling package tarballs together", () => {
expect(createPackedTarballInstallArgs("/tmp/prefix")).toEqual([
"install",
"--prefix",
"/tmp/prefix",
"--ignore-scripts",
"--no-audit",
"--no-fund",
]);
});
it("resolves exactly one prepacked local dependency tarball", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
writeFileSync(join(root, "openclaw-ai-2026.6.33.tgz"), "fixture");
writeFileSync(join(root, "SHA256SUMS"), "fixture");
expect(resolveReleaseCheckLocalPackageTarballs(root)).toEqual([
join(root, "openclaw-ai-2026.6.33.tgz"),
]);
expect(resolveReleaseCheckLocalPackageTarballs(undefined)).toEqual([]);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("writes an explicit local project for unpublished core and AI tarballs", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-install-test-"));
try {
writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", ["/tmp/openclaw-ai.tgz"]);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
private?: boolean;
};
expect(manifest.private).toBe(true);
expect(manifest.dependencies).toEqual({
"@openclaw/ai": "file:///tmp/openclaw-ai.tgz",
openclaw: "file:///tmp/openclaw.tgz",
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("preserves the no-env local release check path", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-install-test-"));
try {
writePackedTarballInstallManifest(root, "/tmp/openclaw.tgz", []);
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
dependencies?: Record<string, string>;
private?: boolean;
};
expect(manifest.private).toBe(true);
expect(manifest.dependencies).toEqual({
openclaw: "file:///tmp/openclaw.tgz",
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("rejects missing, empty, or ambiguous local dependency tarball directories", () => {
const root = mkdtempSync(join(tmpdir(), "openclaw-release-check-tarball-test-"));
try {
expect(() => resolveReleaseCheckLocalPackageTarballs(join(root, "missing"))).toThrow(
RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR_ENV,
);
const empty = join(root, "empty");
mkdirSync(empty);
expect(() => resolveReleaseCheckLocalPackageTarballs(empty)).toThrow("contains 0 tarballs");
writeFileSync(join(empty, "one.tgz"), "fixture");
writeFileSync(join(empty, "two.tgz"), "fixture");
expect(() => resolveReleaseCheckLocalPackageTarballs(empty)).toThrow("contains 2 tarballs");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
it("seeds packaged activation smoke with an included channel plugin", () => { it("seeds packaged activation smoke with an included channel plugin", () => {
const homeDir = mkdtempSync(join(tmpdir(), "openclaw-release-check-test-")); const homeDir = mkdtempSync(join(tmpdir(), "openclaw-release-check-test-"));
try { try {