mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:50:42 +00:00
ci: split package acceptance refs
This commit is contained in:
@@ -14,6 +14,7 @@ function parseArgs(argv) {
|
||||
outputDir: "",
|
||||
outputName: "",
|
||||
skipBuild: false,
|
||||
sourceDir: ROOT_DIR,
|
||||
};
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
@@ -27,6 +28,10 @@ function parseArgs(argv) {
|
||||
options.outputName = arg.slice("--output-name=".length);
|
||||
} else if (arg === "--skip-build") {
|
||||
options.skipBuild = true;
|
||||
} else if (arg === "--source-dir") {
|
||||
options.sourceDir = argv[(index += 1)] ?? "";
|
||||
} else if (arg?.startsWith("--source-dir=")) {
|
||||
options.sourceDir = arg.slice("--source-dir=".length);
|
||||
} else {
|
||||
throw new Error(`unknown argument: ${arg}`);
|
||||
}
|
||||
@@ -34,10 +39,10 @@ function parseArgs(argv) {
|
||||
return options;
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
function run(command, args, cwd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: ROOT_DIR,
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
child.stdout.pipe(process.stderr, { end: false });
|
||||
@@ -53,10 +58,10 @@ function run(command, args) {
|
||||
});
|
||||
}
|
||||
|
||||
async function runCapture(command, args) {
|
||||
async function runCapture(command, args, cwd) {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
cwd: ROOT_DIR,
|
||||
cwd,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
let stdout = "";
|
||||
@@ -100,6 +105,7 @@ async function newestOpenClawTarball(outputDir, packOutput) {
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const sourceDir = path.resolve(ROOT_DIR, options.sourceDir || ROOT_DIR);
|
||||
const outputDir = path.resolve(
|
||||
ROOT_DIR,
|
||||
options.outputDir || path.join(".artifacts", "docker-e2e-package"),
|
||||
@@ -108,26 +114,28 @@ async function main() {
|
||||
|
||||
if (!options.skipBuild) {
|
||||
console.error("==> Building OpenClaw package artifacts");
|
||||
await run("pnpm", ["build"]);
|
||||
await run("pnpm", ["build"], sourceDir);
|
||||
}
|
||||
|
||||
console.error("==> Writing OpenClaw package inventory");
|
||||
await run("node", [
|
||||
"--import",
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
|
||||
]);
|
||||
await run(
|
||||
"node",
|
||||
[
|
||||
"--import",
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"-e",
|
||||
"const { writePackageDistInventory } = await import('./src/infra/package-dist-inventory.ts'); await writePackageDistInventory(process.cwd());",
|
||||
],
|
||||
sourceDir,
|
||||
);
|
||||
|
||||
console.error("==> Packing OpenClaw package");
|
||||
const packOutput = await runCapture("npm", [
|
||||
"pack",
|
||||
"--silent",
|
||||
"--ignore-scripts",
|
||||
"--pack-destination",
|
||||
outputDir,
|
||||
]);
|
||||
const packOutput = await runCapture(
|
||||
"npm",
|
||||
["pack", "--silent", "--ignore-scripts", "--pack-destination", outputDir],
|
||||
sourceDir,
|
||||
);
|
||||
let tarball = await newestOpenClawTarball(outputDir, packOutput);
|
||||
|
||||
if (options.outputName) {
|
||||
@@ -140,7 +148,11 @@ async function main() {
|
||||
}
|
||||
|
||||
console.error("==> Checking OpenClaw package tarball");
|
||||
await run("node", ["scripts/check-openclaw-package-tarball.mjs", tarball]);
|
||||
await run(
|
||||
"node",
|
||||
[path.join(ROOT_DIR, "scripts/check-openclaw-package-tarball.mjs"), tarball],
|
||||
sourceDir,
|
||||
);
|
||||
|
||||
process.stdout.write(`${tarball}\n`);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { spawn } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -18,6 +19,7 @@ function usage() {
|
||||
|
||||
Options:
|
||||
--package-spec <spec> Published npm spec for source=npm.
|
||||
--package-ref <ref> Trusted repo ref for source=ref.
|
||||
--package-url <url> HTTPS tarball URL for source=url.
|
||||
--package-sha256 <sha256> Expected tarball SHA-256 for source=url or source=artifact.
|
||||
--artifact-dir <dir> Directory containing exactly one .tgz for source=artifact.
|
||||
@@ -33,6 +35,7 @@ export function parseArgs(argv) {
|
||||
metadata: "",
|
||||
outputDir: "",
|
||||
outputName: DEFAULT_OUTPUT_NAME,
|
||||
packageRef: "",
|
||||
packageSha256: "",
|
||||
packageSpec: "",
|
||||
packageUrl: "",
|
||||
@@ -59,6 +62,8 @@ export function parseArgs(argv) {
|
||||
options.outputName = readValue(arg);
|
||||
} else if (arg === "--package-sha256") {
|
||||
options.packageSha256 = readValue(arg).toLowerCase();
|
||||
} else if (arg === "--package-ref") {
|
||||
options.packageRef = readValue(arg);
|
||||
} else if (arg === "--package-spec") {
|
||||
options.packageSpec = readValue(arg);
|
||||
} else if (arg === "--package-url") {
|
||||
@@ -167,6 +172,104 @@ async function findSingleTarball(dir) {
|
||||
return files[0];
|
||||
}
|
||||
|
||||
async function revParseTrustedInputRef(ref) {
|
||||
const candidates = [ref, `refs/remotes/origin/${ref}`, `refs/tags/${ref}`];
|
||||
for (const candidate of candidates) {
|
||||
const resolved = await run("git", ["rev-parse", "--verify", `${candidate}^{commit}`], {
|
||||
capture: true,
|
||||
}).then(
|
||||
(value) => value.trim(),
|
||||
() => "",
|
||||
);
|
||||
if (resolved) {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
throw new Error(`package_ref does not resolve to a commit: ${ref}`);
|
||||
}
|
||||
|
||||
async function resolveTrustedRepoRef(ref) {
|
||||
if (!ref || ref.trim() === "" || ref.startsWith("-")) {
|
||||
throw new Error(
|
||||
`package_ref must be a branch, tag, or full commit SHA; got: ${ref || "<empty>"}`,
|
||||
);
|
||||
}
|
||||
|
||||
await run("git", ["fetch", "--no-tags", "origin", "+refs/heads/*:refs/remotes/origin/*"]);
|
||||
await run("git", ["fetch", "--tags", "origin", "+refs/tags/*:refs/tags/*"]);
|
||||
|
||||
const selectedSha = await revParseTrustedInputRef(ref);
|
||||
const isMainAncestor = await run("git", [
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
selectedSha,
|
||||
"refs/remotes/origin/main",
|
||||
]).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
if (isMainAncestor) {
|
||||
return { selectedSha, trustedReason: "main-ancestor" };
|
||||
}
|
||||
|
||||
const releaseTags = (await run("git", ["tag", "--points-at", selectedSha], { capture: true }))
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (releaseTags.some((tag) => tag.startsWith("v"))) {
|
||||
return { selectedSha, trustedReason: "release-tag" };
|
||||
}
|
||||
|
||||
const containingBranches = (
|
||||
await run(
|
||||
"git",
|
||||
[
|
||||
"for-each-ref",
|
||||
"--format=%(refname:short)",
|
||||
"--contains",
|
||||
selectedSha,
|
||||
"refs/remotes/origin",
|
||||
],
|
||||
{ capture: true },
|
||||
)
|
||||
)
|
||||
.split(/\r?\n/u)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
if (containingBranches.some((branch) => branch.startsWith("origin/"))) {
|
||||
return { selectedSha, trustedReason: "repository-branch-history" };
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`package_ref ${ref} resolved to ${selectedSha}, which is not reachable from an OpenClaw branch or release tag`,
|
||||
);
|
||||
}
|
||||
|
||||
async function preparePackageSourceWorktree(ref) {
|
||||
const { selectedSha, trustedReason } = await resolveTrustedRepoRef(ref);
|
||||
const sourceDir = path.join(
|
||||
process.env.RUNNER_TEMP || os.tmpdir(),
|
||||
`openclaw-package-source-${process.pid}`,
|
||||
);
|
||||
await fs.rm(sourceDir, { recursive: true, force: true });
|
||||
await run("git", ["worktree", "add", "--detach", sourceDir, selectedSha]);
|
||||
return { selectedSha, sourceDir, trustedReason };
|
||||
}
|
||||
|
||||
async function installPackageSourceDeps(sourceDir) {
|
||||
await run(
|
||||
"pnpm",
|
||||
[
|
||||
"install",
|
||||
"--frozen-lockfile",
|
||||
"--ignore-scripts=false",
|
||||
"--config.engine-strict=false",
|
||||
"--config.enable-pre-post-scripts=true",
|
||||
],
|
||||
{ cwd: sourceDir },
|
||||
);
|
||||
}
|
||||
|
||||
async function moveNewestPackedTarball(outputDir, packOutput, outputName) {
|
||||
let filename = "";
|
||||
try {
|
||||
@@ -238,39 +341,68 @@ async function resolveCandidate(options) {
|
||||
const target = path.join(outputDir, options.outputName || DEFAULT_OUTPUT_NAME);
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
await fs.rm(target, { force: true });
|
||||
let packageRef = "";
|
||||
let packageSourceSha = "";
|
||||
let packageTrustedReason = "";
|
||||
let packageWorktreeDir = "";
|
||||
|
||||
if (options.source === "ref") {
|
||||
await run("node", [
|
||||
"scripts/package-openclaw-for-docker.mjs",
|
||||
"--output-dir",
|
||||
outputDir,
|
||||
"--output-name",
|
||||
options.outputName || DEFAULT_OUTPUT_NAME,
|
||||
]);
|
||||
} else if (options.source === "npm") {
|
||||
validateOpenClawPackageSpec(options.packageSpec);
|
||||
const packOutput = await run(
|
||||
"npm",
|
||||
["pack", options.packageSpec, "--ignore-scripts", "--json", "--pack-destination", outputDir],
|
||||
{ capture: true },
|
||||
);
|
||||
await moveNewestPackedTarball(outputDir, packOutput, options.outputName || DEFAULT_OUTPUT_NAME);
|
||||
} else if (options.source === "url") {
|
||||
if (!options.packageUrl) {
|
||||
throw new Error("source=url requires --package-url");
|
||||
try {
|
||||
if (options.source === "ref") {
|
||||
packageRef = options.packageRef || "main";
|
||||
const packageSource = await preparePackageSourceWorktree(packageRef);
|
||||
packageWorktreeDir = packageSource.sourceDir;
|
||||
packageSourceSha = packageSource.selectedSha;
|
||||
packageTrustedReason = packageSource.trustedReason;
|
||||
await installPackageSourceDeps(packageSource.sourceDir);
|
||||
await run("node", [
|
||||
"scripts/package-openclaw-for-docker.mjs",
|
||||
"--source-dir",
|
||||
packageSource.sourceDir,
|
||||
"--output-dir",
|
||||
outputDir,
|
||||
"--output-name",
|
||||
options.outputName || DEFAULT_OUTPUT_NAME,
|
||||
]);
|
||||
} else if (options.source === "npm") {
|
||||
validateOpenClawPackageSpec(options.packageSpec);
|
||||
const packOutput = await run(
|
||||
"npm",
|
||||
[
|
||||
"pack",
|
||||
options.packageSpec,
|
||||
"--ignore-scripts",
|
||||
"--json",
|
||||
"--pack-destination",
|
||||
outputDir,
|
||||
],
|
||||
{ capture: true },
|
||||
);
|
||||
await moveNewestPackedTarball(
|
||||
outputDir,
|
||||
packOutput,
|
||||
options.outputName || DEFAULT_OUTPUT_NAME,
|
||||
);
|
||||
} else if (options.source === "url") {
|
||||
if (!options.packageUrl) {
|
||||
throw new Error("source=url requires --package-url");
|
||||
}
|
||||
if (!options.packageSha256) {
|
||||
throw new Error("source=url requires --package-sha256");
|
||||
}
|
||||
await downloadUrl(options.packageUrl, target);
|
||||
} else if (options.source === "artifact") {
|
||||
if (!options.artifactDir) {
|
||||
throw new Error("source=artifact requires --artifact-dir");
|
||||
}
|
||||
const input = await findSingleTarball(options.artifactDir);
|
||||
await fs.copyFile(input, target);
|
||||
} else {
|
||||
throw new Error(`source must be one of: ref, npm, url, artifact. Got: ${options.source}`);
|
||||
}
|
||||
if (!options.packageSha256) {
|
||||
throw new Error("source=url requires --package-sha256");
|
||||
} finally {
|
||||
if (packageWorktreeDir) {
|
||||
await run("git", ["worktree", "remove", "--force", packageWorktreeDir]).catch(() => {});
|
||||
}
|
||||
await downloadUrl(options.packageUrl, target);
|
||||
} else if (options.source === "artifact") {
|
||||
if (!options.artifactDir) {
|
||||
throw new Error("source=artifact requires --artifact-dir");
|
||||
}
|
||||
const input = await findSingleTarball(options.artifactDir);
|
||||
await fs.copyFile(input, target);
|
||||
} else {
|
||||
throw new Error(`source must be one of: ref, npm, url, artifact. Got: ${options.source}`);
|
||||
}
|
||||
|
||||
const digest = await assertExpectedSha256(target, options.packageSha256);
|
||||
@@ -278,7 +410,10 @@ async function resolveCandidate(options) {
|
||||
const pkg = await readPackageJson(target);
|
||||
const metadata = {
|
||||
name: pkg.name,
|
||||
packageRef,
|
||||
packageSpec: options.packageSpec || "",
|
||||
packageSourceSha,
|
||||
packageTrustedReason,
|
||||
sha256: digest,
|
||||
source: options.source,
|
||||
tarball: path.relative(ROOT_DIR, target),
|
||||
|
||||
Reference in New Issue
Block a user