mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-18 00:23:25 -06:00
ci: persist extended-stable Docker completion
This commit is contained in:
@@ -3,6 +3,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { parse } from "yaml";
|
||||
import {
|
||||
createDockerChannelPromotionPlan,
|
||||
createDockerPublicationStatus,
|
||||
findDockerPublicationStatus,
|
||||
promoteDockerChannel,
|
||||
} from "../../scripts/docker-channel-promote.mjs";
|
||||
|
||||
@@ -80,6 +82,72 @@ function requireJob(workflow: Workflow, name: string): WorkflowJob {
|
||||
}
|
||||
|
||||
describe("Docker channel promotion", () => {
|
||||
it("binds durable extended-stable completion to the release SHA and workflow run", () => {
|
||||
const sourceSha = "a".repeat(40);
|
||||
const payload = createDockerPublicationStatus({
|
||||
version: "2026.6.35",
|
||||
repository: "openclaw/openclaw",
|
||||
sourceSha,
|
||||
runId: "12345",
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
context: "openclaw/docker-release/2026.6.35",
|
||||
description:
|
||||
"Verified GHCR + Docker Hub images, attestations, platforms, and channel aliases.",
|
||||
state: "success",
|
||||
target_url: "https://github.com/openclaw/openclaw/actions/runs/12345",
|
||||
});
|
||||
expect(
|
||||
findDockerPublicationStatus({
|
||||
combinedStatus: {
|
||||
sha: sourceSha,
|
||||
statuses: [{ ...payload, creator: { login: "github-actions[bot]" } }],
|
||||
},
|
||||
version: "2026.6.35",
|
||||
repository: "openclaw/openclaw",
|
||||
sourceSha,
|
||||
}),
|
||||
).toEqual({ runId: "12345", targetUrl: payload.target_url });
|
||||
});
|
||||
|
||||
it("does not accept release visibility or malformed status as Docker completion", () => {
|
||||
const sourceSha = "a".repeat(40);
|
||||
expect(
|
||||
findDockerPublicationStatus({
|
||||
combinedStatus: { sha: sourceSha, statuses: [] },
|
||||
version: "2026.6.35",
|
||||
repository: "openclaw/openclaw",
|
||||
sourceSha,
|
||||
}),
|
||||
).toBeNull();
|
||||
|
||||
const payload = createDockerPublicationStatus({
|
||||
version: "2026.6.35",
|
||||
repository: "openclaw/openclaw",
|
||||
sourceSha,
|
||||
runId: "12345",
|
||||
});
|
||||
for (const status of [
|
||||
{ ...payload, state: "pending", creator: { login: "github-actions[bot]" } },
|
||||
{ ...payload, creator: { login: "someone-else" } },
|
||||
{
|
||||
...payload,
|
||||
description: "images probably published",
|
||||
creator: { login: "github-actions[bot]" },
|
||||
},
|
||||
]) {
|
||||
expect(() =>
|
||||
findDockerPublicationStatus({
|
||||
combinedStatus: { sha: sourceSha, statuses: [status] },
|
||||
version: "2026.6.35",
|
||||
repository: "openclaw/openclaw",
|
||||
sourceSha,
|
||||
}),
|
||||
).toThrow("is not canonical");
|
||||
}
|
||||
});
|
||||
|
||||
it("plans every extended-stable image variant in both registries", () => {
|
||||
expect(createDockerChannelPromotionPlan({ version: "2026.6.33", images })).toEqual({
|
||||
channel: "extended-stable",
|
||||
@@ -379,7 +447,11 @@ describe("Docker channel promotion", () => {
|
||||
"cancel-in-progress": false,
|
||||
queue: "max",
|
||||
});
|
||||
expect(verifyAttestations.permissions).toEqual({ contents: "read", packages: "write" });
|
||||
expect(verifyAttestations.permissions).toEqual({
|
||||
contents: "read",
|
||||
packages: "write",
|
||||
statuses: "write",
|
||||
});
|
||||
|
||||
const manifestTagStep = createManifest.steps?.find(
|
||||
(step) => step.name === "Resolve manifest tags",
|
||||
@@ -406,9 +478,21 @@ describe("Docker channel promotion", () => {
|
||||
"node scripts/docker-channel-promote.mjs",
|
||||
);
|
||||
expect(releaseSteps[releasePromotionIndex]?.run).not.toContain("--allow-rollback");
|
||||
const completionIndex = releaseSteps.findIndex(
|
||||
(step) => step.name === "Record durable extended-stable Docker completion",
|
||||
);
|
||||
expect(completionIndex).toBeGreaterThan(releasePromotionIndex);
|
||||
expect(releaseSteps[completionIndex]?.if).toBe(
|
||||
"${{ needs.resolve_release_policy.outputs.channel == 'extended-stable' }}",
|
||||
);
|
||||
expect(releaseSteps[completionIndex]?.run).toContain("--status-payload");
|
||||
expect(releaseSteps[completionIndex]?.run).toContain("statuses/${RELEASE_SHA}");
|
||||
expect(
|
||||
Object.values(releaseWorkflow.jobs ?? {}).flatMap((job) =>
|
||||
(job.steps ?? []).filter((step) => step.run?.includes("docker-channel-promote.mjs")),
|
||||
(job.steps ?? []).filter(
|
||||
(step) =>
|
||||
step.run?.includes("docker-channel-promote.mjs") && step.run.includes("--image"),
|
||||
),
|
||||
),
|
||||
).toHaveLength(1);
|
||||
|
||||
|
||||
@@ -632,11 +632,13 @@ function runReleasePublishInputValidation(overrides: Record<string, string>) {
|
||||
if (!script) {
|
||||
throw new Error("Expected release publish input validation script");
|
||||
}
|
||||
const githubOutput = resolve(tempDirs.make("release-publish-inputs-"), "github-output");
|
||||
return spawnSync("bash", ["-c", script], {
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "1",
|
||||
FULL_RELEASE_VALIDATION_RUN_ID: "222",
|
||||
GITHUB_OUTPUT: githubOutput,
|
||||
OPENCLAW_NPM_RESUME_RUN_ID: "",
|
||||
PATH: process.env.PATH,
|
||||
PLUGINS: "",
|
||||
@@ -5284,6 +5286,7 @@ describe("package artifact reuse", () => {
|
||||
);
|
||||
expect(trustedTooling.env?.WORKFLOW_SHA).toBe("${{ github.sha }}");
|
||||
expect(validateManifest.env).toMatchObject({
|
||||
EXPECTED_WORKFLOW_BRANCH: "${{ steps.inputs.outputs.expected_validation_branch }}",
|
||||
RUN_JSON_FILE: "${{ runner.temp }}/full-release-validation-run.json",
|
||||
TRUSTED_MAIN_REF: "refs/remotes/origin/main",
|
||||
VALIDATOR_FILE:
|
||||
@@ -5352,6 +5355,14 @@ describe("package artifact reuse", () => {
|
||||
extendedPrepareJob,
|
||||
"Create or resume the canonical draft release",
|
||||
);
|
||||
const extendedDockerCompletionJob = workflowJob(
|
||||
RELEASE_PUBLISH_WORKFLOW,
|
||||
"verify_extended_stable_docker_completion",
|
||||
);
|
||||
const extendedDockerCompletion = workflowStep(
|
||||
extendedDockerCompletionJob,
|
||||
"Verify durable Docker completion status",
|
||||
);
|
||||
const extendedFinalizeJob = workflowJob(
|
||||
RELEASE_PUBLISH_WORKFLOW,
|
||||
"finalize_extended_stable_github_release",
|
||||
@@ -5365,15 +5376,26 @@ describe("package artifact reuse", () => {
|
||||
expect(extendedPrepare.run).toContain("verifyGithubReleaseNotes");
|
||||
expect(extendedPrepare.run).toContain("body !== expectedBody");
|
||||
expect(extendedPrepare.run).toContain("release.assets.length !== 0");
|
||||
expect(extendedPrepare.run).toContain("release_already_public=true");
|
||||
expect(extendedPrepare.run).toContain("--find-status-file");
|
||||
expect(extendedPrepare.run).toContain("docker_already_published=true");
|
||||
expect(extendedPrepare.run).toContain("public without Docker completion");
|
||||
expect(extendedPrepare.run).toContain("--draft");
|
||||
expect(extendedPrepare.run).toContain("--latest=false");
|
||||
expect(extendedFinalizeJob.needs).toEqual([
|
||||
expect(extendedDockerCompletionJob.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"prepare_extended_stable_release",
|
||||
"publish_docker",
|
||||
]);
|
||||
expect(extendedFinalizeJob.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(extendedDockerCompletion.run).toContain("--find-status-file");
|
||||
expect(extendedDockerCompletion.run).toContain("Docker publication completed without");
|
||||
expect(extendedFinalizeJob.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"prepare_extended_stable_release",
|
||||
"verify_extended_stable_docker_completion",
|
||||
]);
|
||||
expect(extendedFinalizeJob.if).toContain(
|
||||
"needs.verify_extended_stable_docker_completion.result == 'success'",
|
||||
);
|
||||
expect(extendedFinalize.run).toContain("-f make_latest=false");
|
||||
expect(extendedFinalize.run).toContain("EXPECTED_BODY_SHA256");
|
||||
expect(extendedFinalize.run).toContain("release.assets.length !== 0");
|
||||
|
||||
@@ -1013,6 +1013,13 @@ describe("release validation no-push transport", () => {
|
||||
const releasePublishPath = ".github/workflows/openclaw-release-publish.yml";
|
||||
const releasePublish = readWorkflow(releasePublishPath);
|
||||
const dockerCall = job(releasePublish, "publish_docker");
|
||||
const resolveTarget = job(releasePublish, "resolve_release_target");
|
||||
const validateInputs = step(resolveTarget, "Validate inputs");
|
||||
const validateEvidence = step(resolveTarget, "Validate full release validation manifest");
|
||||
const validateReleaseBranch = step(
|
||||
resolveTarget,
|
||||
"Validate release tag is reachable from a trusted release branch",
|
||||
);
|
||||
|
||||
expect(dockerRelease.on?.push).toBeUndefined();
|
||||
expect(dockerRelease.on?.workflow_dispatch).toBeUndefined();
|
||||
@@ -1035,6 +1042,20 @@ describe("release validation no-push transport", () => {
|
||||
.toSorted();
|
||||
expect(callers).toEqual(["openclaw-release-publish.yml"]);
|
||||
|
||||
expect(validateInputs.id).toBe("inputs");
|
||||
expect(validateInputs.run).toContain(
|
||||
'expected_validation_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"',
|
||||
);
|
||||
expect(validateEvidence.env?.EXPECTED_WORKFLOW_BRANCH).toBe(
|
||||
"${{ steps.inputs.outputs.expected_validation_branch }}",
|
||||
);
|
||||
expect(validateReleaseBranch.run).toContain(
|
||||
'expected_ref="refs/remotes/origin/${EXPECTED_VALIDATION_BRANCH}"',
|
||||
);
|
||||
expect(validateReleaseBranch.run).toContain(
|
||||
"must be reachable from ${EXPECTED_VALIDATION_BRANCH}",
|
||||
);
|
||||
|
||||
expect(dockerCall.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"publish",
|
||||
@@ -1043,7 +1064,7 @@ describe("release validation no-push transport", () => {
|
||||
expect(dockerCall.if).toContain("needs.publish.result == 'success'");
|
||||
expect(dockerCall.if).toContain("needs.prepare_extended_stable_release.result == 'success'");
|
||||
expect(dockerCall.if).toContain(
|
||||
"needs.prepare_extended_stable_release.outputs.release_already_public != 'true'",
|
||||
"needs.prepare_extended_stable_release.outputs.docker_already_published != 'true'",
|
||||
);
|
||||
expect(dockerCall.with).toEqual({
|
||||
tag: "${{ inputs.tag }}",
|
||||
@@ -1085,11 +1106,16 @@ describe("release validation no-push transport", () => {
|
||||
expect(prepareRelease.needs).toEqual(["resolve_release_target"]);
|
||||
expect(prepareRelease.if).toBe("${{ inputs.publish_docker_only }}");
|
||||
expect(prepareRelease.environment).toBe("npm-release");
|
||||
expect(prepareRelease.permissions).toEqual({ contents: "write" });
|
||||
expect(prepareRelease.permissions).toEqual({
|
||||
actions: "read",
|
||||
contents: "write",
|
||||
statuses: "read",
|
||||
});
|
||||
expect(prepareRelease.outputs).toEqual({
|
||||
docker_already_published: "${{ steps.release.outputs.docker_already_published }}",
|
||||
docker_status_run_id: "${{ steps.release.outputs.docker_status_run_id }}",
|
||||
release_id: "${{ steps.release.outputs.release_id }}",
|
||||
release_body_sha256: "${{ steps.release.outputs.release_body_sha256 }}",
|
||||
release_already_public: "${{ steps.release.outputs.release_already_public }}",
|
||||
});
|
||||
expect(verifyNpm.run).toContain('npm view "openclaw@${version}" version');
|
||||
expect(verifyNpm.run).toContain("Published npm tarball does not match");
|
||||
@@ -1104,21 +1130,37 @@ describe("release validation no-push transport", () => {
|
||||
expect(createDraft.run).toContain("release.assets.length !== 0");
|
||||
expect(createDraft.run).toContain('verify_release_resource "${release_id}" false true');
|
||||
expect(createDraft.run).toContain("wait_until_not_latest");
|
||||
expect(createDraft.run).toContain("release_already_public=true");
|
||||
expect(createDraft.run).toContain("Docker will not be rebuilt");
|
||||
expect(createDraft.run).toContain("Docker completion will be checked independently");
|
||||
expect(createDraft.run).toContain("--find-status-file");
|
||||
expect(createDraft.run).toContain("docker_already_published=true");
|
||||
expect(createDraft.run).toContain("public without Docker completion");
|
||||
expect(createDraft.run).toContain("wait_for_release_id");
|
||||
expect(createDraft.run).toContain('sha256sum "${notes_file}"');
|
||||
|
||||
expect(finalizeRelease.needs).toEqual([
|
||||
const verifyDockerCompletion = job(releasePublish, "verify_extended_stable_docker_completion");
|
||||
expect(verifyDockerCompletion.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"prepare_extended_stable_release",
|
||||
"publish_docker",
|
||||
]);
|
||||
expect(verifyDockerCompletion.if).toContain(
|
||||
"needs.prepare_extended_stable_release.outputs.docker_already_published == 'true'",
|
||||
);
|
||||
expect(step(verifyDockerCompletion, "Verify durable Docker completion status").run).toContain(
|
||||
"--find-status-file",
|
||||
);
|
||||
expect(finalizeRelease.needs).toEqual([
|
||||
"resolve_release_target",
|
||||
"prepare_extended_stable_release",
|
||||
"verify_extended_stable_docker_completion",
|
||||
]);
|
||||
expect(finalizeRelease.if).toContain("inputs.publish_docker_only");
|
||||
expect(finalizeRelease.if).toContain(
|
||||
"needs.prepare_extended_stable_release.result == 'success'",
|
||||
);
|
||||
expect(finalizeRelease.if).toContain("needs.publish_docker.result == 'success'");
|
||||
expect(finalizeRelease.if).toContain(
|
||||
"needs.verify_extended_stable_docker_completion.result == 'success'",
|
||||
);
|
||||
expect(finalizeRelease.environment).toBe("npm-release");
|
||||
expect(finalizeRelease.permissions).toEqual({ contents: "write" });
|
||||
expect(publishDraft.env).toMatchObject({
|
||||
|
||||
@@ -154,6 +154,25 @@ describe("full release validation evidence", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts direct evidence from the derived canonical extended-stable branch", () => {
|
||||
const branch = "extended-stable/2026.6.33";
|
||||
const result = validateFullReleaseValidationEvidence({
|
||||
run: releaseRun({ head_branch: branch }),
|
||||
manifest: releaseManifest({
|
||||
workflowRef: branch,
|
||||
workflowFullRef: `refs/heads/${branch}`,
|
||||
targetRef: "v2026.6.35",
|
||||
}),
|
||||
expectedRepository: "openclaw/openclaw",
|
||||
expectedRunId: "123",
|
||||
expectedTargetSha: targetSha,
|
||||
expectedWorkflowBranch: branch,
|
||||
isTrustedMainAncestor: () => false,
|
||||
});
|
||||
|
||||
expect(result.source).toBe("direct");
|
||||
});
|
||||
|
||||
it("rejects direct main evidence outside current main", () => {
|
||||
expect(() =>
|
||||
validate(
|
||||
|
||||
Reference in New Issue
Block a user