diff --git a/scripts/docker-channel-promote.mjs b/scripts/docker-channel-promote.mjs index 7d3714bc12ce..cd5593fddc2a 100644 --- a/scripts/docker-channel-promote.mjs +++ b/scripts/docker-channel-promote.mjs @@ -1,7 +1,6 @@ #!/usr/bin/env node import { execFileSync } from "node:child_process"; -import { readFileSync } from "node:fs"; import process from "node:process"; import { parseArgs } from "node:util"; import { isDirectRunUrl } from "./lib/direct-run.mjs"; @@ -19,106 +18,6 @@ const VARIANTS = Object.freeze([ { aliasKey: "slim", suffix: "-slim" }, { aliasKey: "browser", suffix: "-browser" }, ]); -const DOCKER_PUBLICATION_STATUS_DESCRIPTION = - "Verified GHCR + Docker Hub images, attestations, platforms, and channel aliases."; -const DOCKER_PUBLICATION_STATUS_PREFIX = "openclaw/docker-release"; - -/** @typedef {{ repository: string; sourceSha: string; version: string }} DockerPublicationIdentity */ -/** @typedef {DockerPublicationIdentity & { runId: number | string }} DockerPublicationStatusParams */ -/** @typedef {{ context: string; description: string; state: "success"; target_url: string }} DockerPublicationStatus */ -/** - * @typedef {object} GitHubCommitStatus - * @property {unknown} [context] - * @property {{ login?: unknown }} [creator] - * @property {unknown} [description] - * @property {unknown} [state] - * @property {unknown} [target_url] - * @property {unknown} [url] - */ - -/** @param {DockerPublicationIdentity} params */ -function requireExtendedStableStatusIdentity({ version, repository, sourceSha }) { - const policy = resolveDockerReleasePolicy(version); - if (policy.channel !== "extended-stable") { - throw new Error(`Docker completion status is only valid for extended-stable; got ${version}.`); - } - if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)) { - throw new Error(`Invalid GitHub repository identity ${JSON.stringify(repository)}.`); - } - if (!/^[a-f0-9]{40}$/u.test(sourceSha)) { - throw new Error("Docker completion status requires a full lowercase source SHA."); - } - return policy; -} - -/** - * Build the durable commit status written only after Docker verification completes. - * - * @param {DockerPublicationStatusParams} params - * @returns {DockerPublicationStatus} - */ -export function createDockerPublicationStatus({ version, repository, sourceSha, runId }) { - const policy = requireExtendedStableStatusIdentity({ version, repository, sourceSha }); - if (!/^[1-9][0-9]*$/u.test(String(runId))) { - throw new Error("Docker completion status requires a positive workflow run ID."); - } - return { - context: `${DOCKER_PUBLICATION_STATUS_PREFIX}/${policy.version}`, - description: DOCKER_PUBLICATION_STATUS_DESCRIPTION, - state: "success", - target_url: `https://github.com/${repository}/actions/runs/${runId}`, - }; -} - -/** - * Resolve the newest canonical Docker completion status from GitHub's - * reverse-chronological commit-status history. - * - * @param {DockerPublicationIdentity & { statuses: unknown }} params - * @returns {{ runId: string; targetUrl: string } | null} - */ -export function findDockerPublicationStatus({ statuses, version, repository, sourceSha }) { - const expected = createDockerPublicationStatus({ - version, - repository, - sourceSha, - runId: 1, - }); - if (!Array.isArray(statuses)) { - throw new Error("GitHub commit status history must be an array."); - } - // GitHub returns newest first. Validate that matching record directly so a - // malformed retry cannot be bypassed by an older canonical success. - const status = /** @type {GitHubCommitStatus[]} */ (statuses).find( - (candidate) => - typeof candidate?.context === "string" && - candidate.context.toLowerCase() === expected.context.toLowerCase(), - ); - if (!status) { - return null; - } - const expectedStatusUrl = `https://api.github.com/repos/${repository}/statuses/${sourceSha}`; - const targetUrl = typeof status.target_url === "string" ? status.target_url : ""; - const targetMatch = new RegExp( - `^https://github\\.com/${repository.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}/actions/runs/([1-9][0-9]*)$`, - "u", - ).exec(targetUrl); - if ( - status.state !== expected.state || - status.context !== expected.context || - status.description !== expected.description || - status.creator?.login !== "github-actions[bot]" || - status.url !== expectedStatusUrl || - !targetMatch - ) { - throw new Error(`Docker completion status ${expected.context} is not canonical.`); - } - return { - runId: targetMatch[1], - targetUrl, - }; -} - /** @typedef {{ imageTagSuffix?: string; images: string[]; version: string }} DockerPromotionParams */ /** * @typedef {object} DockerExecOptions @@ -383,9 +282,7 @@ export function promoteDockerChannel({ version, imageTagSuffix = "", images }, o function printHelp() { console.log( - `Usage: node scripts/docker-channel-promote.mjs --version YYYY.M.P --image REGISTRY/IMAGE [--image REGISTRY/IMAGE] [--image-tag-suffix -rYYYYMMDD] [--allow-rollback] - node scripts/docker-channel-promote.mjs --status-payload --version YYYY.M.P --repository OWNER/REPO --source-sha SHA --run-id ID - node scripts/docker-channel-promote.mjs --find-statuses-file FILE --version YYYY.M.P --repository OWNER/REPO --source-sha SHA`, + "Usage: node scripts/docker-channel-promote.mjs --version YYYY.M.P --image REGISTRY/IMAGE [--image REGISTRY/IMAGE] [--image-tag-suffix -rYYYYMMDD] [--allow-rollback]", ); } @@ -397,10 +294,6 @@ function main() { help: { type: "boolean", short: "h" }, image: { type: "string", multiple: true }, "image-tag-suffix": { type: "string", default: "" }, - repository: { type: "string" }, - "run-id": { type: "string" }, - "source-sha": { type: "string" }, - "status-payload": { type: "boolean" }, version: { type: "string" }, }, strict: true, diff --git a/test/scripts/docker-channel-promote.test.ts b/test/scripts/docker-channel-promote.test.ts index 3bed096407e6..4615691c019c 100644 --- a/test/scripts/docker-channel-promote.test.ts +++ b/test/scripts/docker-channel-promote.test.ts @@ -669,10 +669,7 @@ 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" }); const manifestTagStep = createManifest.steps?.find( (step) => step.name === "Resolve manifest tags", @@ -701,10 +698,7 @@ describe("Docker channel promotion", () => { expect(releaseSteps[releasePromotionIndex]?.run).not.toContain("--allow-rollback"); expect( Object.values(releaseWorkflow.jobs ?? {}).flatMap((job) => - (job.steps ?? []).filter( - (step) => - step.run?.includes("docker-channel-promote.mjs") && step.run.includes("--image"), - ), + (job.steps ?? []).filter((step) => step.run?.includes("docker-channel-promote.mjs")), ), ).toHaveLength(1);