mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
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:
parent
9d7a6b5da6
commit
9eeebf7cb1
11 changed files with 452 additions and 107 deletions
65
.github/workflows/openclaw-npm-release.yml
vendored
65
.github/workflows/openclaw-npm-release.yml
vendored
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
? ""
|
? ""
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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"]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue