fix(release): keep source-ref packaging self-contained (#105360)

* fix(release): make ref packaging harness self-contained

* docs(agents): preserve cwd after PR merge

* test(release): track package harness temp dirs

* style(test): format package harness imports
This commit is contained in:
Peter Steinberger
2026-07-12 13:45:24 +01:00
committed by GitHub
parent 42d34eb4c8
commit 9d042e252c
4 changed files with 62 additions and 3 deletions
+1
View File
@@ -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 <PR>`, follow its emitted checkout/guard guidance, initialize and complete review artifacts with `scripts/pr review-artifacts-init <PR>`, validate them with `scripts/pr review-validate-artifacts <PR>`, then run `OPENCLAW_TESTBOX=1 scripts/pr prepare-run <PR>` and `scripts/pr merge-run <PR>`. 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
+8 -2
View File
@@ -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 {
@@ -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<void>> => 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(
@@ -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");