diff --git a/scripts/resolve-openclaw-package-candidate.mjs b/scripts/resolve-openclaw-package-candidate.mjs index e0440301999..d5d4331ad13 100644 --- a/scripts/resolve-openclaw-package-candidate.mjs +++ b/scripts/resolve-openclaw-package-candidate.mjs @@ -189,6 +189,24 @@ async function findSingleTarball(dir) { return files[0]; } +export async function readArtifactPackageCandidateMetadata(dir) { + const metadataPath = path.join(path.resolve(ROOT_DIR, dir), "package-candidate.json"); + let raw = ""; + try { + raw = await fs.readFile(metadataPath, "utf8"); + } catch (error) { + if (error?.code === "ENOENT") { + return {}; + } + throw error; + } + const parsed = JSON.parse(raw); + if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) { + throw new Error(`artifact package-candidate.json must contain a JSON object`); + } + return parsed; +} + async function revParseTrustedInputRef(ref) { const candidates = [ref, `refs/remotes/origin/${ref}`, `refs/tags/${ref}`]; for (const candidate of candidates) { @@ -362,6 +380,7 @@ async function resolveCandidate(options) { let packageSourceSha = ""; let packageTrustedReason = ""; let packageWorktreeDir = ""; + let artifactMetadata = {}; try { if (options.source === "ref") { @@ -411,6 +430,17 @@ async function resolveCandidate(options) { if (!options.artifactDir) { throw new Error("source=artifact requires --artifact-dir"); } + artifactMetadata = await readArtifactPackageCandidateMetadata(options.artifactDir); + packageRef = + typeof artifactMetadata.packageRef === "string" ? artifactMetadata.packageRef : ""; + packageSourceSha = + typeof artifactMetadata.packageSourceSha === "string" + ? artifactMetadata.packageSourceSha + : ""; + packageTrustedReason = + typeof artifactMetadata.packageTrustedReason === "string" + ? artifactMetadata.packageTrustedReason + : ""; const input = await findSingleTarball(options.artifactDir); await fs.copyFile(input, target); } else { @@ -422,7 +452,8 @@ async function resolveCandidate(options) { } } - const digest = await assertExpectedSha256(target, options.packageSha256); + const artifactSha256 = typeof artifactMetadata.sha256 === "string" ? artifactMetadata.sha256 : ""; + const digest = await assertExpectedSha256(target, options.packageSha256 || artifactSha256); console.error(`Checking OpenClaw package tarball: ${target}`); const checkStartedAt = Date.now(); await run("node", ["scripts/check-openclaw-package-tarball.mjs", target], { diff --git a/test/scripts/resolve-openclaw-package-candidate.test.ts b/test/scripts/resolve-openclaw-package-candidate.test.ts index 5796d3aab17..ad573f8d221 100644 --- a/test/scripts/resolve-openclaw-package-candidate.test.ts +++ b/test/scripts/resolve-openclaw-package-candidate.test.ts @@ -1,6 +1,10 @@ +import { mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { describe, expect, it } from "vitest"; import { parseArgs, + readArtifactPackageCandidateMetadata, validateOpenClawPackageSpec, } from "../../scripts/resolve-openclaw-package-candidate.mjs"; @@ -57,4 +61,28 @@ describe("resolve-openclaw-package-candidate", () => { source: "npm", }); }); + + it("reads package source metadata from package artifacts", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-candidate-")); + await writeFile( + path.join(dir, "package-candidate.json"), + JSON.stringify( + { + packageRef: "release/2026.4.30", + packageSourceSha: "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2", + packageTrustedReason: "repository-branch-history", + sha256: "a".repeat(64), + }, + null, + 2, + ), + ); + + await expect(readArtifactPackageCandidateMetadata(dir)).resolves.toMatchObject({ + packageRef: "release/2026.4.30", + packageSourceSha: "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2", + packageTrustedReason: "repository-branch-history", + sha256: "a".repeat(64), + }); + }); });