diff --git a/AGENTS.md b/AGENTS.md index b0b069754d81..4a8f71333c18 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -199,6 +199,7 @@ Skills own workflows; root owns hard policy and routing. - CI polling: exact SHA, relevant checks only, minimal fields. Skip routine noise (`Auto response`, `Labeler`, docs agents, performance/stale). Logs only after failure/completion or concrete need. - Trusted-workflow release-branch CI: pass `target_ref` + `release_candidate_ref`; never `release_gate` (requires workflow head == target). - Agent PR landing to `main`: use only the repo-native `scripts/pr` wrapper: run `scripts/pr review-init `, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init `, validate them with `scripts/pr review-validate-artifacts `, then run `OPENCLAW_TESTBOX=1 scripts/pr prepare-run ` and `scripts/pr merge-run `. The Testbox flag is mandatory for agents so prepare verifies hosted CI/Testbox on the current head or reuses a patch-identical pre-rebase run green within 24 hours instead of running full gates locally. For owner-approved reviewed fork code without hosted Testbox, use `OPENCLAW_PR_GATES_REMOTE=testbox` instead. Do not rebase only because `main` advanced; merge drift is advisory unless strict drift is explicitly enabled, while GitHub still blocks conflicts. Do not idle on `auto-response` or `check-docs`. +- After `scripts/pr merge-run` removes its worktree, `cd` to a persistent repo before follow-up commands. - `scripts/pr` review JSON: land-ready recommendation `READY FOR /prepare-pr`, `issueValidation.status=valid`; never `APPROVE`. ## Code diff --git a/scripts/package-openclaw-for-docker.mjs b/scripts/package-openclaw-for-docker.mjs index b6ecc6730484..a64ca046f5d6 100644 --- a/scripts/package-openclaw-for-docker.mjs +++ b/scripts/package-openclaw-for-docker.mjs @@ -6,7 +6,6 @@ import { spawn } from "node:child_process"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import * as tar from "tar"; import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "./lib/bundled-plugin-build-entries.mjs"; import { preparePackageChangelog, restorePackageChangelog } from "./package-changelog.mjs"; @@ -515,7 +514,14 @@ export async function prepareBundledAiRuntimePackage( const extractAiRuntime = options.extractAiRuntime ?? ((tarballPath, destination) => - Promise.resolve(tar.x({ cwd: destination, file: tarballPath, strip: 1 }))); + // Source-ref validation runs this trusted harness outside the candidate's dependency tree. + // Keep extraction on the system tar contract so only the candidate checkout needs install. + run("tar", ["-xzf", tarballPath, "-C", destination, "--strip-components=1"], destination, { + timeoutMs: resolveTimeoutMs( + "OPENCLAW_DOCKER_PACKAGE_PACK_TIMEOUT_MS", + DEFAULT_PACKAGE_PACK_TIMEOUT_MS, + ), + })); const originalPackageJson = await fs.readFile(packageJsonPath, "utf8"); let packageJson; try { 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 c479ea1f8edf..521520181b81 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,7 +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 { describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { DOCKER_SELECTED_PLUGIN_BUILD_IDS_ENV } from "../../../../scripts/lib/bundled-plugin-build-entries.mjs"; import { buildPackageArtifacts, @@ -14,8 +14,10 @@ import { prepareBundledAiRuntimePackage, runCommandForTest, } from "../../../../scripts/package-openclaw-for-docker.mjs"; +import { useAutoCleanupTempDirTracker } from "../../../helpers/temp-dir.js"; const skipBundledAiRuntime = async (): Promise<() => Promise> => async () => {}; +const tempDirs = useAutoCleanupTempDirTracker(afterEach); function isProcessAlive(pid: number): boolean { if (!Number.isSafeInteger(pid) || pid <= 0) { @@ -134,6 +136,45 @@ describe("package-openclaw-for-docker", () => { } }); + it("loads from a trusted harness checkout without installed dependencies", async () => { + const tempRoot = tempDirs.make("openclaw-package-harness-"); + const copiedFiles = [ + "scripts/package-openclaw-for-docker.mjs", + "scripts/package-changelog.mjs", + "scripts/lib/bundled-plugin-build-entries.mjs", + "scripts/lib/bundled-plugin-paths.mjs", + "scripts/lib/optional-bundled-clusters.mjs", + ]; + try { + for (const relativePath of copiedFiles) { + const target = path.join(tempRoot, relativePath); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.copyFileSync(relativePath, target); + } + const result = await new Promise<{ status: number | null; stderr: string }>( + (resolve, reject) => { + const child = spawn( + process.execPath, + [path.join(tempRoot, "scripts/package-openclaw-for-docker.mjs"), "--invalid"], + { cwd: tempRoot, stdio: ["ignore", "ignore", "pipe"] }, + ); + let stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + child.on("error", reject); + child.on("close", (status) => resolve({ status, stderr })); + }, + ); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("unknown argument: --invalid"); + expect(result.stderr).not.toContain("ERR_MODULE_NOT_FOUND"); + } finally { + fs.rmSync(tempRoot, { force: true, recursive: true }); + } + }); + it("rejects pnpm pack with npm metadata output", () => { expect(parseArgs(["--pnpm-pack"]).pnpmPack).toBe(true); expect(() => parseArgs(["--pnpm-pack", "--pack-json", "pack.json"])).toThrow( diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 6c2619104cca..7124495ed0cc 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -778,6 +778,17 @@ describe("package acceptance workflow", () => { expect(workflow).toContain("package_integrity=${PACKAGE_INTEGRITY_RESULT}"); }); + it("keeps ref packaging independent of workflow-checkout dependencies", () => { + const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); + const resolveJob = workflow.slice( + workflow.indexOf(" resolve_package:"), + workflow.indexOf(" package_integrity:"), + ); + + expect(resolveJob).toContain("scripts/resolve-openclaw-package-candidate.mjs"); + expect(resolveJob).not.toContain("pnpm install"); + }); + it("offers bounded product profiles and can run Telegram against the resolved artifact", () => { const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8"); const npmTelegramWorkflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");