diff --git a/.github/workflows/openclaw-npm-release.yml b/.github/workflows/openclaw-npm-release.yml index f708f05f4caa..f0c04f2fb0bc 100644 --- a/.github/workflows/openclaw-npm-release.yml +++ b/.github/workflows/openclaw-npm-release.yml @@ -397,7 +397,7 @@ jobs: - name: Verify release contents env: OPENCLAW_RELEASE_CHECK_LOCAL_PACKAGE_TARBALL_DIR: ${{ steps.core_package_tarballs.outputs.dir }} - run: pnpm release:check + run: pnpm release:generated:check && node --import tsx scripts/release-check.ts - name: Exercise all extended-stable plugin npm packages id: plugin_npm_preflight diff --git a/package.json b/package.json index e15a57c462d0..6b1be7288d29 100644 --- a/package.json +++ b/package.json @@ -1521,6 +1521,7 @@ "build": "node --import tsx scripts/build-all.mts", "build:ci-artifacts": "node --import tsx scripts/build-all.mts ciArtifacts", "build:docker": "node --import tsx scripts/tsdown-build.mts && node --import tsx scripts/check-cli-bootstrap-imports.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/build-stamp.mts && node --import tsx scripts/runtime-postbuild-stamp.mts && pnpm plugins:assets:build && pnpm plugins:assets:copy && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts && node --import tsx scripts/write-cli-startup-metadata.ts", + "build:package": "pnpm clean:dist && pnpm build", "build:plugin-sdk:dts": "node scripts/run-tsgo.mjs -p tsconfig.plugin-sdk.dts.json --declaration true", "build:plugin-sdk:strict-smoke": "node --import tsx scripts/tsdown-build.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/run-with-env.mts OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/check-plugin-sdk-exports.mts", "build:strict-smoke": "pnpm plugins:assets:build && node --import tsx scripts/tsdown-build.mts && node --import tsx scripts/check-cli-bootstrap-imports.mts && node scripts/runtime-postbuild.mjs && node --import tsx scripts/build-stamp.mts && node --import tsx scripts/runtime-postbuild-stamp.mts && node --import tsx scripts/run-with-env.mts OPENCLAW_PLUGIN_SDK_CANONICAL_DTS=1 -- node --import tsx scripts/write-plugin-sdk-entry-dts.ts && node --import tsx scripts/check-plugin-sdk-exports.mts", @@ -1766,7 +1767,7 @@ "release:beta": "node --import tsx scripts/release-candidate-checklist.mts", "release:beta-smoke": "node --import tsx scripts/release-beta-smoke.ts", "release:candidate": "node --import tsx scripts/release-candidate-checklist.mts", - "release:check": "pnpm release:generated:check && node --import tsx scripts/release-check.ts", + "release:check": "pnpm build:package && pnpm release:generated:check && node --import tsx scripts/release-check.ts", "release:fast-pretag-check": "bash scripts/release-fast-pretag-check.sh", "release:generated:check": "node scripts/release-preflight.mjs --check", "release:openclaw:npm:check": "node --import tsx scripts/openclaw-npm-release-check.ts", diff --git a/scripts/openclaw-prepack.ts b/scripts/openclaw-prepack.ts index 00f86670b2af..05b4734780e2 100644 --- a/scripts/openclaw-prepack.ts +++ b/scripts/openclaw-prepack.ts @@ -307,8 +307,7 @@ function prepackPreparationRestoreError(error: unknown, restoreError: unknown): async function main(): Promise { ensureSupportedSourcePack(); const buildEnv = resolvePrepackBuildEnvironment(); - runPnpm(["build"], buildEnv); - runPnpm(["ui:build"], buildEnv); + runPnpm(["build:package"], buildEnv); await preparePrepackArtifacts(buildEnv); } diff --git a/scripts/package-openclaw-for-docker.mts b/scripts/package-openclaw-for-docker.mts index 0e3bfb5dc99a..f26dfcef3c58 100644 --- a/scripts/package-openclaw-for-docker.mts +++ b/scripts/package-openclaw-for-docker.mts @@ -11,6 +11,7 @@ import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "./lib/bundled-plugin-build import { toErrorObject } from "./lib/error-format.mts"; import { terminateManagedChild } from "./lib/managed-child-process.mts"; import { resolveNpmJsonEntries } from "./lib/npm-json-output.mts"; +import { assertRealOutputRoot } from "./lib/output-root-guard.mjs"; import { isRecord } from "./lib/record-shared.mjs"; import { resolveNpmRunner } from "./npm-runner.mts"; import { preparePackageChangelog, restorePackageChangelog } from "./package-changelog.mjs"; @@ -473,16 +474,6 @@ function run(command: string, args: string[], cwd: string, options: RunOptions = }); } -const PACKAGE_ARTIFACT_BUILD_STEPS = [ - { - label: "Building OpenClaw package artifacts", - command: "pnpm", - // Let the frozen source own its build entrypoint while the packaging env - // keeps canonical declaration emission enabled. - args: ["run", "build"], - }, -]; - export async function buildPackageArtifacts( sourceDir: string, packageOptions: PackageOptions = {}, @@ -496,18 +487,18 @@ export async function buildPackageArtifacts( for (const envName of PACKAGE_BUILD_PLUGIN_SELECTION_ENV_NAMES) { delete buildEnv[envName]; } - for (const step of PACKAGE_ARTIFACT_BUILD_STEPS) { - console.error(`==> ${step.label}`); - await runImpl(step.command, step.args, sourceDir, { - env: { - ...buildEnv, - }, - timeoutMs: resolveTimeoutMs( - "OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS", - DEFAULT_PACKAGE_BUILD_TIMEOUT_MS, - ), - }); - } + const timeoutMs = resolveTimeoutMs( + "OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS", + DEFAULT_PACKAGE_BUILD_TIMEOUT_MS, + ); + const distDir = path.join(sourceDir, "dist"); + assertRealOutputRoot(distDir); + console.error("==> Cleaning OpenClaw package artifacts"); + await fs.rm(distDir, { force: true, recursive: true }); + + // Frozen sources own their build entrypoint and may predate clean:dist. + console.error("==> Building OpenClaw package artifacts"); + await runImpl("pnpm", ["run", "build"], sourceDir, { env: buildEnv, timeoutMs }); } async function runCapture(command: string, args: string[], cwd: string, options: RunOptions = {}) { diff --git a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts index 3a4b94fd5448..3856a8e15ee6 100644 --- a/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts +++ b/test/e2e/qa-lab/runtime/package-openclaw-for-docker.e2e.test.ts @@ -5,6 +5,7 @@ import os from "node:os"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { MAX_TIMER_TIMEOUT_MS } from "@openclaw/normalization-core/number-coercion"; +import * as tar from "tar"; import { afterEach, describe, expect, it, vi } from "vitest"; import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "../../../../scripts/lib/bundled-plugin-build-entries.mjs"; import { @@ -250,6 +251,7 @@ describe("package-openclaw-for-docker", () => { "scripts/lib/managed-child-process.mts", "scripts/lib/npm-json-output.mts", "scripts/lib/optional-bundled-clusters.mjs", + "scripts/lib/output-root-guard.mjs", "scripts/lib/record-shared.mjs", "scripts/lib/windows-cmd-helpers-runtime.mts", "scripts/lib/windows-taskkill.mjs", @@ -361,6 +363,7 @@ describe("package-openclaw-for-docker", () => { }); it("uses the source package build entrypoint with declaration generation", async () => { + const sourceDir = tempDirs.make("openclaw-package-build-source-"); const calls: Array<{ command: string; args: string[]; @@ -387,7 +390,7 @@ describe("package-openclaw-for-docker", () => { process.env.OPENCLAW_BUILD_PRIVATE_QA = "1"; try { - await buildPackageArtifacts("/repo", { + await buildPackageArtifacts(sourceDir, { runImpl: async ( command: string, args: string[], @@ -437,7 +440,7 @@ describe("package-openclaw-for-docker", () => { { command: "pnpm", args: ["run", "build"], - cwd: "/repo", + cwd: sourceDir, dockerBuildExtensions: undefined, internalDockerBuildPluginIds: undefined, noPnpm: "1", @@ -449,6 +452,45 @@ describe("package-openclaw-for-docker", () => { ]); }); + it("omits stale hashed dist output when frozen sources expose only their own build", async () => { + const sourceDir = tempDirs.make("openclaw-package-clean-dist-source-"); + const outputDir = tempDirs.make("openclaw-package-clean-dist-output-"); + const stalePath = path.join(sourceDir, "dist", "runtime-OLDHASH.js"); + fs.mkdirSync(path.dirname(stalePath)); + fs.writeFileSync(stalePath, "export const stale = true;\n"); + fs.writeFileSync( + path.join(sourceDir, "package.json"), + `${JSON.stringify( + { + files: ["dist"], + name: "openclaw", + scripts: { + build: + "node -e \"const fs=require('fs');fs.mkdirSync('dist',{recursive:true});fs.writeFileSync('dist/index.js','export {};\\n')\"", + }, + version: "2026.4.25", + }, + null, + 2, + )}\n`, + ); + + await buildPackageArtifacts(sourceDir); + const tarball = await packOpenClawPackageForDocker(sourceDir, outputDir, { + ...skipDocsMapLifecycle, + prepareChangelog: async () => {}, + restoreChangelog: async () => {}, + }); + const entries: string[] = []; + await tar.t({ + file: tarball, + onentry: (entry) => entries.push(entry.path), + }); + + expect(entries).toContain("package/dist/index.js"); + expect(entries).not.toContain("package/dist/runtime-OLDHASH.js"); + }); + it("rejects loose package artifact timeout env values", async () => { const previousTimeout = process.env.OPENCLAW_DOCKER_PACKAGE_BUILD_TIMEOUT_MS; try { diff --git a/test/package-scripts.test.ts b/test/package-scripts.test.ts index f2cb27b097cc..de093be92e31 100644 --- a/test/package-scripts.test.ts +++ b/test/package-scripts.test.ts @@ -182,6 +182,15 @@ describe("package scripts", () => { ); }); + it("cleans package builds before validating release contents", () => { + const scripts = readPackageJson().scripts; + + expect(scripts["build:package"]).toBe("pnpm clean:dist && pnpm build"); + expect(scripts["release:check"]).toBe( + "pnpm build:package && pnpm release:generated:check && node --import tsx scripts/release-check.ts", + ); + }); + it("uses the shipped package launcher for npm start", () => { expect(readPackageJson().scripts.start).toBe("node openclaw.mjs"); }); diff --git a/test/scripts/openclaw-npm-extended-stable-workflow.test.ts b/test/scripts/openclaw-npm-extended-stable-workflow.test.ts index 63f7681c369e..e3d478f61fad 100644 --- a/test/scripts/openclaw-npm-extended-stable-workflow.test.ts +++ b/test/scripts/openclaw-npm-extended-stable-workflow.test.ts @@ -325,7 +325,11 @@ describe("minimal npm extended-stable workflow", () => { expect(buildControlUi.if).toBe("steps.dist_build_cache.outputs.cache-hit != 'true'"); expect(buildControlUi.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1"); expect(step(preflight, "Check").if).toBeUndefined(); - expect(step(preflight, "Verify release contents").if).toBeUndefined(); + const verifyReleaseContents = step(preflight, "Verify release contents"); + expect(verifyReleaseContents.if).toBeUndefined(); + expect(verifyReleaseContents.run).toBe( + "pnpm release:generated:check && node --import tsx scripts/release-check.ts", + ); expect(step(preflight, "Verify prepared npm tarball install").if).toBeUndefined(); const save = step(preflight, "Save preflight build outputs");