mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 15:59:30 +00:00
fix(release): allow SHA-only extended-stable preflight (#101466)
This commit is contained in:
parent
fdc7892a6e
commit
72ca911e3c
5 changed files with 135 additions and 24 deletions
1
.github/workflows/openclaw-npm-release.yml
vendored
1
.github/workflows/openclaw-npm-release.yml
vendored
|
|
@ -133,6 +133,7 @@ jobs:
|
|||
- name: Validate npm release request
|
||||
env:
|
||||
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
|
||||
PREFLIGHT_ONLY: ${{ inputs.preflight_only }}
|
||||
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
|
||||
RELEASE_TAG: ${{ inputs.tag }}
|
||||
NPM_WORKFLOW_REF: ${{ github.ref }}
|
||||
|
|
|
|||
|
|
@ -72,8 +72,18 @@ export function validateNpmPublishBoundary(
|
|||
export function validateExtendedStableNpmReleaseRequest(request) {
|
||||
const bypassExtendedStableGuard = request.bypassExtendedStableGuard ?? false;
|
||||
requireExtendedStableBypassTag(request.npmDistTag, bypassExtendedStableGuard);
|
||||
const taggedVersion = request.releaseTag.startsWith("v")
|
||||
? parseReleaseVersion(request.releaseTag.slice(1))
|
||||
const shaPreflight =
|
||||
request.preflightOnly === true && /^[0-9a-f]{40}$/iu.test(request.releaseTag);
|
||||
if (
|
||||
shaPreflight &&
|
||||
request.checkoutSha &&
|
||||
request.releaseTag.toLowerCase() !== request.checkoutSha.toLowerCase()
|
||||
) {
|
||||
throw new Error("Validation-only SHA must match the checked-out commit exactly.");
|
||||
}
|
||||
const effectiveReleaseTag = shaPreflight ? `v${request.packageVersion}` : request.releaseTag;
|
||||
const taggedVersion = effectiveReleaseTag.startsWith("v")
|
||||
? parseReleaseVersion(effectiveReleaseTag.slice(1))
|
||||
: null;
|
||||
|
||||
if (request.npmDistTag !== "extended-stable") {
|
||||
|
|
@ -89,7 +99,7 @@ export function validateExtendedStableNpmReleaseRequest(request) {
|
|||
|
||||
if (
|
||||
taggedVersion === null ||
|
||||
request.releaseTag !== `v${taggedVersion.version}` ||
|
||||
effectiveReleaseTag !== `v${taggedVersion.version}` ||
|
||||
taggedVersion.channel !== "stable"
|
||||
) {
|
||||
throw new Error(
|
||||
|
|
@ -294,9 +304,11 @@ function validateRequestFromRepository() {
|
|||
const npmDistTag = process.env.RELEASE_NPM_DIST_TAG ?? "";
|
||||
const releaseTag = process.env.RELEASE_TAG ?? "";
|
||||
const npmWorkflowRef = process.env.NPM_WORKFLOW_REF ?? "";
|
||||
const preflightOnly = process.env.PREFLIGHT_ONLY === "true";
|
||||
const bypassExtendedStableGuard = parseExtendedStableGuardBypass(
|
||||
process.env.BYPASS_EXTENDED_STABLE_GUARD ?? "",
|
||||
);
|
||||
const packageVersion = JSON.parse(readFileSync("package.json", "utf8")).version;
|
||||
if (npmDistTag !== "extended-stable") {
|
||||
return validateExtendedStableNpmReleaseRequest({
|
||||
bypassExtendedStableGuard,
|
||||
|
|
@ -306,26 +318,59 @@ function validateRequestFromRepository() {
|
|||
checkoutSha: "",
|
||||
tagSha: "",
|
||||
extendedStableBranchSha: "",
|
||||
packageVersion: JSON.parse(readFileSync("package.json", "utf8")).version,
|
||||
packageVersion,
|
||||
mainPackageVersion: "",
|
||||
});
|
||||
}
|
||||
|
||||
const parsed = releaseTag.startsWith("v") ? parseReleaseVersion(releaseTag.slice(1)) : null;
|
||||
const shaPreflight = preflightOnly && /^[0-9a-f]{40}$/iu.test(releaseTag);
|
||||
const effectiveReleaseTag = shaPreflight ? `v${packageVersion}` : releaseTag;
|
||||
const parsed = effectiveReleaseTag.startsWith("v")
|
||||
? parseReleaseVersion(effectiveReleaseTag.slice(1))
|
||||
: null;
|
||||
if (!parsed || parsed.channel !== "stable" || parsed.correctionNumber !== undefined) {
|
||||
return validateExtendedStableNpmReleaseRequest({
|
||||
npmDistTag,
|
||||
bypassExtendedStableGuard,
|
||||
releaseTag,
|
||||
preflightOnly,
|
||||
npmWorkflowRef,
|
||||
checkoutSha: "",
|
||||
tagSha: "",
|
||||
extendedStableBranchSha: "",
|
||||
packageVersion: JSON.parse(readFileSync("package.json", "utf8")).version,
|
||||
packageVersion,
|
||||
mainPackageVersion: "",
|
||||
});
|
||||
}
|
||||
const extendedStableBranch = `extended-stable/${parsed.year}.${parsed.month}.33`;
|
||||
if (shaPreflight) {
|
||||
execFileSync(
|
||||
"git",
|
||||
[
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
"origin",
|
||||
`+refs/heads/${extendedStableBranch}:refs/remotes/origin/${extendedStableBranch}`,
|
||||
...(bypassExtendedStableGuard ? [] : ["+refs/heads/main:refs/remotes/origin/main"]),
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
const checkoutSha = git(["rev-parse", "HEAD"]);
|
||||
return validateExtendedStableNpmReleaseRequest({
|
||||
npmDistTag,
|
||||
bypassExtendedStableGuard,
|
||||
releaseTag,
|
||||
preflightOnly,
|
||||
npmWorkflowRef,
|
||||
checkoutSha,
|
||||
tagSha: checkoutSha,
|
||||
extendedStableBranchSha: git(["rev-parse", `refs/remotes/origin/${extendedStableBranch}`]),
|
||||
packageVersion,
|
||||
mainPackageVersion: bypassExtendedStableGuard
|
||||
? ""
|
||||
: packageVersionAt("refs/remotes/origin/main"),
|
||||
});
|
||||
}
|
||||
if (bypassExtendedStableGuard) {
|
||||
execFileSync(
|
||||
"git",
|
||||
|
|
@ -350,7 +395,7 @@ function validateRequestFromRepository() {
|
|||
checkoutSha: git(["rev-parse", "HEAD"]),
|
||||
tagSha: git(["rev-parse", `${releaseTag}^{commit}`]),
|
||||
extendedStableBranchSha: git(["rev-parse", `refs/remotes/origin/${extendedStableBranch}`]),
|
||||
packageVersion: JSON.parse(readFileSync("package.json", "utf8")).version,
|
||||
packageVersion,
|
||||
mainPackageVersion: "",
|
||||
});
|
||||
}
|
||||
|
|
@ -378,7 +423,7 @@ function validateRequestFromRepository() {
|
|||
checkoutSha: git(["rev-parse", "HEAD"]),
|
||||
tagSha: git(["rev-parse", `${releaseTag}^{commit}`]),
|
||||
extendedStableBranchSha: git(["rev-parse", `refs/remotes/origin/${extendedStableBranch}`]),
|
||||
packageVersion: JSON.parse(readFileSync("package.json", "utf8")).version,
|
||||
packageVersion,
|
||||
mainPackageVersion: packageVersionAt("refs/remotes/origin/main"),
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,6 +209,30 @@ describe("extended-stable npm release request", () => {
|
|||
).toEqual({ extendedStable: false });
|
||||
});
|
||||
|
||||
it("accepts a SHA-only extended-stable preflight bound to the branch tip", () => {
|
||||
expect(
|
||||
validateExtendedStableNpmReleaseRequest({
|
||||
...valid,
|
||||
preflightOnly: true,
|
||||
releaseTag: sha,
|
||||
}),
|
||||
).toEqual({
|
||||
extendedStable: true,
|
||||
releaseVersion: "2026.6.33",
|
||||
extendedStableBranch: "extended-stable/2026.6.33",
|
||||
});
|
||||
expect(() =>
|
||||
validateExtendedStableNpmReleaseRequest({
|
||||
...valid,
|
||||
preflightOnly: true,
|
||||
releaseTag: "b".repeat(40),
|
||||
}),
|
||||
).toThrow(/must match the checked-out commit/u);
|
||||
expect(() => validateExtendedStableNpmReleaseRequest({ ...valid, releaseTag: sha })).toThrow(
|
||||
/exact final vYYYY\.M\.P release tag/u,
|
||||
);
|
||||
});
|
||||
|
||||
it("bypasses patch and protected-main policy while preserving canonical branch identity", () => {
|
||||
const bypassed = {
|
||||
...valid,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,9 @@ describe("minimal npm extended-stable workflow", () => {
|
|||
expect(step(parsed.jobs?.preflight_openclaw_npm, "Validate npm release request").run).toContain(
|
||||
"openclaw-npm-extended-stable-release.mjs validate-request",
|
||||
);
|
||||
expect(
|
||||
step(parsed.jobs?.preflight_openclaw_npm, "Validate npm release request").env?.PREFLIGHT_ONLY,
|
||||
).toBe("${{ inputs.preflight_only }}");
|
||||
expect(
|
||||
step(parsed.jobs?.validate_publish_request, "Validate npm release request").run,
|
||||
).toContain("openclaw-npm-extended-stable-release.mjs validate-request");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// OpenClaw NPM Publish tests cover publish wrapper argument safety.
|
||||
import { execFileSync, spawnSync } from "node:child_process";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
|
|
@ -14,14 +14,36 @@ function makeTempDir(prefix: string): string {
|
|||
return dir;
|
||||
}
|
||||
|
||||
function runPublishWrapper(args: string[], env: NodeJS.ProcessEnv = {}) {
|
||||
function runPublishWrapper(
|
||||
args: string[],
|
||||
env: NodeJS.ProcessEnv = {},
|
||||
cwd: string = process.cwd(),
|
||||
) {
|
||||
return spawnSync("bash", [scriptPath, ...args], {
|
||||
cwd: process.cwd(),
|
||||
cwd,
|
||||
encoding: "utf8",
|
||||
env: { ...process.env, ...env },
|
||||
});
|
||||
}
|
||||
|
||||
function makeReleaseCheckout(root: string, version: string): string {
|
||||
const checkout = path.join(root, "checkout");
|
||||
const scriptsDir = path.join(checkout, "scripts");
|
||||
mkdirSync(scriptsDir, { recursive: true });
|
||||
writeFileSync(path.join(checkout, "package.json"), JSON.stringify({ version }), "utf8");
|
||||
copyFileSync(scriptPath, path.join(checkout, scriptPath));
|
||||
copyFileSync(
|
||||
"scripts/openclaw-npm-extended-stable-release.mjs",
|
||||
path.join(checkout, "scripts/openclaw-npm-extended-stable-release.mjs"),
|
||||
);
|
||||
mkdirSync(path.join(scriptsDir, "lib"));
|
||||
copyFileSync(
|
||||
"scripts/lib/npm-publish-plan.mjs",
|
||||
path.join(checkout, "scripts/lib/npm-publish-plan.mjs"),
|
||||
);
|
||||
return checkout;
|
||||
}
|
||||
|
||||
function makePackageTarball(root: string, packageJson?: string): string {
|
||||
const packageDir = path.join(root, "package");
|
||||
const tarball = path.join(root, "openclaw.tgz");
|
||||
|
|
@ -85,7 +107,8 @@ describe("openclaw npm publish wrapper", () => {
|
|||
it.each(["beta", "latest"])("publishes the prepared tarball to the %s dist-tag", (distTag) => {
|
||||
const tempRoot = makeTempDir("openclaw-npm-publish-");
|
||||
const binDir = path.join(tempRoot, "bin");
|
||||
const packageVersion = JSON.parse(readFileSync("package.json", "utf8")).version as string;
|
||||
const packageVersion = distTag === "beta" ? "2026.5.32-beta.1" : "2026.5.32";
|
||||
const checkout = makeReleaseCheckout(tempRoot, packageVersion);
|
||||
const tarball = makePackageTarball(tempRoot, JSON.stringify({ version: packageVersion }));
|
||||
const npmLog = path.join(tempRoot, "npm.log");
|
||||
mkdirSync(binDir);
|
||||
|
|
@ -93,10 +116,14 @@ describe("openclaw npm publish wrapper", () => {
|
|||
mode: 0o755,
|
||||
});
|
||||
|
||||
const result = runPublishWrapper(["--publish", tarball], {
|
||||
OPENCLAW_NPM_PUBLISH_TAG: distTag,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
});
|
||||
const result = runPublishWrapper(
|
||||
["--publish", tarball],
|
||||
{
|
||||
OPENCLAW_NPM_PUBLISH_TAG: distTag,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
},
|
||||
checkout,
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(readFileSync(npmLog, "utf8")).toContain(
|
||||
|
|
@ -136,9 +163,15 @@ describe("openclaw npm publish wrapper", () => {
|
|||
});
|
||||
|
||||
it("rejects publishing the current pre-.33 final version to extended-stable", () => {
|
||||
const result = runPublishWrapper(["--publish"], {
|
||||
OPENCLAW_NPM_PUBLISH_TAG: "extended-stable",
|
||||
});
|
||||
const tempRoot = makeTempDir("openclaw-npm-publish-");
|
||||
const checkout = makeReleaseCheckout(tempRoot, "2026.5.32");
|
||||
const result = runPublishWrapper(
|
||||
["--publish"],
|
||||
{
|
||||
OPENCLAW_NPM_PUBLISH_TAG: "extended-stable",
|
||||
},
|
||||
checkout,
|
||||
);
|
||||
|
||||
expect(result.status).not.toBe(0);
|
||||
expect(result.stderr).toContain(
|
||||
|
|
@ -149,17 +182,22 @@ describe("openclaw npm publish wrapper", () => {
|
|||
it("publishes a pre-.33 final version to extended-stable with the explicit bypass", () => {
|
||||
const tempRoot = makeTempDir("openclaw-npm-publish-");
|
||||
const binDir = path.join(tempRoot, "bin");
|
||||
const checkout = makeReleaseCheckout(tempRoot, "2026.5.32");
|
||||
const npmLog = path.join(tempRoot, "npm.log");
|
||||
mkdirSync(binDir);
|
||||
writeFileSync(path.join(binDir, "npm"), `#!/bin/sh\nprintf '%s\\n' "$*" > "${npmLog}"\n`, {
|
||||
mode: 0o755,
|
||||
});
|
||||
|
||||
const result = runPublishWrapper(["--publish"], {
|
||||
BYPASS_EXTENDED_STABLE_GUARD: "true",
|
||||
OPENCLAW_NPM_PUBLISH_TAG: "extended-stable",
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
});
|
||||
const result = runPublishWrapper(
|
||||
["--publish"],
|
||||
{
|
||||
BYPASS_EXTENDED_STABLE_GUARD: "true",
|
||||
OPENCLAW_NPM_PUBLISH_TAG: "extended-stable",
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
},
|
||||
checkout,
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(readFileSync(npmLog, "utf8")).toContain(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue