openclaw/scripts/full-release-publication-contract.mjs
Peter Steinberger cfd219c519
fix(release): skip pending ClawHub publications and surface recovery for failed ones (#161985)
The ClawHub release planner and prepared-artifact resolver read the new public publication-state endpoint (/api/v1/packages/{name}/versions/{version}/publication). Only absent versions are republished; pending ones are skipped, and failed ones are excluded, with the recover command printed to the step summary. A 404, or a 200 without a state field, falls back to the legacy version probe.
2026-09-30 09:35:54 -07:00

1223 lines
45 KiB
JavaScript

import { createHash } from "node:crypto";
import { appendFileSync, readFileSync, realpathSync } from "node:fs";
import { pathToFileURL } from "node:url";
import { isPreparedClawHubTrustedPublisher } from "./clawhub-prepared-artifact.mjs";
import { canonicalizeJsonValue, compareAscii } from "./lib/canonical-json.mjs";
import { classifyClawHubPublication } from "./lib/clawhub-publication-state.mjs";
import corePackages from "./lib/npm-core-release-packages.json" with { type: "json" };
import { resolveNpmPublishPlan } from "./lib/npm-publish-plan.mjs";
import { classifyReleaseTrain, parseReleaseVersion } from "./lib/release-version.mjs";
export const FULL_RELEASE_SOURCE_ADMISSION_CONTRACT = "1";
export const FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT = "1";
const purposes = ["publish", "diagnostic", "main-qualification", "postpublish-confidence"];
const maximumBytes = 128 * 1024;
const sha = /^[a-f0-9]{40}$/u;
const digest = /^[a-f0-9]{64}$/u;
const packageName = /^@openclaw\/[a-z0-9][a-z0-9._-]*$/u;
const coverageInputs = {
provider: "provider",
mode: "mode",
live_suite_filter: "liveSuiteFilter",
cross_os_suite_filter: "crossOsSuiteFilter",
release_package_spec: "releasePackageSpec",
package_acceptance_package_spec: "packageAcceptancePackageSpec",
codex_plugin_spec: "codexPluginSpec",
npm_telegram_package_spec: "npmTelegramPackageSpec",
npm_telegram_provider_mode: "npmTelegramProviderMode",
npm_telegram_scenario: "npmTelegramScenario",
plugin_prerelease_node_exclude_patterns_json: "pluginPrereleaseNodeExcludePatternsJson",
extension_test_exclude_patterns_json: "extensionTestExcludePatternsJson",
skip_package_telegram_e2e: "skipPackageTelegramE2e",
telegram_waiver: "telegramWaiver",
allow_unreleased_changelog: "allowUnreleasedChangelog",
};
// This marker belongs to the top-level workflow env, not artifact-controlled data.
export function publicationSourceContract(workflowSource) {
if (typeof workflowSource !== "string" || Buffer.byteLength(workflowSource) > 1024 * 1024) {
throw new Error("missing or oversized source-admission workflow contract");
}
const matches = [
...workflowSource.matchAll(/^ {2}FULL_RELEASE_SOURCE_ADMISSION_CONTRACT: *([^\r\n]+)$/gmu),
];
if (!matches.length) {
if (workflowSource.includes("FULL_RELEASE_SOURCE_ADMISSION_CONTRACT")) {
throw new Error("unrecognized source-admission workflow contract encoding");
}
return undefined;
}
if (matches.length !== 1 || !/^(?:"1"|'1'|1)$/u.test(matches[0][1])) {
throw new Error("unsupported source-admission workflow contract");
}
return FULL_RELEASE_SOURCE_ADMISSION_CONTRACT;
}
function object(value, keys, label) {
if (
!value ||
typeof value !== "object" ||
Array.isArray(value) ||
Object.keys(value).some((key) => !keys.includes(key))
) {
throw new Error(`invalid ${label}`);
}
return value;
}
function text(value, label, limit = 4096) {
if (typeof value !== "string" || value.length > limit) {
throw new Error(`invalid ${label}`);
}
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code < 32 || code === 127) {
throw new Error(`invalid ${label}`);
}
}
return value;
}
export function publicationSourceJson(value) {
const json = JSON.stringify(canonicalizeJsonValue(value));
if (Buffer.byteLength(json) > maximumBytes) {
throw new Error("source admission exceeds byte limit");
}
return json;
}
function publicationSourceDigest(value) {
return createHash("sha256").update(publicationSourceJson(value)).digest("hex");
}
export function normalizePublicationIntent(purpose, selectionJson = "") {
if (!purposes.includes(purpose)) {
throw new Error(`validation_purpose must be explicit: ${purposes.join(", ")}`);
}
if (purpose !== "publish") {
if (selectionJson !== "") {
throw new Error("nonpublish purpose must omit publication selection");
}
return { validationPurpose: purpose, publicationSelection: null };
}
if (
typeof selectionJson !== "string" ||
!selectionJson ||
Buffer.byteLength(selectionJson) > 16 * 1024
) {
throw new Error("publish purpose requires bounded publication_selection_json");
}
let selected;
try {
selected = JSON.parse(selectionJson);
} catch {
throw new Error("invalid publication selection JSON");
}
object(
selected,
[
"route",
"npmDistTag",
"publishOpenclawNpm",
"pluginPublishScope",
"plugins",
"windowsNodeTag",
"windowsNodeInstallerDigests",
],
"publication selection",
);
if (selected.route === "alpha" || selected.npmDistTag === "alpha") {
throw new Error("Alpha releases are retired; use a beta prerelease instead.");
}
if (
!["normal", "prepared", "extended-stable"].includes(selected.route) ||
!["beta", "latest", "extended-stable"].includes(selected.npmDistTag) ||
typeof selected.publishOpenclawNpm !== "boolean" ||
!["selected", "all-publishable"].includes(selected.pluginPublishScope) ||
!Array.isArray(selected.plugins) ||
selected.plugins.length > 256 ||
selected.plugins.some((name) => typeof name !== "string" || !packageName.test(name))
) {
throw new Error("invalid publication selection operands");
}
const plugins = [...new Set(selected.plugins)].toSorted(compareAscii);
if ((selected.pluginPublishScope === "selected") !== plugins.length > 0) {
throw new Error("selected publication requires names; all-publishable must omit names");
}
if (selected.publishOpenclawNpm && selected.pluginPublishScope !== "all-publishable") {
throw new Error("core publication requires all-publishable plugins");
}
if ((selected.route === "extended-stable") !== (selected.npmDistTag === "extended-stable")) {
throw new Error("publication route and npm dist-tag disagree");
}
if (
["prepared", "extended-stable"].includes(selected.route) &&
(selected.pluginPublishScope !== "all-publishable" || !selected.publishOpenclawNpm)
) {
throw new Error("prepared and extended-stable require the complete core/plugin publication");
}
const windows = {};
if (selected.windowsNodeTag !== undefined || selected.windowsNodeInstallerDigests !== undefined) {
if (selected.route === "extended-stable") {
throw new Error("extended-stable does not select Windows assets");
}
if (!["beta", "latest"].includes(selected.npmDistTag)) {
throw new Error("Windows assets require a stable publication");
}
windows.windowsNodeTag = text(selected.windowsNodeTag, "Windows source tag", 256);
if (
!/^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z]+([.-][0-9A-Za-z]+)*)?$/u.test(
windows.windowsNodeTag,
)
) {
throw new Error("invalid Windows source tag");
}
const digests = selected.windowsNodeInstallerDigests;
if (
!digests ||
typeof digests !== "object" ||
Array.isArray(digests) ||
!Object.keys(digests).length ||
Object.keys(digests).length > 16 ||
Object.entries(digests).some(
([name, value]) =>
!/^[A-Za-z0-9._-]+$/u.test(name) ||
typeof value !== "string" ||
!/^sha256:[a-f0-9]{64}$/u.test(value),
)
) {
throw new Error("invalid Windows installer digest map");
}
windows.windowsNodeInstallerDigests = digests;
}
return {
validationPurpose: purpose,
publicationSelection: {
route: selected.route,
npmDistTag: selected.npmDistTag,
publishOpenclawNpm: selected.publishOpenclawNpm,
pluginPublishScope: selected.pluginPublishScope,
plugins,
...windows,
},
};
}
export function publicationIntentInputs(intent) {
const normalized = normalizePublicationIntent(
intent.validationPurpose,
intent.publicationSelection === null ? "" : publicationSourceJson(intent.publicationSelection),
);
return {
validationPurpose: normalized.validationPurpose,
publicationSelectionJson:
normalized.publicationSelection === null
? ""
: publicationSourceJson(normalized.publicationSelection),
};
}
export function normalizePublicationLaneInputs(value) {
object(value, ["extension_test_exclude_patterns_json"], "source-admission lane inputs");
return Object.fromEntries(
Object.entries(value).map(([key, raw]) => {
if (typeof raw !== "string" || raw.length > 4096) {
throw new Error(`invalid ${key}`);
}
const entries = JSON.parse(raw);
if (!Array.isArray(entries) || entries.some((entry) => typeof entry !== "string")) {
throw new Error(`${key} must be a JSON array of strings`);
}
return [key, JSON.stringify(entries)];
}),
);
}
export function decodePublicationDispatchEnvelope(raw) {
if (typeof raw !== "string" || !raw || Buffer.byteLength(raw) > maximumBytes) {
throw new Error("trusted_workflow_json requires a bounded source-admission envelope");
}
const value = object(
JSON.parse(raw),
["trustedWorkflow", "validationPurpose", "publicationSelection", "laneInputs"],
"source-admission envelope",
);
if (
["trustedWorkflow", "validationPurpose", "publicationSelection"].some(
(key) => !Object.hasOwn(value, key),
)
) {
throw new Error("source-admission envelope requires identity, purpose and selection");
}
const trustedWorkflow = value.trustedWorkflow;
if (trustedWorkflow !== null) {
object(trustedWorkflow, ["ref", "fullRef", "sha"], "source-admission tooling identity");
if (
Object.keys(trustedWorkflow).length !== 3 ||
typeof trustedWorkflow.ref !== "string" ||
!/^[A-Za-z0-9._/-]+$/u.test(trustedWorkflow.ref) ||
!["refs/heads/", "refs/tags/"].some(
(prefix) => trustedWorkflow.fullRef === prefix + trustedWorkflow.ref,
) ||
typeof trustedWorkflow.sha !== "string" ||
!sha.test(trustedWorkflow.sha)
) {
throw new Error("invalid source-admission tooling identity");
}
}
const laneInputs =
value.laneInputs === undefined ? undefined : normalizePublicationLaneInputs(value.laneInputs);
return {
trustedWorkflow,
...(laneInputs === undefined ? {} : { laneInputs }),
...normalizePublicationIntent(
value.validationPurpose,
value.publicationSelection === null ? "" : publicationSourceJson(value.publicationSelection),
),
};
}
export function publicationDispatchEnvelope(trustedWorkflow, intent, laneInputs) {
return publicationSourceJson(
decodePublicationDispatchEnvelope(
publicationSourceJson({ trustedWorkflow, ...intent, ...(laneInputs ? { laneInputs } : {}) }),
),
);
}
function dispatchEnvelopeFromInputs(inputs) {
if (
Object.hasOwn(inputs, "validation_purpose") ||
Object.hasOwn(inputs, "publication_selection_json") ||
Object.hasOwn(inputs, "extension_test_exclude_patterns_json")
) {
throw new Error("source intent must use only the trusted_workflow_json envelope");
}
return decodePublicationDispatchEnvelope(inputs.trusted_workflow_json);
}
export function publicationSourceRequest(env) {
const inputs = JSON.parse(env.PUBLICATION_INPUTS_JSON);
const { trustedWorkflow, laneInputs, ...intent } = dispatchEnvelopeFromInputs(inputs);
const coverageSource = { ...inputs, extension_test_exclude_patterns_json: "[]", ...laneInputs };
const tooling = JSON.parse(env.PUBLICATION_TOOLING_JSON);
if (
trustedWorkflow &&
["ref", "fullRef", "sha"].some((key) => trustedWorkflow[key] !== tooling[key])
) {
throw new Error("source-admission tooling differs from resolved identity");
}
const coverage = {};
for (const key of [
...Object.keys(coverageInputs),
"release_profile",
"rerun_group",
"evidence_package_spec",
"fail_fast",
"dispatch_release_evidence",
]) {
coverage[key] = text(String(coverageSource[key] ?? ""), key);
}
coverage.live_suite_filter = env.PUBLICATION_LIVE_FILTER ?? coverage.live_suite_filter;
coverage.cross_os_suite_filter =
env.PUBLICATION_CROSS_OS_FILTER ?? coverage.cross_os_suite_filter;
coverage.skip_package_telegram_e2e =
env.PUBLICATION_SKIP_TELEGRAM ?? coverage.skip_package_telegram_e2e;
coverage.allow_unreleased_changelog = String(
inputs.allow_unreleased_changelog === true ||
inputs.allow_unreleased_changelog === "true" ||
(!inputs.target_context_ref && ["main", "refs/heads/main"].includes(inputs.ref)),
);
coverage.run_release_soak = String(
inputs.run_release_soak === true ||
inputs.run_release_soak === "true" ||
["stable", "full"].includes(inputs.release_profile),
);
coverage.coverage_policy = text(env.PUBLICATION_COVERAGE_POLICY ?? "", "coverage policy");
return {
repository: env.GITHUB_REPOSITORY,
candidateSha: env.PUBLICATION_TARGET_SHA,
targetContextRef: text(env.PUBLICATION_TARGET_CONTEXT || inputs.ref, "target context"),
tooling: { ref: tooling.fullRef, sha: tooling.sha },
workflow: { ref: env.GITHUB_REF, sha: env.GITHUB_SHA },
runId: env.GITHUB_RUN_ID,
runAttempt: Number(env.GITHUB_RUN_ATTEMPT),
...intent,
coverage,
};
}
export function createPublicationSourceFact(request, inventory, projection) {
const fact = {
kind: "openclaw.full-release-source-admission/v1",
contract: FULL_RELEASE_SOURCE_ADMISSION_CONTRACT,
...request,
status: request.validationPurpose === "publish" ? "source-admitted" : "not-applicable",
inventoryDigest: inventory === null ? null : publicationSourceDigest(inventory),
projection,
};
const result = { ...fact, digest: publicationSourceDigest(fact) };
return validatePublicationSourceFact(result);
}
function validatePublicationSourceFact(value, expected = {}) {
object(
value,
[
"kind",
"contract",
"repository",
"candidateSha",
"targetContextRef",
"tooling",
"workflow",
"runId",
"runAttempt",
"validationPurpose",
"publicationSelection",
"coverage",
"status",
"inventoryDigest",
"projection",
"digest",
],
"source admission fact",
);
if (
value.kind !== "openclaw.full-release-source-admission/v1" ||
value.contract !== FULL_RELEASE_SOURCE_ADMISSION_CONTRACT ||
value.repository !== "openclaw/openclaw" ||
!sha.test(value.candidateSha) ||
!/^[1-9][0-9]*$/u.test(value.runId) ||
!Number.isSafeInteger(value.runAttempt) ||
value.runAttempt < 1
) {
throw new Error("invalid source admission identity");
}
text(value.targetContextRef, "source target context");
for (const identity of [value.tooling, value.workflow]) {
object(identity, ["ref", "sha"], "source tooling/workflow identity");
if (
!sha.test(identity.sha) ||
!/^refs\/(?:heads|tags)\/[A-Za-z0-9._/-]+$/u.test(identity.ref)
) {
throw new Error("invalid source tooling/workflow identity");
}
}
if (value.tooling.sha !== value.workflow.sha) {
throw new Error("source tooling differs from executed workflow");
}
const intent = publicationIntentInputs(value);
const coverageKeys = [
...Object.keys(coverageInputs),
"release_profile",
"rerun_group",
"evidence_package_spec",
"fail_fast",
"dispatch_release_evidence",
"run_release_soak",
"coverage_policy",
];
// Published admissions bind this retired empty field into their digest.
object(value.coverage, [...coverageKeys, "known_flaky_jobs_json"], "source admission coverage");
if (
Object.hasOwn(value.coverage, "known_flaky_jobs_json") &&
value.coverage.known_flaky_jobs_json !== "[]"
) {
throw new Error("source admission known_flaky_jobs_json must be empty");
}
if (
coverageKeys.some(
(key) =>
key !== "extension_test_exclude_patterns_json" && !Object.hasOwn(value.coverage, key),
)
) {
throw new Error("source admission coverage is incomplete");
}
for (const entry of Object.values(value.coverage)) {
text(entry, "source admission coverage");
}
if (value.validationPurpose === "publish") {
if (value.status !== "source-admitted" || !digest.test(value.inventoryDigest)) {
throw new Error("publish source admission requires verified complete inventory");
}
object(value.projection, ["version", "packages", "platforms"], "publication source projection");
text(value.projection.version, "projection version", 128);
const version = parseReleaseVersion(value.projection.version);
if (!version) {
throw new Error("invalid projection version");
}
if (!Array.isArray(value.projection.packages) || !Array.isArray(value.projection.platforms)) {
throw new Error("invalid publication source projection");
}
if (
(value.publicationSelection.windowsNodeTag ||
value.projection.platforms.some((entry) => entry?.id === "windows")) &&
classifyReleaseTrain(version) !== "stable"
) {
throw new Error("Windows assets require a stable publication");
}
for (const [entries, name] of [
[value.projection.packages, "name"],
[value.projection.platforms, "id"],
]) {
if (
entries.length > 512 ||
new Set(entries.map((entry) => entry?.[name])).size !== entries.length
) {
throw new Error("duplicate or oversized publication source projection");
}
for (const entry of entries) {
if (
!entry ||
typeof entry !== "object" ||
Array.isArray(entry) ||
!text(entry[name], "projection identity", 256)
) {
throw new Error("invalid publication source projection entry");
}
if (name === "name") {
object(entry, ["name", "version", "targets"], "publication package projection");
if (
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u.test(entry.name) ||
typeof entry.version !== "string" ||
!parseReleaseVersion(entry.version) ||
!Array.isArray(entry.targets) ||
entry.targets.length === 0 ||
entry.targets.some((target) => !["npm", "clawhub"].includes(target)) ||
JSON.stringify(entry.targets) !==
JSON.stringify([...new Set(entry.targets)].toSorted(compareAscii))
) {
throw new Error("invalid publication package version or targets");
}
} else {
object(entry, ["id", "source"], "publication platform projection");
if (
!/^[a-z][a-z0-9-]*$/u.test(entry.id) ||
typeof entry.source !== "string" ||
!/^\.github\/workflows\/[a-z0-9][a-z0-9-]*\.yml$/u.test(entry.source)
) {
throw new Error("invalid publication platform source");
}
}
}
if (entries.some((entry, index) => index > 0 && entries[index - 1][name] >= entry[name])) {
throw new Error("publication projection must retain canonical ordering");
}
}
} else if (
value.status !== "not-applicable" ||
value.inventoryDigest !== null ||
value.projection !== null
) {
throw new Error("nonpublish source admission must be not-applicable");
}
const { digest: actualDigest, ...content } = value;
if (!digest.test(actualDigest) || actualDigest !== publicationSourceDigest(content)) {
throw new Error("source admission digest mismatch");
}
const bindings = {
repository: value.repository,
targetSha: value.candidateSha,
targetContextRef: value.targetContextRef,
trustedWorkflowFullRef: value.tooling.ref,
trustedWorkflowSha: value.tooling.sha,
parentRunId: value.runId,
sourceParentRunAttempt: value.runAttempt,
workflowSha: value.workflow.sha,
workflowRef: value.workflow.ref.replace(/^refs\/(?:heads|tags)\//u, ""),
releaseProfile: value.coverage.release_profile,
rerunGroup: value.coverage.rerun_group,
runReleaseSoak: value.coverage.run_release_soak,
...intent,
};
for (const [key, wanted] of Object.entries(expected)) {
if (
wanted !== undefined &&
Object.hasOwn(bindings, key) &&
String(bindings[key]) !== String(wanted)
) {
throw new Error(`source admission ${key} mismatch`);
}
}
return value;
}
export function validatePublicationSourceBinding(record, expected = {}) {
const contract = record.sourceAdmissionContract;
if (
expected.sourceAdmissionContract !== undefined &&
contract !== expected.sourceAdmissionContract
) {
throw new Error("source admission contract missing or mismatched");
}
if (contract === undefined) {
if (
record.sourceAdmission !== undefined ||
record.validationInputs?.validationPurpose !== undefined ||
record.validationInputs?.publicationSelectionJson !== undefined
) {
throw new Error("source admission omitted its workflow contract");
}
return undefined;
}
if (contract !== FULL_RELEASE_SOURCE_ADMISSION_CONTRACT) {
throw new Error("unsupported source admission contract");
}
if (record.runId !== undefined && (!record.validationInputs || !record.trustedWorkflow)) {
throw new Error("source admission manifest omitted its inputs or canonical tooling identity");
}
const fact = validatePublicationSourceFact(record.sourceAdmission, {
targetSha: record.targetSha,
parentRunId: record.runId ?? record.parentRunId,
workflowSha: record.workflowSha,
workflowRef: record.workflowRef,
sourceParentRunAttempt: record.sourceParentRunAttempt ?? record.parentRunAttempt,
releaseProfile: record.releaseProfile,
rerunGroup: record.rerunGroup,
runReleaseSoak: record.runReleaseSoak,
...(record.trustedWorkflow
? {
trustedWorkflowFullRef: record.trustedWorkflow.fullRef,
trustedWorkflowSha: record.trustedWorkflow.sha,
}
: {}),
...expected,
});
if (record.validationInputs) {
const context = record.validationInputs.targetContextRef || record.targetRef;
if (context !== fact.targetContextRef) {
throw new Error("source admission target context differs from manifest");
}
const intent = publicationIntentInputs(fact);
for (const [key, value] of Object.entries(intent)) {
if (record.validationInputs[key] !== value) {
throw new Error(`source admission ${key} differs from manifest`);
}
}
for (const [input, key] of Object.entries(coverageInputs)) {
const historicalDefault = input === "extension_test_exclude_patterns_json" ? "[]" : "";
if (
String(record.validationInputs[key] ?? historicalDefault) !==
(fact.coverage[input] ?? historicalDefault)
) {
throw new Error(`source admission coverage ${key} differs from manifest`);
}
}
if ((record.validationInputs.coveragePolicy ?? "") !== fact.coverage.coverage_policy) {
throw new Error("source admission coverage policy differs from manifest");
}
}
return fact;
}
export function publicationSourceReuseIdentity(fact) {
if (fact === undefined) {
return undefined;
}
validatePublicationSourceFact(fact);
return {
validationPurpose: fact.validationPurpose,
publicationSelection: fact.publicationSelection,
inventoryDigest: fact.inventoryDigest,
projection: fact.projection,
};
}
export function publicationAdmissionContract(workflowSource) {
if (typeof workflowSource !== "string" || Buffer.byteLength(workflowSource) > 1024 * 1024) {
throw new Error("missing or oversized publication-admission workflow contract");
}
const matches = [
...workflowSource.matchAll(/^ {2}FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT: *([^\r\n]+)$/gmu),
];
if (!matches.length && !workflowSource.includes("FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT")) {
return undefined;
}
if (matches.length !== 1 || !/^(?:"1"|'1'|1)$/u.test(matches[0][1])) {
throw new Error("unsupported publication-admission workflow contract");
}
return FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT;
}
function closedObject(value, keys, label) {
object(value, keys, label);
if (keys.some((key) => !Object.hasOwn(value, key))) {
throw new Error(`incomplete ${label}`);
}
return value;
}
function observationTime(value) {
const time = typeof value === "string" ? Date.parse(value) : Number.NaN;
if (!Number.isFinite(time) || new Date(time).toISOString() !== value) {
throw new Error("invalid publication observation time");
}
return time;
}
// These exact canonical bytes are uploaded once; their digest is not the ZIP digest.
export function publicationObservationJson(value) {
return `${JSON.stringify(canonicalizeJsonValue(value))}\n`;
}
function observationDigest(observations) {
return `sha256:${createHash("sha256").update(publicationObservationJson(observations)).digest("hex")}`;
}
function observationNames(rows, label, maximum = 1024) {
if (
!Array.isArray(rows) ||
rows.length > maximum ||
rows.some(
(row, index) =>
!row ||
typeof row.name !== "string" ||
!/^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/u.test(row.name) ||
row.name.length > 256 ||
(index > 0 && compareAscii(rows[index - 1].name, row.name) >= 0),
)
) {
throw new Error(`invalid publication ${label} roster`);
}
}
function validateObservationPlan(plan, registry, required, observations) {
// Retained v1 artifacts keep their original boolean-only, digest-bound shape.
const publicationGroups =
registry === "clawhub" &&
(Object.hasOwn(plan, "pendingPublication") || Object.hasOwn(plan, "failedPublication"))
? ["pendingPublication", "failedPublication"]
: [];
const groups =
registry === "npm"
? ["candidates", "skippedPublished"]
: [
"candidates",
"skippedPublished",
"bootstrapCandidates",
"missingTrustedPublisher",
...publicationGroups,
];
closedObject(plan, ["all", ...groups, "warnings"], "publication planning summary");
observationNames(plan.all, "planning", 512);
for (const entry of plan.all) {
closedObject(
entry,
["name", "version", "alreadyPublished", ...(publicationGroups.length ? ["publication"] : [])],
"publication planning entry",
);
if (
!required.some((row) => row.name === entry.name && row.version === entry.version) ||
typeof entry.alreadyPublished !== "boolean"
) {
throw new Error("publication planning entry differs from selected registry roster");
}
}
if (registry === "clawhub" && plan.all.length !== required.length) {
throw new Error("publication ClawHub planning roster is incomplete");
}
for (const group of groups) {
if (
!Array.isArray(plan[group]) ||
new Set(plan[group]).size !== plan[group].length ||
plan[group].some((name) => !plan.all.some((entry) => entry.name === name))
) {
throw new Error("invalid publication planning group");
}
}
for (const entry of plan.all) {
const observed = observations.find((row) => row.name === entry.name)?.state;
const published =
registry === "npm" ? observed?.selectedVersionExists : observed?.alreadyPublished;
const candidate =
!published &&
(registry === "npm" ||
(observed?.packageExists &&
observed?.hasTrustedPublisher &&
(!observed.publication || observed.publication.state === "absent")));
if (
entry.alreadyPublished !== published ||
plan.skippedPublished.includes(entry.name) !== entry.alreadyPublished ||
plan.candidates.includes(entry.name) !== candidate ||
(registry === "clawhub" &&
(Boolean(publicationGroups.length) !== Object.hasOwn(observed, "publication") ||
(publicationGroups.length > 0 &&
publicationObservationJson(entry.publication) !==
publicationObservationJson(observed.publication)) ||
publicationGroups.some(
(group) =>
plan[group].includes(entry.name) !==
(observed.publication.state ===
(group === "pendingPublication" ? "pending" : "failed")),
) ||
plan.bootstrapCandidates.includes(entry.name) !== !observed.packageExists ||
plan.missingTrustedPublisher.includes(entry.name) !==
(observed.packageExists &&
!observed.hasTrustedPublisher &&
(!observed.publication ||
["absent", "published"].includes(observed.publication.state)))))
) {
throw new Error("publication planning outcome mismatch");
}
}
if (!Array.isArray(plan.warnings) || plan.warnings.length > 4096) {
throw new Error("invalid publication advisory warnings");
}
for (const warning of plan.warnings) {
text(warning, "publication advisory warning");
}
}
// The worker and retained-receipt reader use the same state-to-authority decision.
// Package membership/version is the authenticated A projection, not caller input.
export function publicationPendingAuthority(source, registry, row) {
const selected = source.projection?.packages.find(
(entry) =>
entry.name === row.name && entry.version === row.version && entry.targets.includes(registry),
);
if (!selected || !source.publicationSelection) {
throw new Error("publication authority requires a selected source package");
}
const selection = source.publicationSelection;
let action;
if (registry === "npm") {
if (row.state.packageExists) {
if (!row.state.hasVersionHistory) {
throw new Error(`${row.name}: npm HTTP 200 has empty version history.`);
}
return null;
}
const parsed = parseReleaseVersion(selected.version);
const plugin =
packageName.test(selected.name) && !corePackages.some((entry) => entry.name === selected.name)
? resolveNpmPublishPlan(
selected.version,
undefined,
selection.route === "extended-stable" ? "extended-stable" : undefined,
)
: null;
const supported =
(plugin?.channel === "beta" && plugin.publishTag === "beta") ||
(plugin?.channel === "stable" &&
plugin.publishTag === "latest" &&
selection.npmDistTag === "latest" &&
parsed &&
classifyReleaseTrain(parsed) === "stable");
if (!["normal", "prepared"].includes(selection.route) || !supported) {
throw new Error(`${row.name}: npm bootstrap is unsupported for this publication route.`);
}
action = "owner-preparation-and-access";
} else if (registry === "clawhub") {
if (["pending", "failed"].includes(row.state.publication?.state)) {
return null;
}
if (
selection.route === "prepared" &&
(!row.state.packageExists ||
!row.state.hasTrustedPublisher ||
!isPreparedClawHubTrustedPublisher(row.state.trustedPublisher))
) {
throw new Error("publication ClawHub prepared-trust-required");
}
if (row.state.packageExists && row.state.hasTrustedPublisher) {
return null;
}
action = !row.state.packageExists
? "bootstrap-and-owner-access"
: row.state.alreadyPublished
? "configure-only"
: "publisher-repair";
} else {
throw new Error("invalid publication authority registry");
}
return { registry, name: row.name, action, status: "unresolved" };
}
function validatePublicationObservations(source, value) {
validatePublicationSourceFact(source);
if (source.validationPurpose !== "publish") {
throw new Error("publication observations require a publish source");
}
closedObject(
value,
[
"kind",
"contract",
"sourceDigest",
"prerequisitesCompletedAt",
"collectionStartedAt",
"collectionCompletedAt",
"npm",
"clawhub",
"pendingAuthority",
"plans",
],
"publication observations",
);
if (
value.kind !== "openclaw.full-release-publication-observations/v1" ||
value.contract !== FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT ||
value.sourceDigest !== source.digest
) {
throw new Error("publication observations source binding mismatch");
}
const prerequisites = observationTime(value.prerequisitesCompletedAt);
const start = observationTime(value.collectionStartedAt);
const end = observationTime(value.collectionCompletedAt);
if (prerequisites > start || start > end || end - prerequisites > 300_000) {
throw new Error("publication observation time ordering is invalid");
}
const required = (registry) =>
source.projection.packages.filter((entry) => entry.targets.includes(registry));
for (const registry of ["npm", "clawhub"]) {
observationNames(value[registry], registry);
const selected = required(registry);
const actual = registry === "npm" ? value.npm.filter((entry) => entry.required) : value.clawhub;
if (
actual.length !== selected.length ||
actual.some(
(row) =>
!selected.some((entry) => row.name === entry.name && row.version === entry.version),
)
) {
throw new Error("publication required observation roster differs from selected source");
}
for (const row of value[registry]) {
const observed = observationTime(row.observedAt);
if (observed < start || observed > end) {
throw new Error("publication observation time is outside collection");
}
if (registry === "npm") {
const observedOutcome = row.outcome === "observed";
closedObject(
row,
[
"name",
"version",
"required",
"observedAt",
"outcome",
observedOutcome ? "state" : "error",
],
"publication npm observation",
);
if (
typeof row.required !== "boolean" ||
(row.required ? typeof row.version !== "string" : row.version !== null) ||
(!row.required && selected.some((entry) => entry.name === row.name))
) {
throw new Error("invalid publication npm observation role");
}
if (!observedOutcome) {
if (
row.required ||
row.outcome !== "unavailable" ||
!/^(?:http-[1-5][0-9]{2}|response-too-large|invalid-response|cancelled-or-timeout|read-failed)$/u.test(
row.error,
)
) {
throw new Error("required publication npm observation is unavailable");
}
continue;
}
closedObject(
row.state,
["packageExists", "hasVersionHistory", "selectedVersionExists", "latestVersion"],
"publication npm state",
);
if (
["packageExists", "hasVersionHistory", "selectedVersionExists"].some(
(key) => typeof row.state[key] !== "boolean",
) ||
(!row.state.packageExists &&
(row.state.hasVersionHistory ||
row.state.selectedVersionExists ||
row.state.latestVersion !== null)) ||
(row.required && row.state.packageExists && !row.state.hasVersionHistory) ||
(row.state.latestVersion !== null &&
!text(row.state.latestVersion, "publication latest version", 128))
) {
throw new Error("invalid publication npm history");
}
} else {
closedObject(
row,
["name", "version", "observedAt", "state"],
"publication ClawHub observation",
);
closedObject(
row.state,
[
"packageExists",
"alreadyPublished",
"hasTrustedPublisher",
"trustedPublisher",
...(Object.hasOwn(row.state, "publication") ? ["publication"] : []),
],
"publication ClawHub state",
);
const publication = Object.hasOwn(row.state, "publication")
? classifyClawHubPublication(
{ name: row.name, version: row.version, ...row.state.publication },
row,
)
: undefined;
if (
["packageExists", "alreadyPublished", "hasTrustedPublisher"].some(
(key) => typeof row.state[key] !== "boolean",
) ||
(publication !== undefined &&
(!publication ||
publicationObservationJson(publication) !==
publicationObservationJson(row.state.publication) ||
row.state.alreadyPublished !== (publication.state === "published") ||
(!row.state.packageExists && publication.state !== "absent"))) ||
(!row.state.packageExists &&
(row.state.alreadyPublished ||
row.state.hasTrustedPublisher ||
row.state.trustedPublisher !== null))
) {
throw new Error("invalid publication ClawHub state");
}
if (row.state.trustedPublisher !== null) {
closedObject(
row.state.trustedPublisher,
["provider", "repository", "workflowFilename", "environment"],
"publication trusted publisher",
);
for (const field of Object.values(row.state.trustedPublisher)) {
if (field !== null) {
text(field, "publication trusted publisher field", 256);
}
}
}
}
}
}
if (!Array.isArray(value.pendingAuthority) || value.pendingAuthority.length > 1024) {
throw new Error("invalid publication pending authority");
}
const pendingKeys = new Set();
for (const pending of value.pendingAuthority) {
closedObject(
pending,
["registry", "name", "action", "status"],
"publication pending authority",
);
const key = `${pending.registry}/${pending.name}`;
if (
pendingKeys.has(key) ||
pending.status !== "unresolved" ||
!["npm", "clawhub"].includes(pending.registry) ||
!required(pending.registry).some((entry) => entry.name === pending.name) ||
!(
pending.registry === "npm"
? ["owner-preparation-and-access"]
: ["bootstrap-and-owner-access", "configure-only", "publisher-repair"]
).includes(pending.action)
) {
throw new Error("invalid publication pending authority classification");
}
pendingKeys.add(key);
}
const requiredPending = [
...value.npm
.filter((entry) => entry.required)
.map((row) => publicationPendingAuthority(source, "npm", row)),
...value.clawhub.map((row) => publicationPendingAuthority(source, "clawhub", row)),
]
.filter(Boolean)
.toSorted((a, b) => compareAscii(`${a.registry}/${a.name}`, `${b.registry}/${b.name}`));
if (
publicationObservationJson(value.pendingAuthority) !==
publicationObservationJson(requiredPending)
) {
throw new Error("publication pending authority differs from required observation states");
}
closedObject(value.plans, ["npm", "clawhub"], "publication plans");
for (const registry of ["npm", "clawhub"]) {
validateObservationPlan(value.plans[registry], registry, required(registry), value[registry]);
}
return value;
}
export function createPublicationObservations(source, observations) {
return validatePublicationObservations(source, {
kind: "openclaw.full-release-publication-observations/v1",
contract: FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT,
...observations,
});
}
export function createPublicationAdmission(source, observations, artifact, admittedAt) {
const admission = {
observations,
binding: {
kind: "openclaw.full-release-publication-admission/v1",
contract: FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT,
repository: source.repository,
parentRunId: source.runId,
parentRunAttempt: source.runAttempt,
workflow: {
path: ".github/workflows/full-release-validation.yml",
event: "workflow_dispatch",
...source.workflow,
},
sourceDigest: source.digest,
observationsDigest: observationDigest(observations),
artifact,
admittedAt,
status: "admitted-for-validation",
},
};
return validatePublicationAdmissionBinding({
sourceAdmissionContract: FULL_RELEASE_SOURCE_ADMISSION_CONTRACT,
sourceAdmission: source,
publicationAdmissionContract: FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT,
publicationAdmission: admission,
});
}
export function validatePublicationAdmissionBinding(record, expected = {}) {
const contract = record.publicationAdmissionContract;
if (
expected.publicationAdmissionContract !== undefined &&
contract !== expected.publicationAdmissionContract
) {
throw new Error("publication admission contract missing or mismatched");
}
if (contract === undefined) {
if (record.publicationAdmission !== undefined) {
throw new Error("publication admission omitted its workflow contract");
}
return undefined;
}
if (contract !== FULL_RELEASE_PUBLICATION_ADMISSION_CONTRACT) {
throw new Error("unsupported publication admission contract");
}
const source = validatePublicationSourceBinding(record, {
...expected,
sourceAdmissionContract: FULL_RELEASE_SOURCE_ADMISSION_CONTRACT,
});
if (source.validationPurpose !== "publish") {
if (record.publicationAdmission !== null) {
throw new Error("nonpublish purpose cannot retain publication admission");
}
return null;
}
const admission = closedObject(
record.publicationAdmission,
["observations", "binding"],
"publication admission",
);
const observations = validatePublicationObservations(source, admission.observations);
const binding = closedObject(
admission.binding,
[
"kind",
"contract",
"repository",
"parentRunId",
"parentRunAttempt",
"workflow",
"sourceDigest",
"observationsDigest",
"artifact",
"admittedAt",
"status",
],
"publication admission binding",
);
closedObject(binding.workflow, ["path", "event", "ref", "sha"], "publication workflow");
if (
binding.kind !== "openclaw.full-release-publication-admission/v1" ||
binding.contract !== contract ||
binding.status !== "admitted-for-validation" ||
binding.repository !== source.repository ||
binding.parentRunId !== source.runId ||
binding.parentRunAttempt !== source.runAttempt ||
binding.workflow.path !== ".github/workflows/full-release-validation.yml" ||
binding.workflow.event !== "workflow_dispatch" ||
binding.workflow.ref !== source.workflow.ref ||
binding.workflow.sha !== source.workflow.sha ||
binding.sourceDigest !== source.digest ||
binding.observationsDigest !== observationDigest(observations)
) {
throw new Error("publication admission identity or observation digest mismatch");
}
const artifact = closedObject(
binding.artifact,
["id", "name", "digest", "sizeInBytes"],
"publication observation artifact",
);
if (
typeof artifact.id !== "string" ||
!/^[1-9][0-9]{0,19}$/u.test(artifact.id) ||
artifact.name !==
`full-release-publication-observations-${source.runId}-${source.runAttempt}` ||
!/^sha256:[a-f0-9]{64}$/u.test(artifact.digest) ||
!Number.isSafeInteger(artifact.sizeInBytes) ||
artifact.sizeInBytes < 1 ||
artifact.sizeInBytes > 1024 * 1024 + 8 * 1024
) {
throw new Error("invalid publication observation artifact descriptor");
}
const admitted = observationTime(binding.admittedAt);
if (
admitted < observationTime(observations.collectionCompletedAt) ||
admitted - observationTime(observations.prerequisitesCompletedAt) > 300_000 ||
[...observations.npm.filter((row) => row.required), ...observations.clawhub].some(
(row) => admitted - observationTime(row.observedAt) > 300_000,
)
) {
throw new Error("publication admission freshness window exceeded");
}
return admission;
}
let invokedAsMain = false;
if (process.argv[1]) {
try {
invokedAsMain = import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href;
} catch {
// Inline and stdin importers need not have a filesystem entrypoint.
}
}
if (invokedAsMain) {
try {
if (process.argv[2] === "--dispatch") {
const envelope = dispatchEnvelopeFromInputs(JSON.parse(process.env.PUBLICATION_INPUTS_JSON));
const identity =
envelope.trustedWorkflow === null ? "" : publicationSourceJson(envelope.trustedWorkflow);
appendFileSync(process.env.GITHUB_OUTPUT, `trusted_workflow_json=${identity}\n`);
appendFileSync(
process.env.GITHUB_OUTPUT,
`extension_test_exclude_patterns_json=${envelope.laneInputs?.extension_test_exclude_patterns_json ?? "[]"}\n`,
);
} else if (process.argv[2] === "--request") {
const request = publicationSourceRequest(process.env);
const normalized = publicationIntentInputs(request);
if (process.env.GITHUB_OUTPUT) {
appendFileSync(
process.env.GITHUB_OUTPUT,
`required=${request.validationPurpose === "publish"}\nvalidation_purpose=${normalized.validationPurpose}\npublication_selection_json=${normalized.publicationSelectionJson}\n`,
);
}
process.stdout.write(publicationSourceJson(request) + "\n");
} else if (process.argv[2] === "--not-applicable") {
const retained = JSON.parse(readFileSync(process.argv[3], "utf8"));
if (retained.validationPurpose === "publish") {
throw new Error("publish requires source inventory");
}
process.stdout.write(
publicationSourceJson(createPublicationSourceFact(retained, null, null)) + "\n",
);
} else {
throw new Error("unsupported source admission operation");
}
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}