From 2de84e7dff70a08ad7dc7725d805387442a61674 Mon Sep 17 00:00:00 2001 From: Vincent Koc <25068+vincentkoc@users.noreply.github.com> Date: Thu, 25 Jun 2026 16:34:37 +0800 Subject: [PATCH] fix(release): reject partial ancestry metadata --- scripts/clawhub-cli-npm-release-check.mjs | 10 ++- .../clawhub-cli-npm-release-check.test.mjs | 66 +++++++++++++++---- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/scripts/clawhub-cli-npm-release-check.mjs b/scripts/clawhub-cli-npm-release-check.mjs index ae21afb2..40a824a8 100755 --- a/scripts/clawhub-cli-npm-release-check.mjs +++ b/scripts/clawhub-cli-npm-release-check.mjs @@ -154,16 +154,20 @@ function collectReleaseTagErrors({ packageVersion, releaseTag, releaseSha, relea `Release tag ${normalizedTag} does not match packages/clawhub/package.json version ${normalizedVersion}; expected v${normalizedVersion}.`, ); } - if (releaseSha?.trim() && releaseMainRef?.trim()) { + const normalizedReleaseSha = releaseSha?.trim() ?? ""; + const normalizedReleaseMainRef = releaseMainRef?.trim() ?? ""; + if (Boolean(normalizedReleaseSha) !== Boolean(normalizedReleaseMainRef)) { + errors.push("Release ancestry validation requires both --release-sha and --release-main-ref."); + } else if (normalizedReleaseSha && normalizedReleaseMainRef) { try { execFileSync( "git", - ["merge-base", "--is-ancestor", releaseSha.trim(), releaseMainRef.trim()], + ["merge-base", "--is-ancestor", normalizedReleaseSha, normalizedReleaseMainRef], { stdio: "ignore" }, ); } catch { errors.push( - `Tagged commit ${releaseSha.trim()} is not contained in ${releaseMainRef.trim()}.`, + `Tagged commit ${normalizedReleaseSha} is not contained in ${normalizedReleaseMainRef}.`, ); } } diff --git a/scripts/clawhub-cli-npm-release-check.test.mjs b/scripts/clawhub-cli-npm-release-check.test.mjs index 76470ec1..796c6061 100644 --- a/scripts/clawhub-cli-npm-release-check.test.mjs +++ b/scripts/clawhub-cli-npm-release-check.test.mjs @@ -4,22 +4,64 @@ import { spawnSync } from "node:child_process"; import { describe, expect, it } from "vitest"; describe("clawhub CLI npm release metadata check", () => { + const releaseTag = "v0.23.0"; + + function runCheck(args) { + const env = { ...process.env }; + delete env.RELEASE_TAG; + delete env.RELEASE_SHA; + delete env.RELEASE_MAIN_REF; + return spawnSync("node", ["scripts/clawhub-cli-npm-release-check.mjs", ...args], { + encoding: "utf8", + env, + }); + } + it("rejects option names used as missing flag values", () => { - const result = spawnSync( - "node", - [ - "scripts/clawhub-cli-npm-release-check.mjs", - "--tag", - "--release-sha", - "abc", - "--release-main-ref", - "main", - ], - { encoding: "utf8" }, - ); + const result = runCheck(["--tag", "--release-sha", "abc", "--release-main-ref", "main"]); expect(result.status).toBe(1); expect(result.stderr).toContain("--tag requires a value."); expect(result.stderr).not.toContain('Release tag must match vX.Y.Z; found "--release-sha".'); }); + + it.each([ + ["--release-sha", "HEAD"], + ["--release-main-ref", "HEAD"], + ])("rejects %s without its matching ancestry ref", (flag, value) => { + const result = runCheck(["--tag", releaseTag, flag, value]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain( + "Release ancestry validation requires both --release-sha and --release-main-ref.", + ); + }); + + it("validates that the release commit is an ancestor of the main ref", () => { + const result = runCheck([ + "--tag", + releaseTag, + "--release-sha", + "HEAD^", + "--release-main-ref", + "HEAD", + ]); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Release metadata OK"); + }); + + it("rejects a release commit that is not contained in the main ref", () => { + const result = runCheck([ + "--tag", + releaseTag, + "--release-sha", + "HEAD", + "--release-main-ref", + "HEAD^", + ]); + + expect(result.status).toBe(1); + expect(result.stderr).toContain("Tagged commit HEAD is not contained in HEAD^."); + }); });