diff --git a/scripts/resolve-openclaw-package-candidate.mjs b/scripts/resolve-openclaw-package-candidate.mjs index 89f4f07dff92..355b66062033 100644 --- a/scripts/resolve-openclaw-package-candidate.mjs +++ b/scripts/resolve-openclaw-package-candidate.mjs @@ -487,6 +487,18 @@ export async function readArtifactPackageCandidateMetadata(dir) { if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) { throw new Error(`artifact package-candidate.json must contain a JSON object`); } + const packageSourceSha = + typeof parsed.packageSourceSha === "string" ? parsed.packageSourceSha.trim() : ""; + if (packageSourceSha && !/^[0-9a-f]{40}$/iu.test(packageSourceSha)) { + throw new Error( + "artifact package-candidate.json packageSourceSha must be a 40-character commit SHA", + ); + } + if (typeof parsed.packageSourceSha === "string") { + return packageSourceSha + ? { ...parsed, packageSourceSha: packageSourceSha.toLowerCase() } + : { ...parsed, packageSourceSha: "" }; + } return parsed; } diff --git a/test/scripts/resolve-openclaw-package-candidate.test.ts b/test/scripts/resolve-openclaw-package-candidate.test.ts index 18611e2d7735..eacafdb3f729 100644 --- a/test/scripts/resolve-openclaw-package-candidate.test.ts +++ b/test/scripts/resolve-openclaw-package-candidate.test.ts @@ -1137,6 +1137,51 @@ describe("resolve-openclaw-package-candidate", () => { }); }); + it("normalizes artifact package source SHAs before workflow output", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-candidate-sha-")); + tempDirs.push(dir); + await writeFile( + path.join(dir, "package-candidate.json"), + JSON.stringify({ + packageSourceSha: "66CE632B9B7C5C7FDD3E66C739687D51638AD6E2", + }), + ); + + await expect(readArtifactPackageCandidateMetadata(dir)).resolves.toEqual({ + packageSourceSha: "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2", + }); + }); + + it("normalizes whitespace-only artifact package source SHAs to absent", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-candidate-empty-sha-")); + tempDirs.push(dir); + await writeFile( + path.join(dir, "package-candidate.json"), + JSON.stringify({ + packageSourceSha: " \r\n ", + }), + ); + + await expect(readArtifactPackageCandidateMetadata(dir)).resolves.toEqual({ + packageSourceSha: "", + }); + }); + + it("rejects malformed artifact package source SHAs", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-candidate-bad-sha-")); + tempDirs.push(dir); + await writeFile( + path.join(dir, "package-candidate.json"), + JSON.stringify({ + packageSourceSha: "66ce632b9b7c5c7fdd3e66c739687d51638ad6e2\r\nsource=main", + }), + ); + + await expect(readArtifactPackageCandidateMetadata(dir)).rejects.toThrow( + "artifact package-candidate.json packageSourceSha must be a 40-character commit SHA", + ); + }); + it("accepts uppercase package artifact SHA-256 metadata", async () => { const dir = await mkdtemp(path.join(tmpdir(), "openclaw-package-sha-")); tempDirs.push(dir);