Release validation: add isolated campaign skill runner (#129726)

This commit is contained in:
Patrick Erichsen
2026-08-25 17:58:39 -07:00
committed by GitHub
parent 1ecc35f95f
commit 004b06b6a0
7 changed files with 1035 additions and 55 deletions
@@ -0,0 +1,43 @@
export type ReleaseValidationCampaignArtifact =
| {
schema: "openclaw.release-validation-campaign/v1";
operation: "upsert";
tag: string;
stableTrain: string;
releaseUrl: string;
releaseCommit: string;
guidanceMainSha: string;
title: string;
body: string;
}
| {
schema: "openclaw.release-validation-campaign/v1";
operation: "close";
tag: string;
stableTrain: string;
releaseUrl: string;
};
export function validateReleaseValidationCampaignArtifact(
artifact: unknown,
options?: {
expectedTag?: string;
expectedReleaseCommit?: string;
expectedGuidanceMainSha?: string;
},
): ReleaseValidationCampaignArtifact;
export function runReleaseValidationCampaignPublish(params: {
github: any;
context: { repo: { owner: string; repo: string } };
core: { info(message: string): void; setOutput?(name: string, value: string): void };
artifact: unknown;
expectedTag?: string;
expectedReleaseCommit?: string;
expectedGuidanceMainSha?: string;
campaignIssueNumber?: number;
}): Promise<{
action: "create" | "update" | "close" | "noop";
issueNumber: number | undefined;
issueUrl: string | undefined;
}>;
@@ -0,0 +1,383 @@
const CAMPAIGN_SCHEMA = "openclaw.release-validation-campaign/v1";
const CAMPAIGN_LABEL = "release-validation";
const FINDING_LABEL = "release-validation-finding";
const CAMPAIGN_LABEL_COLOR = "0E8A16";
const FINDING_LABEL_COLOR = "D93F0B";
const MAX_BODY_BYTES = 60_000;
const BETA_TAG_PATTERN = /^v(\d{4})\.(\d+)\.(\d+)-beta\.([1-9]\d*)$/u;
const STABLE_TAG_PATTERN = /^v(\d{4})\.(\d+)\.(\d+)$/u;
const FULL_SHA_PATTERN = /^[0-9a-f]{40}$/u;
const PLACEHOLDER_PATTERN = /\{\{([A-Z0-9_]+)\}\}/gu;
const ALLOWED_PLACEHOLDERS = new Set(["OPENCLAW", "RESTART_GATEWAY"]);
function labelName(label) {
return typeof label === "string" ? label : label?.name;
}
function countOccurrences(value, needle) {
return value.split(needle).length - 1;
}
function assertExactKeys(value, allowedKeys) {
const unexpected = Object.keys(value).filter((key) => !allowedKeys.has(key));
if (unexpected.length > 0) {
throw new Error(`Campaign artifact has unexpected field(s): ${unexpected.join(", ")}`);
}
}
function requireString(value, field) {
if (typeof value !== "string" || value.length === 0) {
throw new Error(`Campaign artifact field ${field} must be a non-empty string`);
}
return value;
}
function parseReleaseTag(tag) {
const beta = BETA_TAG_PATTERN.exec(tag);
if (beta) {
const stableTrain = `v${beta[1]}.${beta[2]}.${beta[3]}`;
return { kind: "beta", stableTrain, displayVersion: stableTrain.slice(1) };
}
const stable = STABLE_TAG_PATTERN.exec(tag);
if (stable) {
const stableTrain = `v${stable[1]}.${stable[2]}.${stable[3]}`;
return { kind: "stable", stableTrain, displayVersion: stableTrain.slice(1) };
}
throw new Error(`Unsupported release-validation campaign tag: ${tag}`);
}
function validateBetaBody(body, { tag, stableTrain, releaseUrl, releaseCommit, guidanceMainSha }) {
if (Buffer.byteLength(body) > MAX_BODY_BYTES) {
throw new Error("Release-validation campaign body exceeds the issue-body safety limit");
}
const marker = `<!-- openclaw-release-validation:${stableTrain} -->`;
const requiredOnce = [
marker,
`- Current beta: [${tag}](${releaseUrl})`,
`- Beta commit: \`${releaseCommit}\``,
`- Guidance main commit: \`${guidanceMainSha}\``,
"- Test target: latest immutable `origin/main`",
"<!-- validation-guidance:start -->",
"<!-- validation-guidance:end -->",
"## Priority surfaces for this release",
`## Priority surfaces since ${tag}`,
"## Participate",
];
for (const required of requiredOnce) {
if (countOccurrences(body, required) !== 1) {
throw new Error(`Campaign body must contain exactly one ${required}`);
}
}
if (countOccurrences(body, "| **Testing notes**") !== 6) {
throw new Error("Campaign body must contain exactly six empty testing-notes rows");
}
for (const line of body.split("\n").filter((candidate) => candidate.includes("Testing notes"))) {
if (!/^\| \*\*Testing notes\*\*\s+\|\s+\|$/u.test(line)) {
throw new Error("Every campaign testing-notes row must be empty");
}
}
for (const match of body.matchAll(PLACEHOLDER_PATTERN)) {
if (!ALLOWED_PLACEHOLDERS.has(match[1])) {
throw new Error(`Campaign body contains unsupported placeholder {{${match[1]}}}`);
}
}
if (body.includes("{{TEST_ENV}}")) {
throw new Error("Campaign body contains the retired TEST_ENV placeholder");
}
if (/(?:file:\/\/|\/(?:Users|home)\/[^\s)]+|[A-Za-z]:\\Users\\[^\s)]+)/u.test(body)) {
throw new Error("Campaign body contains a local filesystem path");
}
}
export function validateReleaseValidationCampaignArtifact(
artifact,
{ expectedTag, expectedReleaseCommit, expectedGuidanceMainSha } = {},
) {
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact)) {
throw new Error("Release-validation campaign artifact must be an object");
}
const schema = requireString(artifact.schema, "schema");
if (schema !== CAMPAIGN_SCHEMA) {
throw new Error(`Unsupported release-validation campaign schema: ${schema}`);
}
const tag = requireString(artifact.tag, "tag");
if (expectedTag !== undefined && tag !== expectedTag) {
throw new Error(`Campaign artifact tag ${tag} does not match ${expectedTag}`);
}
const parsedTag = parseReleaseTag(tag);
const stableTrain = requireString(artifact.stableTrain, "stableTrain");
if (stableTrain !== parsedTag.stableTrain) {
throw new Error(`Campaign artifact stable train ${stableTrain} does not match ${tag}`);
}
const releaseUrl = requireString(artifact.releaseUrl, "releaseUrl");
const expectedReleaseUrl = `https://github.com/openclaw/openclaw/releases/tag/${tag}`;
if (releaseUrl !== expectedReleaseUrl) {
throw new Error(`Campaign artifact release URL must be ${expectedReleaseUrl}`);
}
const operation = requireString(artifact.operation, "operation");
if (parsedTag.kind === "stable") {
assertExactKeys(artifact, new Set(["schema", "operation", "tag", "stableTrain", "releaseUrl"]));
if (operation !== "close") {
throw new Error("A stable campaign artifact must use the close operation");
}
return { schema, operation, tag, stableTrain, releaseUrl };
}
assertExactKeys(
artifact,
new Set([
"schema",
"operation",
"tag",
"stableTrain",
"releaseUrl",
"releaseCommit",
"guidanceMainSha",
"title",
"body",
]),
);
if (operation !== "upsert") {
throw new Error("A beta campaign artifact must use the upsert operation");
}
const releaseCommit = requireString(artifact.releaseCommit, "releaseCommit");
const guidanceMainSha = requireString(artifact.guidanceMainSha, "guidanceMainSha");
if (!FULL_SHA_PATTERN.test(releaseCommit) || !FULL_SHA_PATTERN.test(guidanceMainSha)) {
throw new Error("Campaign artifact commits must be full lowercase Git SHAs");
}
if (expectedReleaseCommit !== undefined && releaseCommit !== expectedReleaseCommit) {
throw new Error("Campaign artifact release commit does not match the release tag");
}
if (expectedGuidanceMainSha !== undefined && guidanceMainSha !== expectedGuidanceMainSha) {
throw new Error("Campaign artifact guidance main SHA does not match the workflow checkout");
}
const title = requireString(artifact.title, "title");
const expectedTitle = `OpenClaw ${parsedTag.displayVersion} beta feedback`;
if (title !== expectedTitle) {
throw new Error(`Campaign artifact title must be ${expectedTitle}`);
}
const body = requireString(artifact.body, "body");
validateBetaBody(body, { tag, stableTrain, releaseUrl, releaseCommit, guidanceMainSha });
return {
schema,
operation,
tag,
stableTrain,
releaseUrl,
releaseCommit,
guidanceMainSha,
title,
body,
};
}
async function ensureLabel({ github, owner, repo, name, color, description }) {
try {
await github.rest.issues.getLabel({ owner, repo, name });
} catch (error) {
if (error?.status !== 404) {
throw error;
}
await github.rest.issues.createLabel({ owner, repo, name, color, description });
}
}
function hasMarker(issue, stableTrain) {
return issue.body?.includes(`<!-- openclaw-release-validation:${stableTrain} -->`);
}
function hasLegacyMarker(issue, stableTrain) {
const escapedTrain = stableTrain.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
return new RegExp(
`<!-- openclaw-release-validation:${escapedTrain}-beta\\.[1-9]\\d* -->`,
"u",
).test(issue.body ?? "");
}
function betaNumberFromBody(body, stableTrain) {
const escapedTrain = stableTrain.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
const match = new RegExp(
`^- Current beta: \\[${escapedTrain}-beta\\.([1-9]\\d*)\\]\\(`,
"mu",
).exec(body ?? "");
return match ? Number(match[1]) : undefined;
}
async function closeCampaign({ github, owner, repo, issue, comment }) {
if (comment) {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: comment,
});
}
const labels = (issue.labels ?? [])
.map(labelName)
.filter((name) => name && name !== CAMPAIGN_LABEL);
await github.rest.issues.update({
owner,
repo,
issue_number: issue.number,
state: "closed",
state_reason: "completed",
labels,
});
}
export async function runReleaseValidationCampaignPublish({
github,
context,
core,
artifact,
expectedTag,
expectedReleaseCommit,
expectedGuidanceMainSha,
campaignIssueNumber,
}) {
const validated = validateReleaseValidationCampaignArtifact(artifact, {
expectedTag,
expectedReleaseCommit,
expectedGuidanceMainSha,
});
const { owner, repo } = context.repo;
const labeled = await github.paginate(github.rest.issues.listForRepo, {
owner,
repo,
state: "open",
labels: CAMPAIGN_LABEL,
per_page: 100,
});
const openCampaigns = labeled.filter((issue) => !issue.pull_request);
let matching = openCampaigns.filter((issue) => hasMarker(issue, validated.stableTrain));
if (campaignIssueNumber !== undefined) {
const { data: requestedIssue } = await github.rest.issues.get({
owner,
repo,
issue_number: campaignIssueNumber,
});
if (
requestedIssue.pull_request ||
requestedIssue.state !== "open" ||
(!hasMarker(requestedIssue, validated.stableTrain) &&
!hasLegacyMarker(requestedIssue, validated.stableTrain))
) {
throw new Error(
`Requested campaign issue #${campaignIssueNumber} is not an open ${validated.stableTrain} campaign`,
);
}
if (matching.some((issue) => issue.number !== requestedIssue.number)) {
throw new Error(
`Requested campaign issue #${campaignIssueNumber} conflicts with the labeled campaign`,
);
}
matching = [requestedIssue];
}
if (matching.length > 1) {
throw new Error(`Multiple open campaigns match ${validated.stableTrain}`);
}
if (validated.operation === "close") {
if (matching.length === 0) {
core.info(`No open ${validated.stableTrain} campaign remains to close.`);
return { action: "noop", issueNumber: undefined, issueUrl: undefined };
}
await closeCampaign({
github,
owner,
repo,
issue: matching[0],
comment: `Stable release: ${validated.releaseUrl}`,
});
core.info(`Closed release-validation campaign #${matching[0].number}.`);
return {
action: "close",
issueNumber: matching[0].number,
issueUrl: matching[0].html_url,
};
}
await ensureLabel({
github,
owner,
repo,
name: CAMPAIGN_LABEL,
color: CAMPAIGN_LABEL_COLOR,
description: "Canonical OpenClaw release-validation campaign",
});
await ensureLabel({
github,
owner,
repo,
name: FINDING_LABEL,
color: FINDING_LABEL_COLOR,
description: "Bug found during release-validation testing",
});
let issue;
let action;
if (matching.length === 1) {
const currentBetaNumber = betaNumberFromBody(matching[0].body, validated.stableTrain);
const nextBetaNumber = Number(BETA_TAG_PATTERN.exec(validated.tag)?.[4]);
if (currentBetaNumber !== undefined && nextBetaNumber < currentBetaNumber) {
throw new Error(
`Refusing to replace beta.${currentBetaNumber} campaign guidance with older ${validated.tag}`,
);
}
const { data } = await github.rest.issues.update({
owner,
repo,
issue_number: matching[0].number,
title: validated.title,
body: validated.body,
state: "open",
labels: [CAMPAIGN_LABEL],
});
issue = data;
action = "update";
} else {
const { data } = await github.rest.issues.create({
owner,
repo,
title: validated.title,
body: validated.body,
labels: [CAMPAIGN_LABEL],
});
issue = data;
action = "create";
}
const { data: readback } = await github.rest.issues.get({
owner,
repo,
issue_number: issue.number,
});
if (
readback.state !== "open" ||
readback.title !== validated.title ||
readback.body !== validated.body ||
readback.labels?.length !== 1 ||
labelName(readback.labels[0]) !== CAMPAIGN_LABEL
) {
throw new Error(`Campaign issue #${issue.number} failed post-write verification`);
}
for (const older of openCampaigns.filter((candidate) => candidate.number !== issue.number)) {
await closeCampaign({
github,
owner,
repo,
issue: older,
comment: `Superseded by ${readback.html_url}`,
});
}
core.info(`${action === "create" ? "Created" : "Updated"} ${readback.html_url}.`);
core.setOutput?.("issue-url", readback.html_url);
return { action, issueNumber: readback.number, issueUrl: readback.html_url };
}