mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 01:29:56 +00:00
* fix(ci): cap Testbox concurrency and idle spend * test(ci): align workflow guards with Testbox admission * fix(ci): declare optional Testbox admission inputs * fix(ci): narrow Testbox CLI rejection errors * test(ci): verify admitted Testbox workflow behavior * fix(ci): reserve large Testboxes for memory-heavy proof * fix(ci): default routine Testboxes to one hour * docs(ci): clarify Testbox profiles and total job deadlines Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
16477 lines
694 KiB
TypeScript
16477 lines
694 KiB
TypeScript
// Package Acceptance Workflow tests cover package acceptance workflow script behavior.
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { createHash } from "node:crypto";
|
|
import {
|
|
chmodSync,
|
|
constants as fsConstants,
|
|
copyFileSync,
|
|
cpSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
renameSync,
|
|
symlinkSync,
|
|
unlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { createRequire } from "node:module";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { runInNewContext } from "node:vm";
|
|
import JSZip from "jszip";
|
|
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
|
import { parse } from "yaml";
|
|
import { buildFullReleaseCandidateBinding } from "../../scripts/full-release-candidate-contract.mjs";
|
|
import { FULL_RELEASE_WAIT_TIMEOUT_MINUTES } from "../../scripts/full-release-validation-at-sha.mts";
|
|
import { resolveRunnerMatrix } from "../../scripts/lib/cross-os-release-checks/config.ts";
|
|
import { runManagedCommand } from "../../scripts/lib/managed-child-process.mts";
|
|
import { parseUpgradeSurvivorScenarios } from "../../scripts/lib/upgrade-survivor-policy.mjs";
|
|
import { createReleaseWorkflowMatrixPlan } from "../../scripts/plan-release-workflow-matrix.mjs";
|
|
import { createBoundedChildOutput } from "../helpers/bounded-child-output.js";
|
|
import { createFixtureLifetime } from "../helpers/fixture-lifetime.js";
|
|
import {
|
|
fullReleaseCandidateArtifact,
|
|
fullReleaseCandidateManifestFixture,
|
|
} from "../helpers/full-release-candidate.js";
|
|
import {
|
|
pluginPrereleaseTimeoutComponents,
|
|
releaseTimeoutForProfile as timeoutForProfile,
|
|
releaseWorkflowJobNeeds as jobNeeds,
|
|
} from "../helpers/release-workflow-timeouts.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
import { evaluateWorkflowExpression, evaluateWorkflowRunner } from "./ci-workflow.test-support.js";
|
|
|
|
const PACKAGE_ACCEPTANCE_WORKFLOW = ".github/workflows/package-acceptance.yml";
|
|
const LIVE_E2E_WORKFLOW = ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml";
|
|
const INSTALL_SMOKE_REUSABLE_WORKFLOW = ".github/workflows/install-smoke-reusable.yml";
|
|
const CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW =
|
|
".github/workflows/openclaw-cross-os-release-checks-reusable.yml";
|
|
const LIVE_MEDIA_RUNNER_DOCKERFILE = ".github/images/live-media-runner/Dockerfile";
|
|
const LIVE_MEDIA_RUNNER_IMAGE = "ghcr.io/openclaw/openclaw-live-media-runner:ubuntu-24.04";
|
|
const LIVE_MEDIA_RUNNER_IMAGE_WORKFLOW = ".github/workflows/live-media-runner-image.yml";
|
|
const NPM_TELEGRAM_WORKFLOW = ".github/workflows/npm-telegram-beta-e2e.yml";
|
|
const MANTIS_DISCORD_SMOKE_WORKFLOW = ".github/workflows/mantis-discord-smoke.yml";
|
|
const MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW =
|
|
".github/workflows/mantis-discord-status-reactions.yml";
|
|
const MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW =
|
|
".github/workflows/mantis-discord-thread-attachment.yml";
|
|
const MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW = ".github/workflows/mantis-slack-desktop-smoke.yml";
|
|
const MANTIS_TELEGRAM_BOT_E2E_PROOF_WORKFLOW =
|
|
".github/workflows/mantis-telegram-bot-e2e-proof.yml";
|
|
const MANTIS_WEB_UI_CHAT_PROOF_WORKFLOW = ".github/workflows/mantis-web-ui-chat-proof.yml";
|
|
const PACKAGE_JSON = "package.json";
|
|
const SETUP_PNPM_STORE_CACHE_ACTION = ".github/actions/setup-pnpm-store-cache/action.yml";
|
|
const SETUP_RELEASE_HARNESS_ACTION = ".github/actions/setup-release-harness/action.yml";
|
|
const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml";
|
|
const RELEASE_TELEGRAM_QA_WORKFLOW = ".github/workflows/openclaw-release-telegram-qa.yml";
|
|
const RELEASE_PUBLISH_WORKFLOW = ".github/workflows/openclaw-release-publish.yml";
|
|
const PLUGIN_PRERELEASE_WORKFLOW = ".github/workflows/plugin-prerelease.yml";
|
|
const OPENCLAW_NPM_RELEASE_WORKFLOW = ".github/workflows/openclaw-npm-release.yml";
|
|
const OPENCLAW_NPM_PREFLIGHT_WORKFLOW = ".github/workflows/openclaw-npm-preflight.yml";
|
|
const PLUGIN_CLAWHUB_RELEASE_WORKFLOW = ".github/workflows/plugin-clawhub-release.yml";
|
|
const PLUGIN_NPM_RELEASE_WORKFLOW = ".github/workflows/plugin-npm-release.yml";
|
|
const ANDROID_RELEASE_WORKFLOW = ".github/workflows/android-release.yml";
|
|
const STABLE_MAIN_CLOSEOUT_WORKFLOW = ".github/workflows/openclaw-stable-main-closeout.yml";
|
|
const WINDOWS_NODE_RELEASE_WORKFLOW = ".github/workflows/windows-node-release.yml";
|
|
const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validation.yml";
|
|
const FULL_RELEASE_CANDIDATE_WORKFLOW = ".github/workflows/full-release-candidate.yml";
|
|
const FULL_RELEASE_ARTIFACTS_WORKFLOW = ".github/workflows/full-release-artifacts.yml";
|
|
const ACTIONS_CACHE_V6 = "actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9";
|
|
const CI_WORKFLOW = ".github/workflows/ci.yml";
|
|
const PERFORMANCE_WORKFLOW = ".github/workflows/openclaw-performance.yml";
|
|
const PUBLICATION_CONTRACT_FILES = [
|
|
"scripts/full-release-publication-contract.mjs",
|
|
"scripts/clawhub-prepared-artifact.mjs",
|
|
"scripts/clawhub-parent-authorization.mjs",
|
|
"scripts/plugin-publication-artifact.mjs",
|
|
"scripts/release-tooling-identity.mjs",
|
|
"scripts/lib/actions-artifact-archive.mjs",
|
|
"scripts/lib/arg-utils.runtime.mjs",
|
|
"scripts/lib/bounded-response.mjs",
|
|
"scripts/lib/clawhub-publication-state.mjs",
|
|
"scripts/lib/canonical-json.mjs",
|
|
"scripts/lib/npm-core-release-packages.json",
|
|
"scripts/lib/npm-publish-plan.mjs",
|
|
"scripts/lib/record-shared.mjs",
|
|
"scripts/lib/release-version.mjs",
|
|
];
|
|
const FULL_RELEASE_CHILD_DISPATCHES = [
|
|
{
|
|
jobName: "normal_ci",
|
|
kind: "ci",
|
|
nonceSuffix: "-ci",
|
|
runName: "CI",
|
|
stepName: "Dispatch CI",
|
|
workflow: "ci.yml",
|
|
},
|
|
{
|
|
jobName: "plugin_prerelease_independent",
|
|
kind: "plugin-prerelease",
|
|
nonceSuffix: "-plugin-prerelease-independent",
|
|
runName: "Plugin Prerelease",
|
|
stepName: "Dispatch plugin prerelease independent phase",
|
|
workflow: "plugin-prerelease.yml",
|
|
},
|
|
{
|
|
jobName: "plugin_prerelease_candidate",
|
|
kind: "plugin-prerelease",
|
|
nonceSuffix: "-plugin-prerelease-candidate",
|
|
runName: "Plugin Prerelease",
|
|
stepName: "Dispatch plugin prerelease candidate phase",
|
|
workflow: "plugin-prerelease.yml",
|
|
},
|
|
{
|
|
jobName: "release_checks_independent",
|
|
kind: "release-checks",
|
|
nonceSuffix: "-release-checks-independent",
|
|
runName: "OpenClaw Release Checks",
|
|
stepName: "Dispatch release checks independent phase",
|
|
workflow: "openclaw-release-checks.yml",
|
|
},
|
|
{
|
|
jobName: "release_checks_candidate",
|
|
kind: "release-checks",
|
|
nonceSuffix: "-release-checks-candidate",
|
|
runName: "OpenClaw Release Checks",
|
|
stepName: "Dispatch release checks candidate phase",
|
|
workflow: "openclaw-release-checks.yml",
|
|
},
|
|
{
|
|
jobName: "npm_telegram",
|
|
kind: "npm-telegram",
|
|
nonceSuffix: "-npm-telegram",
|
|
runName: "NPM Telegram Beta E2E",
|
|
stepName: "Dispatch npm Telegram E2E",
|
|
workflow: "npm-telegram-beta-e2e.yml",
|
|
},
|
|
{
|
|
jobName: "performance",
|
|
kind: "performance",
|
|
nonceSuffix: "",
|
|
runName: "OpenClaw Performance",
|
|
stepName: "Dispatch OpenClaw Performance",
|
|
workflow: "openclaw-performance.yml",
|
|
},
|
|
{
|
|
jobName: "prepare_npm_package",
|
|
kind: "artifact-npm",
|
|
nonceSuffix: "-artifacts-npm",
|
|
runName: "Full Release Artifacts",
|
|
stepName: "Dispatch immutable npm artifact producer",
|
|
workflow: "full-release-artifacts.yml",
|
|
},
|
|
{
|
|
jobName: "prepare_docker_release",
|
|
kind: "artifact-docker",
|
|
nonceSuffix: "-artifacts-docker",
|
|
runName: "Full Release Artifacts",
|
|
stepName: "Dispatch immutable Docker artifact producer",
|
|
workflow: "full-release-artifacts.yml",
|
|
},
|
|
{
|
|
jobName: "candidate_acquisition",
|
|
kind: "artifact-candidate",
|
|
nonceSuffix: "-artifacts-candidate",
|
|
runName: "Full Release Artifacts",
|
|
stepName: "Dispatch immutable validation candidate producer",
|
|
workflow: "full-release-artifacts.yml",
|
|
},
|
|
] as const;
|
|
const REPO_ROOT = process.env.GITHUB_WORKSPACE ?? process.cwd();
|
|
const QA_LIVE_TRANSPORTS_WORKFLOW = ".github/workflows/qa-live-transports-convex.yml";
|
|
const UPDATE_MIGRATION_WORKFLOW = ".github/workflows/update-migration.yml";
|
|
const CI_CHECK_TESTBOX_WORKFLOW = ".github/workflows/ci-check-testbox.yml";
|
|
const CI_CHECK_ARM_TESTBOX_WORKFLOW = ".github/workflows/ci-check-arm-testbox.yml";
|
|
const CI_BUILD_ARTIFACTS_TESTBOX_WORKFLOW = ".github/workflows/ci-build-artifacts-testbox.yml";
|
|
const WINDOWS_BLACKSMITH_TESTBOX_WORKFLOW = ".github/workflows/windows-blacksmith-testbox.yml";
|
|
const CRABBOX_HYDRATE_WORKFLOW = ".github/workflows/crabbox-hydrate.yml";
|
|
const CRABBOX_CONFIG = ".crabbox.yaml";
|
|
const SCHEDULED_LIVE_CHECKS_WORKFLOW = ".github/workflows/openclaw-scheduled-live-checks.yml";
|
|
const CI_HYDRATE_LIVE_AUTH_SCRIPT = "scripts/ci-hydrate-live-auth.sh";
|
|
const RELEASE_CHECK_ARTIFACT_RESOLVER = "scripts/github/resolve-release-check-artifacts.sh";
|
|
const RELEASE_FILTER_VALIDATOR = "scripts/github/validate-release-suite-filters.sh";
|
|
const VERIFY_PROVIDER_SECRETS_SCRIPT =
|
|
".agents/skills/release-openclaw-ci/scripts/verify-provider-secrets.mjs";
|
|
const UPGRADE_SURVIVOR_RUN_SCRIPT = "scripts/e2e/lib/upgrade-survivor/run.sh";
|
|
const SETUP_NODE_V6 = "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020";
|
|
const DOWNLOAD_ARTIFACT_V8 = "actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c";
|
|
const UPLOAD_ARTIFACT_V7 = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a";
|
|
const RUN_TESTBOX_WITH_FAILURE_REPORTING =
|
|
"steipete/run-testbox@2b6b1be536ec7f3c73757fedf5460a27ab4856b4";
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const templateDirs = useAutoCleanupTempDirTracker(afterAll);
|
|
const toolingTemplates = new Map<string, { directory: string; sha: string }>();
|
|
let packageToolingTemplate:
|
|
| { directory: string; capturedSha: string; advancedSha: string }
|
|
| undefined;
|
|
let fixtureGitPath: string | undefined;
|
|
|
|
const frozenAdmissionClosure = [
|
|
"scripts/preflight-frozen-target-contracts.mjs",
|
|
"scripts/lib/frozen-target-source.mjs",
|
|
"scripts/lib/docker-e2e-plan.mts",
|
|
"scripts/lib/docker-e2e-scenarios.mts",
|
|
"scripts/lib/official-external-channel-catalog.json",
|
|
"scripts/lib/official-external-provider-catalog.json",
|
|
"scripts/lib/record-shared.mjs",
|
|
"scripts/lib/update-compat-inventory.json",
|
|
"scripts/lib/update-first-hop-lanes.mjs",
|
|
"scripts/lib/upgrade-survivor-policy.mjs",
|
|
"scripts/lib/upgrade-survivor-scenarios.json",
|
|
"scripts/lib/release-version.mjs",
|
|
"scripts/lib/frozen-target-compat.sh",
|
|
"scripts/lib/trusted-native-typescript.mjs",
|
|
"scripts/lib/native-typescript.mts",
|
|
"scripts/resolve-frozen-codex-live-suite.mjs",
|
|
"scripts/resolve-fs-safe-native-contract.mjs",
|
|
"scripts/e2e/lib/upgrade-survivor/config-recipe.mts",
|
|
"scripts/windows-cmd-helpers.mjs",
|
|
"package.json",
|
|
"pnpm-lock.yaml",
|
|
"scripts/plan-release-workflow-matrix.mjs",
|
|
"scripts/lib/direct-run.mjs",
|
|
"scripts/lib/plugin-prerelease-test-plan.mts",
|
|
"scripts/plan-targeted-docker-lane-groups.mjs",
|
|
"scripts/lib/numeric-options.mjs",
|
|
];
|
|
|
|
function frozenFixtureGit(directory: string, ...args: string[]) {
|
|
return execFileSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"maintenance.auto=false",
|
|
"-c",
|
|
"gc.auto=0",
|
|
"-c",
|
|
"core.hooksPath=/dev/null",
|
|
"-c",
|
|
"commit.gpgsign=false",
|
|
"-c",
|
|
"user.name=Fixture",
|
|
"-c",
|
|
"user.email=fixture@example.test",
|
|
"-C",
|
|
directory,
|
|
...args,
|
|
],
|
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
).trim();
|
|
}
|
|
|
|
function frozenToolingFixture(root: string, toolingPaths: string[]) {
|
|
const tooling = join(root, "tooling");
|
|
const templateKey = JSON.stringify(toolingPaths);
|
|
let template = toolingTemplates.get(templateKey);
|
|
if (!template) {
|
|
const directory = templateDirs.make("frozen-workflow-tooling-template-");
|
|
// The acquisition step also uses the existing npm-output parser.
|
|
for (const path of [...frozenAdmissionClosure, "scripts/lib/npm-json-output.mts"]) {
|
|
mkdirSync(dirname(join(directory, path)), { recursive: true });
|
|
copyFileSync(path, join(directory, path));
|
|
}
|
|
const recipes = "scripts/e2e/lib/upgrade-survivor/config-recipe";
|
|
cpSync(recipes, join(directory, recipes), { recursive: true });
|
|
for (const path of toolingPaths) {
|
|
mkdirSync(dirname(join(directory, path)), { recursive: true });
|
|
cpSync(path, join(directory, path), { recursive: true });
|
|
}
|
|
frozenFixtureGit(directory, "init", "-q");
|
|
frozenFixtureGit(directory, "add", ".");
|
|
frozenFixtureGit(directory, "commit", "-qm", "candidate tooling fixture");
|
|
// Pack the immutable source once; fault cases create their own loose blobs afterward.
|
|
frozenFixtureGit(directory, "repack", "-ad");
|
|
template = { directory, sha: frozenFixtureGit(directory, "rev-parse", "HEAD") };
|
|
toolingTemplates.set(templateKey, template);
|
|
}
|
|
// Fault cases remove objects and change config; never share mutable Git stores.
|
|
cpSync(template.directory, tooling, { recursive: true, mode: fsConstants.COPYFILE_FICLONE });
|
|
return { tooling, toolingSha: template.sha };
|
|
}
|
|
|
|
function frozenWorkflowFixture(
|
|
file: string,
|
|
jobName: string,
|
|
inputs: Record<string, string | boolean | number>,
|
|
files: Record<string, string> = {},
|
|
overrides: Record<string, string> = {},
|
|
toolingPaths: string[] = [],
|
|
) {
|
|
const root = tempDirs.make("frozen-workflow-");
|
|
const target = join(root, "target");
|
|
mkdirSync(target);
|
|
for (const [path, value] of Object.entries({
|
|
"package.json": '{"type":"module","version":"2026.7.33"}',
|
|
...files,
|
|
})) {
|
|
mkdirSync(dirname(join(target, path)), { recursive: true });
|
|
writeFileSync(join(target, path), value);
|
|
}
|
|
const git = (...args: string[]) => frozenFixtureGit(target, ...args);
|
|
git("init", "-q");
|
|
git("add", ".");
|
|
git("commit", "-qm", "fixture");
|
|
const sha = git("rev-parse", "HEAD");
|
|
const { tooling, toolingSha } = frozenToolingFixture(root, toolingPaths);
|
|
const toolingGit = (...args: string[]) => frozenFixtureGit(tooling, ...args);
|
|
const job = workflowJob(file, jobName);
|
|
const plan = workflowStep(job, "Plan frozen source admission");
|
|
const env = {
|
|
PATH: process.env.PATH,
|
|
LANG: "C.UTF-8",
|
|
RUNNER_TEMP: root,
|
|
GITHUB_OUTPUT: join(root, "outputs"),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ID: "123",
|
|
GITHUB_RUN_ATTEMPT: "2",
|
|
ADMISSION_WORKFLOW: plan.env?.ADMISSION_WORKFLOW ?? "",
|
|
ADMISSION_INPUTS: JSON.stringify(inputs, null, 2),
|
|
ADMISSION_SELECTED_ROOT: target,
|
|
ADMISSION_SELECTED_SHA: sha,
|
|
ADMISSION_TOOLING_ROOT: tooling,
|
|
ADMISSION_TOOLING_SHA: toolingSha,
|
|
ADMISSION_WORKFLOW_REF: `openclaw/openclaw/${file}@${toolingSha}`,
|
|
...overrides,
|
|
};
|
|
function run(
|
|
stepName: string,
|
|
extra: Record<string, string> = {},
|
|
suffix = "",
|
|
options: { cwd?: string; timeout?: number } = {},
|
|
) {
|
|
return spawnSync(
|
|
"bash",
|
|
["--noprofile", "--norc", "-c", `${workflowStep(job, stepName).run ?? ""}\n${suffix}`],
|
|
{ encoding: "utf8", cwd: root, env: { ...env, ...extra }, timeout: 30_000, ...options },
|
|
);
|
|
}
|
|
function selection() {
|
|
const result = run("Plan frozen source admission");
|
|
expect(result.status, result.stderr).toBe(0);
|
|
return JSON.parse(readFileSync(join(root, "frozen-admission-selection.json"), "utf8"));
|
|
}
|
|
const forbidden = join(root, "forbidden");
|
|
const bin = join(root, "bin");
|
|
mkdirSync(bin);
|
|
for (const command of [
|
|
"npm",
|
|
"pnpm",
|
|
"npx",
|
|
"tsx",
|
|
"docker",
|
|
"curl",
|
|
"wget",
|
|
"gh",
|
|
"ghx",
|
|
"git-remote-fixture",
|
|
]) {
|
|
writeFileSync(
|
|
join(bin, command),
|
|
`#!/bin/sh\nprintf '${command}\\n' >> '${forbidden}'\nexit 97\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
const gitPath = (fixtureGitPath ??= execFileSync("which", ["git"], { encoding: "utf8" }).trim());
|
|
writeFileSync(
|
|
join(bin, "git"),
|
|
`#!/bin/sh\nfor arg in "$@"; do\ncase "$arg" in fetch|clone) printf 'hydration\\n' >> '${forbidden}'; exit 97;; esac\ndone\nexec '${gitPath}' "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
env.PATH = `${bin}:${process.env.PATH}`;
|
|
function admit(extra: Record<string, string> = {}, stepName = "Admit frozen source contracts") {
|
|
const result = run(
|
|
stepName,
|
|
{
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
NODE_OPTIONS: "--import=forbidden-startup",
|
|
NODE_PATH: join(root, "poison-modules"),
|
|
GH_TOKEN: "synthetic-secret-must-not-survive",
|
|
...extra,
|
|
},
|
|
"printf 'producer-ran\\n'",
|
|
);
|
|
expect(existsSync(forbidden), result.stderr).toBe(false);
|
|
return result;
|
|
}
|
|
function provisionParser() {
|
|
const installedParser = createRequire(import.meta.url).resolve("typescript/package.json");
|
|
const nativeName = `@typescript/typescript-${process.platform}-${process.arch}`;
|
|
const installedNative = createRequire(installedParser).resolve(`${nativeName}/package.json`);
|
|
cpSync(dirname(installedParser), join(tooling, "node_modules/typescript"), {
|
|
recursive: true,
|
|
dereference: true,
|
|
});
|
|
cpSync(dirname(installedNative), join(tooling, "node_modules", nativeName), {
|
|
recursive: true,
|
|
dereference: true,
|
|
});
|
|
}
|
|
return {
|
|
root,
|
|
target,
|
|
sha,
|
|
tooling,
|
|
toolingSha,
|
|
git,
|
|
toolingGit,
|
|
env,
|
|
run,
|
|
selection,
|
|
admit,
|
|
provisionParser,
|
|
};
|
|
}
|
|
|
|
function reconstructAdmissionEvaluations(record: {
|
|
evaluationIdentity: {
|
|
version: number;
|
|
repository: string;
|
|
selectedSha: string;
|
|
toolingSha: string;
|
|
};
|
|
sources: Record<"selected" | "tooling", Array<{ path: string; oid: string }>>;
|
|
evaluations: Array<{
|
|
selection: unknown;
|
|
contracts: unknown[];
|
|
docker?: unknown;
|
|
sourceRefs: Record<"selected" | "tooling", number[]>;
|
|
digest: string;
|
|
}>;
|
|
}) {
|
|
return record.evaluations.map((evaluation) => {
|
|
const sources = {
|
|
selected: [] as Array<{ path: string; oid: string }>,
|
|
tooling: [] as Array<{ path: string; oid: string }>,
|
|
};
|
|
for (const role of ["selected", "tooling"] as const) {
|
|
sources[role] = evaluation.sourceRefs[role].map((index) => {
|
|
expect(Number.isInteger(index)).toBe(true);
|
|
expect(index).toBeGreaterThanOrEqual(0);
|
|
expect(index).toBeLessThan(record.sources[role].length);
|
|
const identity = record.sources[role][index];
|
|
if (identity === undefined) {
|
|
throw new Error("missing admission source identity");
|
|
}
|
|
return identity;
|
|
});
|
|
}
|
|
const child = {
|
|
...record.evaluationIdentity,
|
|
selection: evaluation.selection,
|
|
contracts: evaluation.contracts,
|
|
...(evaluation.docker ? { docker: evaluation.docker } : {}),
|
|
sources,
|
|
};
|
|
expect(evaluation.digest).toBe(
|
|
createHash("sha256").update(JSON.stringify(child)).digest("hex"),
|
|
);
|
|
return { ...child, digest: evaluation.digest };
|
|
});
|
|
}
|
|
|
|
function currentSurvivorScenarioFiles() {
|
|
return Object.fromEntries(
|
|
[
|
|
"scripts/e2e/lib/upgrade-survivor/assertions.mjs",
|
|
"scripts/lib/upgrade-survivor-scenarios.json",
|
|
].map((path) => [path, readFileSync(path, "utf8")]),
|
|
);
|
|
}
|
|
|
|
function packageAdmissionBaselineFixture(
|
|
inputs: Record<string, string | boolean | number>,
|
|
failRegistry = false,
|
|
) {
|
|
const f = frozenWorkflowFixture(
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
"resolve_package",
|
|
{
|
|
source: "artifact",
|
|
suite_profile: "custom",
|
|
telegram_mode: "none",
|
|
...inputs,
|
|
},
|
|
{
|
|
"package.json": '{"type":"module","version":"2026.9.9"}',
|
|
...currentSurvivorScenarioFiles(),
|
|
},
|
|
{},
|
|
["scripts/resolve-upgrade-survivor-baselines.mts", "scripts/lib/release-upgrade-baseline.mjs"],
|
|
);
|
|
// Only the existing trusted acquisition command needs tsx; admission uses plain Node.
|
|
symlinkSync(resolve("node_modules"), join(f.tooling, "node_modules"), "dir");
|
|
const calls = join(f.root, "registry-calls");
|
|
writeFileSync(
|
|
join(f.root, "bin/npm"),
|
|
`#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\n${
|
|
failRegistry
|
|
? "echo 'controlled baseline lookup failure' >&2\nexit 73"
|
|
: "printf '\"2026.9.1\"\\n'"
|
|
}\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const selected = join(f.root, "not-acquired");
|
|
const identity = {
|
|
ADMISSION_SELECTED_ROOT: selected,
|
|
ADMISSION_STAGE: "resolved-package",
|
|
ADMISSION_PACKAGE_SOURCE_SHA: f.sha,
|
|
ADMISSION_PACKAGE_SHA256: "d".repeat(64),
|
|
ADMISSION_PACKAGE_VERSION: "2026.9.9",
|
|
};
|
|
const outputs: Record<string, { outputs: Record<string, string> }> = {};
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const completed: string[] = [];
|
|
const readOutputs = (path: string) => {
|
|
const values: Record<string, string> = {};
|
|
const lines = (existsSync(path) ? readFileSync(path, "utf8") : "").split("\n");
|
|
for (let index = 0; index < lines.length - 1; index++) {
|
|
const line = lines[index];
|
|
if (line === undefined) {
|
|
throw new Error("missing output line");
|
|
}
|
|
const heredoc = line.indexOf("<<");
|
|
const equals = line.indexOf("=");
|
|
if (heredoc >= 0 && (equals < 0 || heredoc < equals)) {
|
|
const delimiter = line.slice(heredoc + 2);
|
|
const content: string[] = [];
|
|
while (++index < lines.length && lines[index] !== delimiter) {
|
|
const value = lines[index];
|
|
if (value === undefined) {
|
|
throw new Error("missing multiline output");
|
|
}
|
|
content.push(value);
|
|
}
|
|
expect(lines[index]).toBe(delimiter);
|
|
values[line.slice(0, heredoc)] = content.join("\n");
|
|
} else {
|
|
expect(equals, line).toBeGreaterThan(0);
|
|
values[line.slice(0, equals)] = line.slice(equals + 1);
|
|
}
|
|
}
|
|
return values;
|
|
};
|
|
function run(stepName: string) {
|
|
const step = workflowStep(job, stepName);
|
|
if (!step.id) {
|
|
throw new Error("missing baseline step identity");
|
|
}
|
|
const outputPath = join(f.root, `${step.id}-outputs`);
|
|
const resolver = outputs.upgrade_survivor_baselines?.outputs ?? {};
|
|
const pin = outputs.exact_baselines?.outputs ?? {};
|
|
if (!runInNewContext(step.if ?? "true", { steps: outputs })) {
|
|
outputs[step.id] = { outputs: {} };
|
|
return { status: 0, stderr: "", stdout: "" };
|
|
}
|
|
const result = f.run(
|
|
stepName,
|
|
{
|
|
...identity,
|
|
GITHUB_OUTPUT: outputPath,
|
|
CANDIDATE_VERSION: "2026.9.9",
|
|
CANDIDATE_PUBLISHED: "false",
|
|
FALLBACK_BASELINE: String(inputs.published_upgrade_survivor_baseline ?? "openclaw@beta"),
|
|
REQUESTED_BASELINES: String(inputs.published_upgrade_survivor_baselines ?? ""),
|
|
TARGET_CONTEXT_REF: "",
|
|
GH_TOKEN: "",
|
|
BASELINE:
|
|
step.id === "resolved_admission" ? (pin.baseline ?? "") : (resolver.baseline ?? ""),
|
|
BASELINES:
|
|
step.id === "resolved_admission" ? (pin.baselines ?? "") : (resolver.baselines ?? ""),
|
|
BASELINE_SCOPE: resolver.baseline_scope ?? "",
|
|
},
|
|
"",
|
|
{ cwd: f.tooling },
|
|
);
|
|
outputs[step.id] = { outputs: readOutputs(outputPath) };
|
|
if (result.status === 0) {
|
|
completed.push(stepName);
|
|
}
|
|
return result;
|
|
}
|
|
const request = () =>
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission-request.json"), "utf8"));
|
|
const plan = () =>
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission-selection.json"), "utf8"));
|
|
const planned = run("Plan frozen source admission");
|
|
expect(planned.status, planned.stderr).toBe(0);
|
|
expect(existsSync(selected)).toBe(false);
|
|
const initialRequest = request();
|
|
const initialPlan = plan();
|
|
function resolveBaselines() {
|
|
for (const name of [
|
|
"Resolve published upgrade survivor baselines",
|
|
"Pin published upgrade baseline versions",
|
|
"Finalize frozen source admission",
|
|
]) {
|
|
const result = run(name);
|
|
if (result.status !== 0) {
|
|
return { ...result, failedStep: name };
|
|
}
|
|
}
|
|
return { status: 0, stderr: "", stdout: "", failedStep: undefined };
|
|
}
|
|
function admit() {
|
|
renameSync(f.target, selected);
|
|
return f.admit();
|
|
}
|
|
return {
|
|
f,
|
|
calls,
|
|
outputs,
|
|
completed,
|
|
initialRequest,
|
|
initialPlan,
|
|
request,
|
|
plan,
|
|
resolveBaselines,
|
|
admit,
|
|
};
|
|
}
|
|
|
|
function packageToolingCheckoutFixture() {
|
|
const root = tempDirs.make("package-tooling-checkout-");
|
|
const repository = join(root, "repository");
|
|
const beforeCheckout = join(root, "before-checkout");
|
|
mkdirSync(repository);
|
|
mkdirSync(beforeCheckout);
|
|
const git = (...args: string[]) =>
|
|
execFileSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"core.hooksPath=/dev/null",
|
|
"-c",
|
|
"commit.gpgsign=false",
|
|
"-c",
|
|
"user.name=Fixture",
|
|
"-c",
|
|
"user.email=fixture@example.test",
|
|
"-C",
|
|
repository,
|
|
...args,
|
|
],
|
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] },
|
|
).trim();
|
|
if (!packageToolingTemplate) {
|
|
const directory = templateDirs.make("package-tooling-history-template-");
|
|
const templateGit = (...args: string[]) => git("-C", directory, ...args);
|
|
templateGit("init", "-q", "--initial-branch=main", "--template=");
|
|
mkdirSync(join(directory, ".github/workflows"), { recursive: true });
|
|
copyFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, join(directory, PACKAGE_ACCEPTANCE_WORKFLOW));
|
|
templateGit("add", ".");
|
|
templateGit("commit", "-qm", "captured workflow");
|
|
const capturedSha = templateGit("rev-parse", "HEAD");
|
|
templateGit("branch", "release/candidate", capturedSha);
|
|
templateGit("branch", "alias", capturedSha);
|
|
templateGit("tag", "release-candidate", capturedSha);
|
|
writeFileSync(join(directory, "advanced.txt"), "main advanced after dispatch\n");
|
|
templateGit("add", ".");
|
|
templateGit("commit", "-qm", "advance main");
|
|
const advancedSha = templateGit("rev-parse", "HEAD");
|
|
templateGit("repack", "-ad");
|
|
packageToolingTemplate = { directory, capturedSha, advancedSha };
|
|
}
|
|
// Checkout and ref mutations stay local to each invocation of the workflow steps.
|
|
cpSync(packageToolingTemplate.directory, repository, {
|
|
recursive: true,
|
|
mode: fsConstants.COPYFILE_FICLONE,
|
|
});
|
|
const { capturedSha, advancedSha } = packageToolingTemplate;
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const checkout = workflowStep(job, "Checkout package workflow ref");
|
|
const validate = workflowStep(job, "Validate exact package tooling checkout");
|
|
const identity = job.steps?.find((step) => step.id === "tooling_identity");
|
|
let sequence = 0;
|
|
function run(
|
|
options: {
|
|
workflowRef?: string;
|
|
capturedRef?: string;
|
|
context?: Record<string, unknown>;
|
|
attempt?: number;
|
|
wrongCheckout?: boolean;
|
|
githubRepository?: string;
|
|
callerWorkflowRef?: string;
|
|
} = {},
|
|
) {
|
|
const context = {
|
|
workflow_repository: "openclaw/openclaw",
|
|
workflow_ref: `openclaw/openclaw/.github/workflows/package-acceptance.yml@${options.capturedRef ?? "refs/heads/main"}`,
|
|
workflow_sha: capturedSha,
|
|
...options.context,
|
|
};
|
|
const identityOutput = join(root, `identity-${sequence}`);
|
|
const validationOutput = join(root, `validation-${sequence++}`);
|
|
writeFileSync(identityOutput, "");
|
|
writeFileSync(validationOutput, "");
|
|
const env = {
|
|
PATH: process.env.PATH,
|
|
JOB_CONTEXT: JSON.stringify(context),
|
|
GITHUB_REPOSITORY: options.githubRepository ?? "openclaw/openclaw",
|
|
GITHUB_SHA: advancedSha,
|
|
GITHUB_REF: "refs/heads/other-caller",
|
|
GITHUB_WORKFLOW_REF:
|
|
options.callerWorkflowRef ??
|
|
"openclaw/openclaw/.github/workflows/other-caller.yml@refs/heads/main",
|
|
GITHUB_RUN_ATTEMPT: String(options.attempt ?? 1),
|
|
WORKFLOW_REF: options.workflowRef ?? "main",
|
|
PACKAGE_REF: advancedSha,
|
|
};
|
|
const outputs = (file: string): Record<string, string> =>
|
|
Object.fromEntries(
|
|
readFileSync(file, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
);
|
|
if (identity) {
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", identity.run ?? ""], {
|
|
cwd: beforeCheckout,
|
|
encoding: "utf8",
|
|
env: { ...env, GITHUB_OUTPUT: identityOutput },
|
|
});
|
|
if (result.status !== 0) {
|
|
return {
|
|
result,
|
|
checkoutReached: false,
|
|
selectedRef: undefined,
|
|
sha: undefined,
|
|
identityOutputs: outputs(identityOutput),
|
|
validationOutputs: outputs(validationOutput),
|
|
};
|
|
}
|
|
}
|
|
const identityOutputs = outputs(identityOutput);
|
|
const selectedRef: unknown = runInNewContext(
|
|
String(checkout.with?.ref).replace(/^\$\{\{(.*)\}\}$/u, "$1"),
|
|
{
|
|
inputs: { workflow_ref: env.WORKFLOW_REF, package_ref: env.PACKAGE_REF },
|
|
steps: { tooling_identity: { outputs: identityOutputs } },
|
|
},
|
|
);
|
|
if (typeof selectedRef !== "string") {
|
|
throw new Error("package workflow checkout did not select a ref");
|
|
}
|
|
git("checkout", "-q", "--detach", options.wrongCheckout ? advancedSha : selectedRef);
|
|
const sha = git("rev-parse", "HEAD");
|
|
const result = spawnSync(
|
|
"bash",
|
|
["--noprofile", "--norc", "-c", `${validate.run}\nprintf 'installation-reachable\\n'`],
|
|
{
|
|
cwd: repository,
|
|
encoding: "utf8",
|
|
env: {
|
|
...env,
|
|
EXPECTED_TOOLING_SHA: identityOutputs.sha ?? "",
|
|
GITHUB_OUTPUT: validationOutput,
|
|
},
|
|
},
|
|
);
|
|
return {
|
|
result,
|
|
checkoutReached: true,
|
|
selectedRef,
|
|
sha,
|
|
identityOutputs,
|
|
validationOutputs: outputs(validationOutput),
|
|
};
|
|
}
|
|
return { capturedSha, advancedSha, git, job, checkout, validate, identity, run };
|
|
}
|
|
|
|
describe("frozen admission workflow barriers", () => {
|
|
it("keeps admission artifacts distinct across the owned nested release callers", () => {
|
|
const jobs = Object.values(readWorkflow(RELEASE_CHECKS_WORKFLOW).jobs ?? {});
|
|
const callers = jobs.filter(
|
|
(job) =>
|
|
job.uses === "./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml" ||
|
|
job.uses === "./.github/workflows/package-acceptance.yml",
|
|
);
|
|
const transports = Object.values(readWorkflow(PACKAGE_ACCEPTANCE_WORKFLOW).jobs ?? {}).filter(
|
|
(job) => job.uses === "./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
|
);
|
|
expect(callers).toHaveLength(3);
|
|
expect(transports).toHaveLength(2);
|
|
for (const policy of ["no-push-artifact", "existing-only"]) {
|
|
expect(
|
|
transports.filter((job) =>
|
|
runInNewContext(job.if ?? "true", {
|
|
inputs: { suite_profile: "custom", shared_image_policy: policy },
|
|
}),
|
|
),
|
|
).toHaveLength(1);
|
|
}
|
|
for (const job of transports) {
|
|
expect(job.with?.shared_image_artifact_namespace).toBe(
|
|
"${{ inputs.shared_image_artifact_namespace }}",
|
|
);
|
|
}
|
|
const upload = workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "validate_selected_ref"),
|
|
"Upload frozen admission diagnostic",
|
|
);
|
|
const template = upload.with?.name;
|
|
if (!template) {
|
|
throw new Error("missing admission artifact name");
|
|
}
|
|
const names = callers.map((job) => {
|
|
const namespace = job.with?.shared_image_artifact_namespace;
|
|
if (typeof namespace !== "string") {
|
|
throw new Error("missing caller namespace");
|
|
}
|
|
return template
|
|
.replaceAll("${{ inputs.shared_image_artifact_namespace }}", namespace)
|
|
.replaceAll("${{ github.run_id }}", "123")
|
|
.replaceAll("${{ github.run_attempt }}", "2");
|
|
});
|
|
expect(new Set(names).size, JSON.stringify(names)).toBe(callers.length);
|
|
expect(upload.with?.overwrite).toBeUndefined();
|
|
expect(upload["continue-on-error"]).toBeUndefined();
|
|
expect(upload.with?.["if-no-files-found"]).toBe("error");
|
|
expect(upload.with?.["retention-days"]).toBe(7);
|
|
});
|
|
|
|
it.each(["beta", "2026.8.1-alpha.1"])(
|
|
"preserves an unused package %s baseline without a registry lookup",
|
|
(tag) => {
|
|
const baseline = `openclaw@${tag}`;
|
|
const f = frozenWorkflowFixture(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", {
|
|
source: "artifact",
|
|
suite_profile: "custom",
|
|
docker_lanes: "onboard",
|
|
telegram_mode: "none",
|
|
published_upgrade_survivor_baseline: baseline,
|
|
});
|
|
const planned = f.run("Plan frozen source admission", {
|
|
ADMISSION_STAGE: "resolved-package",
|
|
ADMISSION_PACKAGE_SOURCE_SHA: f.sha,
|
|
ADMISSION_PACKAGE_SHA256: "d".repeat(64),
|
|
ADMISSION_PACKAGE_VERSION: "2026.7.33",
|
|
});
|
|
expect(planned.status, planned.stderr).toBe(0);
|
|
expect(
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission-selection.json"), "utf8"))
|
|
.obligations,
|
|
).toEqual([]);
|
|
const calls = join(f.root, "registry-calls");
|
|
writeFileSync(
|
|
join(f.root, "bin/npm"),
|
|
`#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\necho 'unexpected unused baseline lookup' >&2\nexit 73\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const step = workflowStep(
|
|
workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"),
|
|
"Pin published upgrade baseline versions",
|
|
);
|
|
const outputs = Object.fromEntries(
|
|
(existsSync(join(f.root, "outputs")) ? readFileSync(join(f.root, "outputs"), "utf8") : "")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const at = line.indexOf("=");
|
|
return [line.slice(0, at), line.slice(at + 1)];
|
|
}),
|
|
);
|
|
const enabled = runInNewContext(step.if ?? "true", {
|
|
steps: { frozen_selection: { outputs } },
|
|
});
|
|
const result = enabled
|
|
? f.run(
|
|
"Pin published upgrade baseline versions",
|
|
{ BASELINE: baseline, BASELINES: "" },
|
|
"",
|
|
{ cwd: f.tooling },
|
|
)
|
|
: { status: 0, stderr: "", stdout: "" };
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(existsSync(calls)).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
"published-upgrade-survivor",
|
|
"update-migration",
|
|
"root-managed-vps-upgrade",
|
|
"update-restart-auth",
|
|
])("pins package %s baselines only after the canonical plan", (lane) => {
|
|
const f = packageAdmissionBaselineFixture({
|
|
docker_lanes: lane,
|
|
published_upgrade_survivor_baseline: "openclaw@beta",
|
|
});
|
|
expect(f.initialPlan.obligations).toContainEqual({
|
|
kind: "upgrade-baselines",
|
|
status: "UNRESOLVED",
|
|
requested: f.initialRequest.requestedBaselines,
|
|
});
|
|
expect(f.initialRequest.options.baselinesResolved).toBe(false);
|
|
const resolved = f.resolveBaselines();
|
|
expect(resolved.status, resolved.stderr).toBe(0);
|
|
expect(readFileSync(f.calls, "utf8").trim().split("\n")).toEqual([
|
|
"view openclaw@beta version --json --silent --prefer-online",
|
|
]);
|
|
const request = f.request();
|
|
expect(request.binding).toEqual(f.initialRequest.binding);
|
|
expect(request.selected).toEqual(f.initialRequest.selected);
|
|
expect(request.requestedBaselines).toEqual(f.initialRequest.requestedBaselines);
|
|
expect(request.options).toMatchObject({
|
|
upgradeSurvivorBaseline: "openclaw@2026.9.1",
|
|
upgradeSurvivorBaselines: "openclaw@2026.9.1",
|
|
baselinesResolved: true,
|
|
});
|
|
expect(f.plan().obligations).toEqual([]);
|
|
const admitted = f.admit();
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(f.f.root, "frozen-admission.json"), "utf8"));
|
|
expect(record.status).toBe("ADMITTED");
|
|
expect(record.binding).toEqual(request.binding);
|
|
expect(record.requestedBaselines.baseline).toBe("openclaw@beta");
|
|
reconstructAdmissionEvaluations(record);
|
|
});
|
|
|
|
it.each([
|
|
"published-upgrade-survivor",
|
|
"update-migration",
|
|
"root-managed-vps-upgrade",
|
|
"update-restart-auth",
|
|
])("blocks package %s admission when baseline lookup fails", (lane) => {
|
|
const f = packageAdmissionBaselineFixture(
|
|
{ docker_lanes: lane, published_upgrade_survivor_baseline: "openclaw@beta" },
|
|
true,
|
|
);
|
|
const result = f.resolveBaselines();
|
|
expect(result.status).toBe(1);
|
|
expect(result.failedStep).toBe("Pin published upgrade baseline versions");
|
|
expect(result.stderr).toContain("controlled baseline lookup failure");
|
|
expect(readFileSync(f.calls, "utf8").trim().split("\n")).toEqual([
|
|
"view openclaw@beta version --json --silent --prefer-online",
|
|
]);
|
|
expect(f.completed).not.toContain("Finalize frozen source admission");
|
|
expect(f.request().options.baselinesResolved).toBe(false);
|
|
expect(existsSync(join(f.f.root, "frozen-admission.json"))).toBe(false);
|
|
});
|
|
|
|
it("does not look up already exact selected package baselines", () => {
|
|
const f = packageAdmissionBaselineFixture(
|
|
{
|
|
docker_lanes: "root-managed-vps-upgrade",
|
|
published_upgrade_survivor_baseline: "openclaw@2026.9.1",
|
|
published_upgrade_survivor_baselines: "openclaw@2026.9.1",
|
|
},
|
|
true,
|
|
);
|
|
const resolved = f.resolveBaselines();
|
|
expect(resolved.status, resolved.stderr).toBe(0);
|
|
expect(existsSync(f.calls)).toBe(false);
|
|
expect(f.request().options.baselinesResolved).toBe(true);
|
|
expect(f.outputs.resolved_admission?.outputs.baseline_scope).toBe("all-scenarios");
|
|
});
|
|
|
|
it("rejects a pre-June dist-tag before publishing resolved baseline admission", () => {
|
|
const f = packageAdmissionBaselineFixture({
|
|
docker_lanes: "published-upgrade-survivor",
|
|
published_upgrade_survivor_baseline: "openclaw@beta",
|
|
});
|
|
writeFileSync(join(f.f.root, "bin/npm"), "#!/bin/sh\nprintf '\"2026.5.31\"\\n'\n", {
|
|
mode: 0o755,
|
|
});
|
|
const result = f.resolveBaselines();
|
|
expect(result.failedStep).toBe("Pin published upgrade baseline versions");
|
|
expect(result.stderr).toContain("Upgrade pre-June installs through OpenClaw 2026.9.5");
|
|
expect(f.outputs.exact_baselines?.outputs).toEqual({});
|
|
expect(f.request().options.baselinesResolved).toBe(false);
|
|
});
|
|
|
|
it("rejects a pre-June inherited baseline at direct frozen preflight admission", () => {
|
|
const f = packageAdmissionBaselineFixture({ docker_lanes: "root-managed-vps-upgrade" });
|
|
const request = f.request();
|
|
Object.assign(request.options, {
|
|
baselinesResolved: true,
|
|
upgradeSurvivorBaseline: "openclaw@2026.5.31",
|
|
upgradeSurvivorBaselines: "",
|
|
});
|
|
const requestPath = join(f.f.root, "frozen-admission-request.json");
|
|
writeFileSync(requestPath, JSON.stringify(request));
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[join(f.f.tooling, "scripts/preflight-frozen-target-contracts.mjs"), "--plan", requestPath],
|
|
{ encoding: "utf8", env: { PATH: process.env.PATH, HOME: f.f.root } },
|
|
);
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("Upgrade pre-June installs through OpenClaw 2026.9.5");
|
|
expect(result.stdout).toBe("");
|
|
});
|
|
|
|
it.each(["onboard", "upgrade-survivor"])(
|
|
"preserves unused multiline package baselines and scope for %s",
|
|
(lane) => {
|
|
const baselines = "OPENCLAW_ADMISSION_OUTPUT\r\nopenclaw@2026.8.1-alpha.1\nopenclaw@beta\n";
|
|
const f = packageAdmissionBaselineFixture(
|
|
{
|
|
docker_lanes: lane,
|
|
published_upgrade_survivor_baseline: "openclaw@beta",
|
|
published_upgrade_survivor_baselines: baselines,
|
|
},
|
|
true,
|
|
);
|
|
expect(f.initialPlan.obligations).toEqual([]);
|
|
const resolved = f.resolveBaselines();
|
|
expect(resolved.status, resolved.stderr).toBe(0);
|
|
expect(existsSync(f.calls)).toBe(false);
|
|
expect(f.request()).toEqual(f.initialRequest);
|
|
expect(f.request().options.baselinesResolved).toBe(false);
|
|
expect(f.outputs.resolved_admission?.outputs).toEqual({
|
|
baseline: "openclaw@beta",
|
|
baselines,
|
|
baseline_scope: "all-scenarios",
|
|
});
|
|
expect(f.completed).not.toContain("Resolve published upgrade survivor baselines");
|
|
expect(f.completed).not.toContain("Pin published upgrade baseline versions");
|
|
},
|
|
);
|
|
|
|
it("orders package planning, acquisition and final outputs around selected baseline resolution", () => {
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const names = (job.steps ?? []).map((step) => step.name);
|
|
const ordered = [
|
|
"Admit known package source before packing",
|
|
"Resolve package candidate",
|
|
"Plan frozen source admission",
|
|
"Resolve published upgrade survivor baselines",
|
|
"Pin published upgrade baseline versions",
|
|
"Finalize frozen source admission",
|
|
"Validate resolved package source",
|
|
"Acquire resolved package source",
|
|
"Acquire selected contract objects",
|
|
"Admit frozen source contracts",
|
|
];
|
|
for (const [index, name] of ordered.entries()) {
|
|
const previous = ordered[index - 1];
|
|
expect(names.indexOf(name), name).toBeGreaterThan(previous ? names.indexOf(previous) : -1);
|
|
}
|
|
for (const name of [
|
|
"Resolve published upgrade survivor baselines",
|
|
"Pin published upgrade baseline versions",
|
|
]) {
|
|
expect(workflowStep(job, name).if).toBe(
|
|
"steps.frozen_selection.outputs.upgrade_baselines_required == 'true'",
|
|
);
|
|
}
|
|
for (const [suffix, output] of [
|
|
["baseline", "baseline"],
|
|
["baselines", "baselines"],
|
|
["baseline_scope", "baseline_scope"],
|
|
] as const) {
|
|
expect(job.outputs?.[`published_upgrade_survivor_${suffix}`]).toBe(
|
|
`\${{ steps.resolved_admission.outputs.${output} }}`,
|
|
);
|
|
}
|
|
const summary = workflowStep(job, "Summarize package candidate");
|
|
expect(summary.env?.PUBLISHED_UPGRADE_SURVIVOR_BASELINE_SCOPE).toBe(
|
|
"${{ steps.resolved_admission.outputs.baseline_scope }}",
|
|
);
|
|
expect(summary.run).toContain("${PUBLISHED_UPGRADE_SURVIVOR_BASELINE_SCOPE}");
|
|
});
|
|
|
|
it(
|
|
"admits the default parent CLI within the final record byte limit",
|
|
{ timeout: 420_000 },
|
|
() => {
|
|
const started = Date.now();
|
|
const files = Object.fromEntries(
|
|
[
|
|
"scripts/runtime-postbuild.mts",
|
|
"src/cli/update-cli/update-command-plugin-preflight.ts",
|
|
"scripts/e2e/lib/upgrade-survivor/assertions.mjs",
|
|
"scripts/lib/upgrade-survivor-scenarios.json",
|
|
"extensions/codex/package.json",
|
|
].map((path) => [path, readFileSync(path, "utf8")]),
|
|
);
|
|
const f = frozenWorkflowFixture(
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"resolve_target",
|
|
{},
|
|
{ ...files, "package.json": '{"type":"module","version":"2026.9.9"}' },
|
|
{},
|
|
[
|
|
"scripts/lib",
|
|
"scripts/e2e/lib",
|
|
"test/e2e/qa-lab/runtime/agent-bundle-mcp-tools-docker-client.ts",
|
|
],
|
|
);
|
|
const plan = f.selection();
|
|
expect(plan.docker.length).toBeGreaterThan(0);
|
|
const planned = Date.now();
|
|
const result = f.run("Admit frozen source contracts", {}, "", { timeout: 360_000 });
|
|
console.info(
|
|
JSON.stringify({
|
|
case: "default parent admission",
|
|
setupAndPlanMs: planned - started,
|
|
evaluationMs: Date.now() - planned,
|
|
status: result.status,
|
|
error: result.error?.message,
|
|
stderr: result.stderr,
|
|
}),
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const bytes = readFileSync(join(f.root, "frozen-admission.json"));
|
|
expect(bytes.length).toBeLessThanOrEqual(262_144);
|
|
const record = JSON.parse(bytes.toString("utf8"));
|
|
expect(record.evaluations).toHaveLength(plan.docker.length + 1);
|
|
const children = reconstructAdmissionEvaluations(record);
|
|
expect(children.map(({ selection }) => selection)).toMatchObject([
|
|
...plan.docker.map((docker: unknown) => ({ docker })),
|
|
{
|
|
consumers: plan.explicitConsumers.toSorted(),
|
|
codexSuites: plan.codexSuites.toSorted(),
|
|
fsSafeNative: plan.fsSafeNative,
|
|
},
|
|
]);
|
|
const { digest, provenance: _provenance, ...content } = record;
|
|
expect(digest).toBe(createHash("sha256").update(JSON.stringify(content)).digest("hex"));
|
|
expect(record.status).toBe("UNRESOLVED");
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
console.info(
|
|
`default parent admission: ${record.evaluations.length} evaluations, ${bytes.length} emitted bytes`,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{ groups: 256, scenarios: "base", admitted: true },
|
|
{ groups: 64, scenarios: "base ".repeat(800).trim(), admitted: false },
|
|
])(
|
|
"bounds the complete CLI record for $groups real planner groups",
|
|
{ timeout: 420_000 },
|
|
({ groups, scenarios, admitted }) => {
|
|
const baselines = Array.from(
|
|
{ length: groups / 2 },
|
|
(_, index) => `openclaw@2026.9.${index + 1}`,
|
|
).join(" ");
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: "published-upgrade-survivor update-migration",
|
|
targeted_docker_lane_group_size: 1,
|
|
include_live_suites: true,
|
|
live_suite_filter: "live-gateway-docker",
|
|
include_release_path_suites: false,
|
|
published_upgrade_survivor_baseline: "openclaw@2026.9.1",
|
|
published_upgrade_survivor_baselines: baselines,
|
|
published_upgrade_survivor_scenarios: scenarios,
|
|
},
|
|
{
|
|
"package.json": '{"type":"module","version":"2026.9.9"}',
|
|
...currentSurvivorScenarioFiles(),
|
|
},
|
|
{ ADMISSION_BASELINES_RESOLVED: "true" },
|
|
);
|
|
const plan = f.selection();
|
|
expect(plan.docker).toHaveLength(groups);
|
|
expect(plan.explicitConsumers).toContain("live-cli-backend");
|
|
const result = f.run("Admit frozen source contracts", {}, "printf 'producer-ran\\n'", {
|
|
timeout: 360_000,
|
|
});
|
|
expect(result.status, result.stderr).toBe(admitted ? 0 : 1);
|
|
const bytes = readFileSync(join(f.root, "frozen-admission.json"));
|
|
if (admitted) {
|
|
expect(bytes.length).toBeLessThanOrEqual(262_144);
|
|
const record = JSON.parse(bytes.toString("utf8"));
|
|
const children = reconstructAdmissionEvaluations(record);
|
|
expect(children).toHaveLength(groups + 1);
|
|
expect(record.status).toBe("ADMITTED");
|
|
expect(
|
|
record.evaluations
|
|
.slice(0, -1)
|
|
.map(
|
|
(evaluation: { selection: { docker: { baselines: string } } }) =>
|
|
evaluation.selection.docker.baselines,
|
|
),
|
|
).toEqual(plan.docker.map((group: { baselines: string }) => group.baselines));
|
|
expect(record.evaluations.at(-1).selection.consumers).toContain("live-cli-backend");
|
|
const { digest, provenance: _provenance, ...content } = record;
|
|
expect(digest).toBe(createHash("sha256").update(JSON.stringify(content)).digest("hex"));
|
|
expect(result.stdout).toContain("producer-ran");
|
|
console.info(
|
|
`high-cardinality admission: ${children.length} evaluations, ${bytes.length} emitted bytes`,
|
|
);
|
|
} else {
|
|
expect(result.stderr).toContain("workflow admission record exceeds limit");
|
|
expect(bytes.length).toBe(0);
|
|
expect(result.stdout).toBe("");
|
|
}
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each(["root-managed-vps-upgrade", "update-restart-auth"])(
|
|
"rejects unresolved published baseline for the exact %s wrapper",
|
|
(lane) => {
|
|
const f = frozenWorkflowFixture(LIVE_E2E_WORKFLOW, "validate_selected_ref", {
|
|
docker_lanes: lane,
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
published_upgrade_survivor_baseline: "openclaw@latest",
|
|
});
|
|
const plan = f.selection();
|
|
const result = f.admit();
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stderr).toContain("unresolved upgrade baselines at the execution boundary");
|
|
expect(result.stdout).toBe("");
|
|
expect(plan.obligations).toContainEqual(
|
|
expect.objectContaining({ kind: "upgrade-baselines" }),
|
|
);
|
|
expect(readFileSync(join(f.root, "frozen-admission.json"), "utf8")).toBe("");
|
|
},
|
|
);
|
|
|
|
it.each(["root-managed-vps-upgrade", "update-restart-auth"])(
|
|
"rejects a falsely resolved moving baseline for %s",
|
|
(lane) => {
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: lane,
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
published_upgrade_survivor_baseline: "openclaw@latest",
|
|
},
|
|
{},
|
|
{ ADMISSION_BASELINES_RESOLVED: "true" },
|
|
);
|
|
const result = f.run("Plan frozen source admission", {}, "printf 'acquisition-reachable\\n'");
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stderr).toContain("unresolved upgrade baselines at the execution boundary");
|
|
expect(result.stdout).toBe("");
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["onboard", false],
|
|
["upgrade-survivor", false],
|
|
["root-managed-vps-upgrade update-restart-auth", true],
|
|
])("leaves unselected published baselines unresolved for %s prepare=%s", (lane, prepareOnly) => {
|
|
const f = frozenWorkflowFixture(LIVE_E2E_WORKFLOW, "validate_selected_ref", {
|
|
docker_lanes: lane,
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
prepare_only: prepareOnly,
|
|
published_upgrade_survivor_baseline: "openclaw@latest",
|
|
});
|
|
expect(f.selection().obligations).toEqual([]);
|
|
expect(f.run("Resolve selected upgrade baseline versions").status).toBe(0);
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
});
|
|
|
|
it("keeps shared verification evidence separate from each child's selected and tooling reads", () => {
|
|
const inputs = {
|
|
docker_lanes: "onboard codex-on-demand",
|
|
targeted_docker_lane_group_size: 1,
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
};
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
inputs,
|
|
{
|
|
"extensions/codex/package.json": readFileSync("extensions/codex/package.json", "utf8"),
|
|
},
|
|
{},
|
|
["scripts/e2e/lib"],
|
|
);
|
|
f.selection();
|
|
const result = f.admit();
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8"));
|
|
const children = reconstructAdmissionEvaluations(record);
|
|
expect(children).toHaveLength(3);
|
|
const extra = "scripts/e2e/lib/codex-install-utils.mjs";
|
|
for (const [index, child] of children.entries()) {
|
|
const toolingPaths = child.sources.tooling.map(({ path }) => path);
|
|
const selectedPaths = child.sources.selected.map(({ path }) => path);
|
|
expect(toolingPaths).toContain("scripts/lib/record-shared.mjs");
|
|
if (index === 1) {
|
|
expect(toolingPaths).toContain(extra);
|
|
expect(selectedPaths).toContain("extensions/codex/package.json");
|
|
} else {
|
|
expect(toolingPaths).not.toContain(extra);
|
|
expect(selectedPaths).not.toContain("extensions/codex/package.json");
|
|
}
|
|
}
|
|
const onlyOnboard = f.run("Plan frozen source admission", {
|
|
ADMISSION_INPUTS: JSON.stringify({ ...inputs, docker_lanes: "onboard" }),
|
|
});
|
|
expect(onlyOnboard.status, onlyOnboard.stderr).toBe(0);
|
|
const single = f.admit();
|
|
expect(single.status, single.stderr).toBe(0);
|
|
const isolated = reconstructAdmissionEvaluations(
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8")),
|
|
);
|
|
expect(children[0]).toEqual(isolated[0]);
|
|
});
|
|
|
|
it.each([
|
|
[false, "openclaw@2026.9.1\r\nopenclaw@2026.7.33"],
|
|
[true, "openclaw@2026.9.1\r\nopenclaw@2026.7.33"],
|
|
[false, "OPENCLAW_ADMISSION_OUTPUT\nOPENCLAW_ADMISSION_OUTPUT_\nopenclaw@2026.9.1\n"],
|
|
])(
|
|
"round-trips unselected multiline baseline outputs with prepare_only=%s %s",
|
|
(prepareOnly, baselines) => {
|
|
const f = frozenWorkflowFixture(LIVE_E2E_WORKFLOW, "validate_selected_ref", {
|
|
docker_lanes: "onboard",
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
prepare_only: prepareOnly,
|
|
published_upgrade_survivor_baselines: baselines,
|
|
});
|
|
expect(f.selection().obligations).toEqual([]);
|
|
const result = f.run("Resolve selected upgrade baseline versions");
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(join(f.root, "outputs"), "utf8");
|
|
const values = new Map<string, string>();
|
|
const lines = output.split("\n");
|
|
for (let index = 0; index < lines.length - 1; index++) {
|
|
const line = lines[index];
|
|
if (line === undefined) {
|
|
throw new Error("missing workflow output line");
|
|
}
|
|
const heredoc = line.indexOf("<<");
|
|
const equals = line.indexOf("=");
|
|
if (heredoc >= 0 && (equals < 0 || heredoc < equals)) {
|
|
const key = line.slice(0, heredoc);
|
|
const delimiter = line.slice(heredoc + 2);
|
|
const content = [];
|
|
while (++index < lines.length && lines[index] !== delimiter) {
|
|
content.push(lines[index]);
|
|
}
|
|
expect(lines[index], key).toBe(delimiter);
|
|
values.set(key, content.join("\n"));
|
|
} else {
|
|
expect(equals, `invalid output line: ${line}`).toBeGreaterThan(0);
|
|
values.set(line.slice(0, equals), line.slice(equals + 1));
|
|
}
|
|
}
|
|
expect(values.get("baselines")).toBe(baselines);
|
|
expect(values.get("baseline")).toBe("openclaw@latest");
|
|
expect(values.get("baseline_scope")).toBe("all-scenarios");
|
|
expect(values.get("parser_required")).toBe("false");
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
expect(
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission-request.json"), "utf8"))
|
|
.requestedBaselines.baselines,
|
|
).toBe(baselines);
|
|
},
|
|
);
|
|
|
|
it.each(
|
|
(
|
|
[
|
|
[
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"resolve_target",
|
|
"Plan frozen source admission",
|
|
"parent",
|
|
],
|
|
[
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
"Validate focused live suite filter",
|
|
"reusable",
|
|
],
|
|
[RELEASE_CHECKS_WORKFLOW, "resolve_target", "Capture selected inputs", "release"],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", "Select acceptance profile", "package"],
|
|
] as const
|
|
).flatMap(([file, jobName, caller, workflow]) => [
|
|
{
|
|
file,
|
|
jobName,
|
|
caller,
|
|
workflow,
|
|
condition: "clean",
|
|
path: "scripts/plan-release-workflow-matrix.mjs",
|
|
},
|
|
{
|
|
file,
|
|
jobName,
|
|
caller,
|
|
workflow,
|
|
condition: "dirty",
|
|
path: "scripts/plan-release-workflow-matrix.mjs",
|
|
},
|
|
{
|
|
file,
|
|
jobName,
|
|
caller,
|
|
workflow,
|
|
condition: "missing",
|
|
path: "scripts/plan-release-workflow-matrix.mjs",
|
|
},
|
|
{
|
|
file,
|
|
jobName,
|
|
caller,
|
|
workflow,
|
|
condition: "dirty transitive",
|
|
path: "scripts/lib/numeric-options.mjs",
|
|
},
|
|
{
|
|
file,
|
|
jobName,
|
|
caller,
|
|
workflow,
|
|
condition: "missing transitive",
|
|
path: "scripts/lib/numeric-options.mjs",
|
|
},
|
|
]),
|
|
)(
|
|
"verifies before the earliest planner command in $workflow: $condition $path",
|
|
({ file, jobName, caller, workflow, condition, path }) => {
|
|
const f = frozenWorkflowFixture(
|
|
file,
|
|
jobName,
|
|
{
|
|
include_live_suites: true,
|
|
include_release_path_suites: false,
|
|
live_suite_filter: "docker-live-models",
|
|
suite_profile: "custom",
|
|
docker_lanes: "onboard",
|
|
},
|
|
{},
|
|
{},
|
|
[RELEASE_FILTER_VALIDATOR],
|
|
);
|
|
symlinkSync(f.tooling, join(f.root, "workflow"), "dir");
|
|
const planner = join(f.tooling, path);
|
|
const sentinel = join(f.root, "planner-executed");
|
|
if (condition !== "clean") {
|
|
writeFileSync(
|
|
planner,
|
|
`import { writeFileSync } from 'node:fs';\nwriteFileSync(${JSON.stringify(sentinel)}, 'executed');\n${readFileSync(planner, "utf8")}`,
|
|
);
|
|
}
|
|
if (condition.startsWith("missing")) {
|
|
f.toolingGit("add", path);
|
|
f.toolingGit("commit", "-qm", "earliest import execution witness");
|
|
f.env.ADMISSION_TOOLING_SHA = f.toolingGit("rev-parse", "HEAD");
|
|
const oid = f.toolingGit("rev-parse", `HEAD:${path}`);
|
|
unlinkSync(join(f.tooling, ".git/objects", oid.slice(0, 2), oid.slice(2)));
|
|
f.toolingGit("config", "remote.origin.url", "fixture::unavailable");
|
|
f.toolingGit("config", "remote.origin.promisor", "true");
|
|
}
|
|
const job = workflowJob(file, jobName);
|
|
const stepNames = (job.steps ?? [])
|
|
.map((step) => step.name)
|
|
.filter(
|
|
(name): name is string => name === caller || name === "Plan frozen source admission",
|
|
);
|
|
const env = {
|
|
...Object.fromEntries(
|
|
Object.keys(workflowStep(job, caller).env ?? {}).map((key) => [key, ""]),
|
|
),
|
|
...f.env,
|
|
ADMISSION_TOOLING_ROOT: f.tooling,
|
|
ADMISSION_TOOLING_SHA: f.env.ADMISSION_TOOLING_SHA,
|
|
ADMISSION_STAGE: "known-source",
|
|
LIVE_SUITE_FILTER: "docker-live-models",
|
|
RELEASE_TEST_PROFILE: "beta",
|
|
INCLUDE_LIVE_SUITES: "true",
|
|
INCLUDE_REPO_E2E: "false",
|
|
LIVE_MODELS_ONLY: "false",
|
|
SOURCE: "ref",
|
|
SUITE_PROFILE: "custom",
|
|
CUSTOM_DOCKER_LANES: "onboard",
|
|
TELEGRAM_MODE: "none",
|
|
PACKAGE_ARTIFACT_NAME: "fixture-package",
|
|
RELEASE_PHASE_INPUT: "all",
|
|
RELEASE_PROFILE_INPUT: "beta",
|
|
RELEASE_REF_INPUT: "main",
|
|
RELEASE_RERUN_GROUP_INPUT: "all",
|
|
RELEASE_FILTER_VALIDATOR: join(f.tooling, RELEASE_FILTER_VALIDATOR),
|
|
};
|
|
const reusablePlannerFailure =
|
|
workflow === "reusable" && (condition === "dirty" || condition === "missing");
|
|
const script = stepNames
|
|
.map((name) => {
|
|
const stepScript = workflowStep(job, name).run;
|
|
return reusablePlannerFailure && name === "Plan frozen source admission"
|
|
? `${stepScript}\nprintf 'acquisition-install-reachable\\n'`
|
|
: stepScript;
|
|
})
|
|
.join("\n");
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", script], {
|
|
cwd: file === PACKAGE_ACCEPTANCE_WORKFLOW ? f.tooling : f.root,
|
|
env: { ...f.env, ...env },
|
|
encoding: "utf8",
|
|
timeout: 30_000,
|
|
});
|
|
expect(existsSync(sentinel), result.stderr).toBe(false);
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
if (condition === "clean") {
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission-selection.json"), "utf8")).docker
|
|
.length,
|
|
).toBeGreaterThan(0);
|
|
return;
|
|
}
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
condition.startsWith("missing")
|
|
? "unable to read selected source"
|
|
: `tooling closure does not match committed source: ${path}`,
|
|
);
|
|
expect(result.stdout).toBe("");
|
|
expect(
|
|
existsSync(join(f.root, "outputs")) ? readFileSync(join(f.root, "outputs"), "utf8") : "",
|
|
).toBe("");
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
if (reusablePlannerFailure) {
|
|
expect(readFileSync(join(f.root, "frozen-admission-selection.json"), "utf8")).toBe("");
|
|
expect(
|
|
JSON.parse(readFileSync(join(f.root, "frozen-admission-request.json"), "utf8")).tooling
|
|
.sha,
|
|
).toBe(f.env.ADMISSION_TOOLING_SHA);
|
|
expect(existsSync(join(f.tooling, "node_modules"))).toBe(false);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("verifies tooling without selected identity, objects, dependencies or admission output", () => {
|
|
const f = frozenWorkflowFixture(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", {});
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
join(f.tooling, "scripts/preflight-frozen-target-contracts.mjs"),
|
|
"--verify-tooling",
|
|
f.tooling,
|
|
f.toolingSha,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: { PATH: f.env.PATH, ADMISSION_SELECTED_ROOT: join(f.root, "not-acquired") },
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toBe("");
|
|
expect(existsSync(join(f.root, "not-acquired"))).toBe(false);
|
|
expect(existsSync(join(f.root, "outputs"))).toBe(false);
|
|
expect(existsSync(join(f.root, "frozen-admission.json"))).toBe(false);
|
|
expect(existsSync(join(f.tooling, "node_modules"))).toBe(false);
|
|
expect(existsSync(join(f.root, "forbidden"))).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
[FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target", "known-source"],
|
|
[RELEASE_CHECKS_WORKFLOW, "resolve_target", "known-source"],
|
|
[LIVE_E2E_WORKFLOW, "validate_selected_ref", "known-source"],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", "known-source"],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", "resolved-package"],
|
|
])("fulfills evaluations before emitting %s %s %s admission", (file, job, stage) => {
|
|
const f = frozenWorkflowFixture(
|
|
file,
|
|
job,
|
|
{
|
|
release_profile: "beta",
|
|
release_test_profile: "beta",
|
|
rerun_group: "live-e2e",
|
|
live_suite_filter: "live-gateway-docker",
|
|
include_live_suites: true,
|
|
include_release_path_suites: false,
|
|
suite_profile: "custom",
|
|
docker_lanes: "onboard plugins-offline",
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{ "package.json": '{"type":"module","version":"2026.9.9"}' },
|
|
{ ADMISSION_STAGE: stage },
|
|
);
|
|
const known = file === PACKAGE_ACCEPTANCE_WORKFLOW && stage === "known-source";
|
|
const result = f.run(
|
|
known ? "Plan known package source admission" : "Plan frozen source admission",
|
|
stage === "resolved-package"
|
|
? {
|
|
ADMISSION_PACKAGE_SOURCE_SHA: f.sha,
|
|
ADMISSION_PACKAGE_SHA256: "d".repeat(64),
|
|
ADMISSION_PACKAGE_VERSION: "2026.9.9",
|
|
}
|
|
: {},
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const plan = JSON.parse(readFileSync(join(f.root, "frozen-admission-selection.json"), "utf8"));
|
|
const admitted = f.admit(
|
|
{},
|
|
known ? "Admit known package source before packing" : "Admit frozen source contracts",
|
|
);
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
expect(admitted.stdout).toContain("producer-ran");
|
|
const record = JSON.parse(
|
|
readFileSync(
|
|
join(f.root, known ? "frozen-admission-known-source.json" : "frozen-admission.json"),
|
|
"utf8",
|
|
),
|
|
);
|
|
expect(record.status).toBe("ADMITTED");
|
|
expect(record.evaluations).toHaveLength(plan.docker.length + 1);
|
|
for (const evaluation of reconstructAdmissionEvaluations(record)) {
|
|
expect(evaluation).toMatchObject({
|
|
version: 1,
|
|
selectedSha: f.sha,
|
|
toolingSha: f.toolingSha,
|
|
});
|
|
expect(Array.isArray(evaluation.contracts)).toBe(true);
|
|
expect(evaluation.digest).toMatch(/^[a-f0-9]{64}$/u);
|
|
const identities = evaluation.sources.tooling.map(
|
|
({ path, oid }: { path: string; oid: string }) => {
|
|
expect(f.toolingGit("rev-parse", `${f.toolingSha}:${path}`)).toBe(oid);
|
|
return path;
|
|
},
|
|
);
|
|
expect(identities).toEqual(expect.arrayContaining(frozenAdmissionClosure));
|
|
}
|
|
const { digest, provenance: _provenance, ...content } = record;
|
|
expect(digest).toBe(createHash("sha256").update(JSON.stringify(content)).digest("hex"));
|
|
expect(existsSync(join(f.target, "node_modules"))).toBe(false);
|
|
expect(existsSync(join(f.tooling, "node_modules"))).toBe(false);
|
|
});
|
|
|
|
it("plans without selected objects and rejects admission before acquisition", () => {
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: "onboard",
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{ "src/config/zod-schema.ts": "lastRunAt:" },
|
|
);
|
|
const oid = f.git("rev-parse", "HEAD:src/config/zod-schema.ts");
|
|
unlinkSync(join(f.target, ".git/objects", oid.slice(0, 2), oid.slice(2)));
|
|
const missingRoot = join(f.root, "not-acquired");
|
|
const unavailable = f.run("Plan frozen source admission", {
|
|
ADMISSION_SELECTED_ROOT: missingRoot,
|
|
});
|
|
expect(unavailable.status, unavailable.stderr).toBe(0);
|
|
expect(existsSync(missingRoot)).toBe(false);
|
|
expect(f.selection().sourcePaths).toContain("src/config/zod-schema.ts");
|
|
const rejected = f.admit();
|
|
expect(rejected.status, rejected.stderr).toBe(1);
|
|
expect(rejected.stderr).toContain("unable to read selected source");
|
|
expect(rejected.stdout).toBe("");
|
|
expect(readFileSync(join(f.root, "frozen-admission.json"), "utf8")).toBe("");
|
|
});
|
|
|
|
it("stops on a later Docker rejection before explicit consumers or success output", () => {
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: "onboard root-managed-vps-upgrade",
|
|
targeted_docker_lane_group_size: 1,
|
|
published_upgrade_survivor_baseline: "openclaw@2026.9.1",
|
|
include_live_suites: true,
|
|
live_suite_filter: "live-gateway-docker",
|
|
include_release_path_suites: false,
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{
|
|
"package.json": '{"type":"module","version":"not-a-release"}',
|
|
"scripts/print-cli-backend-live-metadata.ts":
|
|
"export function resolveCliBackendDockerPackages() {}",
|
|
},
|
|
{ ADMISSION_BASELINES_RESOLVED: "true" },
|
|
);
|
|
const oid = f.git("rev-parse", "HEAD:scripts/print-cli-backend-live-metadata.ts");
|
|
unlinkSync(join(f.target, ".git/objects", oid.slice(0, 2), oid.slice(2)));
|
|
const planned = f.selection();
|
|
expect(planned.explicitConsumers).toContain("live-cli-backend");
|
|
expect(planned.docker.map((group: { lanes: string[] }) => group.lanes)).toEqual([
|
|
["onboard"],
|
|
["root-managed-vps-upgrade"],
|
|
]);
|
|
const outputs = readFileSync(join(f.root, "outputs"), "utf8");
|
|
const rejected = f.admit();
|
|
expect(rejected.status, rejected.stderr).toBe(1);
|
|
expect(rejected.stderr.split("\n")[0]).toBe(
|
|
"frozen admission: selected upgrade target has an invalid release version",
|
|
);
|
|
expect(rejected.stderr).not.toContain("unable to read selected source");
|
|
expect(rejected.stderr).not.toContain("UnhandledPromiseRejection");
|
|
expect(rejected.stdout).toBe("");
|
|
expect(readFileSync(join(f.root, "frozen-admission.json"), "utf8")).toBe("");
|
|
expect(readFileSync(join(f.root, "outputs"), "utf8")).toBe(outputs);
|
|
});
|
|
|
|
it.each([
|
|
[FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"],
|
|
[RELEASE_CHECKS_WORKFLOW, "resolve_target"],
|
|
[LIVE_E2E_WORKFLOW, "validate_selected_ref"],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"],
|
|
])("accepts pretty-printed workflow inputs at the actual %s CLI boundary", (file, jobName) => {
|
|
const inputs = {
|
|
release_profile: "beta",
|
|
release_test_profile: "beta",
|
|
suite_profile: "custom",
|
|
docker_lanes: "onboard",
|
|
targeted_docker_lane_group_size: 2,
|
|
include_live_suites: false,
|
|
allow_frozen_target_scenario_omissions: true,
|
|
};
|
|
const fixture = frozenWorkflowFixture(
|
|
file,
|
|
jobName,
|
|
inputs,
|
|
{},
|
|
{
|
|
ADMISSION_STAGE: "known-source",
|
|
},
|
|
);
|
|
expect(
|
|
workflowStep(workflowJob(file, jobName), "Plan frozen source admission").env,
|
|
).toHaveProperty("ADMISSION_INPUTS", "${{ toJSON(inputs) }}");
|
|
const planned = fixture.run("Plan frozen source admission");
|
|
expect(planned.status, planned.stderr).toBe(0);
|
|
const prettyRequest = JSON.parse(
|
|
readFileSync(join(fixture.root, "frozen-admission-request.json"), "utf8"),
|
|
);
|
|
const compact = spawnSync(
|
|
process.execPath,
|
|
[resolve("scripts/preflight-frozen-target-contracts.mjs"), "--workflow-request"],
|
|
{
|
|
encoding: "utf8",
|
|
env: { ...fixture.env, ADMISSION_INPUTS: JSON.stringify(inputs) },
|
|
timeout: 30_000,
|
|
},
|
|
);
|
|
expect(compact.status, compact.stderr).toBe(0);
|
|
expect(prettyRequest).toEqual(JSON.parse(compact.stdout));
|
|
expect(prettyRequest.options).toMatchObject({
|
|
includeLiveSuites: false,
|
|
targetedDockerLaneGroupSize: "2",
|
|
});
|
|
});
|
|
|
|
it("bounds raw workflow JSON separately from scalar input validation", () => {
|
|
const fixture = frozenWorkflowFixture(LIVE_E2E_WORKFLOW, "validate_selected_ref", {
|
|
include_live_suites: false,
|
|
include_release_path_suites: false,
|
|
});
|
|
const raw = fixture.env.ADMISSION_INPUTS;
|
|
const limit = 48 * 1024;
|
|
const bounded = raw + " ".repeat(limit - Buffer.byteLength(raw));
|
|
expect(Buffer.byteLength(bounded)).toBe(limit);
|
|
const accepted = fixture.run("Plan frozen source admission", { ADMISSION_INPUTS: bounded });
|
|
expect(accepted.status, accepted.stderr).toBe(0);
|
|
for (const value of [
|
|
`${bounded} `,
|
|
JSON.stringify({ label: "\u00e9".repeat(limit / 2) }),
|
|
"{",
|
|
"null",
|
|
"[]",
|
|
"true",
|
|
'{"docker_lanes":null}',
|
|
'{"docker_lanes":[]}',
|
|
'{"docker_lanes":{}}',
|
|
'{"release_test_profile":"beta\\n"}',
|
|
'{"docker_lanes":"onboard\\u0001"}',
|
|
]) {
|
|
const rejected = fixture.run(
|
|
"Plan frozen source admission",
|
|
{ ADMISSION_INPUTS: value },
|
|
"printf 'producer-ran\\n'",
|
|
);
|
|
expect(rejected.status, value.slice(0, 80)).not.toBe(0);
|
|
expect(rejected.stderr).toContain("frozen admission:");
|
|
expect(rejected.stdout).not.toContain("producer-ran");
|
|
}
|
|
const invalidScalar = fixture.run("Plan frozen source admission", {
|
|
ADMISSION_WORKFLOW_REF: `${fixture.env.ADMISSION_WORKFLOW_REF}\n`,
|
|
});
|
|
expect(invalidScalar.status).not.toBe(0);
|
|
expect(invalidScalar.stderr).toContain("invalid workflow ref");
|
|
});
|
|
|
|
it.each<{
|
|
name: string;
|
|
file: string;
|
|
job: string;
|
|
multiline: Record<string, string>;
|
|
normalized: Record<string, string>;
|
|
}>([
|
|
{
|
|
name: "Docker lane lists",
|
|
file: LIVE_E2E_WORKFLOW,
|
|
job: "validate_selected_ref",
|
|
multiline: { docker_lanes: "onboard\n\tplugins-offline" },
|
|
normalized: { docker_lanes: "onboard plugins-offline" },
|
|
},
|
|
{
|
|
name: "survivor baseline and scenario inventories",
|
|
file: LIVE_E2E_WORKFLOW,
|
|
job: "validate_selected_ref",
|
|
multiline: {
|
|
docker_lanes: "published-upgrade-survivor",
|
|
published_upgrade_survivor_baseline: "openclaw@2026.9.1",
|
|
published_upgrade_survivor_baselines: "openclaw@2026.9.1\r\nopenclaw@2026.7.33",
|
|
published_upgrade_survivor_scenarios: "base\nabandoned-update",
|
|
},
|
|
normalized: {
|
|
docker_lanes: "published-upgrade-survivor",
|
|
published_upgrade_survivor_baseline: "openclaw@2026.9.1",
|
|
published_upgrade_survivor_baselines: "openclaw@2026.9.1,openclaw@2026.7.33",
|
|
published_upgrade_survivor_scenarios: "base,abandoned-update",
|
|
},
|
|
},
|
|
{
|
|
name: "Telegram scenario lists",
|
|
file: PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
job: "resolve_package",
|
|
multiline: {
|
|
suite_profile: "telegram",
|
|
telegram_mode: "synthetic",
|
|
telegram_scenarios: "\nmain-telegram\r\n",
|
|
},
|
|
normalized: {
|
|
suite_profile: "telegram",
|
|
telegram_mode: "synthetic",
|
|
telegram_scenarios: "main-telegram",
|
|
},
|
|
},
|
|
])("preserves multiline $name through actual workflow admission", (entry) => {
|
|
const common = { include_live_suites: false, include_release_path_suites: false };
|
|
const fixture = frozenWorkflowFixture(
|
|
entry.file,
|
|
entry.job,
|
|
{ ...common, ...entry.multiline },
|
|
currentSurvivorScenarioFiles(),
|
|
{ ADMISSION_STAGE: "known-source", ADMISSION_BASELINES_RESOLVED: "true" },
|
|
);
|
|
const multilinePlan = fixture.selection();
|
|
const admitted = fixture.admit();
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const multilineRecord = JSON.parse(
|
|
readFileSync(join(fixture.root, "frozen-admission.json"), "utf8"),
|
|
);
|
|
const normalized = fixture.run("Plan frozen source admission", {
|
|
ADMISSION_INPUTS: JSON.stringify({ ...common, ...entry.normalized }, null, 2),
|
|
});
|
|
expect(normalized.status, normalized.stderr).toBe(0);
|
|
const normalizedPlan = JSON.parse(
|
|
readFileSync(join(fixture.root, "frozen-admission-selection.json"), "utf8"),
|
|
);
|
|
const parsedDocker = (plan: { docker: Array<{ scenarios?: string }> }) =>
|
|
plan.docker.map((group) => ({
|
|
...group,
|
|
scenarios: parseUpgradeSurvivorScenarios(group.scenarios ?? ""),
|
|
}));
|
|
expect(parsedDocker(multilinePlan)).toEqual(parsedDocker(normalizedPlan));
|
|
expect(multilinePlan.consumers).toEqual(normalizedPlan.consumers);
|
|
const normalizedAdmission = fixture.admit();
|
|
expect(normalizedAdmission.status, normalizedAdmission.stderr).toBe(0);
|
|
const normalizedRecord = JSON.parse(
|
|
readFileSync(join(fixture.root, "frozen-admission.json"), "utf8"),
|
|
);
|
|
expect(multilineRecord.evaluations).toHaveLength(normalizedRecord.evaluations.length);
|
|
const normalizedChildren = reconstructAdmissionEvaluations(normalizedRecord);
|
|
for (const [index, evaluated] of reconstructAdmissionEvaluations(multilineRecord).entries()) {
|
|
const normalizedChild = normalizedChildren[index];
|
|
if (normalizedChild === undefined) {
|
|
throw new Error("missing normalized admission evaluation");
|
|
}
|
|
expect(evaluated.docker).toEqual(normalizedChild.docker);
|
|
expect(evaluated.contracts).toEqual(normalizedChild.contracts);
|
|
expect(evaluated.sources).toEqual(normalizedChild.sources);
|
|
}
|
|
});
|
|
|
|
it.each([1, 2])(
|
|
"pins package tooling after main advances for queued/rerun attempt %s",
|
|
(attempt) => {
|
|
const fixture = packageToolingCheckoutFixture();
|
|
expect(fixture.git("rev-parse", "refs/heads/main")).toBe(fixture.advancedSha);
|
|
expect(fixture.advancedSha).not.toBe(fixture.capturedSha);
|
|
const actual = fixture.run({ attempt });
|
|
expect(actual.checkoutReached).toBe(true);
|
|
expect(
|
|
actual.result.status,
|
|
JSON.stringify({
|
|
checkoutRef: fixture.checkout.with?.ref,
|
|
selectedRef: actual.selectedRef,
|
|
capturedSha: fixture.capturedSha,
|
|
advancedSha: fixture.advancedSha,
|
|
checkedOutSha: actual.sha,
|
|
stderr: actual.result.stderr,
|
|
}),
|
|
).toBe(0);
|
|
expect(actual.selectedRef).toBe(fixture.capturedSha);
|
|
expect(actual.sha).toBe(fixture.capturedSha);
|
|
expect(actual.validationOutputs).toEqual({ sha: fixture.capturedSha });
|
|
expect(actual.result.stdout).toBe("installation-reachable\n");
|
|
},
|
|
);
|
|
|
|
it("validates exact package tooling before reusing its sole dependency installation", () => {
|
|
const fixture = packageToolingCheckoutFixture();
|
|
const { job, checkout, validate } = fixture;
|
|
const steps = job.steps ?? [];
|
|
const identity = workflowStep(job, "Resolve called package tooling identity");
|
|
const setup = workflowStep(job, "Setup Node environment");
|
|
expect(steps[0]).toEqual({
|
|
name: "Setup supported Node runtime",
|
|
uses: "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020",
|
|
with: { "node-version": "${{ env.NODE_VERSION }}", "package-manager-cache": false },
|
|
});
|
|
expect(steps.indexOf(identity)).toBe(1);
|
|
expect(steps.indexOf(identity)).toBeLessThan(steps.indexOf(checkout));
|
|
expect(steps.indexOf(checkout)).toBeLessThan(steps.indexOf(validate));
|
|
expect(steps.indexOf(validate)).toBeLessThan(steps.indexOf(setup));
|
|
expect(identity.env).toEqual({
|
|
JOB_CONTEXT: "${{ toJSON(job) }}",
|
|
WORKFLOW_REF: "${{ inputs.workflow_ref }}",
|
|
});
|
|
expect(checkout.with).toEqual({
|
|
ref: "${{ steps.tooling_identity.outputs.sha }}",
|
|
"fetch-depth": 0,
|
|
filter: "blob:none",
|
|
"persist-credentials": false,
|
|
});
|
|
expect(validate.env).toEqual({
|
|
EXPECTED_TOOLING_SHA: "${{ steps.tooling_identity.outputs.sha }}",
|
|
});
|
|
expect(
|
|
steps.filter(
|
|
(step) => step.uses?.includes("setup-node-env") || step.run?.includes("pnpm install"),
|
|
),
|
|
).toEqual([setup]);
|
|
const actual = fixture.run({ wrongCheckout: true });
|
|
expect(actual.checkoutReached).toBe(true);
|
|
expect(actual.sha).toBe(fixture.advancedSha);
|
|
expect(actual.result.status).toBe(1);
|
|
expect(actual.result.stderr).toContain("checkout must match the captured called workflow SHA");
|
|
expect(actual.result.stdout).toBe("");
|
|
expect(actual.validationOutputs).toEqual({});
|
|
expect(job.outputs?.tooling_sha).toBe("${{ steps.tooling.outputs.sha }}");
|
|
});
|
|
|
|
it.each([
|
|
["main", "refs/heads/main"],
|
|
["refs/heads/main", "refs/heads/main"],
|
|
["release/candidate", "refs/heads/release/candidate"],
|
|
["refs/heads/release/candidate", "refs/heads/release/candidate"],
|
|
["release-candidate", "refs/tags/release-candidate"],
|
|
["refs/tags/release-candidate", "refs/tags/release-candidate"],
|
|
["captured-sha", "refs/heads/main"],
|
|
["captured-sha", "captured-sha"],
|
|
])("accepts package tooling assertion %s for captured %s", (input, ref) => {
|
|
const fixture = packageToolingCheckoutFixture();
|
|
const actual = fixture.run({
|
|
workflowRef: input === "captured-sha" ? fixture.capturedSha : input,
|
|
capturedRef: ref === "captured-sha" ? fixture.capturedSha : ref,
|
|
});
|
|
expect(actual.result.status, actual.result.stderr).toBe(0);
|
|
expect(actual.selectedRef).toBe(fixture.capturedSha);
|
|
expect(actual.sha).toBe(fixture.capturedSha);
|
|
expect(actual.sha).not.toBe(fixture.advancedSha);
|
|
expect(actual.identityOutputs).toEqual({ sha: fixture.capturedSha });
|
|
expect(actual.validationOutputs).toEqual({ sha: fixture.capturedSha });
|
|
expect(actual.result.stdout).toBe("installation-reachable\n");
|
|
});
|
|
|
|
it.each([
|
|
["advanced-sha", "refs/heads/main"],
|
|
["alias", "refs/heads/main"],
|
|
["refs/heads/alias", "refs/heads/main"],
|
|
["release-candidate", "refs/heads/main"],
|
|
["main", "refs/heads/release/candidate"],
|
|
[" main", "refs/heads/main"],
|
|
["", "refs/heads/main"],
|
|
["refs/heads/main\n", "refs/heads/main"],
|
|
])("rejects package tooling assertion %j for captured %s before checkout", (input, ref) => {
|
|
const fixture = packageToolingCheckoutFixture();
|
|
const actual = fixture.run({
|
|
workflowRef: input === "advanced-sha" ? fixture.advancedSha : input,
|
|
capturedRef: ref,
|
|
});
|
|
expect(actual.checkoutReached).toBe(false);
|
|
expect(actual.result.status).toBe(1);
|
|
expect(actual.result.stderr).toContain(
|
|
"Dispatch or call package-acceptance.yml at the desired ref",
|
|
);
|
|
expect(actual.result.stdout).toBe("");
|
|
expect(actual.identityOutputs).toEqual({});
|
|
expect(actual.validationOutputs).toEqual({});
|
|
});
|
|
|
|
it.each([
|
|
["repository", { workflow_repository: "other/openclaw" }],
|
|
["missing repository", { workflow_repository: undefined }],
|
|
["missing SHA", { workflow_sha: undefined }],
|
|
["short SHA", { workflow_sha: "a".repeat(39) }],
|
|
["uppercase SHA", { workflow_sha: "A".repeat(40) }],
|
|
["newline SHA", { workflow_sha: `${"a".repeat(40)}\n` }],
|
|
["non-string SHA", { workflow_sha: 42 }],
|
|
[
|
|
"wrong file",
|
|
{ workflow_ref: "openclaw/openclaw/.github/workflows/other.yml@refs/heads/main" },
|
|
],
|
|
[
|
|
"wrong path owner",
|
|
{ workflow_ref: "other/openclaw/.github/workflows/package-acceptance.yml@refs/heads/main" },
|
|
],
|
|
["missing ref", { workflow_ref: undefined }],
|
|
["non-string ref", { workflow_ref: 42 }],
|
|
[
|
|
"short captured ref",
|
|
{ workflow_ref: "openclaw/openclaw/.github/workflows/package-acceptance.yml@main" },
|
|
],
|
|
[
|
|
"empty branch",
|
|
{ workflow_ref: "openclaw/openclaw/.github/workflows/package-acceptance.yml@refs/heads/" },
|
|
],
|
|
[
|
|
"invalid branch",
|
|
{
|
|
workflow_ref:
|
|
"openclaw/openclaw/.github/workflows/package-acceptance.yml@refs/heads/../main",
|
|
},
|
|
],
|
|
[
|
|
"newline ref",
|
|
{
|
|
workflow_ref:
|
|
"openclaw/openclaw/.github/workflows/package-acceptance.yml@refs/heads/main\n",
|
|
},
|
|
],
|
|
] as const)("rejects malformed package tooling identity: %s", (_label, context) => {
|
|
const fixture = packageToolingCheckoutFixture();
|
|
const actual = fixture.run({
|
|
context,
|
|
callerWorkflowRef:
|
|
"openclaw/openclaw/.github/workflows/package-acceptance.yml@refs/heads/main",
|
|
});
|
|
expect(actual.checkoutReached).toBe(false);
|
|
expect(actual.result.status).toBe(1);
|
|
expect(actual.result.stderr).toContain(
|
|
"Invalid package tooling identity or workflow_ref assertion",
|
|
);
|
|
expect(actual.result.stdout).toBe("");
|
|
expect(actual.identityOutputs).toEqual({});
|
|
expect(actual.validationOutputs).toEqual({});
|
|
});
|
|
|
|
it.each(["repository", "SHA ref"])(
|
|
"rejects inconsistent package tooling %s before checkout",
|
|
(kind) => {
|
|
const fixture = packageToolingCheckoutFixture();
|
|
const actual = fixture.run(
|
|
kind === "repository"
|
|
? { githubRepository: "other/openclaw" }
|
|
: { capturedRef: fixture.advancedSha },
|
|
);
|
|
expect(actual.checkoutReached).toBe(false);
|
|
expect(actual.result.status).toBe(1);
|
|
expect(actual.result.stderr).toContain(
|
|
"Invalid package tooling identity or workflow_ref assertion",
|
|
);
|
|
expect(actual.result.stdout).toBe("");
|
|
expect(actual.identityOutputs).toEqual({});
|
|
expect(actual.validationOutputs).toEqual({});
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"],
|
|
[RELEASE_CHECKS_WORKFLOW, "resolve_target"],
|
|
[LIVE_E2E_WORKFLOW, "validate_selected_ref"],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"],
|
|
])("dominates every execution job after failed admission in %s", (file, barrier) => {
|
|
const jobs = readWorkflow(file).jobs ?? {};
|
|
const diagnostics = new Set([
|
|
"summary",
|
|
"release_execution_plan",
|
|
"release_decision",
|
|
"diagnostic_drain",
|
|
]);
|
|
const outputs = new Proxy(
|
|
{
|
|
state: "ready",
|
|
reuse: "false",
|
|
rerun_group: "all",
|
|
live_suite_filter: "",
|
|
target_version: "2026.9.1",
|
|
coverage_policy: "full",
|
|
},
|
|
{ get: (values, key) => Reflect.get(values, key) ?? "true" },
|
|
);
|
|
const inputs = {
|
|
rerun_group: "all",
|
|
reuse_evidence: true,
|
|
suite_profile: "full",
|
|
telegram_waiver: "",
|
|
npm_telegram_package_spec: "",
|
|
release_package_spec: "",
|
|
include_live_suites: true,
|
|
include_repo_e2e: true,
|
|
include_release_path_suites: true,
|
|
include_openwebui: true,
|
|
live_suite_filter: "",
|
|
live_model_providers: "",
|
|
docker_lanes: "",
|
|
release_test_profile: "full",
|
|
emit_candidate_evidence: true,
|
|
};
|
|
for (const policy of ["no-push-artifact", "existing-only"]) {
|
|
let enabled = 0;
|
|
for (const [name, job] of Object.entries(jobs)) {
|
|
if (name === barrier || diagnostics.has(name)) {
|
|
continue;
|
|
}
|
|
expect(jobNeeds(job), name).toContain(barrier);
|
|
const expression = (job.if ?? "success()").replace(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/u, "$1");
|
|
const evaluate = (admitted: boolean) => {
|
|
const implicitSuccess =
|
|
/\b(?:always|cancelled|failure|success)\s*\(/u.test(expression) || admitted;
|
|
return (
|
|
implicitSuccess &&
|
|
Boolean(
|
|
runInNewContext(expression, {
|
|
inputs: { ...inputs, shared_image_policy: policy },
|
|
github: { ref: "refs/heads/main", run_attempt: 1 },
|
|
needs: Object.fromEntries(
|
|
Object.keys(jobs).map((key) => [
|
|
key,
|
|
{ result: key === barrier && !admitted ? "failure" : "success", outputs },
|
|
]),
|
|
),
|
|
always: () => true,
|
|
success: () => admitted,
|
|
failure: () => !admitted,
|
|
cancelled: () => false,
|
|
contains: (values: string | string[], value: string) => values.includes(value),
|
|
startsWith: (value: string, prefix: string) => value.startsWith(prefix),
|
|
fromJSON: JSON.parse,
|
|
}),
|
|
)
|
|
);
|
|
};
|
|
expect(evaluate(false), `${policy}: ${name}`).toBe(false);
|
|
if (evaluate(true)) {
|
|
enabled += 1;
|
|
}
|
|
}
|
|
expect(enabled, policy).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it.each(["published-upgrade-survivor", "root-managed-vps-upgrade", "update-restart-auth"])(
|
|
"captures a moving baseline once and carries the exact version through %s evaluation",
|
|
(lane) => {
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: lane,
|
|
include_release_path_suites: false,
|
|
include_live_suites: false,
|
|
published_upgrade_survivor_baseline: "openclaw@latest",
|
|
},
|
|
currentSurvivorScenarioFiles(),
|
|
);
|
|
f.selection();
|
|
const bin = join(f.root, "acquisition-bin");
|
|
const calls = join(f.root, "registry-calls");
|
|
mkdirSync(bin);
|
|
writeFileSync(
|
|
join(bin, "npm"),
|
|
`#!/bin/sh\nprintf '%s\\n' "$*" >> '${calls}'\nprintf '"2026.9.1"\\n'\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const result = f.run("Resolve selected upgrade baseline versions", {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const continuation = f.run("Resolve selected upgrade baseline versions", {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
});
|
|
expect(continuation.status, continuation.stderr).toBe(0);
|
|
expect(readFileSync(calls, "utf8").trim().split("\n")).toEqual([
|
|
"view openclaw@latest version --json --silent --prefer-online",
|
|
]);
|
|
const admitted = f.admit();
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8"));
|
|
expect(record).toMatchObject({
|
|
status: "ADMITTED",
|
|
requestedBaselines: { baseline: "openclaw@latest" },
|
|
options: {
|
|
upgradeSurvivorBaseline: "openclaw@2026.9.1",
|
|
upgradeSurvivorBaselines: "openclaw@2026.9.1",
|
|
baselinesResolved: true,
|
|
},
|
|
obligations: [],
|
|
});
|
|
expect(record.evaluations[0].docker.lanes).toEqual([
|
|
lane === "published-upgrade-survivor" ? `${lane}-2026.9.1` : lane,
|
|
]);
|
|
expect(record.selectedSha).toBe(f.sha);
|
|
expect(record.toolingSha).toBe(f.toolingSha);
|
|
reconstructAdmissionEvaluations(record);
|
|
},
|
|
);
|
|
|
|
it.each(["published-upgrade-survivor", "update-migration"])(
|
|
"acquires selected upgrade metadata before expanded %s admission",
|
|
(lane) => {
|
|
const path = "src/gateway/node-command-policy.ts";
|
|
const membershipOwner = "src/cli/update-cli/update-command-terminal-publication.ts";
|
|
const scenarioCatalog = "scripts/lib/upgrade-survivor-scenarios.json";
|
|
const inputs = {
|
|
docker_lanes: lane,
|
|
include_release_path_suites: false,
|
|
include_live_suites: false,
|
|
allow_frozen_target_scenario_omissions: true,
|
|
published_upgrade_survivor_baseline: "openclaw@2026.9.1",
|
|
published_upgrade_survivor_baselines: "openclaw@2026.9.1",
|
|
published_upgrade_survivor_scenarios: "mobile-pairing-reconnect",
|
|
};
|
|
const fixture = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
inputs,
|
|
{
|
|
"package.json": '{"type":"module","version":"2026.9.2"}',
|
|
[path]: readFileSync(path, "utf8"),
|
|
[membershipOwner]: readFileSync(membershipOwner, "utf8"),
|
|
...currentSurvivorScenarioFiles(),
|
|
},
|
|
{ ADMISSION_BASELINES_RESOLVED: "true" },
|
|
);
|
|
const planned = fixture.selection();
|
|
expect(planned.sourcePaths).toContain(path);
|
|
expect(planned.sourcePaths).toContain(membershipOwner);
|
|
expect(planned.sourcePaths).toContain(scenarioCatalog);
|
|
const origin = join(fixture.root, "origin.git");
|
|
fixture.git("clone", "--bare", "--no-hardlinks", fixture.target, origin);
|
|
fixture.git("-C", origin, "config", "uploadpack.allowFilter", "true");
|
|
fixture.git("remote", "add", "origin", pathToFileURL(origin).href);
|
|
fixture.git("config", "remote.origin.promisor", "true");
|
|
fixture.git("config", "remote.origin.partialclonefilter", "blob:none");
|
|
const oid = fixture.git("rev-parse", `${fixture.sha}:${path}`);
|
|
const scenarioCatalogOid = fixture.git("rev-parse", `${fixture.sha}:${scenarioCatalog}`);
|
|
const membershipOid = fixture.git("rev-parse", `${fixture.sha}:${membershipOwner}`);
|
|
unlinkSync(
|
|
join(fixture.target, ".git", "objects", membershipOid.slice(0, 2), membershipOid.slice(2)),
|
|
);
|
|
unlinkSync(join(fixture.target, ".git", "objects", oid.slice(0, 2), oid.slice(2)));
|
|
unlinkSync(
|
|
join(
|
|
fixture.target,
|
|
".git",
|
|
"objects",
|
|
scenarioCatalogOid.slice(0, 2),
|
|
scenarioCatalogOid.slice(2),
|
|
),
|
|
);
|
|
|
|
const unavailable = fixture.admit();
|
|
expect(unavailable.status).not.toBe(0);
|
|
expect(unavailable.stderr).toContain("unable to read selected source");
|
|
expect(unavailable.stdout).not.toContain("producer-ran");
|
|
const acquisitionBin = join(fixture.root, "acquisition-bin");
|
|
const requested = join(fixture.root, "requested-blob");
|
|
mkdirSync(acquisitionBin);
|
|
const gitPath = execFileSync("which", ["git"], { encoding: "utf8" }).trim();
|
|
writeFileSync(
|
|
join(acquisitionBin, "git"),
|
|
`#!/bin/sh\nif [ "$#" = 5 ] && [ "$3" = cat-file ] && [ "$4" = blob ]; then printf '%s\\n' "$5" >> '${requested}'; fi\nexec '${gitPath}' "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const acquired = fixture.run("Acquire selected contract objects", {
|
|
PATH: `${acquisitionBin}:${process.env.PATH}`,
|
|
});
|
|
expect(acquired.status, acquired.stderr).toBe(0);
|
|
expect(readFileSync(requested, "utf8").trim().split("\n")).toEqual(
|
|
expect.arrayContaining([oid, scenarioCatalogOid, membershipOid]),
|
|
);
|
|
expect(fixture.git("cat-file", "blob", membershipOid)).toBe(
|
|
readFileSync(membershipOwner, "utf8").trim(),
|
|
);
|
|
const admitted = fixture.admit();
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(fixture.root, "frozen-admission.json"), "utf8"));
|
|
expect(
|
|
record.evaluations.flatMap(
|
|
(entry: { docker?: { lanes: string[] } }) => entry.docker?.lanes ?? [],
|
|
),
|
|
).toContain(`${lane}-2026.9.1-mobile-pairing-reconnect`);
|
|
expect(
|
|
reconstructAdmissionEvaluations(record).flatMap((entry) => entry.sources.selected),
|
|
).toEqual(expect.arrayContaining([expect.objectContaining({ path, oid })]));
|
|
for (const [overrides, selected] of [
|
|
[{ published_upgrade_survivor_scenarios: "base" }, false],
|
|
[{ published_upgrade_survivor_scenarios: "reported-issues" }, false],
|
|
[{ allow_frozen_target_scenario_omissions: false }, false],
|
|
[{ docker_lanes: "" }, false],
|
|
[{ docker_lanes: "onboard" }, false],
|
|
[{ prepare_only: true }, false],
|
|
[{ docker_lanes: `${lane}-2026.9.1-mobile-pairing-reconnect` }, true],
|
|
] as const) {
|
|
const sibling = fixture.run("Plan frozen source admission", {
|
|
ADMISSION_INPUTS: JSON.stringify({ ...inputs, ...overrides }, null, 2),
|
|
});
|
|
expect(sibling.status, sibling.stderr).toBe(0);
|
|
const selection = JSON.parse(
|
|
readFileSync(join(fixture.root, "frozen-admission-selection.json"), "utf8"),
|
|
);
|
|
expect(selection.sourcePaths.includes(path), JSON.stringify(overrides)).toBe(selected);
|
|
if (selected) {
|
|
const explicit = fixture.admit();
|
|
expect(explicit.status, explicit.stderr).toBe(0);
|
|
}
|
|
}
|
|
},
|
|
);
|
|
|
|
it("acquires the sparse selected first-hop inventory before admission", () => {
|
|
const inventory = "scripts/lib/update-compat-inventory.json";
|
|
const postbuild = "scripts/runtime-postbuild.mts";
|
|
const lane = "update-first-hop-compat-2026.9.6";
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: lane,
|
|
include_release_path_suites: false,
|
|
include_live_suites: false,
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{
|
|
[inventory]: JSON.stringify({ releases: [{ version: "2026.9.6" }] }),
|
|
[postbuild]: readFileSync(postbuild, "utf8"),
|
|
},
|
|
);
|
|
f.selection();
|
|
const origin = join(f.root, "origin.git");
|
|
f.git("clone", "--bare", "--no-hardlinks", f.target, origin);
|
|
f.git("-C", origin, "config", "uploadpack.allowFilter", "true");
|
|
f.git("remote", "add", "origin", pathToFileURL(origin).href);
|
|
f.git("config", "remote.origin.promisor", "true");
|
|
f.git("config", "remote.origin.partialclonefilter", "blob:none");
|
|
const oid = f.git("rev-parse", f.sha + ":" + inventory);
|
|
unlinkSync(join(f.target, ".git/objects", oid.slice(0, 2), oid.slice(2)));
|
|
|
|
const unavailable = f.admit();
|
|
expect(unavailable.status).not.toBe(0);
|
|
expect(unavailable.stderr).toContain("unable to read selected source");
|
|
const bin = join(f.root, "acquisition-bin");
|
|
const requested = join(f.root, "requested-blobs");
|
|
mkdirSync(bin);
|
|
const gitPath = execFileSync("which", ["git"], { encoding: "utf8" }).trim();
|
|
writeFileSync(
|
|
join(bin, "git"),
|
|
`#!/bin/sh\nif [ "$#" = 5 ] && [ "$3" = cat-file ] && [ "$4" = blob ]; then printf '%s\\n' "$5" >> '${requested}'; fi\nexec '${gitPath}' "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const acquired = f.run("Acquire selected contract objects", {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
});
|
|
expect(acquired.status, acquired.stderr).toBe(0);
|
|
// A local promisor fetch may incidentally return other missing blobs too.
|
|
expect(readFileSync(requested, "utf8").trim().split("\n")).toContain(oid);
|
|
const admitted = f.admit();
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8"));
|
|
expect(record.evaluations[0].docker.lanes).toEqual([lane]);
|
|
expect(record.sources.selected).toContainEqual({ path: inventory, oid });
|
|
});
|
|
|
|
it("acquires a sparse selected typed-onboarding assertion helper before admission", () => {
|
|
const helper = "scripts/e2e/lib/release-assertion-files.mjs";
|
|
const scenario = "scripts/e2e/lib/release-typed-onboarding/scenario.sh";
|
|
const fixture = frozenWorkflowFixture(
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
"resolve_package",
|
|
{
|
|
source: "ref",
|
|
suite_profile: "custom",
|
|
docker_lanes: "release-typed-onboarding",
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{
|
|
[helper]: readFileSync(helper, "utf8"),
|
|
[scenario]: readFileSync(scenario, "utf8"),
|
|
},
|
|
{ ADMISSION_STAGE: "known-source" },
|
|
[
|
|
"scripts/e2e/lib/release-scenarios/assertions.mjs",
|
|
"scripts/e2e/lib/release-assertion-files.mjs",
|
|
"scripts/e2e/lib/fixtures/mock-openai-config.mjs",
|
|
],
|
|
);
|
|
const planned = fixture.run("Plan known package source admission");
|
|
expect(planned.status, planned.stderr).toBe(0);
|
|
expect(fixture.selection().sourcePaths).toContain(helper);
|
|
|
|
const origin = join(fixture.root, "origin.git");
|
|
fixture.git("clone", "--bare", "--no-hardlinks", fixture.target, origin);
|
|
fixture.git("-C", origin, "config", "uploadpack.allowFilter", "true");
|
|
fixture.git("remote", "add", "origin", pathToFileURL(origin).href);
|
|
fixture.git("config", "remote.origin.promisor", "true");
|
|
fixture.git("config", "remote.origin.partialclonefilter", "blob:none");
|
|
const oid = fixture.git("rev-parse", `${fixture.sha}:${helper}`);
|
|
unlinkSync(join(fixture.target, ".git", "objects", oid.slice(0, 2), oid.slice(2)));
|
|
|
|
const unavailable = fixture.admit({}, "Admit known package source before packing");
|
|
expect(unavailable.status).not.toBe(0);
|
|
expect(unavailable.stderr).toContain("unable to read selected source");
|
|
const acquisitionBin = join(fixture.root, "typed-onboarding-acquisition-bin");
|
|
const requested = join(fixture.root, "typed-onboarding-requested-blob");
|
|
mkdirSync(acquisitionBin);
|
|
const gitPath = execFileSync("which", ["git"], { encoding: "utf8" }).trim();
|
|
writeFileSync(
|
|
join(acquisitionBin, "git"),
|
|
`#!/bin/sh\nif [ "$#" = 5 ] && [ "$3" = cat-file ] && [ "$4" = blob ] && [ "$5" = '${oid}' ]; then printf '%s\\n' "$5" >> '${requested}'; fi\nexec '${gitPath}' "$@"\n`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const acquired = fixture.run("Acquire known package contract objects", {
|
|
PATH: `${acquisitionBin}:${process.env.PATH}`,
|
|
});
|
|
expect(acquired.status, acquired.stderr).toBe(0);
|
|
expect(readFileSync(requested, "utf8")).toBe(`${oid}\n`);
|
|
|
|
const admitted = fixture.admit({}, "Admit known package source before packing");
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const record = JSON.parse(
|
|
readFileSync(join(fixture.root, "frozen-admission-known-source.json"), "utf8"),
|
|
);
|
|
expect(
|
|
reconstructAdmissionEvaluations(record).flatMap((entry) => entry.sources.selected),
|
|
).toEqual(expect.arrayContaining([expect.objectContaining({ path: helper, oid })]));
|
|
});
|
|
|
|
it("blocks a known package source before packing and admits a different exact acquired package source", () => {
|
|
const known = frozenWorkflowFixture(
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
"resolve_package",
|
|
{
|
|
source: "ref",
|
|
suite_profile: "custom",
|
|
docker_lanes: "agent-bundle-mcp-tools",
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{},
|
|
{ ADMISSION_STAGE: "known-source" },
|
|
);
|
|
const planned = known.run("Plan known package source admission");
|
|
expect(planned.status, planned.stderr).toBe(0);
|
|
known.provisionParser();
|
|
const rejected = known.admit({}, "Admit known package source before packing");
|
|
expect(rejected.status).not.toBe(0);
|
|
expect(rejected.stdout).not.toContain("producer-ran");
|
|
expect(rejected.stderr).toContain("expected exactly one committed bundle client layout");
|
|
|
|
const acquired = frozenWorkflowFixture(
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
"resolve_package",
|
|
{
|
|
source: "npm",
|
|
suite_profile: "custom",
|
|
docker_lanes: "onboard",
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{ "src/config/zod-schema.ts": "lastRunAt:" },
|
|
{ ADMISSION_STAGE: "resolved-package" },
|
|
);
|
|
const identity = {
|
|
ADMISSION_PACKAGE_SOURCE_SHA: acquired.sha,
|
|
ADMISSION_PACKAGE_SHA256: "d".repeat(64),
|
|
ADMISSION_PACKAGE_VERSION: "2026.7.33",
|
|
};
|
|
const resolved = acquired.run("Plan frozen source admission", identity);
|
|
expect(resolved.status, resolved.stderr).toBe(0);
|
|
const admitted = acquired.admit();
|
|
expect(admitted.status, admitted.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(acquired.root, "frozen-admission.json"), "utf8"));
|
|
expect(record.selectedSha).not.toBe(known.sha);
|
|
expect(record.binding).toMatchObject({
|
|
packageSourceSha: acquired.sha,
|
|
packageSha256: "d".repeat(64),
|
|
});
|
|
const mismatch = acquired.run("Plan frozen source admission", {
|
|
...identity,
|
|
ADMISSION_PACKAGE_SOURCE_SHA: known.sha,
|
|
});
|
|
expect(mismatch.status).not.toBe(0);
|
|
expect(mismatch.stderr).toContain("package source differs");
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"resolve_target",
|
|
"Plan frozen source admission",
|
|
"workflow",
|
|
],
|
|
[RELEASE_CHECKS_WORKFLOW, "resolve_target", "Capture selected inputs", "workflow"],
|
|
[
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
"Plan frozen source admission",
|
|
".release-harness",
|
|
],
|
|
])(
|
|
"initializes Node before planning and fails selected parser installation in %s",
|
|
(file, jobName, firstPlan, toolingRoot) => {
|
|
const job = workflowJob(file, jobName);
|
|
const steps = job.steps ?? [];
|
|
const setup = workflowStep(job, "Setup admission Node.js");
|
|
expect(setup.if).toBeUndefined();
|
|
expect(setup.env).toMatchObject({ REQUESTED_NODE_VERSION: "24.x" });
|
|
expect(setup.run).toContain(
|
|
`source ${toolingRoot}/.github/actions/setup-pnpm-store-cache/ensure-node.sh`,
|
|
);
|
|
expect(setup.run).toContain('openclaw_ensure_node "$REQUESTED_NODE_VERSION"');
|
|
expect(steps.indexOf(setup)).toBeLessThan(steps.indexOf(workflowStep(job, firstPlan)));
|
|
const plan = workflowStep(job, "Plan frozen source admission");
|
|
const provision = workflowStep(job, "Provision trusted admission parser");
|
|
const admission = workflowStep(job, "Admit frozen source contracts");
|
|
expect(steps.indexOf(plan)).toBeLessThan(steps.indexOf(provision));
|
|
expect(steps.indexOf(provision)).toBeLessThan(steps.indexOf(admission));
|
|
expect(provision.if).toBe(
|
|
file === FULL_RELEASE_VALIDATION_WORKFLOW
|
|
? "steps.frozen_selection.outputs.parser_required == 'true' || steps.publication_request.outputs.required == 'true'"
|
|
: "steps.frozen_selection.outputs.parser_required == 'true'",
|
|
);
|
|
let install = provision.run;
|
|
if (provision.uses) {
|
|
expect(provision.uses).toBe("./.release-harness/.github/actions/setup-release-harness");
|
|
const action = parse(readFileSync(SETUP_RELEASE_HARNESS_ACTION, "utf8")) as {
|
|
runs: { steps: WorkflowStep[] };
|
|
};
|
|
install = action.runs.steps.find((step) => step.run?.includes("pnpm install"))?.run;
|
|
} else {
|
|
expect(provision["working-directory"]).toBe("workflow");
|
|
expect(workflowStep(job, "Setup trusted admission package manager").with).toMatchObject({
|
|
"package-manager-file": "workflow/package.json",
|
|
"lockfile-path": "workflow/pnpm-lock.yaml",
|
|
"cache-mode": "off",
|
|
});
|
|
}
|
|
expect(install).toContain("pnpm install --frozen-lockfile --prefer-offline --ignore-scripts");
|
|
const root = tempDirs.make("frozen-parser-prerequisite-");
|
|
writeFileSync(join(root, "pnpm"), "#!/bin/sh\nexit 79\n", { mode: 0o755 });
|
|
const result = spawnSync(
|
|
"bash",
|
|
["-c", `set -euo pipefail\n${install}\nprintf 'producer-ran\\n'`],
|
|
{
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: { PATH: `${root}:${process.env.PATH}` },
|
|
},
|
|
);
|
|
expect(result.status).toBe(79);
|
|
expect(result.stdout).not.toContain("producer-ran");
|
|
},
|
|
);
|
|
|
|
it.each(["June", "July"])(
|
|
"runs the actual %s workflow request and shared parser before producers",
|
|
(layout) => {
|
|
const client =
|
|
layout === "June"
|
|
? "scripts/e2e/agent-bundle-mcp-tools-docker-client.ts"
|
|
: "test/e2e/qa-lab/runtime/agent-bundle-mcp-tools-docker-client.ts";
|
|
const prefix = layout === "June" ? "../.." : "../../../..";
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: "agent-bundle-mcp-tools",
|
|
include_release_path_suites: false,
|
|
include_live_suites: false,
|
|
allow_frozen_target_scenario_omissions: true,
|
|
},
|
|
{
|
|
[client]: `import { getOrCreateSessionMcpRuntime, disposeAllSessionMcpRuntimes } from "${prefix}/dist/agents/agent-bundle-mcp-runtime.js";\nimport { createE2eStateDir } from "${layout === "June" ? "./lib" : `${prefix}/scripts/e2e/lib`}/temp-state-dir.ts";\nthrow new Error("target client executed");`,
|
|
"scripts/e2e/lib/temp-state-dir.ts":
|
|
'export async function createE2eStateDir() { throw new Error("target helper executed"); }',
|
|
"src/agents/agent-bundle-mcp-runtime.ts":
|
|
'export async function getOrCreateSessionMcpRuntime() { throw new Error("target runtime executed"); }\nexport async function disposeAllSessionMcpRuntimes() {}',
|
|
},
|
|
);
|
|
expect(f.selection()).toMatchObject({
|
|
parserRequired: true,
|
|
consumers: ["agent-bundle-mcp-tools"],
|
|
});
|
|
f.provisionParser();
|
|
const result = f.admit();
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain("producer-ran");
|
|
const record = JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8"));
|
|
expect(record).toMatchObject({
|
|
status: "ADMITTED",
|
|
selectedSha: f.sha,
|
|
toolingSha: f.toolingSha,
|
|
provenance: { runId: "123", runAttempt: "2" },
|
|
});
|
|
expect(record.evaluations[0].contracts[0].files).toEqual([
|
|
{ source: "selected", path: client },
|
|
]);
|
|
expect(existsSync(join(f.target, "node_modules"))).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("rejects unresolved survivor baselines at the reusable execution barrier", () => {
|
|
const f = frozenWorkflowFixture(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_selected_ref",
|
|
{
|
|
docker_lanes: "published-upgrade-survivor",
|
|
include_release_path_suites: false,
|
|
include_live_suites: false,
|
|
published_upgrade_survivor_baseline: "openclaw@latest",
|
|
allow_frozen_target_scenario_omissions: false,
|
|
},
|
|
currentSurvivorScenarioFiles(),
|
|
);
|
|
expect(f.selection().obligations).toContainEqual(
|
|
expect.objectContaining({ kind: "upgrade-baselines" }),
|
|
);
|
|
const result = f.admit();
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("unresolved upgrade baselines");
|
|
expect(result.stdout).not.toContain("producer-ran");
|
|
});
|
|
|
|
it("requires the resolved package identity at the final package barrier", () => {
|
|
const f = frozenWorkflowFixture(
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
"resolve_package",
|
|
{
|
|
suite_profile: "custom",
|
|
docker_lanes: "onboard",
|
|
},
|
|
{},
|
|
{ ADMISSION_STAGE: "resolved-package" },
|
|
);
|
|
const result = f.run("Plan frozen source admission");
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("complete resolved package identity");
|
|
});
|
|
|
|
it("records a parser-free unselected workflow without implying execution coverage", () => {
|
|
const f = frozenWorkflowFixture(LIVE_E2E_WORKFLOW, "validate_selected_ref", {
|
|
include_live_suites: true,
|
|
include_release_path_suites: false,
|
|
live_suite_filter: "live-cache",
|
|
allow_frozen_target_scenario_omissions: true,
|
|
});
|
|
expect(f.selection()).toMatchObject({ parserRequired: false, sourcePaths: [], consumers: [] });
|
|
const result = f.admit();
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const record = JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8"));
|
|
expect(record.status).toBe("UNSELECTED");
|
|
const digest = record.digest;
|
|
const nextAttempt = f.run("Plan frozen source admission", { GITHUB_RUN_ATTEMPT: "3" });
|
|
expect(nextAttempt.status, nextAttempt.stderr).toBe(0);
|
|
expect(f.admit().status).toBe(0);
|
|
const continued = JSON.parse(readFileSync(join(f.root, "frozen-admission.json"), "utf8"));
|
|
expect(continued.digest).toBe(digest);
|
|
expect(continued.provenance.runAttempt).toBe("3");
|
|
});
|
|
|
|
it.each([
|
|
[FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"],
|
|
[RELEASE_CHECKS_WORKFLOW, "resolve_target"],
|
|
[LIVE_E2E_WORKFLOW, "validate_selected_ref"],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"],
|
|
])("rejects unsupported selected source before producers in %s", (file, jobName) => {
|
|
const f = frozenWorkflowFixture(
|
|
file,
|
|
jobName,
|
|
{},
|
|
{ "package.json": '{"type":"module","version":"2026.6.33"}' },
|
|
);
|
|
f.provisionParser();
|
|
const request = {
|
|
version: 1,
|
|
repository: "openclaw/openclaw",
|
|
selected: { root: f.target, sha: f.sha },
|
|
tooling: { root: f.tooling, sha: f.toolingSha },
|
|
allowFrozenTargetScenarioOmissions: true,
|
|
selection: { consumers: ["agent-bundle-mcp-tools"] },
|
|
};
|
|
writeFileSync(join(f.root, "frozen-admission-request.json"), JSON.stringify(request));
|
|
const result = f.admit();
|
|
expect(result.stdout).not.toContain("producer-ran");
|
|
expect(result.status, result.stderr).not.toBe(0);
|
|
expect(result.stderr).toContain("expected exactly one committed bundle client layout");
|
|
});
|
|
});
|
|
|
|
type WorkflowStep = {
|
|
"continue-on-error"?: boolean | string;
|
|
env?: Record<string, string>;
|
|
id?: string;
|
|
if?: string;
|
|
name?: string;
|
|
run?: string;
|
|
shell?: string;
|
|
uses?: string;
|
|
with?: Record<string, string>;
|
|
"working-directory"?: string;
|
|
};
|
|
|
|
type WorkflowMatrixEntry = {
|
|
advisory?: boolean;
|
|
chunk_id?: string;
|
|
command?: string;
|
|
profiles?: string;
|
|
suite_group?: string;
|
|
suite_id?: string;
|
|
timeout_minutes?: number;
|
|
};
|
|
|
|
type WorkflowJob = {
|
|
"continue-on-error"?: boolean | string;
|
|
concurrency?: {
|
|
group?: string;
|
|
"cancel-in-progress"?: boolean | string;
|
|
};
|
|
environment?: string;
|
|
env?: Record<string, string>;
|
|
if?: string;
|
|
name?: string;
|
|
needs?: string | string[];
|
|
outputs?: Record<string, string>;
|
|
permissions?: Record<string, string>;
|
|
"runs-on"?: string;
|
|
strategy?: {
|
|
"fail-fast"?: boolean;
|
|
"max-parallel"?: number;
|
|
matrix?: {
|
|
include?: WorkflowMatrixEntry[];
|
|
lane?: string;
|
|
profile?: string[];
|
|
shard?: number[];
|
|
};
|
|
};
|
|
secrets?: string | Record<string, string>;
|
|
"timeout-minutes"?: number | string;
|
|
steps?: WorkflowStep[];
|
|
uses?: string;
|
|
with?: Record<string, boolean | number | string>;
|
|
};
|
|
|
|
type Workflow = {
|
|
concurrency?: {
|
|
group?: string;
|
|
"cancel-in-progress"?: boolean | string;
|
|
queue?: string;
|
|
};
|
|
env?: Record<string, string>;
|
|
jobs?: Record<string, WorkflowJob>;
|
|
on?: {
|
|
schedule?: Array<{ cron?: string }>;
|
|
workflow_call?: {
|
|
inputs?: Record<string, unknown>;
|
|
};
|
|
workflow_dispatch?: {
|
|
inputs?: Record<string, unknown>;
|
|
};
|
|
};
|
|
permissions?: Record<string, string>;
|
|
};
|
|
|
|
const parsedWorkflows = new Map<string, Workflow>();
|
|
|
|
function readWorkflow(path: string): Workflow {
|
|
const cachedWorkflow = parsedWorkflows.get(path);
|
|
if (cachedWorkflow) {
|
|
return cachedWorkflow;
|
|
}
|
|
const workflow = parse(readFileSync(path, "utf8")) as Workflow;
|
|
parsedWorkflows.set(path, workflow);
|
|
return workflow;
|
|
}
|
|
|
|
function workflowPaths(): string[] {
|
|
return readdirSync(".github/workflows")
|
|
.filter((name) => name.endsWith(".yml"))
|
|
.map((name) => `.github/workflows/${name}`);
|
|
}
|
|
|
|
function workflowJob(path: string, jobName: string): WorkflowJob {
|
|
const job = readWorkflow(path).jobs?.[jobName];
|
|
if (!job) {
|
|
throw new Error(`Expected workflow job ${jobName} in ${path}`);
|
|
}
|
|
return job;
|
|
}
|
|
|
|
function workflowStep(job: WorkflowJob, stepName: string): WorkflowStep {
|
|
const step = job.steps?.find((candidate) => candidate.name === stepName);
|
|
if (!step) {
|
|
throw new Error(`Expected workflow step ${stepName}`);
|
|
}
|
|
return step;
|
|
}
|
|
|
|
function releasePublishOrchestration(job: WorkflowJob): WorkflowStep {
|
|
const dispatch = workflowStep(job, "Dispatch publish workflows");
|
|
const phases = job.steps?.filter((step) =>
|
|
[
|
|
"Dispatch publish workflows",
|
|
"Start core npm publication",
|
|
"Complete publish workflows",
|
|
].includes(step.name ?? ""),
|
|
);
|
|
const functions = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
return { ...dispatch, run: [functions, ...(phases ?? []).map((step) => step.run)].join("\n") };
|
|
}
|
|
|
|
function createReleasePublishFixture(
|
|
overrides: Record<string, string> = {},
|
|
{ functions = "", mockEvidence = true } = {},
|
|
) {
|
|
const root = tempDirs.make("release-publish-phases-");
|
|
const eventsPath = join(root, "events");
|
|
const githubEventPath = join(root, "github-event.json");
|
|
const inputs = {
|
|
tag: overrides.RELEASE_TAG ?? "v2026.9.1-beta.1",
|
|
npm_dist_tag: overrides.RELEASE_NPM_DIST_TAG ?? "beta",
|
|
preflight_run_id: overrides.PREFLIGHT_RUN_ID ?? "55",
|
|
full_release_validation_run_id: overrides.FULL_RELEASE_VALIDATION_RUN_ID ?? "66",
|
|
full_release_validation_run_attempt: overrides.FULL_RELEASE_VALIDATION_RUN_ATTEMPT ?? "3",
|
|
publish_openclaw_npm: overrides.PUBLISH_OPENCLAW_NPM ?? "true",
|
|
wait_for_clawhub: overrides.WAIT_FOR_CLAWHUB ?? "true",
|
|
};
|
|
const githubRef = overrides.GITHUB_REF ?? "refs/heads/main";
|
|
const outputPath = join(root, "output");
|
|
const helperDir = join(root, ".release-harness/scripts/lib");
|
|
mkdirSync(helperDir, { recursive: true });
|
|
writeFileSync(join(root, "package.json"), JSON.stringify({ type: "module" }));
|
|
symlinkSync(resolve("node_modules"), join(root, "node_modules"), "dir");
|
|
symlinkSync(
|
|
resolve("scripts/lib/release-beta-verifier.ts"),
|
|
join(helperDir, "release-beta-verifier.ts"),
|
|
"file",
|
|
);
|
|
writeFileSync(eventsPath, "");
|
|
writeFileSync(githubEventPath, JSON.stringify({ inputs }));
|
|
writeFileSync(outputPath, "");
|
|
writeFileSync(
|
|
join(helperDir, "release-publish-children.sh"),
|
|
`${readFileSync("scripts/lib/release-publish-children.sh", "utf8")}
|
|
record() { printf '%s\\n' "$*" >> "$PUBLISH_EVENTS"; }
|
|
verify_release_tag_target() { record verify-tag; }
|
|
resolve_clawhub_release_plan() { :; }
|
|
create_or_update_github_release() { record draft; }
|
|
dispatch_workflow() { record "dispatch:$*"; printf '404\\n'; }
|
|
wait_for_run() {
|
|
record "wait:$1:\${4:-terminal}:\${5:-true}"
|
|
local result=0
|
|
case "$1" in
|
|
plugin-npm-release.yml) result="\${MOCK_PLUGIN_NPM_RESULT:-0}" ;;
|
|
openclaw-npm-release.yml)
|
|
if [[ -n "\${4:-}" ]]; then result="\${MOCK_CORE_START_RESULT:-0}";
|
|
else result="\${MOCK_CORE_RESULT:-0}"; fi ;;
|
|
plugin-clawhub-release.yml) result="\${MOCK_CLAWHUB_RESULT:-0}" ;;
|
|
plugin-clawhub-new.yml) result="\${MOCK_BOOTSTRAP_RESULT:-0}" ;;
|
|
esac
|
|
if [[ -f "$RUNNER_TEMP/cancelled-$2" ]]; then result=1; fi
|
|
record "finished:$1:\${result}"
|
|
return "$result"
|
|
}
|
|
gh() {
|
|
if [[ "$1" == api && "$2" == "repos/$GITHUB_REPOSITORY/actions/runs/"* ]]; then
|
|
if [[ -f "$RUNNER_TEMP/cancelled-\${2##*/}" ]]; then
|
|
printf '%s\\n' '{"status":"completed"}'
|
|
else
|
|
printf '%s\\n' '{"status":"waiting"}'
|
|
fi
|
|
return 0
|
|
fi
|
|
if [[ "$1 $2" != "run cancel" ]]; then return 99; fi
|
|
record "cancel:$*"
|
|
touch "$RUNNER_TEMP/cancelled-\${!#}"
|
|
}
|
|
promote_android_release_asset() { record android; }
|
|
promote_windows_release_assets() { record windows; }
|
|
wait_for_core_npm_visibility() { :; }
|
|
sync_npm_beta_floor() { :; }
|
|
verify_published_release() {
|
|
record "verify:$clawhub_failed:bootstrap=$plugin_clawhub_bootstrap_completed:workflow=$openclaw_npm_expected_workflow_ref"
|
|
record "verify-attempt:\${openclaw_npm_run_attempt:-}"
|
|
}
|
|
${mockEvidence ? "upload_dependency_evidence_release_asset() { record dependency-evidence; }" : ""}
|
|
upload_release_evidence_assets() { record release-evidence; }
|
|
${mockEvidence ? "append_release_proof_to_github_release() { record proof; }" : ""}
|
|
${functions}
|
|
`,
|
|
);
|
|
const outputs = () =>
|
|
Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.split("\n")
|
|
.filter((line) => line.includes("="))
|
|
.map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]),
|
|
);
|
|
return {
|
|
root,
|
|
events: () => readFileSync(eventsPath, "utf8").trim().split("\n"),
|
|
outputs,
|
|
record: (event: string) => writeFileSync(eventsPath, `${event}\n`, { flag: "a" }),
|
|
summary: () => readFileSync(join(root, "summary"), "utf8"),
|
|
run: (step: WorkflowStep, env: NodeJS.ProcessEnv = {}) =>
|
|
spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
timeout: 10_000,
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
GITHUB_WORKSPACE: root,
|
|
RUNNER_TEMP: root,
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ID: "44",
|
|
GITHUB_RUN_ATTEMPT: "2",
|
|
GITHUB_REF: githubRef,
|
|
GITHUB_REF_NAME: githubRef.replace(/^refs\/(?:heads|tags)\//u, ""),
|
|
GITHUB_EVENT_PATH: githubEventPath,
|
|
GITHUB_WORKFLOW_SHA: "d".repeat(40),
|
|
POSTPUBLISH_EVIDENCE_DIR: join(root, "evidence"),
|
|
PUBLISH_EVENTS: eventsPath,
|
|
TARGET_SHA: "a".repeat(40),
|
|
CHILD_WORKFLOW_REF: "main",
|
|
PARENT_WORKFLOW_SHA: "d".repeat(40),
|
|
PARENT_WORKFLOW_BRANCH: "main",
|
|
PARENT_WORKFLOW_FULL_REF: "refs/heads/main",
|
|
RELEASE_TAG: inputs.tag,
|
|
RELEASE_NPM_DIST_TAG: inputs.npm_dist_tag,
|
|
PREFLIGHT_RUN_ID: inputs.preflight_run_id,
|
|
RELEASE_EVIDENCE_MODE: "full-release-validation",
|
|
FULL_RELEASE_VALIDATION_RUN_ID: inputs.full_release_validation_run_id,
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: inputs.full_release_validation_run_attempt,
|
|
PLUGIN_SDK_API_ACKNOWLEDGEMENT: "",
|
|
PUBLISH_OPENCLAW_NPM: inputs.publish_openclaw_npm,
|
|
WAIT_FOR_CLAWHUB: inputs.wait_for_clawhub,
|
|
PLUGINS: "",
|
|
NPM_TELEGRAM_RUN_ID: "",
|
|
CHILD_PLUGIN_NPM_RUN_ID: "101",
|
|
CHILD_PLUGIN_CLAWHUB_RUN_ID: "202",
|
|
CHILD_PLUGIN_CLAWHUB_BOOTSTRAP_RUN_ID: "303",
|
|
CHILD_BOOTSTRAP_WORKFLOW_SHA: "d".repeat(40),
|
|
CHILD_OPENCLAW_NPM_ALREADY_PUBLISHED: "false",
|
|
CHILD_OPENCLAW_NPM_EXPECTED_WORKFLOW_REF: "refs/heads/main",
|
|
CHILD_OPENCLAW_NPM_EXPECTED_WORKFLOW_SHA: "d".repeat(40),
|
|
CHILD_OPENCLAW_NPM_RUN_ATTEMPT: "",
|
|
CHILD_OPENCLAW_NPM_RUN_ID: outputs().openclaw_npm_run_id ?? "",
|
|
CORE_START_OUTCOME: "success",
|
|
CLAWHUB_AUTHORIZATION_OUTCOME: "success",
|
|
CLAWHUB_RECEIPT_OUTCOME: "success",
|
|
...overrides,
|
|
...env,
|
|
},
|
|
}),
|
|
};
|
|
}
|
|
|
|
function workflowStepById(job: WorkflowJob, stepId: string): WorkflowStep {
|
|
const step = job.steps?.find((candidate) => candidate.id === stepId);
|
|
if (!step) {
|
|
throw new Error(`Expected workflow step ID ${stepId}`);
|
|
}
|
|
return step;
|
|
}
|
|
|
|
function evaluatedJobTimeouts(path: string, jobName: string, job: WorkflowJob): number[] {
|
|
const timeout = job["timeout-minutes"];
|
|
if (typeof timeout === "number") {
|
|
return [timeout];
|
|
}
|
|
if (timeout?.includes("inputs.release_")) {
|
|
return (["beta", "stable", "full"] as const).map((profile) =>
|
|
timeoutForProfile(timeout, profile),
|
|
);
|
|
}
|
|
if (timeout === "${{ matrix.group.timeout_minutes || 60 }}") {
|
|
return [60, 90];
|
|
}
|
|
if (path === CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW && jobName === "cross_os_release_checks") {
|
|
return resolveRunnerMatrix({ mode: "both", ref: "main" }).include.map((matrix) => {
|
|
const minutes: unknown = evaluateWorkflowExpression(timeout, {
|
|
eventName: "workflow_dispatch",
|
|
repository: "openclaw/openclaw",
|
|
runAttempt: 1,
|
|
matrix,
|
|
});
|
|
if (typeof minutes !== "number") {
|
|
throw new Error(`Invalid matrix timeout for ${path}:${jobName}`);
|
|
}
|
|
return minutes;
|
|
});
|
|
}
|
|
if (timeout !== "${{ matrix.timeout_minutes }}") {
|
|
throw new Error(`Unsupported timeout for ${path}:${jobName}: ${String(timeout)}`);
|
|
}
|
|
|
|
const matrix = (job.strategy as { matrix?: unknown } | undefined)?.matrix;
|
|
if (matrix && typeof matrix === "object" && "include" in matrix) {
|
|
const include = (matrix as { include?: WorkflowMatrixEntry[] }).include;
|
|
if (!Array.isArray(include) || include.length === 0) {
|
|
throw new Error(`Missing static timeout matrix for ${path}:${jobName}`);
|
|
}
|
|
return include.map((entry) => {
|
|
if (typeof entry.timeout_minutes !== "number") {
|
|
throw new Error(`Missing matrix timeout for ${path}:${jobName}`);
|
|
}
|
|
return entry.timeout_minutes;
|
|
});
|
|
}
|
|
|
|
if (path === LIVE_E2E_WORKFLOW && jobName === "validate_docker_e2e") {
|
|
return (["beta", "stable", "full"] as const).flatMap((releaseProfile) =>
|
|
createReleaseWorkflowMatrixPlan({
|
|
includeReleasePathSuites: true,
|
|
releaseProfile,
|
|
}).dockerE2e.matrix.include.map((entry: WorkflowMatrixEntry) => {
|
|
if (typeof entry.timeout_minutes !== "number") {
|
|
throw new Error(`Missing planned timeout for ${releaseProfile}:${entry.chunk_id}`);
|
|
}
|
|
return entry.timeout_minutes;
|
|
}),
|
|
);
|
|
}
|
|
if (path === LIVE_E2E_WORKFLOW && jobName === "validate_live_docker_provider_suites") {
|
|
return ["beta", "stable", "full"].flatMap((releaseProfile) =>
|
|
createReleaseWorkflowMatrixPlan({
|
|
releaseProfile,
|
|
includeLiveSuites: true,
|
|
}).liveDocker.matrix.include.map(
|
|
(entry: { timeout_minutes: number }) => entry.timeout_minutes,
|
|
),
|
|
);
|
|
}
|
|
|
|
throw new Error(`Missing matrix timeout evaluator for ${path}:${jobName}`);
|
|
}
|
|
|
|
function pluginPrereleaseTimeoutFloor(
|
|
pluginPrerelease: Workflow,
|
|
liveE2e: Workflow,
|
|
profile: "beta" | "stable" | "full",
|
|
): number {
|
|
const components = pluginPrereleaseTimeoutComponents({ pluginPrerelease, liveE2e, profile });
|
|
return Object.values(components).reduce((total, value) => total + value, 0);
|
|
}
|
|
|
|
function runFullReleaseInputValidation(
|
|
releaseProfile: string,
|
|
skipTelegram: string,
|
|
options: {
|
|
telegramWaiver?: string;
|
|
version?: string;
|
|
rerunGroup?: string;
|
|
liveSuiteFilter?: string;
|
|
} = {},
|
|
) {
|
|
const step = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
|
|
"Validate release inputs",
|
|
);
|
|
const workdir = tempDirs.make("full-release-input-validation-");
|
|
mkdirSync(resolve(workdir, "target"));
|
|
writeFileSync(
|
|
resolve(workdir, "target", "package.json"),
|
|
JSON.stringify({ version: options.version ?? "2026.8.1" }),
|
|
"utf8",
|
|
);
|
|
symlinkSync(process.cwd(), resolve(workdir, "workflow"), "dir");
|
|
return spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
GITHUB_OUTPUT: resolve(workdir, "output"),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
RELEASE_PROFILE: releaseProfile,
|
|
SKIP_PACKAGE_TELEGRAM_E2E: skipTelegram,
|
|
TELEGRAM_WAIVER: options.telegramWaiver ?? "",
|
|
LANE_WAIVER: "",
|
|
RERUN_GROUP: options.rerunGroup ?? "all",
|
|
LIVE_SUITE_FILTER: options.liveSuiteFilter ?? "",
|
|
TARGET_CONTEXT_REF: "",
|
|
TARGET_REF: "main",
|
|
},
|
|
});
|
|
}
|
|
|
|
function runFullReleaseTargetIdentityValidation(params: {
|
|
anonymousGitUnavailable?: boolean;
|
|
apiError?: "base" | "context" | "comparison";
|
|
baseTagSha?: string;
|
|
comparisonStatus?: string;
|
|
remoteSha?: string;
|
|
targetContextRef?: string;
|
|
targetRef: string;
|
|
version: string;
|
|
releaseProfile?: string;
|
|
runReleaseSoak?: string;
|
|
rerunGroup?: string;
|
|
crossOsSuiteFilter?: string;
|
|
}) {
|
|
const step = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
|
|
"Validate release inputs",
|
|
);
|
|
const workdir = tempDirs.make("full-release-target-identity-");
|
|
const fakeBin = resolve(workdir, "bin");
|
|
mkdirSync(fakeBin);
|
|
mkdirSync(resolve(workdir, "target"));
|
|
symlinkSync(process.cwd(), resolve(workdir, "workflow"), "dir");
|
|
writeFileSync(
|
|
resolve(workdir, "target", "package.json"),
|
|
`${JSON.stringify({ version: params.version })}\n`,
|
|
"utf8",
|
|
);
|
|
writeFileSync(
|
|
resolve(fakeBin, "git"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$*" == *"ls-remote"* ]]; then
|
|
if [[ "$FAKE_ANONYMOUS_GIT_UNAVAILABLE" == "true" ]]; then
|
|
echo 'fatal: could not read Username for https://github.com: terminal prompts disabled' >&2
|
|
exit 128
|
|
fi
|
|
ref="\${!#}"
|
|
if [[ "$ref" == "refs/tags/v$FAKE_PACKAGE_VERSION" || "$ref" == "refs/tags/v$FAKE_PACKAGE_VERSION^{}" ]]; then
|
|
printf '%s\\t%s\\n' "$FAKE_BASE_SHA" "$ref"
|
|
else
|
|
printf '%s\\t%s\\n' "$FAKE_REMOTE_SHA" "$FAKE_REMOTE_REF"
|
|
fi
|
|
exit 0
|
|
fi
|
|
exit 64
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(
|
|
resolve(fakeBin, "gh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
[[ "\${GH_TOKEN:-}" == "test-token" ]] || exit 4
|
|
[[ "$1" == "api" ]] || exit 64
|
|
shift
|
|
if [[ "$1" == "--method" && "$2" == "GET" ]]; then shift 2; fi
|
|
[[ "$#" == 3 && "$2" == "--jq" ]] || exit 64
|
|
case "$1" in
|
|
"$FAKE_BASE_ENDPOINT") kind=base; value="$FAKE_BASE_SHA"; query=.sha ;;
|
|
"$FAKE_CONTEXT_ENDPOINT") kind=context; value="$FAKE_REMOTE_SHA"; query=.sha ;;
|
|
"$FAKE_COMPARISON_ENDPOINT") kind=comparison; value="$FAKE_COMPARISON_STATUS"; query=.status ;;
|
|
*) echo "Unexpected GitHub API endpoint: $1" >&2; exit 64 ;;
|
|
esac
|
|
[[ "$3" == "$query" ]] || exit 64
|
|
if [[ "$FAKE_API_ERROR" == "$kind" ]]; then
|
|
echo 'gh: Service Unavailable (HTTP 503)' >&2
|
|
exit 1
|
|
fi
|
|
printf '%s\\n' "$value"
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const targetSha = params.targetRef.match(/^[a-f0-9]{40}$/u)?.[0] ?? "a".repeat(40);
|
|
const normalizedContextRef = (params.targetContextRef ?? params.targetRef)
|
|
.replace(/^refs\/heads\//u, "")
|
|
.replace(/^refs\/tags\//u, "");
|
|
const remoteRef = normalizedContextRef.startsWith("v")
|
|
? `refs/tags/${normalizedContextRef}`
|
|
: `refs/heads/${normalizedContextRef}`;
|
|
const remoteSha = params.remoteSha ?? targetSha;
|
|
const commitEndpoint = (ref: string) =>
|
|
`repos/openclaw/openclaw/commits/${encodeURIComponent(ref)}`;
|
|
const outputPath = resolve(workdir, "output");
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
FAKE_ANONYMOUS_GIT_UNAVAILABLE: String(params.anonymousGitUnavailable ?? false),
|
|
FAKE_API_ERROR: params.apiError ?? "",
|
|
FAKE_BASE_ENDPOINT: commitEndpoint(`refs/tags/v${params.version}`),
|
|
FAKE_CONTEXT_ENDPOINT: commitEndpoint(remoteRef),
|
|
FAKE_COMPARISON_ENDPOINT: `repos/openclaw/openclaw/compare/${targetSha}...${remoteSha}`,
|
|
FAKE_COMPARISON_STATUS: params.comparisonStatus ?? "ahead",
|
|
FAKE_REMOTE_REF: remoteRef,
|
|
FAKE_REMOTE_SHA: remoteSha,
|
|
FAKE_BASE_SHA: params.baseTagSha ?? params.remoteSha ?? targetSha,
|
|
FAKE_PACKAGE_VERSION: params.version,
|
|
GH_TOKEN: step.env?.GH_TOKEN === "${{ github.token }}" ? "test-token" : "",
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_OUTPUT: outputPath,
|
|
PATH: `${fakeBin}:${process.env.PATH}`,
|
|
RELEASE_PROFILE: params.releaseProfile ?? "beta",
|
|
LANE_WAIVER: "",
|
|
RUN_RELEASE_SOAK: params.runReleaseSoak ?? "false",
|
|
RERUN_GROUP: params.rerunGroup ?? "all",
|
|
CROSS_OS_SUITE_FILTER: params.crossOsSuiteFilter ?? "",
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "false",
|
|
TARGET_CONTEXT_REF: params.targetContextRef ?? "",
|
|
TARGET_REF: params.targetRef,
|
|
TARGET_SHA: targetSha,
|
|
},
|
|
});
|
|
return { ...result, output: existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "" };
|
|
}
|
|
|
|
function runReleaseChecksInputValidation(
|
|
releaseProfile: string,
|
|
skipTelegram: string,
|
|
rerunGroup = "all",
|
|
runReleaseSoak = "false",
|
|
liveSuiteFilter = "",
|
|
options: {
|
|
candidateArtifactJson?: string;
|
|
telegramWaiver?: string;
|
|
version?: string;
|
|
packageAcceptancePackageSpec?: string;
|
|
phase?: "all" | "candidate" | "independent";
|
|
releasePackageSpec?: string;
|
|
runMaturityScorecard?: string;
|
|
} = {},
|
|
) {
|
|
const step = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target"),
|
|
"Capture selected inputs",
|
|
);
|
|
const workdir = tempDirs.make("release-checks-input-validation-");
|
|
const fixture = frozenToolingFixture(workdir, [
|
|
"scripts/full-release-validation-policy.mjs",
|
|
"scripts/full-release-flake-classification.mjs",
|
|
...PUBLICATION_CONTRACT_FILES,
|
|
"scripts/lib/release-changelog.mjs",
|
|
"scripts/full-release-candidate-contract.mjs",
|
|
"scripts/lib/full-release-candidate-reuse.mjs",
|
|
"scripts/lib/full-release-evidence.mjs",
|
|
"scripts/lib/cross-os-release-checks/suite-filter.mjs",
|
|
"scripts/lib/canonical-json.mjs",
|
|
"scripts/lib/record-shared.mjs",
|
|
]);
|
|
const outputPath = resolve(workdir, "github-output");
|
|
mkdirSync(resolve(workdir, "waiver-target"));
|
|
writeFileSync(
|
|
resolve(workdir, "waiver-target", "package.json"),
|
|
JSON.stringify({ version: options.version ?? "2026.8.1" }),
|
|
"utf8",
|
|
);
|
|
symlinkSync(fixture.tooling, resolve(workdir, "workflow"), "dir");
|
|
const stepEnv = Object.fromEntries(Object.keys(step.env ?? {}).map((name) => [name, ""]));
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
...stepEnv,
|
|
ADMISSION_TOOLING_ROOT: fixture.tooling,
|
|
ADMISSION_TOOLING_SHA: fixture.toolingSha,
|
|
CANDIDATE_ARTIFACT_JSON_INPUT: options.candidateArtifactJson ?? "",
|
|
GITHUB_OUTPUT: outputPath,
|
|
PATH: process.env.PATH,
|
|
RELEASE_FAIL_FAST_INPUT: "false",
|
|
RELEASE_FILTER_VALIDATOR: resolve(RELEASE_FILTER_VALIDATOR),
|
|
RELEASE_LIVE_SUITE_FILTER_INPUT: liveSuiteFilter,
|
|
RELEASE_MODE_INPUT: "both",
|
|
RELEASE_PACKAGE_ACCEPTANCE_PACKAGE_SPEC_INPUT: options.packageAcceptancePackageSpec ?? "",
|
|
RELEASE_PACKAGE_SPEC_INPUT: options.releasePackageSpec ?? "",
|
|
RELEASE_PHASE_INPUT: options.phase ?? "all",
|
|
RELEASE_PROFILE_INPUT: releaseProfile,
|
|
RELEASE_PROVIDER_INPUT: "openai",
|
|
RELEASE_QA_DISCORD_LIVE_CI_ENABLED: "false",
|
|
RELEASE_QA_SLACK_LIVE_CI_ENABLED: "false",
|
|
RELEASE_QA_WHATSAPP_LIVE_CI_ENABLED: "false",
|
|
RELEASE_REF_INPUT: "main",
|
|
RELEASE_RERUN_GROUP_INPUT: rerunGroup,
|
|
RELEASE_RUN_MATURITY_SCORECARD_INPUT: options.runMaturityScorecard ?? "false",
|
|
RELEASE_RUN_RELEASE_SOAK_INPUT: runReleaseSoak,
|
|
RELEASE_SKIP_PACKAGE_TELEGRAM_E2E_INPUT: skipTelegram,
|
|
TELEGRAM_WAIVER: options.telegramWaiver ?? "",
|
|
LANE_WAIVER: "",
|
|
},
|
|
});
|
|
return { outputPath, result };
|
|
}
|
|
|
|
function releaseCandidateArtifactJson(selectedSha = "a".repeat(40), packagePublished = false) {
|
|
return JSON.stringify({
|
|
packagePublished,
|
|
packageArtifactName: "docker-e2e-package-123-1",
|
|
packageArtifactId: "456",
|
|
packageArtifactDigest: "b".repeat(64),
|
|
packageArtifactRunId: "123",
|
|
packageArtifactRunAttempt: "1",
|
|
packageFileName: "openclaw-current.tgz",
|
|
packageSourceSha: selectedSha,
|
|
packageSha256: "c".repeat(64),
|
|
packageVersion: "2026.8.1",
|
|
imageArtifactName: "docker-e2e-shared-images-123-1",
|
|
imageArtifactId: "789",
|
|
imageArtifactDigest: "d".repeat(64),
|
|
imageArtifactRunId: "123",
|
|
imageArtifactRunAttempt: "1",
|
|
imageArchiveSha256: "e".repeat(64),
|
|
});
|
|
}
|
|
|
|
function runReleaseChecksShellStep(
|
|
stepName: string,
|
|
env: Record<string, string>,
|
|
workdir = tempDirs.make("release-checks-shell-step-"),
|
|
) {
|
|
const step = workflowStep(workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target"), stepName);
|
|
mkdirSync(join(workdir, "workflow", "scripts"), { recursive: true });
|
|
copyFileSync(
|
|
"scripts/release-context-contains.sh",
|
|
join(workdir, "workflow", "scripts", "release-context-contains.sh"),
|
|
);
|
|
const outputPath = resolve(workdir, "github-output");
|
|
writeFileSync(outputPath, "", "utf8");
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
...env,
|
|
GITHUB_WORKSPACE: workdir,
|
|
GITHUB_OUTPUT: outputPath,
|
|
PATH: process.env.PATH,
|
|
},
|
|
});
|
|
return { output: readFileSync(outputPath, "utf8"), result };
|
|
}
|
|
|
|
function runCandidateAcquisitionStep(
|
|
stepName: string,
|
|
env: Record<string, string>,
|
|
workdir = tempDirs.make("candidate-acquisition-step-"),
|
|
) {
|
|
const step = workflowStep(
|
|
workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "resolve_candidate"),
|
|
stepName,
|
|
);
|
|
const outputPath = resolve(workdir, "github-output");
|
|
writeFileSync(outputPath, "", "utf8");
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
env: { ...env, GITHUB_OUTPUT: outputPath, PATH: process.env.PATH, RUNNER_TEMP: workdir },
|
|
});
|
|
const output = Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
);
|
|
return { output, result };
|
|
}
|
|
|
|
function runFullReleaseCandidateRequest(packagePublished: boolean) {
|
|
const step = workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"),
|
|
"Build full release candidate request",
|
|
);
|
|
const workdir = tempDirs.make("full-release-candidate-request-");
|
|
const harnessRoot = resolve(workdir, ".release-harness");
|
|
const outputPath = resolve(workdir, "github-output");
|
|
mkdirSync(harnessRoot);
|
|
symlinkSync(resolve("scripts"), resolve(harnessRoot, "scripts"), "dir");
|
|
writeFileSync(outputPath, "", "utf8");
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
ALLOW_FROZEN_TARGET_SCENARIO_OMISSIONS: "false",
|
|
ALLOW_UNRELEASED_CHANGELOG: "false",
|
|
GITHUB_OUTPUT: outputPath,
|
|
NODE_OPTIONS: "--preserve-symlinks-main",
|
|
PACKAGE_PUBLISHED: String(packagePublished),
|
|
PATH: process.env.PATH,
|
|
RELEASE_PROFILE: "beta",
|
|
RELEASE_SOAK: "false",
|
|
RUNNER_TEMP: workdir,
|
|
SHARED_IMAGE_POLICY: "no-push-artifact",
|
|
TARGET_REPOSITORY: "openclaw/openclaw",
|
|
TARGET_SHA: "a".repeat(40),
|
|
TOOLING_SHA: "b".repeat(40),
|
|
UPGRADE_SURVIVOR_BASELINE: "openclaw@latest",
|
|
UPGRADE_SURVIVOR_BASELINES: "",
|
|
UPGRADE_SURVIVOR_SCENARIOS: "",
|
|
LANE_WAIVER: "",
|
|
},
|
|
});
|
|
const output = Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
);
|
|
return { output, result };
|
|
}
|
|
|
|
function createReleaseChecksContextFixture() {
|
|
const root = tempDirs.make("release-checks-context-repo-");
|
|
const repo = resolve(root, "context-source");
|
|
mkdirSync(repo);
|
|
const git = (...args: string[]) =>
|
|
execFileSync("git", args, { cwd: repo, encoding: "utf8" }).trim();
|
|
git("init", "-q", "--initial-branch=release/2026.8.1");
|
|
git("config", "user.name", "OpenClaw Test");
|
|
git("config", "user.email", "openclaw-test@example.com");
|
|
writeFileSync(resolve(repo, "package.json"), '{"version":1}\n', "utf8");
|
|
git("add", "package.json");
|
|
git("commit", "-qm", "candidate");
|
|
const candidateSha = git("rev-parse", "HEAD");
|
|
writeFileSync(resolve(repo, "package.json"), '{"version":2}\n', "utf8");
|
|
git("commit", "-qam", "branch advance");
|
|
const branchHeadSha = git("rev-parse", "HEAD");
|
|
git("tag", "-a", "v2026.8.1", "-m", "release", branchHeadSha);
|
|
const tree = git("rev-parse", "HEAD^{tree}");
|
|
const unrelatedSha = git("commit-tree", tree, "-m", "unrelated root");
|
|
return { branchHeadSha, candidateSha, repoUrl: pathToFileURL(repo).href, unrelatedSha };
|
|
}
|
|
|
|
function runFullReleaseTargetSummary(
|
|
rerunGroup: string,
|
|
skipTelegram: string,
|
|
overrides: Record<string, string> = {},
|
|
) {
|
|
const step = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
|
|
"Summarize target",
|
|
);
|
|
const workdir = tempDirs.make("full-release-target-summary-");
|
|
const summaryPath = resolve(workdir, "github-summary");
|
|
const stepEnv = Object.fromEntries(Object.keys(step.env ?? {}).map((name) => [name, ""]));
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...stepEnv,
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
PATH: process.env.PATH,
|
|
RELEASE_PROFILE: "beta",
|
|
RERUN_GROUP: rerunGroup,
|
|
SKIP_PACKAGE_TELEGRAM_E2E: skipTelegram,
|
|
TARGET_REF: "main",
|
|
TARGET_SHA: "a".repeat(40),
|
|
...overrides,
|
|
},
|
|
});
|
|
const summary = result.status === 0 ? readFileSync(summaryPath, "utf8") : "";
|
|
return { result, summary };
|
|
}
|
|
|
|
function shellFunctionSource(source: string, functionName: string): string {
|
|
const startMarker = `${functionName}() {`;
|
|
const endMarker = "\n}\n";
|
|
const start = source.indexOf(startMarker);
|
|
if (start < 0) {
|
|
throw new Error(`Expected shell function ${functionName}`);
|
|
}
|
|
const end = source.indexOf(endMarker, start);
|
|
if (end < 0) {
|
|
throw new Error(`Expected shell function terminator for ${functionName}`);
|
|
}
|
|
return source.slice(start, end + endMarker.length);
|
|
}
|
|
|
|
function workflowMatrixEntry(path: string, jobName: string, suiteId: string): WorkflowMatrixEntry {
|
|
const entry = workflowJob(path, jobName).strategy?.matrix?.include?.find(
|
|
(candidate) => candidate.suite_id === suiteId,
|
|
);
|
|
if (!entry) {
|
|
throw new Error(`Expected workflow matrix entry ${suiteId} in ${jobName}`);
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
function runFocusedLiveSuiteValidation(suiteId: string, overrides: Record<string, string> = {}) {
|
|
const step = workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "validate_selected_ref"),
|
|
"Validate focused live suite filter",
|
|
);
|
|
const runnerTemp = tempDirs.make("focused-live-suite-");
|
|
return spawnSync("bash", ["-c", step.run ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
LIVE_SUITE_FILTER: suiteId,
|
|
RELEASE_TEST_PROFILE: "full",
|
|
INCLUDE_REPO_E2E: "true",
|
|
INCLUDE_LIVE_SUITES: "true",
|
|
LIVE_MODELS_ONLY: "false",
|
|
LIVE_MODEL_PROVIDERS: "",
|
|
ADMISSION_TOOLING_ROOT: resolve("."),
|
|
RUNNER_TEMP: runnerTemp,
|
|
...overrides,
|
|
},
|
|
});
|
|
}
|
|
|
|
function expectTextToIncludeAll(text: string | undefined, snippets: string[]): void {
|
|
if (text === undefined) {
|
|
throw new Error("Expected text to be defined before checking snippets");
|
|
}
|
|
for (const snippet of snippets) {
|
|
expect(text).toContain(snippet);
|
|
}
|
|
}
|
|
|
|
function runFullReleaseChildDispatch(
|
|
child: (typeof FULL_RELEASE_CHILD_DISPATCHES)[number],
|
|
overrides: Record<string, string> = {},
|
|
) {
|
|
const job = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, child.jobName);
|
|
const step = workflowStep(job, child.stepName);
|
|
const script = step.run;
|
|
if (!script) {
|
|
throw new Error(`Expected full release child dispatch script for ${child.jobName}`);
|
|
}
|
|
|
|
const workdir = tempDirs.make("full-release-child-dispatch-");
|
|
const ghPath = resolve(workdir, "gh");
|
|
const sleepPath = resolve(workdir, "sleep");
|
|
const callsPath = resolve(workdir, "gh-calls");
|
|
const statusPath = resolve(workdir, "status-polls");
|
|
writeFileSync(callsPath, "");
|
|
const parentSha = "a".repeat(40);
|
|
const stepValues: Record<string, string> = {
|
|
ALLOW_UNRELEASED_CHANGELOG: "false",
|
|
ARTIFACT_STAGE: child.kind.replace("artifact-", ""),
|
|
ARTIFACT_DISPATCH_ID: `full-release-validation-77-2-${child.kind}`,
|
|
CANDIDATE_ARTIFACT_JSON: "",
|
|
CANDIDATE_REQUEST_JSON: '{"targetSha":"' + "b".repeat(40) + '"}',
|
|
CHILD_WORKFLOW_KIND: child.kind,
|
|
CHILD_WORKFLOW_REF: "main",
|
|
CI_RELEASE_SCOPE: "full",
|
|
CODEX_PLUGIN_SPEC: "",
|
|
CROSS_OS_SUITE_FILTER: "",
|
|
EXTENSION_TEST_EXCLUDE_PATTERNS_JSON: "[]",
|
|
FAIL_FAST: "false",
|
|
GH_TOKEN: "fixture-token",
|
|
LIVE_SUITE_FILTER: "",
|
|
MODE: "both",
|
|
NPM_DIST_TAG: "beta",
|
|
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: "",
|
|
PACKAGE_SPEC: "openclaw@beta",
|
|
PARENT_WORKFLOW_SHA: parentSha,
|
|
PREFLIGHT_PHASE: "all",
|
|
PREPARED_NPM_BUNDLE_JSON: '{"schema":"openclaw.prepared-npm-bundle/v1"}',
|
|
PHASE: child.jobName.endsWith("_candidate") ? "candidate" : "independent",
|
|
PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: "[]",
|
|
PROVIDER: "openai",
|
|
PROVIDER_MODE: "mock-openai",
|
|
RELEASE_PACKAGE_SPEC: "",
|
|
RELEASE_PROFILE: "stable",
|
|
RELEASE_TAG: "v2026.8.1",
|
|
RELEASE_CANDIDATE_BRANCH: "",
|
|
RERUN_GROUP: "all",
|
|
RUN_RELEASE_SOAK: "false",
|
|
SCENARIO: "",
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "false",
|
|
TELEGRAM_WAIVER: "",
|
|
LANE_WAIVER: "",
|
|
TARGET_CONTEXT_REF: "",
|
|
TARGET_REF: "main",
|
|
TARGET_SHA: "b".repeat(40),
|
|
};
|
|
const stepEnv = Object.fromEntries(
|
|
Object.keys({ ...job.env, ...step.env }).map((name) => {
|
|
const value = stepValues[name];
|
|
if (value === undefined) {
|
|
throw new Error(`Missing child dispatch fixture value for ${child.jobName}.${name}`);
|
|
}
|
|
return [name, value];
|
|
}),
|
|
);
|
|
const env = {
|
|
...stepEnv,
|
|
GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN:
|
|
readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).env?.GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ??
|
|
"HTTP 5[0-9][0-9]",
|
|
GITHUB_OUTPUT: resolve(workdir, "github-output"),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ATTEMPT: "2",
|
|
GITHUB_RUN_ID: "77",
|
|
GITHUB_STEP_SUMMARY: resolve(workdir, "github-summary"),
|
|
MOCK_GH_CALLS: callsPath,
|
|
MOCK_GH_CHILD_SHA: parentSha,
|
|
MOCK_GH_CONCLUSION: "success",
|
|
MOCK_GH_CURRENT_SHA: parentSha,
|
|
MOCK_GH_DISPATCH_OUTPUT: "Created workflow_dispatch event.",
|
|
MOCK_GH_MATCHES: "[101]",
|
|
MOCK_GH_RUN_EVENT: "workflow_dispatch",
|
|
MOCK_GH_RUN_HEAD_BRANCH:
|
|
overrides.MOCK_GH_RUN_HEAD_BRANCH ??
|
|
overrides.CHILD_WORKFLOW_REF ??
|
|
stepEnv.CHILD_WORKFLOW_REF,
|
|
MOCK_GH_RUN_ID: "101",
|
|
MOCK_GH_RUN_PATH: `.github/workflows/${child.workflow}`,
|
|
MOCK_GH_RUN_ATTEMPT: "1",
|
|
MOCK_GH_RUN_TITLE: `${child.runName} full-release-validation-77-2${child.nonceSuffix}`,
|
|
MOCK_GH_RUN_TITLES: "[]",
|
|
MOCK_GH_RUN_WORKFLOW_ID: "789",
|
|
MOCK_GH_STATUSES: '["completed"]',
|
|
MOCK_GH_STATUS_POLLS: statusPath,
|
|
MOCK_GH_WORKFLOW_ID: "789",
|
|
PATH: `${workdir}:${process.env.PATH}`,
|
|
...overrides,
|
|
};
|
|
// Serialize responses once: title polling must not start a Node runtime per read.
|
|
const statuses = JSON.parse(env.MOCK_GH_STATUSES) as string[];
|
|
const titles = JSON.parse(env.MOCK_GH_RUN_TITLES) as string[];
|
|
const observations = Array.from(
|
|
{ length: Math.max(statuses.length, titles.length, 1) },
|
|
(_, index) =>
|
|
JSON.stringify({
|
|
conclusion: env.MOCK_GH_CONCLUSION,
|
|
display_title:
|
|
titles.length > 0 ? titles[Math.min(index, titles.length - 1)] : env.MOCK_GH_RUN_TITLE,
|
|
event: env.MOCK_GH_RUN_EVENT,
|
|
head_branch: env.MOCK_GH_RUN_HEAD_BRANCH,
|
|
head_sha: env.MOCK_GH_CHILD_SHA,
|
|
html_url: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
id: Number(env.MOCK_GH_RUN_ID),
|
|
path: env.MOCK_GH_RUN_PATH,
|
|
run_attempt: Number(env.MOCK_GH_RUN_ATTEMPT),
|
|
status: statuses[Math.min(index, statuses.length - 1)],
|
|
workflow_id: Number(env.MOCK_GH_RUN_WORKFLOW_ID),
|
|
}),
|
|
);
|
|
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
writeFileSync(
|
|
ghPath,
|
|
`#!/bin/sh
|
|
printf '%s\\0' "$#" "$CHILD_WORKFLOW_REF" "\${DISPATCH_RUN_NAME:-}" "$@" >> "$MOCK_GH_CALLS"
|
|
if [ "$1" = workflow ] && [ "$2" = run ]; then
|
|
if [ -n "\${MOCK_GH_DISPATCH_ERROR:-}" ]; then
|
|
printf '%s\\n' "$MOCK_GH_DISPATCH_ERROR" >&2
|
|
exit 1
|
|
fi
|
|
printf '%s\\n' "$MOCK_GH_DISPATCH_OUTPUT"
|
|
exit 0
|
|
fi
|
|
if [ "$1" = api ]; then
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
*/commits/*) printf '%s\\n' "$MOCK_GH_CURRENT_SHA"; exit 0 ;;
|
|
*/actions/workflows/*/runs) printf '%s\\n' "$MOCK_GH_MATCHES"; exit 0 ;;
|
|
*/actions/workflows/*) printf '%s\\n' "$MOCK_GH_WORKFLOW_ID"; exit 0 ;;
|
|
*/actions/runs/*)
|
|
poll=0
|
|
if [ -f "$MOCK_GH_STATUS_POLLS" ]; then
|
|
read -r poll < "$MOCK_GH_STATUS_POLLS"
|
|
fi
|
|
printf '%s\\n' "$((poll + 1))" > "$MOCK_GH_STATUS_POLLS"
|
|
case "$poll" in
|
|
${observations.map((json, index) => ` ${index === observations.length - 1 ? "*" : index}) printf '%s\\n' ${quote(json)} ;;`).join("\n")}
|
|
esac
|
|
exit 0 ;;
|
|
esac
|
|
done
|
|
fi
|
|
printf 'Unexpected mock gh invocation: %s\\n' "$*" >&2
|
|
exit 2
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(sleepPath, "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env,
|
|
timeout: 10_000,
|
|
});
|
|
// A count-prefixed NUL record preserves empty, quoted, and multiline arguments.
|
|
const fields = readFileSync(callsPath, "utf8").split("\0");
|
|
const calls: { args: string[]; childWorkflowRef: string; dispatchRunName?: string }[] = [];
|
|
for (let cursor = 0; cursor < fields.length - 1;) {
|
|
const argc = Number(fields[cursor++]);
|
|
const childWorkflowRef = fields[cursor++]!;
|
|
const dispatchRunName = fields[cursor++] || undefined;
|
|
calls.push({ args: fields.slice(cursor, cursor + argc), childWorkflowRef, dispatchRunName });
|
|
cursor += argc;
|
|
}
|
|
return { calls, result };
|
|
}
|
|
|
|
function fullReleaseChild(kind: string) {
|
|
const child = FULL_RELEASE_CHILD_DISPATCHES.find((candidate) => candidate.kind === kind);
|
|
if (!child) {
|
|
throw new Error(`Expected full release child ${kind}`);
|
|
}
|
|
return child;
|
|
}
|
|
|
|
function runPackageAcceptanceSummary(params: {
|
|
dockerArtifactResult?: string;
|
|
dockerRegistryResult?: string;
|
|
npm12InstallResult?: string;
|
|
suiteProfile?: string;
|
|
telegramEnabled: boolean;
|
|
telegramResult: string;
|
|
}) {
|
|
const summary = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary");
|
|
const script = workflowStep(summary, "Verify package acceptance results").run;
|
|
if (!script) {
|
|
throw new Error("Expected package acceptance summary script");
|
|
}
|
|
return spawnSync("bash", ["-c", script], {
|
|
encoding: "utf8",
|
|
env: {
|
|
DOCKER_ARTIFACT_RESULT: params.dockerArtifactResult ?? "success",
|
|
DOCKER_REGISTRY_RESULT: params.dockerRegistryResult ?? "skipped",
|
|
PACKAGE_INTEGRITY_RESULT: "success",
|
|
NPM_12_INSTALL_RESULT: params.npm12InstallResult ?? "success",
|
|
PACKAGE_TELEGRAM_RESULT: params.telegramResult,
|
|
PATH: process.env.PATH,
|
|
RESOLVE_RESULT: "success",
|
|
SUITE_PROFILE: params.suiteProfile ?? "package",
|
|
TELEGRAM_ENABLED: String(params.telegramEnabled),
|
|
},
|
|
});
|
|
}
|
|
|
|
function runPackageAcceptanceProfile(params: {
|
|
dockerLanes?: string;
|
|
suiteProfile: string;
|
|
telegramMode?: string;
|
|
telegramScenarios?: string;
|
|
}) {
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const script = workflowStep(job, "Select acceptance profile").run;
|
|
if (!script) {
|
|
throw new Error("Expected package acceptance profile script");
|
|
}
|
|
const workdir = tempDirs.make("package-acceptance-profile-");
|
|
const fixture = frozenToolingFixture(workdir, []);
|
|
const outputPath = resolve(workdir, "github-output");
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
cwd: fixture.tooling,
|
|
encoding: "utf8",
|
|
env: {
|
|
ADMISSION_TOOLING_ROOT: fixture.tooling,
|
|
ADMISSION_TOOLING_SHA: fixture.toolingSha,
|
|
CUSTOM_DOCKER_LANES: params.dockerLanes ?? "",
|
|
GITHUB_OUTPUT: outputPath,
|
|
PACKAGE_ARTIFACT_NAME: "package-under-test",
|
|
PATH: process.env.PATH,
|
|
SOURCE: "ref",
|
|
SUITE_PROFILE: params.suiteProfile,
|
|
TELEGRAM_MODE: params.telegramMode ?? "none",
|
|
TELEGRAM_SCENARIOS: params.telegramScenarios ?? "",
|
|
},
|
|
});
|
|
const outputs =
|
|
result.status === 0
|
|
? Object.fromEntries(
|
|
readFileSync(outputPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const separator = line.indexOf("=");
|
|
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
}),
|
|
)
|
|
: {};
|
|
return { outputs, result };
|
|
}
|
|
|
|
function runPackageAcceptanceRegistryInputValidation(params: {
|
|
candidateArtifactJson?: string;
|
|
prepublishPluginRegistryJson?: string;
|
|
}) {
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const script = workflowStep(job, "Validate prerelease plugin registry input").run;
|
|
if (!script) {
|
|
throw new Error("Expected package acceptance registry input validation script");
|
|
}
|
|
const workdir = tempDirs.make("package-acceptance-registry-input-");
|
|
const outputPath = resolve(workdir, "github-output");
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
encoding: "utf8",
|
|
env: {
|
|
CANDIDATE_ARTIFACT_JSON: params.candidateArtifactJson ?? "",
|
|
GITHUB_OUTPUT: outputPath,
|
|
PATH: process.env.PATH,
|
|
PREPUBLISH_PLUGIN_REGISTRY_JSON: params.prepublishPluginRegistryJson ?? "",
|
|
},
|
|
});
|
|
const output = result.status === 0 ? readFileSync(outputPath, "utf8") : "";
|
|
return { output, result };
|
|
}
|
|
|
|
function packageAcceptanceRegistryTuple(overrides: Record<string, string> = {}) {
|
|
return {
|
|
prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-123-2",
|
|
prepublishPluginRegistryArtifactId: "456",
|
|
prepublishPluginRegistryArtifactDigest: "a".repeat(64),
|
|
prepublishPluginRegistryArtifactRunId: "123",
|
|
prepublishPluginRegistryArtifactRunAttempt: "2",
|
|
prepublishPluginRegistryManifestSha256: "b".repeat(64),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function runPackageAcceptanceResolveScript(params: {
|
|
candidateArtifactJson?: string;
|
|
prepublishPluginRegistryJson?: string;
|
|
source: "artifact" | "npm" | "ref" | "trusted-url" | "url";
|
|
telegramMode: "mock-openai" | "none";
|
|
}) {
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const script = workflowStep(job, "Resolve package candidate").run;
|
|
if (!script) {
|
|
throw new Error("Expected package acceptance resolve script");
|
|
}
|
|
const workdir = tempDirs.make("package-acceptance-resolve-");
|
|
const binDir = resolve(workdir, "bin");
|
|
const capturePath = resolve(workdir, "node-args");
|
|
const outputPath = resolve(workdir, "github-output");
|
|
const artifactDir = resolve(workdir, ".artifacts/package-candidate-input");
|
|
mkdirSync(binDir, { recursive: true });
|
|
mkdirSync(artifactDir, { recursive: true });
|
|
const nodePath = resolve(binDir, "node");
|
|
const artifactPath = resolve(artifactDir, "openclaw-current.tgz");
|
|
const artifactBody = "package acceptance artifact";
|
|
writeFileSync(artifactPath, artifactBody);
|
|
writeFileSync(
|
|
nodePath,
|
|
`#!/bin/sh
|
|
printf "%s\\n" "$@" > "$CAPTURE_PATH"
|
|
if [ "$SOURCE" = "artifact" ]; then
|
|
mkdir -p .artifacts/docker-e2e-package
|
|
printf '{"name":"openclaw","sha256":"%s","packageSourceSha":"%s","version":"%s"}\\n' \
|
|
"$PACKAGE_SHA256" "$PACKAGE_SOURCE_SHA" "$PACKAGE_VERSION" \
|
|
> .artifacts/docker-e2e-package/package-candidate.json
|
|
fi
|
|
`,
|
|
);
|
|
chmodSync(nodePath, 0o755);
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
CAPTURE_PATH: capturePath,
|
|
GITHUB_OUTPUT: outputPath,
|
|
OPENCLAW_TRUSTED_PACKAGE_TOKEN: "",
|
|
PACKAGE_FILE_NAME: "openclaw-current.tgz",
|
|
PACKAGE_REF: "HEAD",
|
|
PACKAGE_SHA256: createHash("sha256").update(artifactBody).digest("hex"),
|
|
PACKAGE_SOURCE_SHA: "a".repeat(40),
|
|
PACKAGE_SPEC: "openclaw@beta",
|
|
PACKAGE_URL: "https://example.invalid/openclaw.tgz",
|
|
PACKAGE_VERSION: "2026.8.26",
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
PREPUBLISH_PLUGIN_REGISTRY_JSON: params.prepublishPluginRegistryJson ?? "",
|
|
PUBLISHED_ARTIFACT: String(
|
|
JSON.parse(params.candidateArtifactJson ?? "{}").packagePublished === true,
|
|
),
|
|
SOURCE: params.source,
|
|
TELEGRAM_MODE: params.telegramMode,
|
|
TRUSTED_SOURCE_ID: "",
|
|
},
|
|
});
|
|
const args = result.status === 0 ? readFileSync(capturePath, "utf8") : "";
|
|
return { args, result };
|
|
}
|
|
|
|
function runPackageAcceptanceBaselineStep(params: {
|
|
candidateArtifactJson?: string;
|
|
candidateVersion: string;
|
|
fallbackBaseline?: string;
|
|
source: "artifact" | "npm" | "ref";
|
|
targetContextRef?: string;
|
|
}) {
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
const script = workflowStep(job, "Resolve published upgrade survivor baselines").run;
|
|
if (!script) {
|
|
throw new Error("Expected package acceptance baseline script");
|
|
}
|
|
const executableScript = script.replace(
|
|
"node scripts/lib/release-upgrade-baseline.mjs",
|
|
`node ${JSON.stringify(resolve("scripts/lib/release-upgrade-baseline.mjs"))}`,
|
|
);
|
|
const workdir = tempDirs.make("package-acceptance-baseline-");
|
|
const binDir = resolve(workdir, "bin");
|
|
const outputPath = resolve(workdir, "github-output");
|
|
const npmLog = resolve(workdir, "npm.log");
|
|
mkdirSync(binDir, { recursive: true });
|
|
symlinkSync(resolve("node_modules"), resolve(workdir, "node_modules"), "dir");
|
|
symlinkSync(resolve("scripts"), resolve(workdir, "scripts"), "dir");
|
|
writeFileSync(
|
|
resolve(binDir, "npm"),
|
|
`#!/bin/sh
|
|
printf '%s\n' "$*" >> "$NPM_LOG"
|
|
if [ "$1 $2 $3" = "view openclaw versions" ]; then
|
|
printf '%s\n' '["2026.6.34","2026.6.35","2026.7.1","2026.7.1-2","2026.8.1","2026.9.1"]'
|
|
elif [ "$1 $2" = "view openclaw@latest" ]; then
|
|
printf '%s\n' '2026.9.1'
|
|
else
|
|
exit 64
|
|
fi
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const result = spawnSync("bash", ["-c", executableScript], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
CANDIDATE_VERSION: params.candidateVersion,
|
|
CANDIDATE_PUBLISHED: String(
|
|
params.source === "npm" ||
|
|
JSON.parse(params.candidateArtifactJson ?? "{}").packagePublished === true,
|
|
),
|
|
FALLBACK_BASELINE: params.fallbackBaseline ?? "openclaw@latest",
|
|
GH_TOKEN: "",
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
NPM_LOG: npmLog,
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
REQUESTED_BASELINES: "",
|
|
RUNNER_TEMP: workdir,
|
|
TARGET_CONTEXT_REF: params.targetContextRef ?? "",
|
|
},
|
|
});
|
|
return {
|
|
npmCalls: existsSync(npmLog) ? readFileSync(npmLog, "utf8").trim().split("\n") : [],
|
|
output: existsSync(outputPath) ? readFileSync(outputPath, "utf8") : "",
|
|
result,
|
|
};
|
|
}
|
|
|
|
function runReleaseSurvivorProfileStep(params: {
|
|
candidateVersion?: string;
|
|
published?: boolean;
|
|
soak?: boolean;
|
|
context?: string;
|
|
ref?: string;
|
|
override?: string;
|
|
supportsScenario?: boolean;
|
|
metadataError?: boolean;
|
|
}) {
|
|
const step = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"),
|
|
"Select candidate-compatible upgrade survivor profile",
|
|
);
|
|
const root = tempDirs.make("release-survivor-profile-");
|
|
const bin = join(root, "bin");
|
|
const output = join(root, "output");
|
|
const calls = join(root, "calls");
|
|
mkdirSync(bin);
|
|
symlinkSync(resolve("scripts"), join(root, "scripts"), "dir");
|
|
writeFileSync(join(root, "package.json"), JSON.stringify({ version: "2026.9.3" }));
|
|
for (const tool of ["gh", "npm"]) {
|
|
writeFileSync(
|
|
join(bin, tool),
|
|
`#!${process.execPath}
|
|
const fs = require("node:fs");
|
|
const tool = require("node:path").basename(process.argv[1]);
|
|
fs.appendFileSync(process.env.FIXTURE_CALLS, JSON.stringify({ tool, args: process.argv.slice(2) }) + "\\n");
|
|
if (tool === "gh") {
|
|
if (process.env.FIXTURE_METADATA_ERROR === "true") process.exit(1);
|
|
process.stdout.write(process.env.FIXTURE_DIRECTORY);
|
|
} else {
|
|
process.exit(75);
|
|
}
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
GITHUB_OUTPUT: output,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
CANDIDATE_PUBLISHED: String(params.published ?? false),
|
|
CANDIDATE_SOURCE_SHA: "a".repeat(40),
|
|
CANDIDATE_VERSION: params.candidateVersion ?? "2026.9.3",
|
|
CANDIDATE_REF: params.ref ?? "main",
|
|
PACKAGE_ACCEPTANCE_PACKAGE_SPEC: params.override ?? "",
|
|
TARGET_CONTEXT_REF: params.context ?? "",
|
|
RUN_RELEASE_SOAK: String(params.soak ?? false),
|
|
FIXTURE_CALLS: calls,
|
|
FIXTURE_METADATA_ERROR: String(params.metadataError ?? false),
|
|
FIXTURE_DIRECTORY: JSON.stringify(
|
|
params.supportsScenario === false
|
|
? ["run.sh"]
|
|
: ["run.sh", "legacy-operator-state.mjs", "custom-plugin-siblings.mjs"],
|
|
),
|
|
},
|
|
});
|
|
return {
|
|
result,
|
|
output: existsSync(output)
|
|
? Object.fromEntries(
|
|
readFileSync(output, "utf8")
|
|
.trimEnd()
|
|
.split("\n")
|
|
.map((line) => {
|
|
const split = line.indexOf("=");
|
|
return [line.slice(0, split), line.slice(split + 1)];
|
|
}),
|
|
)
|
|
: {},
|
|
calls: existsSync(calls)
|
|
? readFileSync(calls, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => JSON.parse(line))
|
|
: [],
|
|
};
|
|
}
|
|
|
|
function runNpmTelegramInputValidation(overrides: Record<string, string>) {
|
|
const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e");
|
|
const script = workflowStep(job, "Validate inputs and secrets").run;
|
|
if (!script) {
|
|
throw new Error("Expected npm Telegram input validation script");
|
|
}
|
|
return spawnSync("bash", ["-c", script], {
|
|
encoding: "utf8",
|
|
env: {
|
|
OPENCLAW_QA_CONVEX_SECRET_CI: "test-secret",
|
|
OPENCLAW_QA_CONVEX_SITE_URL: "https://example.invalid",
|
|
PACKAGE_ARTIFACT_DIGEST: "",
|
|
PACKAGE_ARTIFACT_ID: "",
|
|
PACKAGE_ARTIFACT_NAME: "",
|
|
PACKAGE_ARTIFACT_RUN_ATTEMPT: "",
|
|
PACKAGE_ARTIFACT_RUN_ID: "",
|
|
PACKAGE_FILE_NAME: "",
|
|
PACKAGE_SHA256: "",
|
|
PACKAGE_SOURCE_SHA: "",
|
|
PACKAGE_SPEC: "openclaw@beta",
|
|
PACKAGE_VERSION: "",
|
|
PATH: process.env.PATH,
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST: "",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID: "",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME: "",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT: "",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID: "",
|
|
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: "",
|
|
PROVIDER_MODE: "mock-openai",
|
|
...overrides,
|
|
},
|
|
});
|
|
}
|
|
|
|
function runNpmTelegramArtifactValidation(params: {
|
|
currentRunAttempt?: string;
|
|
currentRunId: string;
|
|
producerJobConclusion?: "failure" | "success";
|
|
producerRunId: string;
|
|
producerStatus: "completed" | "in_progress" | "pending" | "queued" | "requested" | "waiting";
|
|
producerConclusion: "failure" | "success" | null;
|
|
}) {
|
|
const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e");
|
|
const script = workflowStep(job, "Validate package artifact identity").run;
|
|
if (!script) {
|
|
throw new Error("Expected npm Telegram artifact identity script");
|
|
}
|
|
const binDir = tempDirs.make("npm-telegram-artifact-gh-");
|
|
const ghPath = `${binDir}/gh`;
|
|
writeFileSync(
|
|
ghPath,
|
|
`#!/bin/sh
|
|
case "$*" in
|
|
*actions/artifacts*) printf '%s\\n' "$MOCK_ARTIFACT_JSON" ;;
|
|
*actions/runs*/jobs*) printf '%s\\n' "$MOCK_JOBS_JSON" ;;
|
|
*actions/runs*) printf '%s\\n' "$MOCK_ATTEMPT_JSON" ;;
|
|
*) exit 2 ;;
|
|
esac
|
|
`,
|
|
);
|
|
chmodSync(ghPath, 0o755);
|
|
const attempt = "2";
|
|
const artifactId = "987";
|
|
const artifactName = `package-under-test-${params.producerRunId}-${attempt}`;
|
|
const digest = "a".repeat(64);
|
|
return spawnSync("bash", ["-c", script], {
|
|
encoding: "utf8",
|
|
env: {
|
|
ARTIFACT_DIGEST: digest,
|
|
ARTIFACT_ID: artifactId,
|
|
ARTIFACT_NAME: artifactName,
|
|
ARTIFACT_RUN_ATTEMPT: attempt,
|
|
ARTIFACT_RUN_ID: params.producerRunId,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ATTEMPT: params.currentRunAttempt ?? attempt,
|
|
GITHUB_RUN_ID: params.currentRunId,
|
|
MOCK_ARTIFACT_JSON: JSON.stringify({
|
|
created_at: "2026-07-15T08:49:20Z",
|
|
digest: `sha256:${digest}`,
|
|
expired: false,
|
|
id: Number(artifactId),
|
|
name: artifactName,
|
|
workflow_run: { id: Number(params.producerRunId) },
|
|
}),
|
|
MOCK_ATTEMPT_JSON: JSON.stringify({
|
|
conclusion: params.producerConclusion,
|
|
id: Number(params.producerRunId),
|
|
run_attempt: Number(attempt),
|
|
run_started_at: "2026-07-15T08:39:00Z",
|
|
status: params.producerStatus,
|
|
updated_at: "2026-07-15T08:49:30Z",
|
|
}),
|
|
MOCK_JOBS_JSON: JSON.stringify({
|
|
jobs: [
|
|
{
|
|
completed_at: "2026-07-15T08:49:30Z",
|
|
conclusion: params.producerJobConclusion ?? "success",
|
|
id: 654,
|
|
name: "Run package acceptance / Resolve package candidate",
|
|
run_attempt: Number(attempt),
|
|
run_id: Number(params.producerRunId),
|
|
started_at: "2026-07-15T08:39:00Z",
|
|
status: "completed",
|
|
},
|
|
],
|
|
}),
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
},
|
|
});
|
|
}
|
|
|
|
function runReleasePublishInputValidation(overrides: Record<string, string>) {
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const script = workflowStep(job, "Validate inputs").run;
|
|
if (!script) {
|
|
throw new Error("Expected release publish input validation script");
|
|
}
|
|
const binDir = tempDirs.make("release-publish-input-gh-");
|
|
writeFileSync(join(binDir, "gh"), '#!/bin/sh\nprintf "%s\\n" "$WORKFLOW_SHA"\n', {
|
|
mode: 0o755,
|
|
});
|
|
const githubOutput = resolve(tempDirs.make("release-publish-inputs-"), "github-output");
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-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: "",
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
PLUGINS: "",
|
|
PLUGIN_PUBLISH_SCOPE: "all-publishable",
|
|
PREFLIGHT_RUN_ID: "111",
|
|
PUBLISH_DOCKER_ONLY: "false",
|
|
PUBLISH_OPENCLAW_NPM: "true",
|
|
RELEASE_NPM_DIST_TAG: "beta",
|
|
RELEASE_PROFILE: "beta",
|
|
RELEASE_TAG: "v2026.7.1-beta.3",
|
|
WINDOWS_NODE_INSTALLER_DIGESTS: "",
|
|
WINDOWS_NODE_TAG: "",
|
|
WORKFLOW_REF: `refs/tags/release-publish/${"a".repeat(12)}-123`,
|
|
WORKFLOW_SHA: "a".repeat(40),
|
|
...overrides,
|
|
},
|
|
});
|
|
return result;
|
|
}
|
|
|
|
function runReleasePublishTagSignatureVerification(params: { tagRef: object; tagObject?: object }) {
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const script = workflowStep(job, "Verify signed release tag").run;
|
|
if (!script) {
|
|
throw new Error("Expected release publish tag signature verification script");
|
|
}
|
|
const binDir = tempDirs.make("release-publish-signature-gh-");
|
|
writeFileSync(
|
|
join(binDir, "gh"),
|
|
`#!/bin/sh
|
|
case "$*" in
|
|
*git/ref/tags/*) printf '%s\\n' "$MOCK_TAG_REF" ;;
|
|
*git/tags/*) printf '%s\\n' "$MOCK_TAG_OBJECT" ;;
|
|
*) exit 2 ;;
|
|
esac
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const runnerTemp = tempDirs.make("release-publish-signature-");
|
|
const githubOutput = resolve(runnerTemp, "github-output");
|
|
const result = spawnSync("bash", ["--noprofile", "--norc", "-c", script], {
|
|
encoding: "utf8",
|
|
env: {
|
|
GITHUB_OUTPUT: githubOutput,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
MOCK_TAG_OBJECT: JSON.stringify(params.tagObject ?? {}),
|
|
MOCK_TAG_REF: JSON.stringify(params.tagRef),
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
RELEASE_TAG: "v2026.9.7",
|
|
RUNNER_TEMP: runnerTemp,
|
|
},
|
|
});
|
|
return { ...result, githubOutput };
|
|
}
|
|
|
|
function runReleaseTagTargetVerification(params: {
|
|
directSha: string;
|
|
peeledSha: string;
|
|
expectedTagObjectSha: string;
|
|
}) {
|
|
const helper = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
const verification = shellFunctionSource(helper, "verify_release_tag_target");
|
|
const remoteRefs = [
|
|
`${params.directSha}\trefs/tags/v2026.9.7`,
|
|
`${params.peeledSha}\trefs/tags/v2026.9.7^{}`,
|
|
].join("\n");
|
|
return spawnSync(
|
|
"bash",
|
|
[
|
|
"--noprofile",
|
|
"--norc",
|
|
"-c",
|
|
`git() { printf '%s\\n' "$REMOTE_REFS"; }\n${verification}\nverify_release_tag_target`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
RELEASE_TAG: "v2026.9.7",
|
|
REMOTE_REFS: remoteRefs,
|
|
SIGNED_RELEASE_TAG_OBJECT_SHA: params.expectedTagObjectSha,
|
|
TARGET_SHA: params.peeledSha,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function runReleasePublishChildWorkflowRef(overrides: Record<string, string> = {}) {
|
|
const script = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
const resolveChildRef = shellFunctionSource(script, "resolve_child_workflow_ref");
|
|
return spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`gh() { echo unexpected-github-lookup >&2; return 64; }\n${resolveChildRef}\nresolve_child_workflow_ref "$WORKFLOW_FULL_REF"`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
WORKFLOW_FULL_REF: "refs/heads/main",
|
|
...overrides,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function runOpenClawNpmTrustedRefGuard(overrides: Record<string, string>) {
|
|
const job = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "validate_publish_request");
|
|
const script = workflowStep(job, "Require trusted workflow ref for publish").run;
|
|
if (!script) {
|
|
throw new Error("Expected OpenClaw npm trusted ref guard");
|
|
}
|
|
const binDir = tempDirs.make("openclaw-npm-trusted-ref-");
|
|
const ghPath = `${binDir}/gh`;
|
|
const gitPath = `${binDir}/git`;
|
|
const timeoutPath = `${binDir}/timeout`;
|
|
writeFileSync(ghPath, `#!/bin/sh\nprintf '%s\\n' "\${MOCK_REMOTE_TAG_SHA}"\n`);
|
|
chmodSync(ghPath, 0o755);
|
|
writeFileSync(
|
|
gitPath,
|
|
`#!/bin/sh\nif [ "$1" = "fetch" ]; then exit 0; fi\nif [ "$1" = "merge-base" ]; then [ "\${MOCK_WORKFLOW_ANCESTOR}" = "true" ]; exit $?; fi\nexit 2\n`,
|
|
);
|
|
chmodSync(gitPath, 0o755);
|
|
writeFileSync(
|
|
timeoutPath,
|
|
`#!/bin/sh\n[ "$1" = "--signal=TERM" ] && [ "$2" = "--kill-after=10s" ] && [ "$3" = "120s" ] || exit 2\nshift 3\nexec "$@"\n`,
|
|
);
|
|
chmodSync(timeoutPath, 0o755);
|
|
return spawnSync("bash", ["--noprofile", "--norc", "-c", script], {
|
|
encoding: "utf8",
|
|
env: {
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
MOCK_REMOTE_TAG_SHA: "a".repeat(40),
|
|
MOCK_WORKFLOW_ANCESTOR: "true",
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
RELEASE_NPM_DIST_TAG: "beta",
|
|
RELEASE_TAG: "v2026.7.2-beta.1",
|
|
WORKFLOW_REF: "refs/heads/release/2026.7.2",
|
|
WORKFLOW_SHA: "a".repeat(40),
|
|
...overrides,
|
|
},
|
|
});
|
|
}
|
|
|
|
function runPluginNpmPreflightToolingGuard(overrides: Record<string, string>) {
|
|
const job = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm");
|
|
const script = workflowStep(job, "Verify trusted preflight or recovery tooling identity").run;
|
|
if (!script) {
|
|
throw new Error("Expected plugin npm preflight tooling identity guard");
|
|
}
|
|
const workdir = tempDirs.make("plugin-npm-preflight-tooling-");
|
|
const binDir = resolve(workdir, "bin");
|
|
const toolingDir = resolve(workdir, ".release-tooling/scripts");
|
|
const toolingLibDir = resolve(toolingDir, "lib");
|
|
mkdirSync(binDir, { recursive: true });
|
|
mkdirSync(toolingLibDir, { recursive: true });
|
|
writeFileSync(
|
|
resolve(toolingDir, "release-tooling-identity.mjs"),
|
|
readFileSync(resolve(REPO_ROOT, "scripts/release-tooling-identity.mjs")),
|
|
);
|
|
writeFileSync(
|
|
resolve(toolingLibDir, "record-shared.mjs"),
|
|
readFileSync(resolve(REPO_ROOT, "scripts/lib/record-shared.mjs")),
|
|
);
|
|
writeFileSync(
|
|
resolve(binDir, "gh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
[[ "$1" == "api" ]] || exit 64
|
|
case "$2" in
|
|
*/git/ref/tags/*)
|
|
[[ "$MOCK_TAG_MISSING" != "true" ]] || exit 1
|
|
jq -cn \
|
|
--arg ref "$MOCK_TAG_FULL_REF" \
|
|
--arg sha "$MOCK_TAG_SHA" \
|
|
--arg type "$MOCK_TAG_TYPE" \
|
|
'{ref: $ref, object: {sha: $sha, type: $type}}'
|
|
;;
|
|
*/compare/*)
|
|
jq -cn --arg status "$MOCK_COMPARE_STATUS" '{status: $status}'
|
|
;;
|
|
*)
|
|
exit 64
|
|
;;
|
|
esac
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
return spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
MOCK_COMPARE_STATUS: "identical",
|
|
MOCK_TAG_FULL_REF: "",
|
|
MOCK_TAG_MISSING: "false",
|
|
MOCK_TAG_SHA: "",
|
|
MOCK_TAG_TYPE: "commit",
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
...overrides,
|
|
},
|
|
});
|
|
}
|
|
|
|
type ProtectedPreflightConsumerParams = {
|
|
currentRef: string;
|
|
currentWorkflowSha: string;
|
|
fullReleasePreflight?: boolean;
|
|
independentProducer?: boolean;
|
|
fullReleaseRunId?: string;
|
|
fullReleaseRunAttempt?: string;
|
|
npmDistTag?: string;
|
|
expectedExtendedStableBranch?: string;
|
|
toolingAncestor?: boolean;
|
|
liveTagSha?: string;
|
|
preflightHeadBranch: string;
|
|
preflightHeadSha: string;
|
|
producerBranches?: unknown[];
|
|
producerTagSha?: string;
|
|
producerTagType?: string;
|
|
mainComparisonStatus?: string;
|
|
publisherComparisonStatus?: string;
|
|
preflightArtifactName?: string;
|
|
additionalPreflightArtifactNames?: string[];
|
|
archiveFailureStatus?: number;
|
|
repeatDownload?: boolean;
|
|
};
|
|
|
|
function writePreflightConsumerTooling(toolingDir: string) {
|
|
mkdirSync(resolve(toolingDir, "lib"), { recursive: true });
|
|
for (const source of [
|
|
"npm-preflight-tooling-identity.mjs",
|
|
"npm-prepared-bundle.mjs",
|
|
"openclaw-npm-extended-stable-release.mjs",
|
|
"release-tooling-identity.mjs",
|
|
"lib/actions-artifact-archive.mjs",
|
|
"lib/npm-core-release-packages.mjs",
|
|
"lib/npm-core-release-packages.json",
|
|
"lib/npm-shrinkwrap-dependencies.mjs",
|
|
"lib/record-shared.mjs",
|
|
"lib/release-version.mjs",
|
|
]) {
|
|
copyFileSync(resolve(REPO_ROOT, "scripts", source), resolve(toolingDir, source));
|
|
}
|
|
}
|
|
|
|
const PREFLIGHT_PRODUCER_GH_CASES = `
|
|
case "$2" in
|
|
*/actions/runs/*/attempts/*/jobs*)
|
|
printf '%s\\n' "$MOCK_QUALIFIED_JOBS"
|
|
exit 0
|
|
;;
|
|
*/actions/runs/*/attempts/*)
|
|
printf '%s\\n' "$MOCK_QUALIFIED_RUN"
|
|
exit 0
|
|
;;
|
|
*/actions/runs/"\${MOCK_QUALIFIED_RUN_ID:-}")
|
|
printf '%s\\n' "$MOCK_QUALIFIED_RUN"
|
|
exit 0
|
|
;;
|
|
*/actions/artifacts/555)
|
|
printf '%s\\n' "$MOCK_QUALIFIED_ARTIFACT"
|
|
exit 0
|
|
;;
|
|
*/contents/scripts/*)
|
|
source="\${2#*/contents/scripts/}"
|
|
source="\${source%%\\?*}"
|
|
base64 < "$MOCK_REPO_ROOT/scripts/$source"
|
|
exit 0
|
|
;;
|
|
*/git/ref/tags/*)
|
|
if [[ "$*" == *"--jq .object"* ]]; then
|
|
printf '%s\\n' "$MOCK_REMOTE_TAG_SHA"
|
|
else
|
|
printf '%s\\n' "$MOCK_PRODUCER_TAG"
|
|
fi
|
|
exit 0
|
|
;;
|
|
*/git/matching-refs/heads/*)
|
|
printf '%s\\n' "$MOCK_PRODUCER_BRANCHES"
|
|
exit 0
|
|
;;
|
|
*/compare/*)
|
|
if [[ "$2" == *"...main" ]]; then
|
|
printf '{"status":"%s"}\\n' "$MOCK_MAIN_COMPARISON"
|
|
else
|
|
printf '{"status":"%s"}\\n' "$MOCK_PUBLISHER_COMPARISON"
|
|
fi
|
|
exit 0
|
|
;;
|
|
esac
|
|
`;
|
|
|
|
function preflightProducerFixtureEnv(params: ProtectedPreflightConsumerParams) {
|
|
return {
|
|
MOCK_REPO_ROOT: REPO_ROOT,
|
|
MOCK_PRODUCER_TAG: JSON.stringify({
|
|
ref: `refs/tags/${params.preflightHeadBranch}`,
|
|
object: {
|
|
sha: params.producerTagSha ?? params.preflightHeadSha,
|
|
type: params.producerTagType ?? "commit",
|
|
},
|
|
}),
|
|
MOCK_PRODUCER_BRANCHES: JSON.stringify(params.producerBranches ?? []),
|
|
MOCK_MAIN_COMPARISON: params.mainComparisonStatus ?? "ahead",
|
|
MOCK_PUBLISHER_COMPARISON: params.publisherComparisonStatus ?? "ahead",
|
|
};
|
|
}
|
|
|
|
async function qualifiedPreflightConsumerFixture(
|
|
workdir: string,
|
|
params: ProtectedPreflightConsumerParams,
|
|
) {
|
|
if (!params.fullReleasePreflight) {
|
|
return {};
|
|
}
|
|
const runId = params.independentProducer ? "333" : "111";
|
|
const workflowPath = params.independentProducer
|
|
? FULL_RELEASE_ARTIFACTS_WORKFLOW
|
|
: FULL_RELEASE_VALIDATION_WORKFLOW;
|
|
const fullRef = `refs/${params.preflightHeadBranch.startsWith("release-publish/") ? "tags" : "heads"}/${params.preflightHeadBranch}`;
|
|
const producer = {
|
|
repository: "openclaw/openclaw",
|
|
workflowRef: `openclaw/openclaw/${workflowPath}@${fullRef}`,
|
|
workflowSha: params.preflightHeadSha,
|
|
runId,
|
|
runAttempt: "1",
|
|
jobId: "999",
|
|
jobName: "Prepare release npm artifacts / Qualify prepared npm package",
|
|
producerWorkflowPath: OPENCLAW_NPM_PREFLIGHT_WORKFLOW,
|
|
};
|
|
const manifest = Buffer.from(
|
|
JSON.stringify({ version: 3, releaseSha: "d".repeat(40), producer }),
|
|
);
|
|
const zip = new JSZip();
|
|
zip.file("preflight-manifest.json", manifest);
|
|
const archive = await zip.generateAsync({
|
|
type: "nodebuffer",
|
|
compression: "STORE",
|
|
platform: "UNIX",
|
|
});
|
|
const artifact = {
|
|
id: "555",
|
|
name: params.preflightArtifactName ?? "openclaw-npm-preflight-v2026.8.1-beta.3",
|
|
digest: createHash("sha256").update(archive).digest("hex"),
|
|
runId,
|
|
runAttempt: "1",
|
|
};
|
|
const fullManifest = {
|
|
workflowName: "Full Release Validation",
|
|
runId: "111",
|
|
runAttempt: params.independentProducer ? "3" : "1",
|
|
workflowFullRef: fullRef,
|
|
targetSha: "d".repeat(40),
|
|
workflowSha: params.preflightHeadSha,
|
|
publicationArtifacts: {
|
|
npmPreflight: {
|
|
schema: "openclaw.qualified-npm-preflight/v1",
|
|
source: { sha: "d".repeat(40) },
|
|
producer,
|
|
artifact,
|
|
manifestSha256: createHash("sha256").update(manifest).digest("hex"),
|
|
},
|
|
},
|
|
};
|
|
for (const directory of ["runner/full-release-validation-manifest", "full-release-validation"]) {
|
|
mkdirSync(resolve(workdir, directory), { recursive: true });
|
|
writeFileSync(
|
|
resolve(workdir, directory, "full-release-validation-manifest.json"),
|
|
JSON.stringify(fullManifest),
|
|
);
|
|
}
|
|
const artifactMetadata = {
|
|
...artifact,
|
|
id: 555,
|
|
digest: `sha256:${artifact.digest}`,
|
|
size_in_bytes: archive.length,
|
|
expired: false,
|
|
expires_at: "2099-10-01T00:00:00Z",
|
|
workflow_run: { id: Number(runId), head_sha: params.preflightHeadSha },
|
|
};
|
|
const archivePath = resolve(workdir, "qualified.zip");
|
|
writeFileSync(archivePath, archive);
|
|
const preload = resolve(workdir, "artifact-fetch.mjs");
|
|
writeFileSync(
|
|
preload,
|
|
`import { readFileSync } from 'node:fs';
|
|
globalThis.fetch = async (url) => {
|
|
if (url === 'https://api.github.com/repos/openclaw/openclaw/actions/artifacts/555') return Response.json(JSON.parse(process.env.MOCK_QUALIFIED_ARTIFACT));
|
|
if (url === 'https://api.github.com/repos/openclaw/openclaw/actions/artifacts/555/zip') return new Response(new Uint8Array(readFileSync(process.env.MOCK_QUALIFIED_ARCHIVE)));
|
|
throw new Error('Unexpected fixture network request: ' + url);
|
|
};\n`,
|
|
);
|
|
return {
|
|
GH_TOKEN: "test-artifact-token",
|
|
NODE_OPTIONS: `--import=${pathToFileURL(preload).href}`,
|
|
MOCK_QUALIFIED_ARCHIVE: archivePath,
|
|
MOCK_QUALIFIED_ARTIFACT: JSON.stringify(artifactMetadata),
|
|
MOCK_QUALIFIED_RUN_ID: runId,
|
|
MOCK_QUALIFIED_RUN: JSON.stringify({
|
|
id: Number(runId),
|
|
run_attempt: 1,
|
|
path: `${workflowPath}@${fullRef}`,
|
|
head_sha: params.preflightHeadSha,
|
|
head_branch: params.preflightHeadBranch,
|
|
event: "workflow_dispatch",
|
|
status: "completed",
|
|
conclusion: "success",
|
|
repository: { full_name: "openclaw/openclaw" },
|
|
head_repository: { full_name: "openclaw/openclaw" },
|
|
}),
|
|
MOCK_QUALIFIED_JOBS: JSON.stringify({
|
|
total_count: 1,
|
|
jobs: [
|
|
{
|
|
id: 999,
|
|
name: producer.jobName,
|
|
run_id: Number(runId),
|
|
run_attempt: 1,
|
|
head_sha: params.preflightHeadSha,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
}
|
|
|
|
async function runReleasePublishPreflightConsumerGuard(params: ProtectedPreflightConsumerParams) {
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const script = workflowStep(job, "Download OpenClaw npm preflight manifest").run;
|
|
if (!script) {
|
|
throw new Error("Expected release publish preflight consumer guard");
|
|
}
|
|
const workdir = tempDirs.make("release-publish-preflight-consumer-");
|
|
const binDir = resolve(workdir, "bin");
|
|
const runnerTemp = resolve(workdir, "runner");
|
|
mkdirSync(binDir);
|
|
mkdirSync(runnerTemp);
|
|
const qualificationEnv = await qualifiedPreflightConsumerFixture(workdir, params);
|
|
writePreflightConsumerTooling(resolve(workdir, ".release-validation-tooling/scripts"));
|
|
const preflightName = params.preflightArtifactName ?? "openclaw-npm-preflight-v2026.8.1-beta.3";
|
|
const producerRun = qualificationEnv.MOCK_QUALIFIED_RUN
|
|
? JSON.parse(qualificationEnv.MOCK_QUALIFIED_RUN)
|
|
: { id: 111, run_attempt: 1 };
|
|
const archives = [];
|
|
if (qualificationEnv.MOCK_QUALIFIED_ARCHIVE && qualificationEnv.MOCK_QUALIFIED_ARTIFACT) {
|
|
archives.push({
|
|
bytes: readFileSync(qualificationEnv.MOCK_QUALIFIED_ARCHIVE).toString("base64"),
|
|
metadata: JSON.parse(qualificationEnv.MOCK_QUALIFIED_ARTIFACT),
|
|
});
|
|
}
|
|
for (const [name, files] of [
|
|
...(params.fullReleasePreflight
|
|
? []
|
|
: [[preflightName, ["preflight-manifest.json", "dependency-evidence/proof.json"]]]),
|
|
[
|
|
`plugin-sdk-api-release-diff-${producerRun.id}-${producerRun.run_attempt}`,
|
|
["plugin-sdk-api-release-diff.json", "plugin-sdk-api-release-evidence.json"],
|
|
],
|
|
...(params.additionalPreflightArtifactNames ?? []).map((artifactName) => [
|
|
artifactName,
|
|
["preflight-manifest.json"],
|
|
]),
|
|
] as [string, string[]][]) {
|
|
const zip = new JSZip();
|
|
for (const file of files) {
|
|
zip.file(file, JSON.stringify({ fixture: file }), { createFolders: false });
|
|
}
|
|
const archive = await zip.generateAsync({
|
|
type: "nodebuffer",
|
|
compression: "STORE",
|
|
platform: "UNIX",
|
|
});
|
|
archives.push({
|
|
bytes: archive.toString("base64"),
|
|
metadata: {
|
|
id: 301 + archives.length,
|
|
name,
|
|
digest: `sha256:${createHash("sha256").update(archive).digest("hex")}`,
|
|
size_in_bytes: archive.length,
|
|
expired: false,
|
|
expires_at: "2099-10-01T00:00:00Z",
|
|
workflow_run: { id: producerRun.id, head_sha: params.preflightHeadSha },
|
|
},
|
|
});
|
|
}
|
|
const fixturePath = resolve(workdir, "artifacts.json");
|
|
const requestLog = resolve(workdir, "requests.txt");
|
|
writeFileSync(fixturePath, JSON.stringify(archives));
|
|
const fetchMock = resolve(workdir, "artifact-fetch.mjs");
|
|
writeFileSync(
|
|
fetchMock,
|
|
`import { appendFileSync, readFileSync } from 'node:fs';
|
|
const archives = JSON.parse(readFileSync(process.env.MOCK_ARTIFACT_FIXTURE, 'utf8'));
|
|
globalThis.fetch = async (input) => {
|
|
const url = String(input);
|
|
appendFileSync(process.env.MOCK_REQUEST_LOG, url + '\\n');
|
|
if (url.endsWith('/attempts/1')) return Response.json(JSON.parse(process.env.MOCK_PREFLIGHT_RUN));
|
|
const artifact = archives.find((entry) => url.endsWith('/artifacts/' + entry.metadata.id) || url.endsWith('/artifacts/' + entry.metadata.id + '/zip'));
|
|
if (!artifact) throw new Error('Unexpected artifact request: ' + url);
|
|
if (!url.endsWith('/zip')) return Response.json(artifact.metadata);
|
|
if (process.env.MOCK_ARCHIVE_FAILURE_STATUS) return new Response(null, { status: Number(process.env.MOCK_ARCHIVE_FAILURE_STATUS) });
|
|
return new Response(Buffer.from(artifact.bytes, 'base64'));
|
|
};
|
|
`,
|
|
);
|
|
writeFileSync(
|
|
resolve(binDir, "gh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$1" == "run" && "$2" == "download" ]]; then
|
|
exit 0
|
|
fi
|
|
if [[ "$1" == "api" ]]; then
|
|
${PREFLIGHT_PRODUCER_GH_CASES}
|
|
if [[ "$2" == *"/artifacts?"* ]]; then
|
|
printf '%s\\n' "$MOCK_PREFLIGHT_ARTIFACTS"
|
|
exit 0
|
|
fi
|
|
printf '%s\\n' "$MOCK_PREFLIGHT_RUN"
|
|
exit 0
|
|
fi
|
|
exit 64
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const env = {
|
|
...preflightProducerFixtureEnv(params),
|
|
...qualificationEnv,
|
|
EXPECTED_SHA: "d".repeat(40),
|
|
GH_TOKEN: "synthetic-token",
|
|
MOCK_ARTIFACT_FIXTURE: fixturePath,
|
|
MOCK_REQUEST_LOG: requestLog,
|
|
MOCK_ARCHIVE_FAILURE_STATUS: String(params.archiveFailureStatus ?? ""),
|
|
MOCK_PREFLIGHT_ARTIFACTS: JSON.stringify([
|
|
{ total_count: archives.length, artifacts: archives.map((entry) => entry.metadata) },
|
|
]),
|
|
NODE_OPTIONS: `--import=${pathToFileURL(fetchMock).href}`,
|
|
GITHUB_OUTPUT: resolve(workdir, "github-output"),
|
|
GITHUB_REF: params.currentRef,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_WORKSPACE: workdir,
|
|
FULL_RELEASE_VALIDATION_RUN_ID:
|
|
params.fullReleaseRunId ?? (params.fullReleasePreflight ? "111" : "222"),
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT:
|
|
params.fullReleaseRunAttempt ?? (params.independentProducer ? "3" : "1"),
|
|
MOCK_PREFLIGHT_RUN: JSON.stringify({
|
|
id: 111,
|
|
repository: { full_name: "openclaw/openclaw" },
|
|
head_repository: { full_name: "openclaw/openclaw" },
|
|
status: "completed",
|
|
conclusion: "success",
|
|
event: "workflow_dispatch",
|
|
head_branch: params.preflightHeadBranch,
|
|
head_sha: params.preflightHeadSha,
|
|
path: params.fullReleasePreflight
|
|
? ".github/workflows/full-release-validation.yml"
|
|
: ".github/workflows/openclaw-npm-release.yml",
|
|
run_attempt: 1,
|
|
}),
|
|
PATH: `${binDir}:${dirname(process.execPath)}:${process.env.PATH}`,
|
|
PREFLIGHT_RUN_ID: "111",
|
|
RELEASE_NPM_DIST_TAG: params.npmDistTag ?? "beta",
|
|
RELEASE_TAG: "v2026.8.1-beta.3",
|
|
RUNNER_TEMP: runnerTemp,
|
|
WORKFLOW_SHA: params.currentWorkflowSha,
|
|
};
|
|
const run = () =>
|
|
spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env,
|
|
});
|
|
const result = run();
|
|
const repeated = params.repeatDownload && result.status === 0 ? run() : result;
|
|
return {
|
|
...repeated,
|
|
outputPath: env.GITHUB_OUTPUT,
|
|
outputs: existsSync(env.GITHUB_OUTPUT) ? readFileSync(env.GITHUB_OUTPUT, "utf8") : "",
|
|
requests: existsSync(requestLog) ? readFileSync(requestLog, "utf8").trim().split("\n") : [],
|
|
runnerTemp,
|
|
};
|
|
}
|
|
|
|
async function runOpenClawNpmPreflightConsumerGuard(params: ProtectedPreflightConsumerParams) {
|
|
const job = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm");
|
|
const script = workflowStep(job, "Verify preflight run metadata").run;
|
|
if (!script) {
|
|
throw new Error("Expected OpenClaw npm preflight consumer guard");
|
|
}
|
|
const workdir = tempDirs.make("openclaw-npm-preflight-consumer-");
|
|
const binDir = resolve(workdir, "bin");
|
|
mkdirSync(binDir);
|
|
const qualificationEnv = await qualifiedPreflightConsumerFixture(workdir, params);
|
|
const toolingDir = resolve(workdir, "trusted-workflow/scripts");
|
|
writePreflightConsumerTooling(toolingDir);
|
|
writeFileSync(
|
|
resolve(binDir, "gh"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$1" == "run" && "$2" == "view" ]]; then
|
|
printf '%s\\n' "$MOCK_PREFLIGHT_RUN"
|
|
exit 0
|
|
fi
|
|
if [[ "$1" == "api" ]]; then
|
|
${PREFLIGHT_PRODUCER_GH_CASES}
|
|
printf '%s\\n' "$MOCK_PREFLIGHT_METADATA"
|
|
exit 0
|
|
fi
|
|
exit 64
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(
|
|
resolve(binDir, "git"),
|
|
`#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ "$*" == "rev-parse HEAD" ]]; then
|
|
printf '%s\\n' "$MOCK_RELEASE_SHA"
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"merge-base --is-ancestor"* ]]; then
|
|
[[ "$MOCK_TOOLING_ANCESTOR" == "true" ]]
|
|
exit
|
|
fi
|
|
if [[ "$*" == *"cat-file -e"* || "$*" == *" fetch "* ]]; then
|
|
exit 0
|
|
fi
|
|
exit 64
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
return spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
...preflightProducerFixtureEnv(params),
|
|
...qualificationEnv,
|
|
EXPECTED_EXTENDED_STABLE_BRANCH: params.expectedExtendedStableBranch ?? "",
|
|
FULL_RELEASE_VALIDATION_RUN_ID:
|
|
params.fullReleaseRunId ?? (params.fullReleasePreflight ? "111" : "222"),
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT:
|
|
params.fullReleaseRunAttempt ?? (params.independentProducer ? "3" : "1"),
|
|
GITHUB_OUTPUT: resolve(workdir, "github-output"),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
MOCK_PREFLIGHT_RUN: JSON.stringify({
|
|
databaseId: 111,
|
|
attempt: 1,
|
|
status: "completed",
|
|
conclusion: "success",
|
|
event: "workflow_dispatch",
|
|
headBranch: params.preflightHeadBranch,
|
|
headSha: params.preflightHeadSha,
|
|
url: "https://github.com/openclaw/openclaw/actions/runs/111",
|
|
workflowName: params.fullReleasePreflight
|
|
? "Full Release Validation"
|
|
: "OpenClaw NPM Release",
|
|
}),
|
|
MOCK_PREFLIGHT_METADATA: JSON.stringify({
|
|
run_attempt: 1,
|
|
path: params.fullReleasePreflight
|
|
? ".github/workflows/full-release-validation.yml"
|
|
: ".github/workflows/openclaw-npm-release.yml",
|
|
}),
|
|
MOCK_TOOLING_ANCESTOR: String(params.toolingAncestor !== false),
|
|
MOCK_RELEASE_SHA: "d".repeat(40),
|
|
MOCK_REMOTE_TAG_SHA: params.liveTagSha ?? params.currentWorkflowSha,
|
|
PATH: `${binDir}:${dirname(process.execPath)}:${process.env.PATH}`,
|
|
PREFLIGHT_RUN_ID: "111",
|
|
RELEASE_NPM_DIST_TAG: params.npmDistTag ?? "beta",
|
|
RUN_KIND: "preflight",
|
|
WORKFLOW_REF: params.currentRef,
|
|
WORKFLOW_SHA: params.currentWorkflowSha,
|
|
},
|
|
});
|
|
}
|
|
|
|
type ReleaseCheckArtifact = {
|
|
expired: boolean;
|
|
id: number;
|
|
name: string;
|
|
workflow_run: { id: number };
|
|
};
|
|
|
|
type ReleaseCheckArtifactPair = {
|
|
job: string;
|
|
payloadBase: string;
|
|
statusBase: string;
|
|
variant?: string;
|
|
};
|
|
|
|
type ResolvedReleaseCheckArtifact = {
|
|
job: string;
|
|
payload_id: number;
|
|
payload_name: string;
|
|
producer_attempt: number;
|
|
run_id: string;
|
|
status_id: number;
|
|
status_name: string;
|
|
target_sha: string;
|
|
variant: string;
|
|
};
|
|
|
|
function releaseCheckArtifact(params: {
|
|
expired?: boolean;
|
|
id: number;
|
|
name: string;
|
|
runId?: string;
|
|
}): ReleaseCheckArtifact {
|
|
return {
|
|
expired: params.expired ?? false,
|
|
id: params.id,
|
|
name: params.name,
|
|
workflow_run: { id: Number(params.runId ?? "123456") },
|
|
};
|
|
}
|
|
|
|
function runReleaseCheckArtifactResolve(params: {
|
|
artifacts: ReleaseCheckArtifact[];
|
|
consumerAttempt: string;
|
|
pairs: ReleaseCheckArtifactPair[];
|
|
runId?: string;
|
|
targetSha?: string;
|
|
}) {
|
|
const workdir = tempDirs.make("release-check-artifact-resolver-");
|
|
const binDir = resolve(workdir, "bin");
|
|
mkdirSync(binDir, { recursive: true });
|
|
const ghPath = resolve(binDir, "gh");
|
|
writeFileSync(ghPath, "#!/bin/sh\nprintf '%s\\n' \"$MOCK_ARTIFACT_RESPONSE\"\n");
|
|
chmodSync(ghPath, 0o755);
|
|
const runId = params.runId ?? "123456";
|
|
const targetSha = params.targetSha ?? "a".repeat(40);
|
|
const selectionFile = resolve(workdir, "selection.json");
|
|
const githubOutput = resolve(workdir, "github-output");
|
|
const args = [
|
|
resolve(REPO_ROOT, RELEASE_CHECK_ARTIFACT_RESOLVER),
|
|
"resolve",
|
|
"--repository",
|
|
"openclaw/openclaw",
|
|
"--run-id",
|
|
runId,
|
|
"--consumer-attempt",
|
|
params.consumerAttempt,
|
|
"--target-sha",
|
|
targetSha,
|
|
];
|
|
for (const pair of params.pairs) {
|
|
args.push(
|
|
"--pair",
|
|
[pair.job, pair.variant ?? "", pair.statusBase, pair.payloadBase].join("|"),
|
|
);
|
|
}
|
|
args.push("--selection-file", selectionFile, "--github-output", githubOutput);
|
|
const result = spawnSync("bash", args, {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
MOCK_ARTIFACT_RESPONSE: JSON.stringify({ artifacts: params.artifacts }),
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
},
|
|
});
|
|
const selection =
|
|
result.status === 0
|
|
? (JSON.parse(readFileSync(selectionFile, "utf8")) as ResolvedReleaseCheckArtifact[])
|
|
: [];
|
|
return { result, selection, selectionFile, targetSha, workdir };
|
|
}
|
|
|
|
function releaseCheckStatusText(
|
|
selection: ResolvedReleaseCheckArtifact,
|
|
status: "cancelled" | "failure" | "skipped" | "success" = "success",
|
|
): string {
|
|
return [
|
|
`run_id=${selection.run_id}`,
|
|
`run_attempt=${selection.producer_attempt}`,
|
|
`target_sha=${selection.target_sha}`,
|
|
`job=${selection.job}`,
|
|
`variant=${selection.variant}`,
|
|
`status=${status}`,
|
|
"job_status=success",
|
|
"step_outcomes=success",
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
function runReleaseCheckArtifactValidation(params: {
|
|
selection: ResolvedReleaseCheckArtifact[];
|
|
statusText?: (selection: ResolvedReleaseCheckArtifact) => string;
|
|
}) {
|
|
const workdir = tempDirs.make("release-check-artifact-validation-");
|
|
const selectionFile = resolve(workdir, "selection.json");
|
|
const statusDir = resolve(workdir, "statuses");
|
|
const validatedFile = resolve(workdir, "validated.json");
|
|
mkdirSync(statusDir, { recursive: true });
|
|
writeFileSync(selectionFile, JSON.stringify(params.selection));
|
|
for (const selection of params.selection) {
|
|
const variant = selection.variant ? `-${selection.variant}` : "";
|
|
writeFileSync(
|
|
resolve(
|
|
statusDir,
|
|
`${selection.job}${variant}-${selection.run_id}-${selection.producer_attempt}.env`,
|
|
),
|
|
params.statusText?.(selection) ?? releaseCheckStatusText(selection),
|
|
);
|
|
}
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
resolve(REPO_ROOT, RELEASE_CHECK_ARTIFACT_RESOLVER),
|
|
"validate",
|
|
"--selection-file",
|
|
selectionFile,
|
|
"--status-dir",
|
|
statusDir,
|
|
"--validated-file",
|
|
validatedFile,
|
|
],
|
|
{
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: { PATH: process.env.PATH },
|
|
},
|
|
);
|
|
const validated =
|
|
result.status === 0
|
|
? (JSON.parse(readFileSync(validatedFile, "utf8")) as Array<
|
|
ResolvedReleaseCheckArtifact & { status: string }
|
|
>)
|
|
: [];
|
|
return { result, validated };
|
|
}
|
|
|
|
function runReleaseChecksSummary(params: {
|
|
currentAttempt: string;
|
|
currentResult: "cancelled" | "failure" | "skipped" | "success";
|
|
discordResult?: "failure" | "skipped" | "success";
|
|
phase?: "all" | "candidate" | "independent";
|
|
resolveResult?: "failure" | "success";
|
|
resultOverrides?: Record<string, "cancelled" | "failure" | "skipped" | "success">;
|
|
telegramSelected?: boolean;
|
|
telegramIdentityVerified?: boolean;
|
|
validatedStatuses?: Array<{ job: string; status: string; variant: string }>;
|
|
workflowRef?: string;
|
|
}) {
|
|
const summary = workflowJob(RELEASE_CHECKS_WORKFLOW, "summary");
|
|
const script = workflowStep(summary, "Verify release check results").run;
|
|
if (!script) {
|
|
throw new Error("Expected release checks summary script");
|
|
}
|
|
const runId = "123456";
|
|
const targetSha = "a".repeat(40);
|
|
const workdir = tempDirs.make("openclaw-release-check-status-");
|
|
const selectionDir = resolve(workdir, ".artifacts/release-check-selection");
|
|
mkdirSync(selectionDir, { recursive: true });
|
|
writeFileSync(
|
|
resolve(selectionDir, "advisory-evidence-validated.json"),
|
|
JSON.stringify(params.validatedStatuses ?? []),
|
|
);
|
|
return spawnSync("bash", ["-c", script], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
CROSS_OS_RELEASE_CHECKS_RESULT: "success",
|
|
DOCKER_E2E_RELEASE_CHECKS_RESULT: "success",
|
|
GITHUB_RUN_ATTEMPT: params.currentAttempt,
|
|
GITHUB_RUN_ID: runId,
|
|
INSTALL_SMOKE_RELEASE_CHECKS_RESULT: "success",
|
|
LIVE_REPO_E2E_RELEASE_CHECKS_RESULT: "success",
|
|
MATURITY_SCORECARD_RELEASE_CHECKS_RESULT: "skipped",
|
|
PACKAGE_ACCEPTANCE_RELEASE_CHECKS_RESULT: "success",
|
|
PATH: process.env.PATH,
|
|
PREPARE_RELEASE_PACKAGE_RESULT: "success",
|
|
QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT: "skipped",
|
|
QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT: "skipped",
|
|
QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT: "skipped",
|
|
QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT: "skipped",
|
|
QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT: params.discordResult ?? "skipped",
|
|
QA_LIVE_RELEASE_CHECKS_RESULT: "skipped",
|
|
QA_LIVE_SLACK_RELEASE_CHECKS_RESULT: "skipped",
|
|
QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT: params.currentResult,
|
|
QA_LIVE_TELEGRAM_SELECTED: String(params.telegramSelected ?? true),
|
|
QA_LIVE_TELEGRAM_IDENTITY_VERIFIED: String(params.telegramIdentityVerified ?? true),
|
|
QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT: "skipped",
|
|
RELEASE_CHECK_RUN_ATTEMPT: params.currentAttempt,
|
|
RELEASE_CHECK_RUN_ID: runId,
|
|
RELEASE_CHECK_TARGET_SHA: targetSha,
|
|
RELEASE_PHASE: params.phase ?? "all",
|
|
RESOLVE_ADVISORY_EVIDENCE_OUTCOME: "success",
|
|
RESOLVE_TARGET_RESULT: params.resolveResult ?? "success",
|
|
RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT: "skipped",
|
|
VALIDATE_ADVISORY_STATUSES_OUTCOME: "success",
|
|
WORKFLOW_REF: params.workflowRef ?? "refs/heads/release/2026.7.1",
|
|
...params.resultOverrides,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("package acceptance workflow", () => {
|
|
it("keeps manual frozen-target adaptation explicit and forwards the reusable contract", () => {
|
|
const workflow = readWorkflow(PACKAGE_ACCEPTANCE_WORKFLOW);
|
|
const name = "allow_frozen_target_scenario_omissions";
|
|
const input = workflow.on?.workflow_dispatch?.inputs?.[name];
|
|
expect(input).toEqual(workflow.on?.workflow_call?.inputs?.[name]);
|
|
expect(input).toMatchObject({ required: false, default: false, type: "boolean" });
|
|
for (const job of ["docker_acceptance", "docker_acceptance_registry", "package_telegram"]) {
|
|
expect(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, job).with?.[name]).toBe(
|
|
"${{ inputs.allow_frozen_target_scenario_omissions || false }}",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("forwards sealed publication inputs through the canonical publish dispatch", () => {
|
|
const workflow = readWorkflow(RELEASE_PUBLISH_WORKFLOW);
|
|
const input = workflow.on?.workflow_dispatch?.inputs?.plugin_sdk_api_acknowledgement;
|
|
const resolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const downloadPreflight = workflowStep(resolveJob, "Download OpenClaw npm preflight manifest");
|
|
const validateEvidence = workflowStep(resolveJob, "Validate OpenClaw npm preflight manifest");
|
|
const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const dispatch = releasePublishOrchestration(publishJob);
|
|
|
|
expect(readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8")).toContain(
|
|
"group: openclaw-release-publish-${{ inputs.npm_dist_tag }}",
|
|
);
|
|
|
|
expect(input).toEqual({
|
|
default: "",
|
|
description:
|
|
"Optional override for the Plugin SDK API acknowledgement sealed by Full Release Validation",
|
|
required: false,
|
|
type: "string",
|
|
});
|
|
expect(resolveJob.outputs).toMatchObject({
|
|
plugin_sdk_api_acknowledgement:
|
|
"${{ fromJSON(steps.full_manifest.outcome == 'success' && toJSON(steps.full_manifest.outputs.plugin_sdk_api_acknowledgement) || toJSON(inputs.plugin_sdk_api_acknowledgement)) }}",
|
|
npm_decisions: "${{ steps.full_manifest.outputs.npm_decisions }}",
|
|
});
|
|
expect(dispatch.env?.PLUGIN_SDK_API_ACKNOWLEDGEMENT).toBe(
|
|
"${{ needs.resolve_release_target.outputs.plugin_sdk_api_acknowledgement }}",
|
|
);
|
|
expect(validateEvidence.env?.PLUGIN_SDK_API_ACKNOWLEDGEMENT).toBe(
|
|
"${{ fromJSON(steps.full_manifest.outcome == 'success' && toJSON(steps.full_manifest.outputs.plugin_sdk_api_acknowledgement) || toJSON(inputs.plugin_sdk_api_acknowledgement)) }}",
|
|
);
|
|
for (const name of ["Start core npm publication", "Complete publish workflows"]) {
|
|
expect(workflowStep(publishJob, name).env?.PLUGIN_SDK_API_ACKNOWLEDGEMENT).toBe(
|
|
dispatch.env?.PLUGIN_SDK_API_ACKNOWLEDGEMENT,
|
|
);
|
|
}
|
|
expect(
|
|
workflowStep(publishJob, "Start core npm publication").env?.STABLE_SOAK_WAIVER,
|
|
).toBeUndefined();
|
|
expect(
|
|
workflowStep(resolveJob, "Summarize sealed npm publication decisions").env
|
|
?.NPM_PUBLICATION_DECISIONS,
|
|
).toBe("${{ steps.full_manifest.outputs.npm_decisions }}");
|
|
expect(validateEvidence.env?.PLUGIN_SDK_API_VALIDATOR).toContain(
|
|
"plugin-sdk-api-release-evidence.mjs",
|
|
);
|
|
expect(validateEvidence.run).toContain('node "$PLUGIN_SDK_API_VALIDATOR"');
|
|
expect(validateEvidence.run).toContain('--acknowledge "$PLUGIN_SDK_API_ACKNOWLEDGEMENT"');
|
|
expect(validateEvidence.run).toContain('npm view "openclaw@${RELEASE_NPM_DIST_TAG}" version');
|
|
expect(validateEvidence.run).toContain('--current-selector-ref "$current_selector_ref"');
|
|
expect(validateEvidence.run).toContain('--current-selector-sha "$current_selector_sha"');
|
|
expect(validateEvidence.run).toContain("Immutable Plugin SDK API evidence artifact is missing");
|
|
expect(validateEvidence.run).toContain("cmp -s <(jq -S '.pluginSdkApi'");
|
|
expect(downloadPreflight.run).toContain('preflight_conclusion" != "success"');
|
|
expect(downloadPreflight.run).toContain(
|
|
'expected_extended_stable_branch="extended-stable/${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.33"',
|
|
);
|
|
expect(downloadPreflight.run).toContain(
|
|
'preflight_path" != ".github/workflows/openclaw-npm-release.yml"',
|
|
);
|
|
expect(validateEvidence.run).toContain(
|
|
'git merge-base --is-ancestor "$PREFLIGHT_WORKFLOW_SHA" "$WORKFLOW_SHA"',
|
|
);
|
|
expect(validateEvidence.run).toContain('"$PREFLIGHT_WORKFLOW_SHA" == "$EXPECTED_SHA"');
|
|
expect(validateEvidence.run).toContain('--workflow-sha "$PREFLIGHT_WORKFLOW_SHA"');
|
|
expect(publishJob.needs).toEqual(["resolve_release_target"]);
|
|
expect(dispatch.run).toContain(
|
|
'-f plugin_sdk_api_acknowledgement="${PLUGIN_SDK_API_ACKNOWLEDGEMENT}"',
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "lightweight",
|
|
tagRef: { object: { sha: "a".repeat(40), type: "commit" } },
|
|
tagObject: undefined,
|
|
error: "must be an annotated, signed tag",
|
|
},
|
|
{
|
|
name: "unverified",
|
|
tagRef: { object: { sha: "b".repeat(40), type: "tag" } },
|
|
tagObject: {
|
|
object: { sha: "a".repeat(40), type: "commit" },
|
|
tag: "v2026.9.7",
|
|
verification: { reason: "unsigned", verified: false },
|
|
},
|
|
error: "must have a signature verified by GitHub",
|
|
},
|
|
])("rejects a $name release tag before publication", ({ tagRef, tagObject, error }) => {
|
|
const result = runReleasePublishTagSignatureVerification({ tagRef, tagObject });
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(error);
|
|
});
|
|
|
|
it("admits a verified signed release tag", () => {
|
|
const targetSha = "a".repeat(40);
|
|
const tagObjectSha = "b".repeat(40);
|
|
const result = runReleasePublishTagSignatureVerification({
|
|
tagRef: { object: { sha: tagObjectSha, type: "tag" } },
|
|
tagObject: {
|
|
object: { sha: targetSha, type: "commit" },
|
|
tag: "v2026.9.7",
|
|
verification: { reason: "valid", verified: true },
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(result.githubOutput, "utf8")).toBe(
|
|
`sha=${targetSha}\ntag_object_sha=${tagObjectSha}\n`,
|
|
);
|
|
});
|
|
|
|
it("keeps publication bound to the verified signed tag object", () => {
|
|
const targetSha = "a".repeat(40);
|
|
const signedTagObjectSha = "b".repeat(40);
|
|
const unchanged = runReleaseTagTargetVerification({
|
|
directSha: signedTagObjectSha,
|
|
peeledSha: targetSha,
|
|
expectedTagObjectSha: signedTagObjectSha,
|
|
});
|
|
expect(unchanged.status, unchanged.stderr).toBe(0);
|
|
|
|
const replaced = runReleaseTagTargetVerification({
|
|
directSha: targetSha,
|
|
peeledSha: targetSha,
|
|
expectedTagObjectSha: signedTagObjectSha,
|
|
});
|
|
expect(replaced.status).toBe(1);
|
|
expect(replaced.stderr).toContain("changed after signature verification");
|
|
});
|
|
|
|
it.each([
|
|
["publish_android", "Dispatch qualified Android publication"],
|
|
["finalize_github_release", "Publish the verified draft release"],
|
|
["dispatch_linux_mirror", "Dispatch detached Linux mirror"],
|
|
["publish_linux", "Dispatch detached Linux release request"],
|
|
["publish_windows", "Dispatch detached Windows promotion"],
|
|
])("pins the verified tag object in %s", (jobName, stepName) => {
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, jobName);
|
|
const step = workflowStep(job, stepName);
|
|
expect(step.env?.SIGNED_RELEASE_TAG_OBJECT_SHA).toContain("signed_tag_object_sha");
|
|
});
|
|
|
|
it("requires selected plugin names or complete immutable evidence for broad publication", () => {
|
|
const selected = runReleasePublishInputValidation({
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "",
|
|
FULL_RELEASE_VALIDATION_RUN_ID: "",
|
|
PLUGINS: "@openclaw/meta",
|
|
PLUGIN_PUBLISH_SCOPE: "selected",
|
|
PREFLIGHT_RUN_ID: "",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
});
|
|
expect(selected.status, selected.stderr).toBe(0);
|
|
|
|
const emptySelected = runReleasePublishInputValidation({
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "",
|
|
FULL_RELEASE_VALIDATION_RUN_ID: "",
|
|
PLUGINS: " ",
|
|
PLUGIN_PUBLISH_SCOPE: "selected",
|
|
PREFLIGHT_RUN_ID: "",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
});
|
|
expect(emptySelected.status).toBe(1);
|
|
expect(emptySelected.stderr).toContain("plugin_publish_scope=selected requires plugins");
|
|
|
|
const broadWithoutEvidence = runReleasePublishInputValidation({
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "",
|
|
FULL_RELEASE_VALIDATION_RUN_ID: "",
|
|
PREFLIGHT_RUN_ID: "",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
});
|
|
expect(broadWithoutEvidence.status).toBe(1);
|
|
expect(broadWithoutEvidence.stderr).toContain("require preflight_run_id");
|
|
|
|
const partialEvidence = runReleasePublishInputValidation({
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "",
|
|
FULL_RELEASE_VALIDATION_RUN_ID: "",
|
|
PLUGINS: "@openclaw/meta",
|
|
PLUGIN_PUBLISH_SCOPE: "selected",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
});
|
|
expect(partialEvidence.status).toBe(1);
|
|
expect(partialEvidence.stderr).toContain("require full_release_validation_run_id");
|
|
|
|
expect(runReleasePublishInputValidation({ PUBLISH_OPENCLAW_NPM: "false" }).status).toBe(0);
|
|
|
|
const invalidResumeRun = runReleasePublishInputValidation({
|
|
OPENCLAW_NPM_RESUME_RUN_ID: "not-a-run-id",
|
|
});
|
|
expect(invalidResumeRun.status).toBe(1);
|
|
expect(invalidResumeRun.stderr).toContain(
|
|
"openclaw_npm_resume_run_id must be a positive GitHub Actions run id",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{ scope: "all-publishable", core: "true", plugins: "" },
|
|
{ scope: "all-publishable", core: "false", plugins: "" },
|
|
{ scope: "selected", core: "false", plugins: "@openclaw/meta" },
|
|
])("rejects main before publishing children (scope=$scope, core=$core)", (mode) => {
|
|
const result = runReleasePublishInputValidation({
|
|
WORKFLOW_REF: "refs/heads/main",
|
|
PLUGIN_PUBLISH_SCOPE: mode.scope,
|
|
PUBLISH_OPENCLAW_NPM: mode.core,
|
|
PLUGINS: mode.plugins,
|
|
});
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stderr).toContain("git tag release-publish/");
|
|
expect(result.stderr).toContain("git push origin refs/tags/");
|
|
expect(result.stderr).toContain("gh workflow run openclaw-release-publish.yml --ref");
|
|
});
|
|
|
|
it.each<Record<string, string>>([
|
|
{ RELEASE_TAG: "v2026.9.1-alpha.1", RELEASE_NPM_DIST_TAG: "alpha" },
|
|
{ RELEASE_TAG: "v2026.9.1-alpha.1" },
|
|
{ RELEASE_NPM_DIST_TAG: "alpha" },
|
|
{ WORKFLOW_REF: "refs/heads/tideclaw/alpha/2026-09-03-1200Z" },
|
|
])("rejects retired alpha publication inputs %j", (inputs) => {
|
|
const result = runReleasePublishInputValidation(inputs);
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stderr).toContain("Alpha releases are retired;");
|
|
});
|
|
|
|
it.each([
|
|
{ core: "true", prepared: "", allowed: true },
|
|
{ core: "false", prepared: "", allowed: false },
|
|
{ core: "true", prepared: '{"npm":{},"clawhub":{}}', allowed: false },
|
|
])(
|
|
"admits early GitHub activation only for direct core publication: $core/$prepared",
|
|
({ core, prepared, allowed }) => {
|
|
const result = runReleasePublishInputValidation({
|
|
FINALIZE_RELEASE_BEFORE_DOCKER: "true",
|
|
PUBLISH_OPENCLAW_NPM: core,
|
|
PREPARED_PLUGINS: prepared,
|
|
});
|
|
expect(result.status, result.stderr).toBe(allowed ? 0 : 1);
|
|
if (!allowed) {
|
|
expect(result.stderr).toContain(
|
|
"finalize_release_before_docker requires direct publication",
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("allows Docker-only recovery for beta, stable, and extended-stable releases", () => {
|
|
for (const release of [
|
|
{ distTag: "beta", tag: "v2026.8.1-beta.2" },
|
|
{ distTag: "extended-stable", tag: "v2026.7.33" },
|
|
{ distTag: "latest", tag: "v2026.8.1" },
|
|
{ distTag: "latest", tag: "v2026.12.32-1" },
|
|
]) {
|
|
const result = runReleasePublishInputValidation({
|
|
WORKFLOW_REF: "refs/heads/main",
|
|
PUBLISH_DOCKER_ONLY: "true",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
RELEASE_NPM_DIST_TAG: release.distTag,
|
|
RELEASE_TAG: release.tag,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
}
|
|
|
|
for (const tag of [
|
|
"v2026.8.1-alpha.1",
|
|
"v2026.8.1-beta.1",
|
|
"v2026.7.33",
|
|
"v2026.7.33-1",
|
|
"v2026.13.1",
|
|
"v2026.08.1",
|
|
"v2026.8.01",
|
|
"v2026.8.1-0",
|
|
]) {
|
|
const result = runReleasePublishInputValidation({
|
|
PUBLISH_DOCKER_ONLY: "true",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
RELEASE_NPM_DIST_TAG: "latest",
|
|
RELEASE_TAG: tag,
|
|
});
|
|
expect(result.status, tag).toBe(1);
|
|
}
|
|
const rejectedInputs: Record<string, string>[] = [
|
|
{ PUBLISH_OPENCLAW_NPM: "true" },
|
|
{ PREFLIGHT_RUN_ID: "" },
|
|
{ FULL_RELEASE_VALIDATION_RUN_ID: "", FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "" },
|
|
{ RELEASE_NPM_DIST_TAG: "alpha", RELEASE_TAG: "v2026.8.1-alpha.1" },
|
|
];
|
|
for (const overrides of rejectedInputs) {
|
|
expect(
|
|
runReleasePublishInputValidation({
|
|
PUBLISH_DOCKER_ONLY: "true",
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
RELEASE_NPM_DIST_TAG: "latest",
|
|
RELEASE_TAG: "v2026.8.1",
|
|
...overrides,
|
|
}).status,
|
|
).toBe(1);
|
|
}
|
|
|
|
const workflow = readWorkflow(RELEASE_PUBLISH_WORKFLOW);
|
|
const input = workflow.on?.workflow_dispatch?.inputs?.publish_docker_only as
|
|
| { description?: string }
|
|
| undefined;
|
|
const verifyJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "verify_core_npm_registry");
|
|
const verifyStep = workflowStep(
|
|
verifyJob,
|
|
"Verify exact npm and selector readback matches preflight bytes",
|
|
);
|
|
expect(input?.description).toContain("beta, stable, or extended-stable");
|
|
expect(verifyStep.env?.RELEASE_NPM_DIST_TAG).toBe("${{ inputs.npm_dist_tag }}");
|
|
expect(verifyStep.run).toContain('npm view "openclaw@${RELEASE_NPM_DIST_TAG}" version');
|
|
expect(verifyStep.run).not.toContain("npm view openclaw@extended-stable version");
|
|
});
|
|
|
|
it("accepts only main-reachable protected SHA-pinned release publish tags", () => {
|
|
const workflowSha = "a".repeat(40);
|
|
const binDir = tempDirs.make("release-publish-gh-");
|
|
const ghPath = `${binDir}/gh`;
|
|
writeFileSync(ghPath, `#!/bin/sh\nprintf '%s\\n' "\${MOCK_REMOTE_TAG_SHA}"\n`);
|
|
chmodSync(ghPath, 0o755);
|
|
const pinnedEnv = {
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
PATH: `${binDir}:${process.env.PATH}`,
|
|
WORKFLOW_REF: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`,
|
|
WORKFLOW_SHA: workflowSha,
|
|
};
|
|
|
|
const valid = runReleasePublishInputValidation({
|
|
...pinnedEnv,
|
|
MOCK_REMOTE_TAG_SHA: workflowSha,
|
|
});
|
|
expect(valid.status, valid.stderr).toBe(0);
|
|
|
|
const mismatchedName = runReleasePublishInputValidation({
|
|
...pinnedEnv,
|
|
WORKFLOW_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
|
|
});
|
|
expect(mismatchedName.status).toBe(1);
|
|
expect(mismatchedName.stderr).toContain(
|
|
"SHA-pinned release publish tag does not match workflow SHA",
|
|
);
|
|
|
|
const moved = runReleasePublishInputValidation({
|
|
...pinnedEnv,
|
|
MOCK_REMOTE_TAG_SHA: "c".repeat(40),
|
|
});
|
|
expect(moved.status).toBe(1);
|
|
expect(moved.stderr).toContain(
|
|
"SHA-pinned release publish tag does not resolve to workflow SHA",
|
|
);
|
|
});
|
|
|
|
it("keeps publish children on the parent's protected tag and rejects Tideclaw", () => {
|
|
const workflowSha = "a".repeat(40);
|
|
const workflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
|
|
const main = runReleasePublishChildWorkflowRef();
|
|
expect(main.status, main.stderr).toBe(1);
|
|
expect(main.stderr).toContain("protected release-publish tag");
|
|
expect(main.stderr).not.toContain("unexpected-github-lookup");
|
|
|
|
const protectedTag = runReleasePublishChildWorkflowRef({
|
|
WORKFLOW_FULL_REF: `refs/tags/${workflowRef}`,
|
|
});
|
|
expect(protectedTag.status, protectedTag.stderr).toBe(0);
|
|
expect(protectedTag.stdout.trim()).toBe(workflowRef);
|
|
|
|
const alphaRef = "tideclaw/alpha/2026-08-28-1400Z";
|
|
const alpha = runReleasePublishChildWorkflowRef({
|
|
WORKFLOW_FULL_REF: `refs/heads/${alphaRef}`,
|
|
});
|
|
expect(alpha.status, alpha.stderr).toBe(1);
|
|
expect(alpha.stderr).toContain("Alpha releases are retired;");
|
|
|
|
const plan = workflowStep(
|
|
workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"),
|
|
"Resolve ClawHub release plan",
|
|
);
|
|
expect(plan.env?.PARENT_WORKFLOW_SHA).toBe("${{ github.workflow_sha }}");
|
|
expectTextToIncludeAll(plan.run, [
|
|
'source "${GITHUB_WORKSPACE}/.release-harness/scripts/lib/release-publish-children.sh"',
|
|
'resolve_child_workflow_ref "${WORKFLOW_FULL_REF}"',
|
|
'BOOTSTRAP_WORKFLOW_REF="${CHILD_WORKFLOW_REF}"',
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
{ state: "requested", blocked: true },
|
|
{ state: "action_required", blocked: true },
|
|
{ state: "waiting", blocked: true },
|
|
{ state: "pending", blocked: true },
|
|
{ state: "queued", blocked: true },
|
|
{ state: "in_progress", blocked: true },
|
|
{ state: "completed", blocked: false },
|
|
{ state: "waiting", otherRef: true, blocked: false },
|
|
{ state: "unavailable", blocked: true },
|
|
])(
|
|
"prevents a ClawHub dispatch from queuing behind $state (otherRef=$otherRef)",
|
|
({ state, otherRef, blocked }) => {
|
|
const root = tempDirs.make("clawhub-dispatch-collision-");
|
|
const dispatchPath = join(root, "dispatch.json");
|
|
const workflowRef = "release-publish/aaaaaaaaaaaa-123";
|
|
const workflowSha = "a".repeat(40);
|
|
const runUrl = "https://github.com/openclaw/openclaw/actions/runs/91";
|
|
writeFileSync(
|
|
join(root, "gh"),
|
|
`#!${process.execPath}
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
const args = process.argv.slice(2);
|
|
if (args[0] === 'run' && args[1] === 'list') {
|
|
if (${JSON.stringify(state)} === 'unavailable') process.exit(42);
|
|
const matches = args[args.indexOf('--status') + 1] === ${JSON.stringify(state)};
|
|
console.log(JSON.stringify(matches ? [{ databaseId: 91, headBranch: ${JSON.stringify(otherRef ? "release-publish/bbbbbbbbbbbb-456" : workflowRef)}, status: ${JSON.stringify(state)}, url: ${JSON.stringify(runUrl)} }] : []));
|
|
} else if (args[0] === 'run' && args[1] === 'view') {
|
|
console.log(JSON.stringify({ headSha: ${JSON.stringify(workflowSha)}, url: 'https://github.com/openclaw/openclaw/actions/runs/92' }));
|
|
} else if (args[0] === 'api' && args.some(arg => arg.includes('/commits/'))) {
|
|
console.log(${JSON.stringify(workflowSha)});
|
|
} else if (args[0] === 'api' && args.some(arg => arg.endsWith('/dispatches'))) {
|
|
writeFileSync(${JSON.stringify(dispatchPath)}, readFileSync(0, 'utf8'));
|
|
console.log(JSON.stringify({ workflow_run_id: 92, html_url: 'https://github.com/openclaw/openclaw/actions/runs/92' }));
|
|
} else throw new Error('Unexpected GitHub operation: ' + JSON.stringify(args));
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
// The publish parent checks the slot before its first dispatch.
|
|
`source "$HELPER_SCRIPT"
|
|
require_clawhub_dispatch_available "$WORKFLOW_REF" plugin-clawhub-release.yml &&
|
|
dispatch_workflow_at_ref "$WORKFLOW_REF" "$PARENT_WORKFLOW_SHA" plugin-clawhub-release.yml -f ref="$TARGET_SHA"
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${root}:${process.env.PATH}`,
|
|
HELPER_SCRIPT: resolve("scripts/lib/release-publish-children.sh"),
|
|
GITHUB_REF: `refs/tags/${workflowRef}`,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
|
WORKFLOW_REF: workflowRef,
|
|
PARENT_WORKFLOW_SHA: workflowSha,
|
|
TARGET_SHA: "b".repeat(40),
|
|
},
|
|
},
|
|
);
|
|
expect(existsSync(dispatchPath)).toBe(!blocked);
|
|
if (blocked) {
|
|
expect(result.status).not.toBe(0);
|
|
if (state !== "unavailable") {
|
|
expect(result.stderr).toContain(runUrl);
|
|
expect(result.stderr).toContain("pending_deployments");
|
|
expect(result.stderr).toContain("state=rejected");
|
|
expect(result.stderr).toContain("wait");
|
|
}
|
|
} else {
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout.trim()).toBe("92");
|
|
}
|
|
},
|
|
);
|
|
|
|
it("allows protected SHA-pinned tooling tags through the core npm publish guard", () => {
|
|
const workflowSha = "a".repeat(40);
|
|
const protectedRef = `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
|
|
const valid = runOpenClawNpmTrustedRefGuard({
|
|
WORKFLOW_REF: protectedRef,
|
|
WORKFLOW_SHA: workflowSha,
|
|
MOCK_REMOTE_TAG_SHA: workflowSha,
|
|
});
|
|
expect(valid.status, valid.stderr).toBe(0);
|
|
|
|
const mismatchedName = runOpenClawNpmTrustedRefGuard({
|
|
WORKFLOW_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
|
|
WORKFLOW_SHA: workflowSha,
|
|
});
|
|
expect(mismatchedName.status).toBe(1);
|
|
expect(mismatchedName.stderr).toContain(
|
|
"SHA-pinned release-publish tag does not match the OpenClaw npm workflow SHA",
|
|
);
|
|
|
|
const moved = runOpenClawNpmTrustedRefGuard({
|
|
MOCK_REMOTE_TAG_SHA: "c".repeat(40),
|
|
WORKFLOW_REF: protectedRef,
|
|
WORKFLOW_SHA: workflowSha,
|
|
});
|
|
expect(moved.status).toBe(1);
|
|
expect(moved.stderr).toContain(
|
|
"SHA-pinned release-publish tag does not resolve to the OpenClaw npm workflow SHA",
|
|
);
|
|
});
|
|
|
|
it.each(["source", "prepared", "dry-run"])(
|
|
"keeps staged ClawHub publications out of the %s publish roster",
|
|
(mode) => {
|
|
const root = tempDirs.make("clawhub-publication-plan-");
|
|
const bin = join(root, "bin");
|
|
mkdirSync(bin);
|
|
mkdirSync(join(root, ".release-tooling"));
|
|
symlinkSync(resolve("scripts"), join(root, ".release-tooling/scripts"), "dir");
|
|
writeFileSync(
|
|
join(bin, "node"),
|
|
`#!/bin/sh
|
|
case "$1" in
|
|
--input-type=module) exec "$REAL_NODE" "$@" ;;
|
|
*.mjs) cp "$MOCK_MATRIX" .local/clawhub-matrix.json ;;
|
|
*) cat "$MOCK_PLUGIN_PLAN" ;;
|
|
esac
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
const all = [
|
|
{ state: "absent" },
|
|
{ state: "published" },
|
|
{ state: "pending", stage: "checks", attemptId: "attempt_pending" },
|
|
{ state: "failed", recoverable: true, attemptId: "attempt_recover" },
|
|
{ state: "failed", recoverable: false, attemptId: "attempt_blocked" },
|
|
{ state: "failed", recoverable: true },
|
|
].map((publication, index) => ({
|
|
packageName: `@openclaw/example-${index}`,
|
|
packageDir: `extensions/example-${index}`,
|
|
version: "2026.9.1",
|
|
publishTag: "latest",
|
|
artifactName: `artifact-${index}`,
|
|
publication,
|
|
alreadyPublished: publication.state === "published",
|
|
prepared: { artifactId: index + 1 },
|
|
}));
|
|
const plan = {
|
|
all,
|
|
candidates: [all[0]],
|
|
skippedPublished: [all[1]],
|
|
pendingPublication: [all[2]],
|
|
failedPublication: all.slice(3),
|
|
bootstrapCandidates: [],
|
|
missingTrustedPublisher: [],
|
|
warnings: [],
|
|
};
|
|
const planPath = join(root, "plan.json");
|
|
const matrixPath = join(root, "matrix.json");
|
|
const summaryPath = join(root, "summary.md");
|
|
writeFileSync(planPath, JSON.stringify(plan));
|
|
writeFileSync(matrixPath, JSON.stringify(all));
|
|
const script = workflowStep(
|
|
workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub"),
|
|
"Resolve plugin release plan",
|
|
).run!;
|
|
const result = spawnSync("bash", ["-c", script], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
REAL_NODE: process.execPath,
|
|
MOCK_PLUGIN_PLAN: planPath,
|
|
MOCK_MATRIX: matrixPath,
|
|
GITHUB_OUTPUT: join(root, "output"),
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
GITHUB_RUN_ID: "1",
|
|
GITHUB_RUN_ATTEMPT: "1",
|
|
PUBLISH_SCOPE: "all-publishable",
|
|
RELEASE_PLUGINS: "",
|
|
DRY_RUN: mode === "dry-run" ? "true" : "false",
|
|
PREPARED_ARTIFACT: mode === "prepared" ? "{}" : "",
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const outputs = Object.fromEntries(
|
|
readFileSync(join(root, "output"), "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => {
|
|
const equals = line.indexOf("=");
|
|
return [line.slice(0, equals), line.slice(equals + 1)];
|
|
}),
|
|
);
|
|
if (!outputs.matrix || !outputs.publish_matrix) {
|
|
throw new Error("Release plan did not emit both matrices.");
|
|
}
|
|
const matrix = JSON.parse(outputs.matrix) as typeof all;
|
|
const publish = JSON.parse(outputs.publish_matrix) as typeof all;
|
|
expect(matrix.map((entry) => entry.publication.state)).toEqual(
|
|
mode === "dry-run"
|
|
? all.map((entry) => entry.publication.state)
|
|
: mode === "prepared"
|
|
? ["absent", "published"]
|
|
: ["absent"],
|
|
);
|
|
expect(publish.map((entry) => entry.publication.state)).toEqual(
|
|
mode === "dry-run" ? all.map((entry) => entry.publication.state) : ["absent"],
|
|
);
|
|
expect(publish.every((entry) => !("prepared" in entry))).toBe(true);
|
|
const resolvedPlan = JSON.parse(
|
|
readFileSync(join(root, ".local/plugin-clawhub-release-plan.json"), "utf8"),
|
|
);
|
|
expect(resolvedPlan.pendingPublication).toEqual(plan.pendingPublication);
|
|
expect(resolvedPlan.failedPublication).toEqual(plan.failedPublication);
|
|
const summary = readFileSync(summaryPath, "utf8");
|
|
expect(summary).toContain("### Pending publications");
|
|
expect(summary).toContain("### Failed publications");
|
|
expect(summary).toContain(
|
|
"bun '<PINNED_CLAWHUB_CHECKOUT>/packages/clawhub/src/cli.ts' --no-input package recover 'attempt_recover' --manual-override-reason '<EDIT_RECOVERY_REASON>' --wait --wait-timeout 1800 --json",
|
|
);
|
|
expect(summary).not.toContain("package recover 'attempt_blocked'");
|
|
expect(summary).toContain("new version or ask the ClawHub operator to discard");
|
|
expect(summary).toContain("Locate the bound attempt");
|
|
expect(result.stdout.match(/::warning::/gu)).toHaveLength(4);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm", "Resolve plugin release plan"],
|
|
[PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub", "Resolve plugin release plan"],
|
|
[RELEASE_PUBLISH_WORKFLOW, "publish", "Resolve ClawHub release plan"],
|
|
[
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"resolve_bootstrap_plan",
|
|
"Resolve plugin bootstrap plan",
|
|
],
|
|
])("keeps plugin freshness warnings visible without blocking %s", (workflow, job, step) => {
|
|
const root = tempDirs.make("plugin-plan-warnings-");
|
|
const bin = join(root, "bin");
|
|
mkdirSync(bin);
|
|
writeFileSync(join(bin, "node"), '#!/bin/sh\ncat "$MOCK_PLUGIN_PLAN"\n', { mode: 0o755 });
|
|
const parentPlan = workflow === RELEASE_PUBLISH_WORKFLOW;
|
|
if (parentPlan) {
|
|
const helperDir = join(root, ".release-harness/scripts/lib");
|
|
mkdirSync(helperDir, { recursive: true });
|
|
writeFileSync(
|
|
join(helperDir, "release-publish-children.sh"),
|
|
"resolve_child_workflow_ref() { printf '%s\\n' \"release-publish/aaaaaaaaaaaa-1\"; }\n",
|
|
);
|
|
}
|
|
const plugin = {
|
|
packageName: "@openclaw/example",
|
|
packageDir: "extensions/example",
|
|
version: "2026.9.1",
|
|
channel: "stable",
|
|
publishTag: "latest",
|
|
};
|
|
const warning =
|
|
'@openclaw/example@2026.9.1: example-runtime pinned "1.2.3", npm latest is "1.2.4".';
|
|
const planPath = join(root, "plan.json");
|
|
writeFileSync(
|
|
planPath,
|
|
JSON.stringify({
|
|
all: [plugin],
|
|
candidates: [plugin],
|
|
bootstrapCandidates: [plugin],
|
|
missingTrustedPublisher: [],
|
|
skippedPublished: [],
|
|
warnings: [warning],
|
|
bootstrap: { shouldDispatch: false },
|
|
}),
|
|
);
|
|
const script = workflowStep(workflowJob(workflow, job), step).run;
|
|
if (!script) {
|
|
throw new Error(`Missing plugin plan step in ${workflow}`);
|
|
}
|
|
const summaryPath = join(root, "summary.md");
|
|
const run = () =>
|
|
spawnSync("bash", ["-c", script], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
MOCK_PLUGIN_PLAN: planPath,
|
|
GITHUB_OUTPUT: join(root, "output"),
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
GITHUB_WORKSPACE: root,
|
|
RUNNER_TEMP: root,
|
|
GITHUB_SHA: "a".repeat(40),
|
|
GITHUB_RUN_ID: "1",
|
|
GITHUB_RUN_ATTEMPT: "1",
|
|
WORKFLOW_FULL_REF: "refs/heads/main",
|
|
PARENT_WORKFLOW_BRANCH: "main",
|
|
PARENT_WORKFLOW_FULL_REF: "refs/heads/main",
|
|
RELEASE_TAG: "v2026.9.1",
|
|
TARGET_SHA: "b".repeat(40),
|
|
PLUGIN_PUBLISH_SCOPE: "all-publishable",
|
|
PLUGINS: "",
|
|
PUBLISH_SCOPE: "all-publishable",
|
|
RELEASE_PLUGINS: plugin.packageName,
|
|
BASE_REF: "",
|
|
NPM_DIST_TAG: "default",
|
|
RELEASE_NPM_DIST_TAG: "latest",
|
|
PREFLIGHT_ONLY: "false",
|
|
DRY_RUN: "false",
|
|
PREPARED_ARTIFACT: "",
|
|
PREPARED_PLUGINS: "",
|
|
},
|
|
});
|
|
const result = run();
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(summaryPath, "utf8")).toBe(`- Warning: ${warning}\n`);
|
|
expect(result.stdout).toContain(plugin.packageName);
|
|
expect(readFileSync(join(root, "output"), "utf8")).toContain(
|
|
parentPlan ? "bootstrap_should_dispatch=false" : "candidate_count=1",
|
|
);
|
|
if (
|
|
workflow === PLUGIN_CLAWHUB_RELEASE_WORKFLOW ||
|
|
workflow === ".github/workflows/plugin-clawhub-new.yml"
|
|
) {
|
|
const originalPlan = readFileSync(planPath, "utf8");
|
|
for (const retired of [
|
|
{ version: "2026.9.1-alpha.1" },
|
|
{ publishTag: "alpha" },
|
|
{ channel: "alpha" },
|
|
]) {
|
|
const plan = JSON.parse(originalPlan);
|
|
for (const entries of [plan.all, plan.candidates, plan.bootstrapCandidates]) {
|
|
Object.assign(entries[0], retired);
|
|
}
|
|
writeFileSync(planPath, JSON.stringify(plan));
|
|
const rejected = run();
|
|
expect(rejected.status, rejected.stderr).toBe(1);
|
|
expect(rejected.stderr).toContain("Alpha releases are retired;");
|
|
}
|
|
if (workflow === ".github/workflows/plugin-clawhub-new.yml") {
|
|
for (const publication of [
|
|
{ state: "pending", stage: "checks", attemptId: "attempt_pending" },
|
|
{ state: "failed", recoverable: true, attemptId: "attempt_failed" },
|
|
]) {
|
|
const plan = JSON.parse(originalPlan);
|
|
plan.bootstrapCandidates = [];
|
|
plan.missingTrustedPublisher = [{ ...plugin, publication, alreadyPublished: false }];
|
|
writeFileSync(planPath, JSON.stringify(plan));
|
|
const rejected = run();
|
|
expect(rejected.status, rejected.stderr).toBe(1);
|
|
expect(rejected.stdout).toContain(
|
|
"Pending or failed ClawHub publications cannot be bootstrapped again",
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
PLUGIN_NPM_RELEASE_WORKFLOW,
|
|
"preview_plugins_npm",
|
|
"Checkout trusted planning tooling",
|
|
"Validate publishable plugin metadata",
|
|
".release-tooling",
|
|
"${{ github.workflow_sha }}",
|
|
],
|
|
[
|
|
PLUGIN_NPM_RELEASE_WORKFLOW,
|
|
"preview_plugin_pack",
|
|
"Checkout trusted packaging tooling",
|
|
"Prepare immutable npm preflight artifact",
|
|
".release-tooling",
|
|
"${{ github.workflow_sha }}",
|
|
],
|
|
[
|
|
PLUGIN_CLAWHUB_RELEASE_WORKFLOW,
|
|
"preview_plugins_clawhub",
|
|
"Checkout trusted planning tooling",
|
|
"Validate publishable plugin metadata",
|
|
".release-tooling",
|
|
"${{ github.workflow_sha }}",
|
|
],
|
|
[
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"resolve_bootstrap_plan",
|
|
"Checkout trusted planning tooling",
|
|
"Validate publishable plugin metadata",
|
|
".release-tooling",
|
|
"${{ github.workflow_sha }}",
|
|
],
|
|
[
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"pack_bootstrap_plugins",
|
|
"Checkout trusted workflow tooling",
|
|
"Pack immutable ClawHub bootstrap artifacts",
|
|
".release-harness",
|
|
"${{ github.sha }}",
|
|
],
|
|
])(
|
|
"initializes independent plugin tooling before %s/%s",
|
|
(file, jobName, checkoutName, consumerName, toolingPath, toolingRef) => {
|
|
const job = workflowJob(file, jobName);
|
|
const checkout = workflowStep(job, checkoutName);
|
|
const setup = workflowStep(job, "Setup Node environment");
|
|
const install = workflowStep(job, "Install trusted plugin tooling dependencies");
|
|
const consumer = workflowStep(job, consumerName);
|
|
expect(checkout.with).toMatchObject({
|
|
ref: toolingRef,
|
|
path: toolingPath,
|
|
"persist-credentials": false,
|
|
});
|
|
expect(checkout.with?.["sparse-checkout"]).toBeUndefined();
|
|
expect(install["working-directory"]).toBe(toolingPath);
|
|
expect(install.env).toMatchObject({ CI: "true" });
|
|
expect(install.run).toBe("pnpm install --frozen-lockfile --prefer-offline --ignore-scripts");
|
|
expect(job.steps!.indexOf(install)).toBeGreaterThan(job.steps!.indexOf(checkout));
|
|
expect(job.steps!.indexOf(install)).toBeGreaterThan(job.steps!.indexOf(setup));
|
|
expect(job.steps!.indexOf(install)).toBeLessThan(job.steps!.indexOf(consumer));
|
|
},
|
|
);
|
|
|
|
it("runs plugin npm preflight trust from the exact workflow tooling checkout", () => {
|
|
const job = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm");
|
|
const checkout = workflowStep(job, "Checkout trusted planning tooling");
|
|
const identity = workflowStep(job, "Verify trusted preflight or recovery tooling identity");
|
|
const target = workflowStep(job, "Validate ref is on a trusted publish branch");
|
|
|
|
expect(checkout.if).toBeUndefined();
|
|
expect(checkout.with).toMatchObject({
|
|
"fetch-depth": 1,
|
|
path: ".release-tooling",
|
|
"persist-credentials": false,
|
|
ref: "${{ github.workflow_sha }}",
|
|
});
|
|
expect(checkout.with?.["sparse-checkout"]).toBeUndefined();
|
|
for (const [name, script] of [
|
|
["Validate publishable plugin metadata", "plugin-npm-release-check.ts"],
|
|
["Resolve plugin release plan", "plugin-npm-release-plan.ts"],
|
|
] as const) {
|
|
const planner = workflowStep(job, name);
|
|
expect(planner.run).toContain(`node --import tsx .release-tooling/scripts/${script}`);
|
|
expect(planner.env?.TSX_TSCONFIG_PATH).toBe(
|
|
"${{ github.workspace }}/.release-tooling/tsconfig.json",
|
|
);
|
|
expect(planner["working-directory"]).toBeUndefined();
|
|
}
|
|
expect(identity.if).toBe("github.event_name == 'workflow_dispatch'");
|
|
expect(identity.env).toMatchObject({
|
|
GH_TOKEN: "${{ github.token }}",
|
|
WORKFLOW_FULL_REF: "${{ github.ref }}",
|
|
WORKFLOW_REF: "${{ github.ref_name }}",
|
|
WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
expect(identity.run).toContain(
|
|
"node .release-tooling/scripts/release-tooling-identity.mjs verify",
|
|
);
|
|
expect(target.run).not.toContain('WORKFLOW_REF}" != "refs/heads/main');
|
|
expect(target.run).not.toContain('git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main');
|
|
});
|
|
|
|
it("accepts only the live exact lightweight protected tag for plugin npm preflight", () => {
|
|
const workflowSha = "a".repeat(40);
|
|
const workflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
const workflowFullRef = `refs/tags/${workflowRef}`;
|
|
const baseEnv = {
|
|
MOCK_TAG_FULL_REF: workflowFullRef,
|
|
MOCK_TAG_SHA: workflowSha,
|
|
WORKFLOW_FULL_REF: workflowFullRef,
|
|
WORKFLOW_REF: workflowRef,
|
|
WORKFLOW_SHA: workflowSha,
|
|
};
|
|
|
|
const valid = runPluginNpmPreflightToolingGuard(baseEnv);
|
|
expect(valid.status, valid.stderr).toBe(0);
|
|
|
|
for (const rejected of [
|
|
{
|
|
name: "moved tag",
|
|
env: { ...baseEnv, MOCK_TAG_SHA: "b".repeat(40) },
|
|
error: "missing, moved, annotated, or bound to the wrong SHA",
|
|
},
|
|
{
|
|
name: "annotated tag",
|
|
env: { ...baseEnv, MOCK_TAG_TYPE: "tag" },
|
|
error: "missing, moved, annotated, or bound to the wrong SHA",
|
|
},
|
|
{
|
|
name: "wrong SHA prefix",
|
|
env: {
|
|
...baseEnv,
|
|
MOCK_TAG_FULL_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
|
|
WORKFLOW_FULL_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
|
|
WORKFLOW_REF: `release-publish/${"b".repeat(12)}-123`,
|
|
},
|
|
error: "SHA prefix does not match",
|
|
},
|
|
{
|
|
name: "same-name branch",
|
|
env: { ...baseEnv, WORKFLOW_FULL_REF: `refs/heads/${workflowRef}` },
|
|
error: "exact tag full ref",
|
|
},
|
|
]) {
|
|
const result = runPluginNpmPreflightToolingGuard(rejected.env);
|
|
expect(result.status, rejected.name).toBe(1);
|
|
expect(result.stderr, rejected.name).toContain(rejected.error);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
["aggregate", runReleasePublishPreflightConsumerGuard],
|
|
["core npm", runOpenClawNpmPreflightConsumerGuard],
|
|
] as const)(
|
|
"binds %s historical preflight consumption to independent producer provenance",
|
|
async (_name, run) => {
|
|
const producerSha = "a".repeat(40);
|
|
const producerTag = `release-publish/${producerSha.slice(0, 12)}-123`;
|
|
const publisherSha = "b".repeat(40);
|
|
const params = {
|
|
currentRef: `refs/tags/release-publish/${publisherSha.slice(0, 12)}-456`,
|
|
currentWorkflowSha: publisherSha,
|
|
preflightHeadBranch: producerTag,
|
|
preflightHeadSha: producerSha,
|
|
};
|
|
const valid = await run(params);
|
|
expect(valid.status, valid.stderr).toBe(0);
|
|
|
|
for (const rejected of [
|
|
{ preflightHeadBranch: `${producerTag}-wrong` },
|
|
{ preflightHeadSha: "c".repeat(40) },
|
|
{ producerTagSha: "c".repeat(40) },
|
|
{ producerTagType: "tag" },
|
|
{ producerBranches: [{ ref: `refs/heads/${producerTag}` }] },
|
|
{ mainComparisonStatus: "diverged" },
|
|
{ publisherComparisonStatus: "behind" },
|
|
]) {
|
|
const result = await run({ ...params, ...rejected });
|
|
expect(result.status, JSON.stringify(rejected)).toBe(1);
|
|
expect(result.stderr).toMatch(/protected|reachable/u);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("rejects a protected tooling tag moved after request validation and environment approval", async () => {
|
|
const workflowSha = "a".repeat(40);
|
|
const workflowTag = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
const protectedRef = `refs/tags/${workflowTag}`;
|
|
const predecessor = runOpenClawNpmTrustedRefGuard({
|
|
MOCK_REMOTE_TAG_SHA: workflowSha,
|
|
WORKFLOW_REF: protectedRef,
|
|
WORKFLOW_SHA: workflowSha,
|
|
});
|
|
expect(predecessor.status, predecessor.stderr).toBe(0);
|
|
|
|
const consumer = await runOpenClawNpmPreflightConsumerGuard({
|
|
currentRef: protectedRef,
|
|
currentWorkflowSha: workflowSha,
|
|
liveTagSha: "b".repeat(40),
|
|
preflightHeadBranch: workflowTag,
|
|
preflightHeadSha: workflowSha,
|
|
});
|
|
expect(consumer.status).toBe(1);
|
|
expect(consumer.stderr).toContain(
|
|
"Protected release-publish tag moved after npm-release approval",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
["aggregate", runReleasePublishPreflightConsumerGuard, false],
|
|
["core npm", runOpenClawNpmPreflightConsumerGuard, false],
|
|
["aggregate with independent producer", runReleasePublishPreflightConsumerGuard, true],
|
|
["core npm with independent producer", runOpenClawNpmPreflightConsumerGuard, true],
|
|
] as const)(
|
|
"admits only the exact selected FRV producer in %s publication",
|
|
async (_name, run, independentProducer) => {
|
|
const params = {
|
|
currentRef: "refs/heads/main",
|
|
currentWorkflowSha: "b".repeat(40),
|
|
fullReleasePreflight: true,
|
|
independentProducer,
|
|
preflightHeadBranch: `release-ci/${"a".repeat(12)}-111`,
|
|
preflightHeadSha: "a".repeat(40),
|
|
};
|
|
const accepted = await run(params);
|
|
expect(accepted.status, accepted.stderr).toBe(0);
|
|
for (const rejected of [
|
|
{ fullReleaseRunId: "222" },
|
|
{ fullReleaseRunAttempt: "2" },
|
|
{ fullReleasePreflight: false },
|
|
]) {
|
|
const result = await run({ ...params, ...rejected });
|
|
expect(result.status, JSON.stringify(rejected)).toBe(1);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("reuses exact core and SDK archives while preserving historical preflight names", async () => {
|
|
const historicalName = `openclaw-npm-preflight-${"a".repeat(40)}`;
|
|
const result = await runReleasePublishPreflightConsumerGuard({
|
|
currentRef: "refs/heads/main",
|
|
currentWorkflowSha: "a".repeat(40),
|
|
preflightHeadBranch: "main",
|
|
preflightHeadSha: "a".repeat(40),
|
|
preflightArtifactName: historicalName,
|
|
repeatDownload: true,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.outputs).toContain(`name=${historicalName}`);
|
|
expect(result.requests.filter((url) => url.endsWith("/zip"))).toHaveLength(2);
|
|
for (const [directory, name] of [
|
|
["openclaw-npm-preflight-manifest", "dependency-evidence/proof.json"],
|
|
["openclaw-plugin-sdk-api-evidence", "plugin-sdk-api-release-evidence.json"],
|
|
] as const) {
|
|
expect(readFileSync(join(result.runnerTemp, directory, name), "utf8")).toBe(
|
|
JSON.stringify({ fixture: name }),
|
|
);
|
|
}
|
|
});
|
|
|
|
it("never retries permanent core download failures or switches to a historical alias", async () => {
|
|
const result = await runReleasePublishPreflightConsumerGuard({
|
|
currentRef: "refs/heads/main",
|
|
currentWorkflowSha: "a".repeat(40),
|
|
preflightHeadBranch: "main",
|
|
preflightHeadSha: "a".repeat(40),
|
|
additionalPreflightArtifactNames: [`openclaw-npm-preflight-${"a".repeat(40)}`],
|
|
archiveFailureStatus: 401,
|
|
});
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("HTTP 401");
|
|
expect(result.requests.filter((url) => url.endsWith("/zip"))).toEqual([
|
|
"https://api.github.com/repos/openclaw/openclaw/actions/artifacts/301/zip",
|
|
]);
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"keeps FRV tooling ancestry mandatory for extended-stable npm publication (independent=%s)",
|
|
async (independentProducer) => {
|
|
const params = {
|
|
currentRef: "refs/heads/main",
|
|
currentWorkflowSha: "b".repeat(40),
|
|
fullReleasePreflight: true,
|
|
independentProducer,
|
|
npmDistTag: "extended-stable",
|
|
expectedExtendedStableBranch: "extended-stable/2026.6.33",
|
|
preflightHeadBranch: `release-ci/${"a".repeat(12)}-111`,
|
|
preflightHeadSha: "a".repeat(40),
|
|
};
|
|
const accepted = await runOpenClawNpmPreflightConsumerGuard(params);
|
|
expect(accepted.status, accepted.stderr).toBe(0);
|
|
for (const preflightHeadBranch of [
|
|
params.preflightHeadBranch,
|
|
params.expectedExtendedStableBranch,
|
|
]) {
|
|
const rejected = await runOpenClawNpmPreflightConsumerGuard({
|
|
...params,
|
|
preflightHeadBranch,
|
|
toolingAncestor: false,
|
|
});
|
|
expect(rejected.status, rejected.stderr).toBe(1);
|
|
expect(rejected.stderr).toContain("trusted publish workflow lineage");
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
name: "standalone FRV",
|
|
fullReleasePreflight: true,
|
|
independentProducer: true,
|
|
version: "2026.8.1",
|
|
},
|
|
{
|
|
name: "direct FRV",
|
|
fullReleasePreflight: true,
|
|
independentProducer: false,
|
|
version: "2026.9.1",
|
|
},
|
|
{
|
|
name: "historical NPM",
|
|
fullReleasePreflight: false,
|
|
independentProducer: false,
|
|
version: "2026.8.1-beta.3",
|
|
},
|
|
])("carries the $name artifact owner without replacing publication authority", async (mode) => {
|
|
const producerRunId = mode.independentProducer ? "333" : "111";
|
|
const fullReleaseRunId = mode.fullReleasePreflight ? "111" : "222";
|
|
const qualifiedName = `openclaw-npm-preflight-${"a".repeat(40)}`;
|
|
const resolved = await runReleasePublishPreflightConsumerGuard({
|
|
...mode,
|
|
currentRef: "refs/heads/main",
|
|
currentWorkflowSha: "b".repeat(40),
|
|
preflightHeadBranch: "main",
|
|
preflightHeadSha: "a".repeat(40),
|
|
preflightArtifactName: mode.fullReleasePreflight ? qualifiedName : undefined,
|
|
additionalPreflightArtifactNames: mode.fullReleasePreflight
|
|
? ["openclaw-npm-preflight-v2026.8.1-beta.3"]
|
|
: undefined,
|
|
repeatDownload: mode.fullReleasePreflight,
|
|
});
|
|
expect(resolved.status, resolved.stderr).toBe(0);
|
|
if (mode.fullReleasePreflight) {
|
|
expect(resolved.outputs).toContain(`name=${qualifiedName}`);
|
|
expect(resolved.requests.filter((url) => url.endsWith("/zip"))).toHaveLength(2);
|
|
}
|
|
const stepOutputs = Object.fromEntries(
|
|
readFileSync(resolved.outputPath, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => line.split("=")),
|
|
);
|
|
const target = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const expressions: Record<string, string> = {
|
|
"${{ inputs.preflight_run_id }}": "111",
|
|
"${{ needs.resolve_release_target.outputs.plugin_sdk_api_acknowledgement }}": "0123abcd",
|
|
"${{ inputs.full_release_validation_run_id }}": fullReleaseRunId,
|
|
};
|
|
for (const [name, value] of Object.entries(target.outputs ?? {})) {
|
|
const field = value.match(/^\$\{\{ steps\.preflight_artifact\.outputs\.(\w+) \}\}$/u)?.[1];
|
|
if (field) {
|
|
expressions[`\${{ needs.resolve_release_target.outputs.${name} }}`] = stepOutputs[field];
|
|
}
|
|
}
|
|
const publish = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const stepEnv = (step: WorkflowStep) =>
|
|
Object.fromEntries(
|
|
Object.entries({ ...publish.env, ...step.env })
|
|
.filter(([, value]) => value in expressions)
|
|
.map(([key, value]) => [key, expressions[value]]),
|
|
);
|
|
const fixture = createReleasePublishFixture(
|
|
{
|
|
EXPECTED_PRODUCER_RUN_ID: producerRunId,
|
|
RELEASE_TAG: `v${mode.version}`,
|
|
TARGET_SHA: "d".repeat(40),
|
|
OPENCLAW_NPM_RESUME_RUN_ID: "777",
|
|
NPM_TELEGRAM_RUN_ID: "",
|
|
},
|
|
{
|
|
mockEvidence: false,
|
|
functions: `
|
|
gh() {
|
|
if [[ "$1 $2" == "run download" ]]; then
|
|
record "download:$3"
|
|
if [[ "$3" != "$EXPECTED_PRODUCER_RUN_ID" ]]; then
|
|
echo "artifact belongs to $EXPECTED_PRODUCER_RUN_ID, not $3" >&2
|
|
return 41
|
|
fi
|
|
shift 3
|
|
local destination=""
|
|
while (( $# )); do
|
|
if [[ "$1" == "--dir" ]]; then destination="$2"; shift; fi
|
|
shift
|
|
done
|
|
cp "$RUNNER_TEMP/preflight-manifest.json" "$destination/"
|
|
cp -R "$RUNNER_TEMP/dependency-evidence" "$destination/"
|
|
elif [[ "$1 $2" == "release view" ]]; then
|
|
printf '%s\\n' 'Initial release notes'
|
|
elif [[ "$1 $2" == "release edit" ]]; then
|
|
record notes
|
|
else return 99; fi
|
|
}
|
|
npm() { printf '%s\\n' 'https://example.invalid/openclaw.tgz'; }
|
|
curl() {
|
|
while [[ "$1" != "-o" ]]; do shift; done
|
|
cp "$RUNNER_TEMP/registry.tgz" "$2"
|
|
}
|
|
node() {
|
|
if [[ "\${3:-}" == "$GITHUB_WORKSPACE/.release-harness/scripts/openclaw-npm-resume-run.mts" ]]; then
|
|
printf '%s\\n' '{"runId":"777","runAttempt":1,"url":"https://example.invalid/runs/777","workflowRef":"refs/tags/release-publish-verified","workflowSha":"${"a".repeat(40)}"}'
|
|
else command node "$@"; fi
|
|
}
|
|
zip() { cat > "$3"; }
|
|
attach_or_verify_release_asset() { record "asset:$(cat "$1")"; }
|
|
write_clawhub_runtime_state() { printf '%s\\n' '{"proofLines":{"normal":"normal","bootstrap":"bootstrap"}}' > "$1"; }
|
|
render_github_release_notes() { cp "$2" "$1"; printf '%s\\n' '{"verificationIncluded":true}' > "$3"; }
|
|
`,
|
|
},
|
|
);
|
|
const tarball = Buffer.from("published candidate bytes");
|
|
writeFileSync(join(fixture.root, "registry.tgz"), tarball);
|
|
writeFileSync(
|
|
join(fixture.root, "preflight-manifest.json"),
|
|
JSON.stringify({
|
|
releaseSha: "d".repeat(40),
|
|
tarballSha256: createHash("sha256").update(tarball).digest("hex"),
|
|
}),
|
|
);
|
|
mkdirSync(join(fixture.root, "dependency-evidence"));
|
|
writeFileSync(join(fixture.root, "dependency-evidence/report.json"), "{}");
|
|
writeFileSync(join(fixture.root, "dependency-evidence/npm-package-locks.json"), "{}");
|
|
writeFileSync(join(fixture.root, "dependency-evidence/npm-package-locks.md"), "# npm locks\n");
|
|
writeFileSync(
|
|
join(fixture.root, "release-postpublish-evidence.json"),
|
|
JSON.stringify({
|
|
openclawNpmTarball: "https://example.invalid/openclaw.tgz",
|
|
openclawNpmIntegrity: "sha512-fixture",
|
|
...(mode.fullReleasePreflight ? { telegramWaiver: `${mode.version}-owner-approved` } : {}),
|
|
}),
|
|
);
|
|
const resume = fixture.run(
|
|
{
|
|
run: 'set -euo pipefail; source "$GITHUB_WORKSPACE/.release-harness/scripts/lib/release-publish-children.sh"; resolve_openclaw_npm_publish_state; [[ "$openclaw_npm_already_published" == true ]]',
|
|
},
|
|
stepEnv(workflowStep(publish, "Dispatch publish workflows")),
|
|
);
|
|
expect.soft(resume.status, resume.stderr).toBe(0);
|
|
expect(fixture.outputs().openclaw_npm_resume_run_id).toBe("777");
|
|
expect(fixture.outputs().openclaw_npm_resume_run_attempt).toBe("1");
|
|
const evidence = fixture.run(
|
|
{
|
|
run: 'set -euo pipefail; source "$GITHUB_WORKSPACE/.release-harness/scripts/lib/release-publish-children.sh"; upload_dependency_evidence_release_asset',
|
|
},
|
|
stepEnv(workflowStep(publish, "Complete publish workflows")),
|
|
);
|
|
expect.soft(evidence.status, evidence.stderr).toBe(0);
|
|
expect
|
|
.soft(fixture.events().join("\n"))
|
|
.toContain(
|
|
"asset:dependency-evidence/npm-package-locks.json\ndependency-evidence/npm-package-locks.md\ndependency-evidence/report.json",
|
|
);
|
|
|
|
const core = workflowStep(publish, "Start core npm publication");
|
|
const dispatched = fixture.run(core, stepEnv(core));
|
|
expect(dispatched.status, dispatched.stderr).toBe(0);
|
|
const dispatch = fixture.events().find((event) => event.startsWith("dispatch:"));
|
|
expect(dispatch).toContain("-f preflight_run_id=111");
|
|
expect(dispatch).not.toContain("stable_soak_waiver");
|
|
expect(dispatch).toContain("-f plugin_sdk_api_acknowledgement=0123abcd");
|
|
expect(dispatch).not.toContain("lane_waiver");
|
|
expect(dispatch).toContain(`-f full_release_validation_run_id=${fullReleaseRunId}`);
|
|
|
|
const proof = fixture.run(
|
|
{
|
|
run: 'set -euo pipefail; source "$GITHUB_WORKSPACE/.release-harness/scripts/lib/release-publish-children.sh"; plugin_npm_run_id=101; openclaw_npm_run_id=404; windows_node_run_id=""; android_release_note=""; append_release_proof_to_github_release',
|
|
},
|
|
{
|
|
...stepEnv(workflowStep(publish, "Complete publish workflows")),
|
|
POSTPUBLISH_EVIDENCE_DIR: fixture.root,
|
|
},
|
|
);
|
|
expect(proof.status, proof.stderr).toBe(0);
|
|
const proofText = readFileSync(join(fixture.root, "release-verification.md"), "utf8");
|
|
expect(proofText).not.toContain("Stable soak waived by operator:");
|
|
expect(proofText).toContain(
|
|
mode.fullReleasePreflight
|
|
? `Telegram integration checks: waived by the release owner for ${mode.version} (source QA, Package Acceptance, published-package E2E); not run.`
|
|
: "npm Telegram beta E2E: not supplied",
|
|
);
|
|
expect
|
|
.soft(proofText)
|
|
.toContain(
|
|
`- npm preflight: https://github.com/openclaw/openclaw/actions/runs/${producerRunId}`,
|
|
);
|
|
expect(proofText).toContain(
|
|
`- full release validation: https://github.com/openclaw/openclaw/actions/runs/${fullReleaseRunId}`,
|
|
);
|
|
});
|
|
|
|
it("uses the canonical tooling identity verifier for token-bootstrap evidence", () => {
|
|
const publishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
|
|
const evidenceStep = workflowStep(publishJob, "Resolve immutable npm publication artifact");
|
|
|
|
expect(evidenceStep.env?.RELEASE_PUBLISH_RUN_ID).toBe("${{ inputs.release_publish_run_id }}");
|
|
expect(evidenceStep.env?.RELEASE_PUBLISH_RUN_ATTEMPT).toBe(
|
|
"${{ inputs.release_publish_run_attempt }}",
|
|
);
|
|
expect(evidenceStep.env?.RELEASE_PUBLISH_REF).toBe("${{ inputs.release_publish_branch }}");
|
|
expect(evidenceStep.env?.RELEASE_PUBLISH_FULL_REF).toBe(
|
|
"${{ inputs.release_publish_full_ref }}",
|
|
);
|
|
expect(evidenceStep.env?.RELEASE_PUBLISH_PARENT_STATE_POLICY).toBe(
|
|
"${{ inputs.release_publish_run_id != '' && (needs.validate_release_publish_approval.outputs.parent_approval == 'receipt' && 'active' || (github.actor == 'github-actions[bot]' && 'active-or-failure' || 'manual-recovery')) || '' }}",
|
|
);
|
|
expect(evidenceStep.run).toContain("node scripts/release-tooling-identity.mjs verify");
|
|
expect(evidenceStep.run).toContain('--workflow-ref "$WORKFLOW_HEAD_BRANCH"');
|
|
expect(evidenceStep.run).toContain('--workflow-full-ref "$WORKFLOW_REF"');
|
|
expect(evidenceStep.run).toContain('--workflow-sha "$WORKFLOW_SHA"');
|
|
expect(evidenceStep.run).toContain('--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID"');
|
|
expect(evidenceStep.run).toContain(
|
|
'--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT"',
|
|
);
|
|
expect(evidenceStep.run).toContain('--release-publish-ref "$RELEASE_PUBLISH_REF"');
|
|
expect(evidenceStep.run).toContain('--release-publish-full-ref "$RELEASE_PUBLISH_FULL_REF"');
|
|
expect(evidenceStep.run).toContain(
|
|
'--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"',
|
|
);
|
|
expect(evidenceStep.run).not.toContain("--allow-prevalidated-ref");
|
|
});
|
|
|
|
it("revalidates protected tooling immediately before every core and plugin npm publish", () => {
|
|
const corePublish = workflowStep(
|
|
workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm"),
|
|
"Publish",
|
|
);
|
|
expect(corePublish.env).toMatchObject({
|
|
GH_TOKEN: "${{ github.token }}",
|
|
RELEASE_PUBLISH_PARENT_STATE_POLICY:
|
|
"${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}",
|
|
RELEASE_PUBLISH_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
|
|
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
|
RELEASE_PUBLISH_REF: "${{ inputs.release_publish_branch }}",
|
|
RELEASE_PUBLISH_FULL_REF: "${{ inputs.release_publish_full_ref }}",
|
|
WORKFLOW_FULL_REF: "${{ github.ref }}",
|
|
WORKFLOW_REF: "${{ github.ref_name }}",
|
|
WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
expect(corePublish.run).toContain(
|
|
"node trusted-workflow/scripts/release-tooling-identity.mjs verify",
|
|
);
|
|
expect(corePublish.run).toContain("--allow-prevalidated-ref");
|
|
expect(corePublish.run).toContain(
|
|
'--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT"',
|
|
);
|
|
expect(corePublish.run).toContain('--release-publish-ref "$RELEASE_PUBLISH_REF"');
|
|
expect(corePublish.run).toContain('--release-publish-full-ref "$RELEASE_PUBLISH_FULL_REF"');
|
|
expect(corePublish.run).toContain(
|
|
'--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"',
|
|
);
|
|
expect(corePublish.run).toMatch(
|
|
/verify_release_tooling_identity\s+bash scripts\/openclaw-npm-publish\.sh --publish "\.\/\$\{tarball_path\}"/u,
|
|
);
|
|
expect(corePublish.run).toMatch(
|
|
/verify_release_tooling_identity\s+bash scripts\/openclaw-npm-publish\.sh --publish "\$\{publish_target\}"/u,
|
|
);
|
|
|
|
const pluginPublishJob = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
|
|
const oidcPublish = workflowStep(pluginPublishJob, "Publish with trusted publisher");
|
|
expect(oidcPublish.env).toMatchObject({
|
|
GH_TOKEN: "${{ github.token }}",
|
|
OPENCLAW_RELEASE_PUBLISH_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
|
|
OPENCLAW_RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
|
OPENCLAW_RELEASE_PUBLISH_REF: "${{ inputs.release_publish_branch }}",
|
|
OPENCLAW_RELEASE_PUBLISH_FULL_REF: "${{ inputs.release_publish_full_ref }}",
|
|
OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY:
|
|
"${{ inputs.release_publish_run_id != '' && (needs.validate_release_publish_approval.outputs.parent_approval == 'receipt' && 'active' || (github.actor == 'github-actions[bot]' && 'active-or-failure' || 'manual-recovery')) || '' }}",
|
|
OPENCLAW_RELEASE_TOOLING_FULL_REF: "${{ github.ref }}",
|
|
OPENCLAW_RELEASE_TOOLING_REF: "${{ github.ref_name }}",
|
|
OPENCLAW_RELEASE_TOOLING_REPOSITORY: "${{ github.repository }}",
|
|
OPENCLAW_RELEASE_TOOLING_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
|
|
expect(oidcPublish.env).toMatchObject({
|
|
TARBALL_PATH: "${{ steps.publication_evidence.outputs.tarball_path }}",
|
|
PUBLISH_TAG: "${{ steps.publication_evidence.outputs.publish_tag }}",
|
|
});
|
|
expect(oidcPublish.run).toContain("node scripts/release-tooling-identity.mjs verify");
|
|
expect(oidcPublish.run).toContain(
|
|
'--release-publish-parent-state-policy "$OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY"',
|
|
);
|
|
expect(oidcPublish.run).not.toContain("plugin-npm-publish.sh");
|
|
|
|
const bootstrapPublish = workflowStep(pluginPublishJob, "Publish approved bootstrap tarball");
|
|
expect(bootstrapPublish.env).toMatchObject({
|
|
GH_TOKEN: "${{ github.token }}",
|
|
RELEASE_PUBLISH_PARENT_STATE_POLICY:
|
|
"${{ inputs.release_publish_run_id != '' && (needs.validate_release_publish_approval.outputs.parent_approval == 'receipt' && 'active' || (github.actor == 'github-actions[bot]' && 'active-or-failure' || 'manual-recovery')) || '' }}",
|
|
RELEASE_PUBLISH_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
|
|
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
|
RELEASE_PUBLISH_REF: "${{ inputs.release_publish_branch }}",
|
|
RELEASE_PUBLISH_FULL_REF: "${{ inputs.release_publish_full_ref }}",
|
|
WORKFLOW_FULL_REF: "${{ github.ref }}",
|
|
WORKFLOW_REF: "${{ github.ref_name }}",
|
|
WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
const identityIndex =
|
|
bootstrapPublish.run?.indexOf("node scripts/release-tooling-identity.mjs verify") ?? -1;
|
|
const publishIndex = bootstrapPublish.run?.indexOf('npm publish "$TARBALL_PATH"') ?? -1;
|
|
expect(identityIndex).toBeGreaterThan(-1);
|
|
expect(publishIndex).toBeGreaterThan(identityIndex);
|
|
expect(bootstrapPublish.run?.slice(identityIndex, publishIndex)).not.toContain("npm view");
|
|
expect(bootstrapPublish.run).toContain(
|
|
'--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"',
|
|
);
|
|
expect(bootstrapPublish.run).toContain('--release-publish-ref "$RELEASE_PUBLISH_REF"');
|
|
expect(bootstrapPublish.run).toContain(
|
|
'--release-publish-full-ref "$RELEASE_PUBLISH_FULL_REF"',
|
|
);
|
|
|
|
const packageCheck = workflowStep(pluginPublishJob, "Check immutable npm package version");
|
|
expect(packageCheck.run).toContain("scripts/plugin-npm-prepared-release.mjs registry");
|
|
expect(packageCheck.run).toContain("--allow-missing true");
|
|
expect(oidcPublish.if).toContain(
|
|
"steps.npm_package_version.outputs.already_published != 'true'",
|
|
);
|
|
const readback = workflowStep(pluginPublishJob, "Verify immutable npm registry readback");
|
|
expect(readback.if).toBeUndefined();
|
|
expect(readback.run).toContain("--allow-missing false");
|
|
expect(readback.env).toMatchObject({
|
|
TARBALL_PATH: "${{ steps.publication_evidence.outputs.tarball_path }}",
|
|
});
|
|
|
|
const pluginWrapper = readFileSync("scripts/plugin-npm-publish.sh", "utf8");
|
|
expect(pluginWrapper).toContain('--release-publish-ref "${OPENCLAW_RELEASE_PUBLISH_REF:-}"');
|
|
expect(pluginWrapper).toContain(
|
|
'--release-publish-full-ref "${OPENCLAW_RELEASE_PUBLISH_FULL_REF:-}"',
|
|
);
|
|
expect(pluginWrapper).toContain(
|
|
'--release-publish-parent-state-policy "${OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY:-}"',
|
|
);
|
|
const distTagIndex = pluginWrapper.indexOf(
|
|
'npm dist-tag add "${package_name}@${package_version}"',
|
|
);
|
|
const distTagIdentityIndex = pluginWrapper.lastIndexOf(
|
|
"verify_release_tooling_identity",
|
|
distTagIndex,
|
|
);
|
|
expect(distTagIdentityIndex).toBeGreaterThan(-1);
|
|
expect(distTagIndex).toBeGreaterThan(distTagIdentityIndex);
|
|
});
|
|
|
|
it("binds release evidence validation to the exact trusted workflow ref", () => {
|
|
for (const [workflowPath, jobName, stepName] of [
|
|
[
|
|
RELEASE_PUBLISH_WORKFLOW,
|
|
"resolve_release_target",
|
|
"Validate full release validation manifest",
|
|
],
|
|
[
|
|
OPENCLAW_NPM_RELEASE_WORKFLOW,
|
|
"publish_openclaw_npm",
|
|
"Verify full release validation evidence",
|
|
],
|
|
] as const) {
|
|
const step = workflowStep(workflowJob(workflowPath, jobName), stepName);
|
|
expect(step.env).toMatchObject({
|
|
TRUSTED_WORKFLOW_FULL_REF: "${{ github.ref }}",
|
|
TRUSTED_WORKFLOW_REF: "${{ github.ref_name }}",
|
|
TRUSTED_WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
expect(step.run).toContain("^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$");
|
|
expect(step.run).toContain('trusted_publisher_ref="refs/tags/${TRUSTED_WORKFLOW_REF}"');
|
|
expect(step.run).toContain('git rev-parse "${trusted_publisher_ref}^{commit}")" != "${');
|
|
expect(step.run).toContain('"+refs/heads/main:${trusted_main_ref}"');
|
|
expect(step.run).toContain('TRUSTED_MAIN_REF="${trusted_main_ref}"');
|
|
if (workflowPath === RELEASE_PUBLISH_WORKFLOW) {
|
|
expect(step.env?.VALIDATOR_FILE).toBe(
|
|
"${{ github.workspace }}/.release-validation-tooling/scripts/validate-full-release-validation-evidence.mjs",
|
|
);
|
|
expect(step.env?.STRICT_VALIDATOR_FILE).toBe(
|
|
"${{ github.workspace }}/.release-validation-tooling/scripts/release-ci-summary.mjs",
|
|
);
|
|
expect(step.run).toContain('MANIFEST_FILE="$manifest"');
|
|
expect(step.run).toContain('node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"');
|
|
} else {
|
|
expect(step.env?.STRICT_VALIDATOR_FILE).toBe(
|
|
"${{ github.workspace }}/trusted-workflow/scripts/release-ci-summary.mjs",
|
|
);
|
|
expect(step.env?.WORKFLOW_SHA).toBe("${{ github.workflow_sha }}");
|
|
expect(step.run).toContain("export EXPECTED_SHA MANIFEST_FILE");
|
|
expect(step.run).toContain(
|
|
'gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}"',
|
|
);
|
|
expect(step.run).toContain(
|
|
"node trusted-workflow/scripts/validate-full-release-validation-evidence.mjs",
|
|
);
|
|
}
|
|
expect(step.run).not.toContain('node "$STRICT_VALIDATOR_FILE"');
|
|
}
|
|
});
|
|
|
|
it("starts core npm without child approvals before the ClawHub receipt and bootstrap barriers", () => {
|
|
const fixture = createReleasePublishFixture();
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
for (const step of job.steps ?? []) {
|
|
if (step.name === "Authorize exact ClawHub package transactions") {
|
|
fixture.record("authorize-clawhub");
|
|
} else if (step.name === "Upload immutable ClawHub parent authorization") {
|
|
fixture.record("upload-receipt");
|
|
} else if (
|
|
step.name === "Start core npm publication" ||
|
|
step.name === "Complete publish workflows"
|
|
) {
|
|
const result = fixture.run(step);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
}
|
|
}
|
|
const events = fixture.events();
|
|
const dispatchIndex = events.findIndex((event) =>
|
|
event.startsWith("dispatch:openclaw-npm-release.yml"),
|
|
);
|
|
const receiptIndex = events.indexOf("authorize-clawhub");
|
|
expect(dispatchIndex).toBeGreaterThanOrEqual(0);
|
|
expect(dispatchIndex).toBeLessThan(receiptIndex);
|
|
const draftIndex = events.indexOf("draft");
|
|
expect(draftIndex).toBeGreaterThanOrEqual(0);
|
|
expect(events).not.toContain("windows");
|
|
expect(events).not.toContain("android");
|
|
expect(dispatchIndex).toBeGreaterThan(draftIndex);
|
|
const startIndex = events.indexOf("wait:openclaw-npm-release.yml:publish_openclaw_npm:false");
|
|
expect(startIndex).toBeGreaterThan(dispatchIndex);
|
|
expect(startIndex).toBeLessThan(receiptIndex);
|
|
expect(events).toContain("wait:plugin-npm-release.yml:terminal:false");
|
|
expect(events.indexOf("wait:plugin-clawhub-new.yml:terminal:true")).toBeGreaterThan(
|
|
dispatchIndex,
|
|
);
|
|
expect(events[dispatchIndex]).toContain("-f release_publish_run_id=44");
|
|
expect(events[dispatchIndex]).toContain("-f release_publish_run_attempt=2");
|
|
expect(events[dispatchIndex]).toContain("-f release_publish_full_ref=refs/heads/main");
|
|
expect(events[dispatchIndex]).toContain("-f full_release_validation_run_attempt=3");
|
|
expect(events).toContain("verify:0:bootstrap=true:workflow=refs/heads/main");
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"receipt preparation",
|
|
{ CLAWHUB_AUTHORIZATION_OUTCOME: "failure", CLAWHUB_RECEIPT_OUTCOME: "skipped" },
|
|
false,
|
|
false,
|
|
],
|
|
["receipt upload", { CLAWHUB_RECEIPT_OUTCOME: "failure" }, false, false],
|
|
["bootstrap publication", { MOCK_BOOTSTRAP_RESULT: "1" }, false, true],
|
|
["normal ClawHub publication", { MOCK_CLAWHUB_RESULT: "1" }, true, true],
|
|
])(
|
|
"collects core evidence after %s fails",
|
|
(_failure, env, bootstrapCompleted, approvesClawHub) => {
|
|
const fixture = createReleasePublishFixture();
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const initialize = job.steps?.find(
|
|
(step) => step.name === "Initialize postpublish diagnostics",
|
|
);
|
|
if (initialize) {
|
|
const initialized = fixture.run(initialize);
|
|
expect(initialized.status, initialized.stderr).toBe(0);
|
|
}
|
|
const start = fixture.run(workflowStep(job, "Start core npm publication"));
|
|
expect(start.status, start.stderr).toBe(0);
|
|
const completed = fixture.run(workflowStep(job, "Complete publish workflows"), env);
|
|
expect(completed.status, completed.stderr).toBe(1);
|
|
const events = fixture.events();
|
|
expect(events).toContain("wait:openclaw-npm-release.yml:terminal:false");
|
|
const approval = String(approvesClawHub);
|
|
expect(events).toContain(`wait:plugin-clawhub-release.yml:terminal:${approval}`);
|
|
expect(events).toContain(`wait:plugin-clawhub-new.yml:terminal:${approval}`);
|
|
const verification = `verify:1:bootstrap=${bootstrapCompleted}:workflow=refs/heads/main`;
|
|
expect(events.indexOf(verification)).toBeGreaterThan(
|
|
events.lastIndexOf("finished:openclaw-npm-release.yml:0"),
|
|
);
|
|
expect(events).toContain("release-evidence");
|
|
expect(events).not.toContain("windows");
|
|
expect(fixture.summary()).toContain("evidence updated; a required publish child failed");
|
|
expect(fixture.summary()).not.toContain("left as draft");
|
|
},
|
|
);
|
|
|
|
it.each(["plugin", "core"] as const)(
|
|
"collects failed %s npm publication without repeating approval",
|
|
(failedPublisher) => {
|
|
const fixture = createReleasePublishFixture({
|
|
MOCK_PLUGIN_NPM_RESULT: failedPublisher === "plugin" ? "1" : "0",
|
|
MOCK_CORE_START_RESULT: failedPublisher === "core" ? "1" : "0",
|
|
MOCK_CORE_RESULT: "1",
|
|
});
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const initialize = job.steps?.find(
|
|
(step) => step.name === "Initialize postpublish diagnostics",
|
|
);
|
|
if (initialize) {
|
|
const initialized = fixture.run(initialize);
|
|
expect(initialized.status, initialized.stderr).toBe(0);
|
|
}
|
|
const start = fixture.run(workflowStep(job, "Start core npm publication"));
|
|
expect(start.status, start.stderr).toBe(1);
|
|
if (failedPublisher === "plugin") {
|
|
expect(fixture.outputs().plugin_npm_completed).toBeUndefined();
|
|
expect(fixture.events().some((event) => event.startsWith("dispatch:"))).toBe(false);
|
|
expect(fixture.events().filter((event) => event.startsWith("cancel:"))).toHaveLength(2);
|
|
} else {
|
|
expect(fixture.outputs()).toMatchObject({
|
|
plugin_npm_completed: "true",
|
|
openclaw_npm_run_id: "404",
|
|
});
|
|
const completed = fixture.run(workflowStep(job, "Complete publish workflows"), {
|
|
CORE_START_OUTCOME: "failure",
|
|
CLAWHUB_AUTHORIZATION_OUTCOME: "skipped",
|
|
CLAWHUB_RECEIPT_OUTCOME: "skipped",
|
|
});
|
|
expect(completed.status, completed.stderr).toBe(1);
|
|
expect(fixture.events()).toContain("cancel:run cancel --repo openclaw/openclaw 404");
|
|
expect(fixture.events()).toContain("wait:openclaw-npm-release.yml:terminal:false");
|
|
expect(fixture.events().some((event) => event.startsWith("verify:"))).toBe(false);
|
|
}
|
|
const terminal = job.steps?.find((step) => step.name === "Record postpublish outcome");
|
|
if (terminal) {
|
|
const recorded = fixture.run(terminal, {
|
|
CORE_START_OUTCOME: "failure",
|
|
COMPLETION_OUTCOME: failedPublisher === "plugin" ? "skipped" : "failure",
|
|
PUBLISH_JOB_STATUS: "failure",
|
|
});
|
|
expect(recorded.status, recorded.stderr).toBe(0);
|
|
expect(fixture.summary()).toContain("gh workflow run openclaw-release-publish.yml");
|
|
}
|
|
expect(
|
|
JSON.parse(
|
|
readFileSync(join(fixture.root, "evidence/release-postpublish-diagnostics.json"), "utf8"),
|
|
),
|
|
).toMatchObject({
|
|
verification: "unattempted",
|
|
context: { parentRunId: "44", parentRunAttempt: "2" },
|
|
stages: { coreNpm: { state: "unattempted" } },
|
|
children: {
|
|
pluginNpm: { suppliedRunId: "101", runAttempt: null },
|
|
openclawNpm: { suppliedRunId: failedPublisher === "core" ? "404" : null },
|
|
},
|
|
});
|
|
expect(existsSync(join(fixture.root, "evidence/release-postpublish-evidence.json"))).toBe(
|
|
false,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([false, true])("preserves detached ClawHub and npm resume=%s", (resume) => {
|
|
const fixture = createReleasePublishFixture({
|
|
WAIT_FOR_CLAWHUB: "false",
|
|
CHILD_OPENCLAW_NPM_ALREADY_PUBLISHED: String(resume),
|
|
CHILD_OPENCLAW_NPM_EXPECTED_WORKFLOW_REF: "refs/tags/release-publish/aaaaaaaaaaaa-42",
|
|
CHILD_OPENCLAW_NPM_RUN_ATTEMPT: resume ? "1" : "",
|
|
});
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
for (const stepName of ["Start core npm publication", "Complete publish workflows"]) {
|
|
const result = fixture.run(workflowStep(job, stepName));
|
|
expect(result.status, result.stderr).toBe(0);
|
|
}
|
|
const events = fixture.events();
|
|
expect(events.some((event) => event.startsWith("wait:plugin-clawhub"))).toBe(false);
|
|
expect(events.some((event) => event.startsWith("dispatch:"))).toBe(!resume);
|
|
expect(events).toContain(`verify-attempt:${resume ? "1" : ""}`);
|
|
expect(events).toContain(
|
|
"verify:0:bootstrap=false:workflow=refs/tags/release-publish/aaaaaaaaaaaa-42",
|
|
);
|
|
});
|
|
|
|
it.each(["success", "binding", "assets"] as const)(
|
|
"keeps verifier success separate from postpublish %s completion",
|
|
(outcome) => {
|
|
const version = "2026.9.1";
|
|
const fixture = createReleasePublishFixture(
|
|
{
|
|
RELEASE_TAG: `v${version}`,
|
|
PUBLISH_OPENCLAW_NPM: "false",
|
|
CHILD_PLUGIN_CLAWHUB_RUN_ID: "",
|
|
CHILD_PLUGIN_CLAWHUB_BOOTSTRAP_RUN_ID: "",
|
|
},
|
|
{
|
|
functions: `
|
|
${shellFunctionSource(readFileSync("scripts/lib/release-publish-children.sh", "utf8"), "verify_published_release")}
|
|
clawhub_workflow_ref=main
|
|
bootstrap_plugins=""
|
|
write_clawhub_runtime_state() { printf '%s\\n' '{"verifierArgs":["--skip-clawhub"]}' > "$1"; }
|
|
${outcome === "assets" ? "upload_release_evidence_assets() { echo asset-upload-denied >&2; return 17; }" : ""}
|
|
`,
|
|
},
|
|
);
|
|
writeFileSync(
|
|
join(fixture.root, "package.json"),
|
|
JSON.stringify({ type: "module", version }),
|
|
);
|
|
mkdirSync(join(fixture.root, "extensions"));
|
|
mkdirSync(join(fixture.root, "scripts"));
|
|
writeFileSync(join(fixture.root, "scripts/openclaw-npm-postpublish-verify.ts"), "");
|
|
copyFileSync(
|
|
resolve("scripts/release-verify-beta.ts"),
|
|
join(fixture.root, ".release-harness/scripts/release-verify-beta.ts"),
|
|
);
|
|
const bin = join(fixture.root, "bin");
|
|
mkdirSync(bin);
|
|
writeFileSync(
|
|
join(bin, "git"),
|
|
`#!/bin/sh
|
|
if [ "$PWD" = "$GITHUB_WORKSPACE" ] && [ "$*" = "rev-parse HEAD" ]; then printf '%s\\n' "$TARGET_SHA"; else exec /usr/bin/git "$@"; fi
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
for (const command of ["npm", "gh"]) {
|
|
writeFileSync(
|
|
join(bin, command),
|
|
`#!${process.execPath}
|
|
const args = process.argv.slice(2);
|
|
if (args[0] === "view") {
|
|
console.log(JSON.stringify({ version: "${version}", "dist-tags.beta": "${version}", "dist.integrity": "sha512-fixture", "dist.tarball": "https://example.invalid/openclaw.tgz" }));
|
|
} else if (args[0] === "run" && args[1] === "view") {
|
|
console.log(JSON.stringify({ workflowName: args[2] === "101" ? "Plugin NPM Release" : "OpenClaw NPM Release", headBranch: "main", event: "workflow_dispatch", status: "completed", conclusion: "success", jobs: [] }));
|
|
} else { throw new Error("Unexpected verifier mutation: " + args.join(" ")); }
|
|
`,
|
|
{ mode: 0o755 },
|
|
);
|
|
}
|
|
const manifest = join(fixture.root, "manifest");
|
|
mkdirSync(manifest);
|
|
writeFileSync(
|
|
join(manifest, "full-release-validation-manifest.json"),
|
|
JSON.stringify({
|
|
runId: "66",
|
|
runAttempt: outcome === "binding" ? "4" : "3",
|
|
workflowRef: "release-ci/tooling",
|
|
targetSha: "a".repeat(40),
|
|
}),
|
|
);
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const env = {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
FULL_RELEASE_VALIDATION_MANIFEST_DIR: manifest,
|
|
CHILD_OPENCLAW_NPM_RUN_ID: "404",
|
|
};
|
|
const initialized = fixture.run(workflowStep(job, "Initialize postpublish diagnostics"), env);
|
|
expect(initialized.status, initialized.stderr).toBe(0);
|
|
const completed = fixture.run(workflowStep(job, "Complete publish workflows"), env);
|
|
expect(completed.status, completed.stderr).toBe(
|
|
outcome === "success" ? 0 : outcome === "assets" ? 17 : 1,
|
|
);
|
|
const terminal = fixture.run(workflowStep(job, "Record postpublish outcome"), {
|
|
...env,
|
|
COMPLETION_OUTCOME: outcome === "success" ? "success" : "failure",
|
|
PUBLISH_JOB_STATUS: outcome === "success" ? "success" : "failure",
|
|
});
|
|
expect(terminal.status, terminal.stderr).toBe(0);
|
|
const diagnostic = JSON.parse(
|
|
readFileSync(join(fixture.root, "evidence/release-postpublish-diagnostics.json"), "utf8"),
|
|
);
|
|
expect(diagnostic.verification).toBe("success");
|
|
expect(diagnostic.stages.coreNpm.state).toBe("success");
|
|
expect(diagnostic.stages.binding.state).toBe(outcome === "binding" ? "failure" : "success");
|
|
expect(diagnostic.stages.assets.state).toBe(
|
|
outcome === "binding" ? "unattempted" : outcome === "assets" ? "failure" : "success",
|
|
);
|
|
expect(diagnostic.jobOutcomeBeforeArtifactUploads).toBe(
|
|
outcome === "success" ? "success" : "failure",
|
|
);
|
|
const canonical = join(fixture.root, "evidence/release-postpublish-evidence.json");
|
|
expect(existsSync(canonical)).toBe(outcome !== "binding");
|
|
if (outcome !== "binding") {
|
|
const receipt = JSON.parse(readFileSync(canonical, "utf8"));
|
|
expect(receipt).toMatchObject({
|
|
version: 1,
|
|
releasePublishRunId: "44",
|
|
workflowRuns: expect.arrayContaining([
|
|
expect.objectContaining({
|
|
id: "66",
|
|
runAttempt: "3",
|
|
targetSha: "a".repeat(40),
|
|
}),
|
|
]),
|
|
});
|
|
expect(receipt.kind).toBeUndefined();
|
|
expect(fixture.outputs().postpublish_evidence_ready).toBe("true");
|
|
}
|
|
},
|
|
);
|
|
|
|
it("retains cancelled or skipped verification without authorizing recovery", () => {
|
|
const fixture = createReleasePublishFixture();
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const diagnosticPath = join(fixture.root, "evidence/release-postpublish-diagnostics.json");
|
|
const imported = fixture.run({
|
|
run: `node --import tsx --input-type=module -e 'await import("./.release-harness/scripts/lib/release-beta-verifier.ts")' diagnostic-import initialize`,
|
|
});
|
|
expect(imported.status, imported.stderr).toBe(0);
|
|
expect(imported.stderr).toBe("");
|
|
expect(existsSync(diagnosticPath)).toBe(false);
|
|
const initialized = fixture.run(workflowStep(job, "Initialize postpublish diagnostics"));
|
|
expect(initialized.status, initialized.stderr).toBe(0);
|
|
const terminal = fixture.run(workflowStep(job, "Record postpublish outcome"), {
|
|
CORE_START_OUTCOME: "skipped",
|
|
COMPLETION_OUTCOME: "skipped",
|
|
PUBLISH_JOB_STATUS: "cancelled",
|
|
});
|
|
expect(terminal.status, terminal.stderr).toBe(0);
|
|
const diagnostic = JSON.parse(readFileSync(diagnosticPath, "utf8"));
|
|
expect(diagnostic.verification).toBe("unattempted");
|
|
expect(diagnostic.jobOutcomeBeforeArtifactUploads).toBe("cancelled");
|
|
expect(fixture.events()).toEqual([""]);
|
|
});
|
|
|
|
it("selects diagnostics separately without accepting them as successful evidence", () => {
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const initialize = workflowStep(job, "Initialize postpublish diagnostics");
|
|
expect(job.steps!.indexOf(initialize)).toBeLessThan(
|
|
job.steps!.indexOf(workflowStep(job, "Verify all prepared plugin bytes before publication")),
|
|
);
|
|
const diagnostics = workflowStep(job, "Upload postpublish diagnostics");
|
|
expect(diagnostics.if).toBe("${{ always() }}");
|
|
expect(diagnostics["continue-on-error"]).toBe(true);
|
|
expect(diagnostics.uses).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(diagnostics.with).toMatchObject({
|
|
name: "openclaw-release-postpublish-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
path: "${{ runner.temp }}/openclaw-release-postpublish-evidence/release-postpublish-diagnostics.json",
|
|
"if-no-files-found": "warn",
|
|
});
|
|
const success = workflowStep(job, "Upload postpublish evidence");
|
|
expect(success.with?.path).toBe(
|
|
"${{ runner.temp }}/openclaw-release-postpublish-evidence/release-postpublish-evidence.json",
|
|
);
|
|
expect(success.with?.["if-no-files-found"]).toBe("error");
|
|
expect(success.if).toContain("steps.complete.outcome == 'success'");
|
|
const fixture = createReleasePublishFixture(
|
|
{},
|
|
{
|
|
functions: shellFunctionSource(
|
|
readFileSync("scripts/lib/release-publish-children.sh", "utf8"),
|
|
"upload_release_evidence_assets",
|
|
),
|
|
},
|
|
);
|
|
const initialized = fixture.run(initialize);
|
|
expect(initialized.status, initialized.stderr).toBe(0);
|
|
const manifestDir = join(fixture.root, "manifest");
|
|
mkdirSync(manifestDir);
|
|
writeFileSync(join(manifestDir, "full-release-validation-manifest.json"), "{}");
|
|
const assets = fixture.run(
|
|
{
|
|
run: 'source "$GITHUB_WORKSPACE/.release-harness/scripts/lib/release-publish-children.sh"\nupload_release_evidence_assets',
|
|
},
|
|
{ FULL_RELEASE_VALIDATION_MANIFEST_DIR: manifestDir },
|
|
);
|
|
expect(assets.status).toBe(1);
|
|
expect(assets.stderr).toContain("Postpublish release evidence is missing");
|
|
expect(fixture.events()).toEqual([""]);
|
|
});
|
|
|
|
it("fetches release diagnostics only for completed failed jobs", () => {
|
|
const root = tempDirs.make("release-failed-job-summary-");
|
|
const jobsPath = join(root, "jobs.json");
|
|
const callsPath = join(root, "log-requests");
|
|
writeFileSync(callsPath, "");
|
|
writeFileSync(
|
|
jobsPath,
|
|
JSON.stringify({
|
|
jobs: [
|
|
{ databaseId: 101, name: "failed", status: "completed", conclusion: "failure" },
|
|
{ databaseId: 102, name: "cancelled", status: "completed", conclusion: "cancelled" },
|
|
{ databaseId: 103, name: "passed", status: "completed", conclusion: "success" },
|
|
{ databaseId: 104, name: "skipped", status: "completed", conclusion: "skipped" },
|
|
{ databaseId: 105, name: "running", status: "in_progress", conclusion: "" },
|
|
{ databaseId: 106, name: "queued", status: "queued", conclusion: "" },
|
|
],
|
|
}),
|
|
);
|
|
const source = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -euo pipefail
|
|
gh() {
|
|
if [[ "$*" == *"--json jobs"* ]]; then
|
|
local query
|
|
for query; do :; done
|
|
jq "$query" "$JOBS_FILE"
|
|
return
|
|
fi
|
|
while (( $# )); do
|
|
if [[ "$1" == "--job" ]]; then
|
|
printf '%s\\n' "$2" >> "$LOG_REQUESTS"
|
|
return
|
|
fi
|
|
shift
|
|
done
|
|
return 1
|
|
}
|
|
${shellFunctionSource(source, "gh_read")}
|
|
${shellFunctionSource(source, "print_failed_run_summary")}
|
|
print_failed_run_summary 404
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
JOBS_FILE: jobsPath,
|
|
LOG_REQUESTS: callsPath,
|
|
},
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(callsPath, "utf8")).toBe("101\n102\n");
|
|
});
|
|
|
|
it.each([
|
|
["queued", 0, 0],
|
|
["started", 0, 0],
|
|
["duplicate", 1, 0],
|
|
["wrong-sha", 1, 0],
|
|
["changed-sha-after-start", 1, 0],
|
|
["terminal", 0, 0],
|
|
["bootstrap-lagged", 0, 1],
|
|
["bootstrap-approval-failed", 1, 1],
|
|
])("observes child publication with only bootstrap approvals: %s", (mode, exit, approvals) => {
|
|
const root = tempDirs.make("release-publish-wait-");
|
|
const calls = join(root, "calls");
|
|
writeFileSync(calls, "");
|
|
const source = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
const bootstrap = mode.startsWith("bootstrap-");
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -euo pipefail
|
|
iteration=0
|
|
gh() {
|
|
if [[ "$1 $2" == "run cancel" ]]; then return 0; fi
|
|
if [[ "$1 $2" == "run view" ]]; then
|
|
if [[ "$*" == *"--json headSha,url"* ]]; then
|
|
local sha="$EXPECTED_SHA"
|
|
if [[ "$MODE" == "wrong-sha" || ( "$MODE" == "changed-sha-after-start" && "$iteration" -gt 0 ) ]]; then
|
|
sha="${"b".repeat(40)}"
|
|
fi
|
|
printf '{"headSha":"%s","url":"https://example.invalid/run/404"}\\n' "$sha"
|
|
elif [[ "$*" == *"--json status,url,updatedAt"* ]]; then
|
|
local state=in_progress
|
|
if [[ ( "$MODE" == "terminal" && "$iteration" -gt 0 ) || -f "$APPROVED" ]]; then state=completed; fi
|
|
printf '{"status":"%s","url":"https://example.invalid/run/404","updatedAt":"2026-09-01T00:00:00Z"}\\n' "$state"
|
|
elif [[ "$*" == *"--json jobs"* ]]; then
|
|
local jobs
|
|
if [[ "$MODE" == "duplicate" ]]; then
|
|
jobs='[{"name":"publish_openclaw_npm","status":"in_progress"},{"name":"publish_openclaw_npm","status":"in_progress"}]'
|
|
elif [[ "$MODE" == "started" || "$iteration" -gt 0 ]]; then
|
|
jobs='[{"name":"publish_openclaw_npm","status":"in_progress"}]'
|
|
else
|
|
jobs='[{"name":"publish_openclaw_npm","status":"queued"}]'
|
|
fi
|
|
printf '{"jobs":%s}\\n' "$jobs" | jq -c "\${!#}"
|
|
else
|
|
printf '{"conclusion":"success","url":"https://example.invalid/run/404","createdAt":"2026-09-01T00:00:00Z","updatedAt":"2026-09-01T00:00:01Z"}\\n'
|
|
fi
|
|
elif [[ "$1 $2 $3" == "api -X GET" && "$MODE" == bootstrap-* ]]; then
|
|
if [[ "$MODE" == "bootstrap-lagged" && "$iteration" -eq 0 ]]; then printf '[]\\n';
|
|
else printf '[{"environment":{"id":7,"name":"clawhub-plugin-bootstrap"},"current_user_can_approve":true}]\\n'; fi
|
|
elif [[ "$1 $2 $3" == "api -X POST" && "$MODE" == bootstrap-* ]]; then
|
|
printf 'approval\\n' >> "$CALLS"
|
|
if [[ "$MODE" == "bootstrap-approval-failed" ]]; then return 42; fi
|
|
touch "$APPROVED"
|
|
else printf 'unexpected:%s\\n' "$*" >> "$CALLS"; return 99; fi
|
|
}
|
|
sleep() { iteration=$((iteration + 1)); if [[ "$iteration" -gt 3 ]]; then exit 91; fi; }
|
|
print_failed_run_summary() { :; }
|
|
${shellFunctionSource(source, "gh_read")}
|
|
${shellFunctionSource(source, "verify_child_run_sha")}
|
|
${shellFunctionSource(source, "print_pending_deployments")}
|
|
${shellFunctionSource(source, "approve_pending_deployments")}
|
|
${shellFunctionSource(source, "wait_for_run")}
|
|
wait_for_run "$WORKFLOW" 404 "$EXPECTED_SHA" "$STARTED_JOB" "$APPROVE_ENVIRONMENTS"
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
timeout: 10_000,
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
MODE: mode,
|
|
EXPECTED_SHA: "a".repeat(40),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
|
APPROVED: join(root, "approved"),
|
|
CALLS: calls,
|
|
WORKFLOW: bootstrap ? "plugin-clawhub-new.yml" : "openclaw-npm-release.yml",
|
|
STARTED_JOB: bootstrap || mode === "terminal" ? "" : "publish_openclaw_npm",
|
|
APPROVE_ENVIRONMENTS: String(bootstrap),
|
|
},
|
|
},
|
|
);
|
|
expect(result.status, result.stderr || result.stdout).toBe(exit);
|
|
expect(readFileSync(calls, "utf8").split("\n").filter(Boolean)).toEqual(
|
|
Array.from({ length: approvals }, () => "approval"),
|
|
);
|
|
if (!bootstrap) {
|
|
expect(result.stdout).not.toContain("approve: gh api");
|
|
}
|
|
});
|
|
|
|
it.each(["2026.9.1\nsha=" + "b".repeat(40), "", "v2026.9.1", "2026.13.1", "2026.9"])(
|
|
"rejects an Android pin that is not an exact version before writing outputs (%j)",
|
|
(pin) => {
|
|
const target = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const ref = workflowStep(target, "Resolve checked-out release ref");
|
|
const root = tempDirs.make("release-android-pin-reject-");
|
|
mkdirSync(join(root, "apps/android"), { recursive: true });
|
|
writeFileSync(join(root, "apps/android/version.json"), JSON.stringify({ version: pin }));
|
|
writeFileSync(join(root, "git"), `#!/bin/sh\nprintf '%s\\n' '${"a".repeat(40)}'\n`, {
|
|
mode: 0o755,
|
|
});
|
|
writeFileSync(join(root, "gh"), `#!/bin/sh\nprintf '%s\\n' '${"c".repeat(40)}'\n`, {
|
|
mode: 0o755,
|
|
});
|
|
const result = spawnSync("bash", ["-c", ref.run ?? ""], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${root}:${process.env.PATH}`,
|
|
EXPECTED_SIGNED_TAG_SHA: "a".repeat(40),
|
|
EXPECTED_SIGNED_TAG_OBJECT_SHA: "c".repeat(40),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
RELEASE_TAG: "v2026.9.1",
|
|
RELEASE_NPM_DIST_TAG: "latest",
|
|
PUBLISH_OPENCLAW_NPM: "true",
|
|
PUBLISH_DOCKER_ONLY: "false",
|
|
GITHUB_OUTPUT: join(root, "output"),
|
|
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
|
},
|
|
});
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("exact YYYY.M.PATCH Android version");
|
|
// A rejected pin must not reach GITHUB_OUTPUT at all, so it can never add output records.
|
|
expect(existsSync(join(root, "output"))).toBe(false);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["v2026.9.1", "2026.7.4", false],
|
|
["v2026.9.1", "2026.9.1", true],
|
|
["v2026.9.1-1", "2026.9.1", true],
|
|
["v2026.9.1-1", "2026.7.4", false],
|
|
["v2026.9.1-beta.1", "2026.9.1", false],
|
|
])("admits Android only when %s pins its native train (%s)", (tag, pin, native) => {
|
|
const target = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const ref = workflowStep(target, "Resolve checked-out release ref");
|
|
const root = tempDirs.make("release-android-pin-");
|
|
mkdirSync(join(root, "apps/android"), { recursive: true });
|
|
writeFileSync(join(root, "apps/android/version.json"), JSON.stringify({ version: pin }));
|
|
writeFileSync(join(root, "git"), `#!/bin/sh\nprintf '%s\\n' '${"a".repeat(40)}'\n`, {
|
|
mode: 0o755,
|
|
});
|
|
writeFileSync(join(root, "gh"), `#!/bin/sh\nprintf '%s\\n' '${"c".repeat(40)}'\n`, {
|
|
mode: 0o755,
|
|
});
|
|
const summaryPath = join(root, "summary");
|
|
writeFileSync(summaryPath, "");
|
|
const result = spawnSync("bash", ["-c", ref.run ?? ""], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: `${root}:${process.env.PATH}`,
|
|
EXPECTED_SIGNED_TAG_SHA: "a".repeat(40),
|
|
EXPECTED_SIGNED_TAG_OBJECT_SHA: "c".repeat(40),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
RELEASE_TAG: tag,
|
|
RELEASE_NPM_DIST_TAG: tag.includes("-beta.") ? "beta" : "latest",
|
|
PUBLISH_OPENCLAW_NPM: "true",
|
|
PUBLISH_DOCKER_ONLY: "false",
|
|
GITHUB_OUTPUT: join(root, "output"),
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const stepOutputs = Object.fromEntries(
|
|
readFileSync(join(root, "output"), "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]),
|
|
);
|
|
const outputs: Record<string, string> = { coverage_policy: "npm-stable-v1" };
|
|
for (const [name, expression] of Object.entries(target.outputs ?? {})) {
|
|
const field = expression.match(/^\$\{\{ steps\.ref\.outputs\.(\w+) \}\}$/u)?.[1];
|
|
if (field) {
|
|
outputs[name] = stepOutputs[field] ?? "";
|
|
}
|
|
}
|
|
const inputs = { tag, publish_openclaw_npm: true, publish_docker_only: false };
|
|
const needs = {
|
|
resolve_release_target: { result: "success", outputs },
|
|
publish: { result: "success" },
|
|
qualify_android_native: { outputs: { qualified: "true" } },
|
|
};
|
|
const admitted = (jobName: string) =>
|
|
runInNewContext(workflowJob(RELEASE_PUBLISH_WORKFLOW, jobName).if?.slice(3, -2) ?? "false", {
|
|
inputs,
|
|
needs,
|
|
always: () => true,
|
|
cancelled: () => false,
|
|
contains: (value: string, needle: string) => value.includes(needle),
|
|
});
|
|
expect(admitted("qualify_android_native")).toBe(native);
|
|
expect(admitted("publish_android")).toBe(native);
|
|
// Broader evidence can omit the extra native CI, but never bypass the pin.
|
|
outputs.coverage_policy = "";
|
|
expect(admitted("publish_android")).toBe(native);
|
|
outputs.coverage_policy = "npm-stable-v1";
|
|
needs.qualify_android_native.outputs.qualified = "";
|
|
expect(admitted("publish_android")).toBe(false);
|
|
inputs.publish_openclaw_npm = false;
|
|
expect(admitted("qualify_android_native")).toBe(false);
|
|
expect(admitted("publish_android")).toBe(false);
|
|
expect(outputs.android_pin_version).toBe(pin);
|
|
expect(workflowStep(target, "Checkout release tag").with?.["sparse-checkout"]).toContain(
|
|
"apps/android/version.json",
|
|
);
|
|
|
|
if (tag.includes("beta") || native) {
|
|
return;
|
|
}
|
|
const note = `- Android APK: skipped — apps/android/version.json is ${pin}, release train is 2026.9.1; run pnpm android:version:pin -- --from-gateway before the next tag.`;
|
|
expect(readFileSync(summaryPath, "utf8")).toContain(note);
|
|
expect(outputs.android_release_note).toBe(note);
|
|
const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
expect(publishJob.env?.ANDROID_RELEASE_NOTE).toBe(
|
|
"${{ needs.resolve_release_target.outputs.android_release_note }}",
|
|
);
|
|
const fixture = createReleasePublishFixture(
|
|
{
|
|
RELEASE_TAG: tag,
|
|
ANDROID_RELEASE_NOTE: outputs.android_release_note ?? "",
|
|
CHILD_OPENCLAW_NPM_ALREADY_PUBLISHED: "true",
|
|
},
|
|
{ functions: 'append_release_proof_to_github_release() { record "$android_release_note"; }' },
|
|
);
|
|
const completed = fixture.run(workflowStep(publishJob, "Complete publish workflows"));
|
|
expect(completed.status, completed.stderr).toBe(0);
|
|
expect(fixture.events()).toContain(note);
|
|
});
|
|
|
|
it("resolves broad release evidence and exact-binds every publish child", () => {
|
|
const resolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const publishOrchestration = releasePublishOrchestration(publishJob);
|
|
|
|
for (const stepName of [
|
|
"Download OpenClaw npm preflight manifest",
|
|
"Resolve full release validation run",
|
|
"Download full release validation manifest",
|
|
"Validate OpenClaw npm preflight manifest",
|
|
"Validate full release validation manifest",
|
|
]) {
|
|
expect(workflowStep(resolveJob, stepName).if).toContain(
|
|
"inputs.plugin_publish_scope == 'all-publishable'",
|
|
);
|
|
}
|
|
expect(
|
|
workflowStep(resolveJob, "Checkout trusted release validation tooling").with,
|
|
).toMatchObject({
|
|
ref: "${{ github.workflow_sha }}",
|
|
"persist-credentials": false,
|
|
"sparse-checkout": "scripts",
|
|
});
|
|
for (const stepName of [
|
|
"Write Android release approval",
|
|
"Attest Android release approval",
|
|
"Upload Android release approval",
|
|
]) {
|
|
const androidJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish_android");
|
|
expect(workflowStep(androidJob, stepName)).toBeDefined();
|
|
expect(androidJob.if).toContain("inputs.publish_openclaw_npm");
|
|
}
|
|
const nativeJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "qualify_android_native");
|
|
const androidJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish_android");
|
|
expect(nativeJob.needs).toEqual(["resolve_release_target"]);
|
|
expect(nativeJob.if).toContain("coverage_policy == 'npm-stable-v1'");
|
|
expect(androidJob.needs).toEqual([
|
|
"resolve_release_target",
|
|
"publish",
|
|
"qualify_android_native",
|
|
]);
|
|
expect(androidJob.if).toContain("needs.qualify_android_native.outputs.qualified == 'true'");
|
|
expect(publishJob.needs).toEqual(["resolve_release_target"]);
|
|
expect(workflowJob(RELEASE_PUBLISH_WORKFLOW, "finalize_github_release").needs).toEqual([
|
|
"publish",
|
|
"publish_docker",
|
|
"approve_github_release",
|
|
"finalize_github_release_before_docker",
|
|
]);
|
|
expect(nativeJob["continue-on-error"]).toBe(true);
|
|
expect(androidJob["continue-on-error"]).toBe(true);
|
|
|
|
expect(publishOrchestration.env?.PARENT_WORKFLOW_SHA).toBe("${{ github.sha }}");
|
|
expect(publishOrchestration.env?.PARENT_WORKFLOW_BRANCH).toBe("${{ github.ref_name }}");
|
|
expect(publishOrchestration.env?.PARENT_WORKFLOW_FULL_REF).toBe("${{ github.ref }}");
|
|
expect(publishOrchestration.env?.CHILD_WORKFLOW_REF).toBe(
|
|
"${{ steps.clawhub_plan.outputs.child_workflow_ref }}",
|
|
);
|
|
expect(readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8")).toContain(
|
|
"public verification follows terminal parent success",
|
|
);
|
|
expectTextToIncludeAll(publishOrchestration.run, [
|
|
'gh_read api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}"',
|
|
'if [[ "$resolved_workflow_sha" != "$expected_sha" ]]',
|
|
'verify_child_run_sha "$workflow" "$run_id" "$expected_sha" || return 1',
|
|
'approve_pending_deployments "${workflow}" "${run_id}" "${expected_sha}"',
|
|
'dispatch_workflow_at_ref "${RELEASE_TAG}" "${TARGET_SHA}" android-release.yml',
|
|
'if ! wait_for_run plugin-npm-release.yml "${plugin_npm_run_id}" "${PARENT_WORKFLOW_SHA}" "" false true; then',
|
|
'wait_for_run_background openclaw-npm-release.yml "${openclaw_npm_run_id}" "${PARENT_WORKFLOW_SHA}"',
|
|
'-f release_publish_branch="${PARENT_WORKFLOW_BRANCH}"',
|
|
'-f release_publish_full_ref="${PARENT_WORKFLOW_FULL_REF}"',
|
|
"plugin-clawhub-release.yml: detached; approval and publish not awaited",
|
|
"plugin-clawhub-new.yml: detached; approvals and bootstrap not awaited",
|
|
]);
|
|
});
|
|
|
|
it.each([
|
|
[false, false],
|
|
[true, false],
|
|
[false, true],
|
|
])(
|
|
"dispatches qualified Android without awaiting it (dispatch failure: %s, existing assets: %s)",
|
|
(dispatchFailure, assetsVerified) => {
|
|
const script = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
const root = tempDirs.make("android-detached-publish-");
|
|
const summaryPath = join(root, "summary");
|
|
writeFileSync(summaryPath, "");
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -euo pipefail
|
|
is_android_release() { return 0; }
|
|
verify_android_release_asset_contract() { return ${assetsVerified ? 0 : 1}; }
|
|
dispatch_workflow_at_ref() { ${dispatchFailure ? "return 1" : "echo 456"}; }
|
|
wait_for_run() { echo unexpected-android-wait >&2; return 1; }
|
|
${shellFunctionSource(script, "promote_android_release_asset")}
|
|
native_failed=0
|
|
promote_android_release_asset || native_failed=$?
|
|
printf 'native_failed=%s\\n' "$native_failed"
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
RUNNER_TEMP: root,
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ID: "123",
|
|
GITHUB_RUN_ATTEMPT: "2",
|
|
RELEASE_TAG: "v2026.8.1",
|
|
TARGET_SHA: "a".repeat(40),
|
|
PARENT_WORKFLOW_BRANCH: "main",
|
|
PARENT_WORKFLOW_FULL_REF: "refs/heads/main",
|
|
PARENT_WORKFLOW_SHA: "d".repeat(40),
|
|
},
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(
|
|
`native_failed=${dispatchFailure && !assetsVerified ? 1 : 0}`,
|
|
);
|
|
expect(result.stderr).not.toContain("unexpected-android-wait");
|
|
const summary = readFileSync(summaryPath, "utf8");
|
|
if (assetsVerified) {
|
|
expect(summary).toContain("previously published assets verified");
|
|
expect(summary).toContain("releases/download/v2026.8.1/OpenClaw-Android.apk");
|
|
expect(summary).not.toContain("actions/runs/456");
|
|
} else if (dispatchFailure) {
|
|
expect(summary).not.toContain("actions/runs/456");
|
|
} else {
|
|
expect(summary).toContain("completion not awaited");
|
|
expect(summary).toContain("https://github.com/openclaw/openclaw/actions/runs/456");
|
|
}
|
|
},
|
|
);
|
|
|
|
it.for([
|
|
"success",
|
|
"rerun-before-approval",
|
|
"rerun-at-dispatch",
|
|
"moved-protected-tag",
|
|
"failed",
|
|
"rerun",
|
|
"other-source",
|
|
"other-ref",
|
|
"other-workflow",
|
|
"other-repository",
|
|
"other-tooling",
|
|
])(
|
|
"binds native qualification through Android approval and dispatch: %s",
|
|
(mode, { signal, onTestFinished }) => {
|
|
const lifetime = createFixtureLifetime();
|
|
const finished = new AbortController();
|
|
onTestFinished(async () => {
|
|
finished.abort();
|
|
await lifetime.cleanup();
|
|
});
|
|
return lifetime.run(async () => {
|
|
const root = lifetime.createTempDir("android-native-qualification-");
|
|
const bin = join(root, "bin");
|
|
mkdirSync(bin);
|
|
mkdirSync(join(root, ".release-harness"));
|
|
symlinkSync(resolve("scripts"), join(root, ".release-harness/scripts"), "dir");
|
|
const targetSha = "a".repeat(40);
|
|
const workflowSha = "d".repeat(40);
|
|
const workflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
const dispatchId = `release-native-android-123-2-${targetSha}`;
|
|
const run = {
|
|
id: 91,
|
|
run_attempt: mode === "rerun" ? 2 : 1,
|
|
event: "workflow_dispatch",
|
|
path:
|
|
mode === "other-workflow" ? ".github/workflows/other.yml" : ".github/workflows/ci.yml",
|
|
display_title: `CI ${mode === "other-source" ? dispatchId.replace(targetSha, "b".repeat(40)) : dispatchId}`,
|
|
head_branch: mode === "other-ref" ? "main" : workflowRef,
|
|
head_sha: mode === "other-tooling" ? "c".repeat(40) : workflowSha,
|
|
repository: {
|
|
full_name: mode === "other-repository" ? "example/other" : "openclaw/openclaw",
|
|
},
|
|
actor: { login: "github-actions[bot]" },
|
|
triggering_actor: { login: "github-actions[bot]" },
|
|
status: "completed",
|
|
conclusion: mode === "failed" ? "failure" : "success",
|
|
};
|
|
const dispatchPath = join(root, "dispatch.json");
|
|
const androidDispatchPath = join(root, "android-dispatch.json");
|
|
const nativeRunPath = join(root, "native-run.json");
|
|
writeFileSync(nativeRunPath, JSON.stringify(run));
|
|
const gh = join(bin, "gh");
|
|
writeFileSync(
|
|
gh,
|
|
`#!${process.execPath}
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
const args = process.argv.slice(2);
|
|
if (args[0] === 'api' && args.some(arg => arg.endsWith('/workflows/ci.yml/dispatches'))) {
|
|
writeFileSync(${JSON.stringify(dispatchPath)}, readFileSync(0, 'utf8'));
|
|
console.log(JSON.stringify({ workflow_run_id: 91, html_url: 'https://github.com/openclaw/openclaw/actions/runs/91' }));
|
|
} else if (args[0] === 'api' && args.some(arg => arg.endsWith('/workflows/android-release.yml/dispatches'))) {
|
|
writeFileSync(${JSON.stringify(androidDispatchPath)}, readFileSync(0, 'utf8'));
|
|
console.log(JSON.stringify({ workflow_run_id: 92, html_url: 'https://github.com/openclaw/openclaw/actions/runs/92' }));
|
|
} else if (args[0] === 'api' && args.some(arg => arg.includes('/commits/'))) {
|
|
const endpoint = args.find(arg => arg.includes('/commits/'));
|
|
if (endpoint.endsWith('/v2026.8.1')) {
|
|
if (${mode === "rerun-at-dispatch"}) {
|
|
const run = JSON.parse(readFileSync(${JSON.stringify(nativeRunPath)}, 'utf8'));
|
|
writeFileSync(${JSON.stringify(nativeRunPath)}, JSON.stringify({ ...run, run_attempt: 2, status: 'queued', conclusion: null }));
|
|
}
|
|
console.log(${JSON.stringify(targetSha)});
|
|
} else {
|
|
console.log(endpoint.endsWith('/main') || ${mode === "moved-protected-tag"} ? ${JSON.stringify("e".repeat(40))} : ${JSON.stringify(workflowSha)});
|
|
}
|
|
} else if (args[0] === 'api' && args[1].endsWith('/actions/runs/91')) {
|
|
console.log(readFileSync(${JSON.stringify(nativeRunPath)}, 'utf8'));
|
|
} else if (args[0] === 'run' && args[1] === 'view') {
|
|
if (args.includes('jobs')) process.exit(0);
|
|
const run = JSON.parse(readFileSync(${JSON.stringify(nativeRunPath)}, 'utf8'));
|
|
console.log(JSON.stringify({
|
|
headSha: args.includes('92') ? ${JSON.stringify(targetSha)} : run.head_sha,
|
|
status: run.status, conclusion: run.conclusion,
|
|
createdAt: '2026-08-30T10:00:00Z', updatedAt: '2026-08-30T10:01:00Z',
|
|
url: 'https://github.com/openclaw/openclaw/actions/runs/91',
|
|
}));
|
|
} else throw new Error('Unexpected GitHub operation: ' + JSON.stringify(args));
|
|
`,
|
|
);
|
|
chmodSync(gh, 0o755);
|
|
const nativeJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "qualify_android_native");
|
|
const approvalJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish_android");
|
|
const dispatch = workflowStep(nativeJob, "Dispatch and await exact-source native CI");
|
|
const proof = workflowStep(nativeJob, "Verify exact native CI qualification");
|
|
const approval = workflowStep(approvalJob, "Write Android release approval");
|
|
expect(nativeJob.outputs).toMatchObject({
|
|
workflow_ref: "${{ steps.dispatch.outputs.workflow_ref }}",
|
|
});
|
|
expect(proof.env?.NATIVE_CI_WORKFLOW_REF).toBe(
|
|
"${{ steps.dispatch.outputs.workflow_ref }}",
|
|
);
|
|
expect(approval.env?.NATIVE_CI_WORKFLOW_REF).toBe(
|
|
"${{ needs.qualify_android_native.outputs.workflow_ref }}",
|
|
);
|
|
const outputPath = join(root, "output");
|
|
writeFileSync(outputPath, "");
|
|
const rerunBeforeApproval =
|
|
mode === "rerun-before-approval"
|
|
? `
|
|
node --input-type=module <<'NODE'
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
const path = ${JSON.stringify(nativeRunPath)};
|
|
const run = JSON.parse(readFileSync(path, 'utf8'));
|
|
writeFileSync(path, JSON.stringify({ ...run, run_attempt: 2, status: 'queued', conclusion: null }));
|
|
NODE
|
|
`
|
|
: "";
|
|
const maxBuffer = 1024 * 1024;
|
|
const stdout = createBoundedChildOutput(maxBuffer);
|
|
const stderr = createBoundedChildOutput(maxBuffer);
|
|
const overflow = new AbortController();
|
|
let outputBytes = 0;
|
|
let childStatus: number;
|
|
try {
|
|
childStatus = await lifetime.track(
|
|
runManagedCommand({
|
|
bin: "bash",
|
|
args: [
|
|
"-c",
|
|
[
|
|
dispatch.run,
|
|
proof.run,
|
|
rerunBeforeApproval,
|
|
approval.run,
|
|
'dispatch_workflow_at_ref "$RELEASE_TAG" "$TARGET_SHA" android-release.yml -f tag="$RELEASE_TAG"',
|
|
].join("\n"),
|
|
],
|
|
cwd: root,
|
|
env: {
|
|
PATH: `${bin}:${process.env.PATH}`,
|
|
GITHUB_WORKSPACE: root,
|
|
RUNNER_TEMP: root,
|
|
GITHUB_OUTPUT: outputPath,
|
|
GITHUB_STEP_SUMMARY: join(root, "summary"),
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
GITHUB_RUN_ID: "123",
|
|
GITHUB_RUN_ATTEMPT: "2",
|
|
GITHUB_REF: `refs/tags/${workflowRef}`,
|
|
WORKFLOW_FULL_REF: `refs/tags/${workflowRef}`,
|
|
PARENT_WORKFLOW_SHA: workflowSha,
|
|
TARGET_SHA: targetSha,
|
|
RELEASE_TAG: "v2026.8.1",
|
|
RELEASE_COVERAGE_POLICY: "npm-stable-v1",
|
|
NATIVE_CI_RUN_ID: "91",
|
|
NATIVE_CI_WORKFLOW_REF: workflowRef,
|
|
NATIVE_CI_WORKFLOW_SHA: workflowSha,
|
|
RELEASE_PUBLISH_BRANCH: "main",
|
|
RELEASE_PUBLISH_FULL_REF: "refs/heads/main",
|
|
RELEASE_PUBLISH_WORKFLOW_SHA: workflowSha,
|
|
RELEASE_PUBLISH_RUN_ATTEMPT: "2",
|
|
RELEASE_PUBLISH_RUN_ID: "123",
|
|
},
|
|
shell: false,
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
signal: AbortSignal.any([signal, finished.signal, overflow.signal]),
|
|
requireProcessTreeExit: process.platform !== "win32",
|
|
onReady(spawnedChild) {
|
|
for (const [pipe, output] of [
|
|
[spawnedChild.stdout!, stdout],
|
|
[spawnedChild.stderr!, stderr],
|
|
] as const) {
|
|
pipe.on("data", (chunk: Buffer) => {
|
|
output.append(chunk);
|
|
outputBytes += chunk.byteLength;
|
|
if (outputBytes > maxBuffer) {
|
|
overflow.abort();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
}),
|
|
);
|
|
} catch (cause) {
|
|
if (overflow.signal.aborted) {
|
|
throw new Error("Android qualification output exceeded maxBuffer", { cause });
|
|
}
|
|
throw cause;
|
|
}
|
|
const result = { status: childStatus, stdout: stdout.text(), stderr: stderr.text() };
|
|
if (mode === "moved-protected-tag") {
|
|
expect(existsSync(dispatchPath)).toBe(false);
|
|
expect(result.stderr).toContain("refusing dispatch");
|
|
} else {
|
|
expect(JSON.parse(readFileSync(dispatchPath, "utf8"))).toEqual({
|
|
ref: workflowRef,
|
|
inputs: {
|
|
target_ref: targetSha,
|
|
historical_target_tag: "v2026.8.1",
|
|
include_android: "true",
|
|
release_scope: "full",
|
|
dispatch_id: dispatchId,
|
|
},
|
|
});
|
|
if (mode === "other-tooling") {
|
|
expect(readFileSync(outputPath, "utf8")).toBe("");
|
|
expect(result.stderr).toContain("used workflow SHA");
|
|
} else {
|
|
expect(readFileSync(outputPath, "utf8")).toContain(`workflow_ref=${workflowRef}`);
|
|
}
|
|
}
|
|
const approvalPath = join(root, "android-release-approval/approval.json");
|
|
const approved = mode === "success" || mode === "rerun-at-dispatch";
|
|
const qualified = approved || mode === "rerun-before-approval";
|
|
expect(existsSync(approvalPath)).toBe(approved);
|
|
if (approved) {
|
|
expect(JSON.parse(readFileSync(approvalPath, "utf8"))).toEqual({
|
|
version: 3,
|
|
repository: "openclaw/openclaw",
|
|
workflow: "OpenClaw Release Publish",
|
|
parentRunId: "123",
|
|
parentRunAttempt: 2,
|
|
workflowBranch: "main",
|
|
workflowFullRef: "refs/heads/main",
|
|
parentWorkflowSha: workflowSha,
|
|
releaseTag: "v2026.8.1",
|
|
targetSha,
|
|
nativeCi: { runId: "91", runAttempt: 1, workflowRef },
|
|
});
|
|
}
|
|
expect(existsSync(androidDispatchPath)).toBe(mode === "success");
|
|
expect(readFileSync(outputPath, "utf8").includes("qualified=true")).toBe(qualified);
|
|
expect(result.status === 0, result.stderr).toBe(mode === "success");
|
|
if (mode === "rerun-before-approval" || mode === "rerun-at-dispatch") {
|
|
expect(result.stderr).toContain("exact completed successful native CI attempt");
|
|
}
|
|
});
|
|
},
|
|
);
|
|
|
|
it("requires native-bound approval support only for Android qualification", () => {
|
|
const job = workflowJob(RELEASE_PUBLISH_WORKFLOW, "qualify_android_native");
|
|
const step = workflowStep(job, "Verify Android approval consumer capability");
|
|
const root = tempDirs.make("android-frozen-approval-consumer-");
|
|
const git = (...args: string[]) =>
|
|
execFileSync("git", args, { cwd: root, encoding: "utf8" }).trim();
|
|
git("init", "-q");
|
|
git("remote", "add", "origin", root);
|
|
git(
|
|
"-c",
|
|
"user.name=OpenClaw Test",
|
|
"-c",
|
|
"user.email=openclaw-test@example.com",
|
|
"commit",
|
|
"--allow-empty",
|
|
"-qm",
|
|
"legacy consumer",
|
|
);
|
|
const legacySource = git("rev-parse", "HEAD");
|
|
mkdirSync(join(root, "scripts"));
|
|
writeFileSync(join(root, "scripts/android-native-ci.mjs"), "export {};\n");
|
|
git("add", "scripts/android-native-ci.mjs");
|
|
git(
|
|
"-c",
|
|
"user.name=OpenClaw Test",
|
|
"-c",
|
|
"user.email=openclaw-test@example.com",
|
|
"commit",
|
|
"-qm",
|
|
"native-bound consumer",
|
|
);
|
|
const supportedSource = git("rev-parse", "HEAD");
|
|
for (const source of [legacySource, supportedSource]) {
|
|
const result = spawnSync("bash", ["-c", step.run ?? ""], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: { PATH: process.env.PATH, EXPECTED_SHA: source },
|
|
});
|
|
expect(result.status === 0, result.stderr).toBe(source === supportedSource);
|
|
}
|
|
});
|
|
|
|
it("compares dependency evidence zip contents independently of archive timestamps", () => {
|
|
const orchestration = releasePublishOrchestration(
|
|
workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"),
|
|
).run;
|
|
if (!orchestration) {
|
|
throw new Error("Expected release publish orchestration script");
|
|
}
|
|
const tempDir = tempDirs.make("release-evidence-zip-");
|
|
const sourceDir = `${tempDir}/source`;
|
|
const existingDir = `${tempDir}/existing`;
|
|
const sourceZip = `${tempDir}/source.zip`;
|
|
const existingZip = `${tempDir}/existing.zip`;
|
|
const symlinkZip = `${tempDir}/symlink.zip`;
|
|
const corruptZip = `${tempDir}/corrupt.zip`;
|
|
const npmLocks = `${JSON.stringify({ packages: [{ lock: "x".repeat(3 * 1024 * 1024) }] })}\n`;
|
|
const evidenceNames = ["proof.json", "npm-package-locks.json", "npm-package-locks.md"].map(
|
|
(name) => `dependency-evidence/${name}`,
|
|
);
|
|
for (const dir of [sourceDir, existingDir]) {
|
|
mkdirSync(`${dir}/dependency-evidence`, { recursive: true });
|
|
writeFileSync(`${dir}/dependency-evidence/proof.json`, '{"ok":true}\n');
|
|
writeFileSync(`${dir}/dependency-evidence/npm-package-locks.json`, npmLocks);
|
|
writeFileSync(`${dir}/dependency-evidence/npm-package-locks.md`, "# npm locks\n");
|
|
}
|
|
execFileSync("touch", [
|
|
"-t",
|
|
"198001010000",
|
|
...evidenceNames.map((name) => `${sourceDir}/${name}`),
|
|
]);
|
|
execFileSync("touch", [
|
|
"-t",
|
|
"202001010000",
|
|
...evidenceNames.map((name) => `${existingDir}/${name}`),
|
|
]);
|
|
execFileSync("zip", ["-X", "-q", sourceZip, ...evidenceNames], {
|
|
cwd: sourceDir,
|
|
});
|
|
execFileSync("zip", ["-X", "-q", existingZip, ...evidenceNames], {
|
|
cwd: existingDir,
|
|
});
|
|
symlinkSync("../../outside", `${existingDir}/dependency-evidence/link`);
|
|
execFileSync("zip", ["-X", "-y", "-q", symlinkZip, "dependency-evidence/link"], {
|
|
cwd: existingDir,
|
|
});
|
|
const sourceArchive = readFileSync(sourceZip);
|
|
writeFileSync(corruptZip, sourceArchive.subarray(0, -10));
|
|
|
|
const compare = (left: string, right: string) =>
|
|
spawnSync("python3", ["scripts/compare-release-evidence-zip.py", left, right], {
|
|
encoding: "utf8",
|
|
});
|
|
|
|
const result = compare(sourceZip, existingZip);
|
|
const symlinkResult = compare(symlinkZip, symlinkZip);
|
|
const corruptResult = compare(corruptZip, corruptZip);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
writeFileSync(`${existingDir}/dependency-evidence/npm-package-locks.json`, `${npmLocks} `);
|
|
execFileSync("zip", ["-X", "-q", existingZip, "dependency-evidence/npm-package-locks.json"], {
|
|
cwd: existingDir,
|
|
});
|
|
expect(compare(sourceZip, existingZip).status).toBe(1);
|
|
expect(symlinkResult.status).toBe(1);
|
|
expect(symlinkResult.stderr).toContain("unsupported dependency evidence archive entry");
|
|
expect(corruptResult.status).toBe(1);
|
|
expect(corruptResult.stderr).toContain("dependency evidence ZIP comparison failed");
|
|
expect(orchestration).toContain("find dependency-evidence -type f -exec touch -t 198001010000");
|
|
expect(orchestration).toContain(
|
|
'attach_or_verify_release_asset "${asset_path}" "${asset_name}" zip-tree',
|
|
);
|
|
expect(orchestration).toContain(
|
|
'"${GITHUB_WORKSPACE}/.release-harness/scripts/compare-release-evidence-zip.py"',
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
["current Docker publication", "current", "success", true, true],
|
|
["historical Docker publication", "historical", "success", true, true],
|
|
["failed Docker publication", "current", "failure", true, false],
|
|
["missing Docker publication", "missing", "success", true, false],
|
|
["ambiguous Docker publication", "both", "success", true, false],
|
|
["missing npm publication", "current", "success", false, false],
|
|
])(
|
|
"checks %s for failed-parent closeout without app evidence",
|
|
(_label, docker, conclusion, npm, ok) => {
|
|
const evidenceStep = workflowStep(
|
|
workflowJob(STABLE_MAIN_CLOSEOUT_WORKFLOW, "verify"),
|
|
"Verify release workflow evidence",
|
|
);
|
|
const script = evidenceStep.run
|
|
?.match(
|
|
/node --input-type=module - "\$RUNNER_TEMP\/release-publish-run.json"[^\n]* <<'NODE'\n([\s\S]*?)\nNODE/u,
|
|
)?.[1]
|
|
?.replace(
|
|
'"./.closeout-tooling/scripts/lib/stable-release-closeout.mjs"',
|
|
JSON.stringify(pathToFileURL(resolve("scripts/lib/stable-release-closeout.mjs")).href),
|
|
);
|
|
expect(script).toBeDefined();
|
|
const root = tempDirs.make("stable-closeout-recovery-");
|
|
const runPath = join(root, "run.json");
|
|
const jobs = npm ? [{ name: "Publish plugins, then OpenClaw", conclusion: "success" }] : [];
|
|
if (docker === "current" || docker === "both") {
|
|
jobs.push({ name: "Publish Docker images / Publish prepared Docker images", conclusion });
|
|
}
|
|
if (docker === "historical" || docker === "both") {
|
|
jobs.push({
|
|
name: "Publish Docker images / Verify attestations and promote channel",
|
|
conclusion,
|
|
});
|
|
}
|
|
writeFileSync(
|
|
runPath,
|
|
JSON.stringify({
|
|
workflowName: "OpenClaw Release Publish",
|
|
event: "workflow_dispatch",
|
|
status: "completed",
|
|
conclusion: "failure",
|
|
jobs,
|
|
}),
|
|
);
|
|
const evidencePath = join(root, "evidence.json");
|
|
const manifestPath = join(root, "manifest.json");
|
|
writeFileSync(evidencePath, JSON.stringify({ releasePublishRunId: "12" }));
|
|
writeFileSync(
|
|
`${evidencePath}.sha256`,
|
|
`${createHash("sha256").update(readFileSync(evidencePath)).digest("hex")} evidence.json\n`,
|
|
);
|
|
writeFileSync(manifestPath, "{}");
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
["--input-type=module", "-", runPath, manifestPath, evidencePath],
|
|
{
|
|
input: script,
|
|
encoding: "utf8",
|
|
env: { ...process.env, ALLOW_FAILED_PUBLISH_RECOVERY: "true" },
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(ok ? 0 : 1);
|
|
if (ok) {
|
|
expect(result.stdout).toContain("apps may be pending");
|
|
} else {
|
|
expect(result.stderr).toContain("requires one successful publication job");
|
|
}
|
|
},
|
|
);
|
|
|
|
it("publishes approved bootstrap tarballs without removed waiver authority reads", () => {
|
|
const publish = workflowStep(
|
|
workflowJob(".github/workflows/plugin-npm-release.yml", "publish_plugins_npm"),
|
|
"Publish approved bootstrap tarball",
|
|
);
|
|
const run = publish.run ?? "";
|
|
expect(run).toContain('npm publish "$TARBALL_PATH"');
|
|
expect(run).toContain("release-tooling-identity.mjs verify");
|
|
expect(run).not.toContain("actions/variables/");
|
|
expect(run).not.toContain("assertStableSoakWaiverStillHeld");
|
|
});
|
|
|
|
it("does not expose removed publication waivers in stable closeout", () => {
|
|
const workflow = readFileSync(STABLE_MAIN_CLOSEOUT_WORKFLOW, "utf8");
|
|
expect(workflow).not.toMatch(/stable_soak_waiver|lane_waiver|STABLE_SOAK_WAIVER|LANE_WAIVER/);
|
|
expect(workflow).toContain("verify-stable-main-closeout.mjs");
|
|
});
|
|
|
|
it("verifies immutable postpublish evidence before stable closeout reads it", () => {
|
|
const workflow = readFileSync(STABLE_MAIN_CLOSEOUT_WORKFLOW, "utf8");
|
|
const evidenceStep = workflowStep(
|
|
workflowJob(STABLE_MAIN_CLOSEOUT_WORKFLOW, "verify"),
|
|
"Verify release workflow evidence",
|
|
);
|
|
const attachStep = workflowStep(
|
|
workflowJob(STABLE_MAIN_CLOSEOUT_WORKFLOW, "verify"),
|
|
"Attach immutable closeout evidence",
|
|
);
|
|
const checksumIndex = workflow.indexOf(
|
|
'sha256sum --strict --status -c "$evidence_checksum_asset"',
|
|
);
|
|
const evidenceReadIndex = workflow.indexOf('evidence_release_tag="$(jq -r');
|
|
const releaseVersionGateIndex = workflow.indexOf(
|
|
'if [[ "$main_version" != "$release_package_version" &&',
|
|
);
|
|
const evidenceDownloadIndex = workflow.indexOf(
|
|
'gh_with_retry release download "$evidence_source_tag"',
|
|
);
|
|
const partialRepairIndex = workflow.indexOf('if [[ -f "$closeout_json_path" ]]; then');
|
|
const existingCloseoutEvidenceMatchIndex = workflow.indexOf(
|
|
'if [[ -n "$existing_closeout_full_release_validation_run_id" &&',
|
|
);
|
|
const rollbackDrillGateIndex = workflow.indexOf(
|
|
'if [[ -z "$ROLLBACK_DRILL_ID" || -z "$ROLLBACK_DRILL_DATE" ]]; then',
|
|
);
|
|
const rollbackDrillPushSkipIndex = workflow.indexOf(
|
|
"Stable closeout skipped: rollback drill repository variables are missing",
|
|
);
|
|
const evidenceScriptSyntax = spawnSync("bash", ["-n"], {
|
|
encoding: "utf8",
|
|
input: evidenceStep.run,
|
|
});
|
|
const attachScriptSyntax = spawnSync("bash", ["-n"], {
|
|
encoding: "utf8",
|
|
input: attachStep.run,
|
|
});
|
|
|
|
expect(evidenceScriptSyntax.status, evidenceScriptSyntax.stderr).toBe(0);
|
|
expect(attachScriptSyntax.status, attachScriptSyntax.stderr).toBe(0);
|
|
expect(workflow).toContain('evidence_checksum_asset="${evidence_asset}.sha256"');
|
|
expect(workflow).toContain('--pattern "$evidence_checksum_asset"');
|
|
expect(workflow).toContain('fallback_package_version="${BASH_REMATCH[1]}"');
|
|
expect(workflow).toContain('tag_package_content="$RUNNER_TEMP/tag-package-content.b64"');
|
|
expect(workflow).toContain(
|
|
'gh_with_retry api "repos/$GITHUB_REPOSITORY/contents/package.json?ref=$tag"',
|
|
);
|
|
expect(workflow).toContain("for attempt in 1 2 3; do");
|
|
expect(workflow).toContain("sleep $((attempt * 5))");
|
|
expect(workflow).toContain(
|
|
"Stable closeout could not read package.json for $tag from GitHub API.",
|
|
);
|
|
expect(workflow).toContain(
|
|
"Stable closeout package.json content for $tag was not valid base64.",
|
|
);
|
|
expect(workflow).toContain('tag_package_version="$(jq -r');
|
|
expect(workflow).toContain('evidence_source_tag="v$fallback_package_version"');
|
|
expect(workflow).toContain('gh_with_retry release download "$evidence_source_tag"');
|
|
expect(workflow).toContain("Checkout fallback evidence tag");
|
|
expect(workflow).toContain("Bind fallback correction to the published package source");
|
|
expect(workflow).toContain(
|
|
"Fallback correction ${{ needs.resolve.outputs.tag }} must point to the same source commit",
|
|
);
|
|
expect(workflow).toContain("main_ref: ${{ steps.inputs.outputs.main_ref }}");
|
|
expect(workflow).toContain("TRIGGER_SHA: ${{ github.sha }}");
|
|
expect(workflow).toContain('main_ref="$TRIGGER_SHA"');
|
|
expect(workflow).toContain('classifyReleaseTrain(parsed) === "stable"');
|
|
expect(workflow).toContain('classifyReleaseTrain(parsed) !== "stable"');
|
|
expect(workflow).toContain("below the extended-stable .33 boundary");
|
|
expect(workflow).toContain("ref: ${{ needs.resolve.outputs.main_ref }}");
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(STABLE_MAIN_CLOSEOUT_WORKFLOW, "verify"),
|
|
"Checkout trusted closeout tooling",
|
|
).with,
|
|
).toMatchObject({
|
|
ref: "${{ github.workflow_sha }}",
|
|
path: ".closeout-tooling",
|
|
"persist-credentials": false,
|
|
});
|
|
expect(workflow).toContain("node .closeout-tooling/scripts/verify-stable-main-closeout.mjs");
|
|
expect(workflow).toContain(
|
|
"Stable closeout skipped: $evidence_source_tag predates immutable postpublish evidence.",
|
|
);
|
|
expect(workflow).toContain("Stable closeout is required for $tag");
|
|
expect(workflow).toContain('closeout_checksum_asset="${closeout_asset}.sha256"');
|
|
expect(workflow).toContain('expected_closeout_digest="$(awk');
|
|
expect(workflow).toContain('actual_closeout_digest="$(sha256sum "$closeout_json_path"');
|
|
expect(workflow).toContain(
|
|
"Stable closeout manifest for $tag is incomplete; refusing to repair it.",
|
|
);
|
|
expect(workflow).toContain(
|
|
'if [[ -f "$closeout_checksum_path" && ! -f "$closeout_json_path" ]]; then',
|
|
);
|
|
expect(workflow).toContain(
|
|
"Stable closeout evidence for $tag has an invalid checksum; refusing to repair it.",
|
|
);
|
|
expect(workflow).toContain("repair_partial_closeout=false");
|
|
expect(workflow).toContain(
|
|
"Stable closeout manifest for $tag does not match immutable postpublish evidence; refusing to accept it.",
|
|
);
|
|
expect(workflow).toContain("Stable closeout already complete for $tag.");
|
|
expect(workflow).toContain("allow_failed_publish_recovery:");
|
|
expect(workflow).toContain(
|
|
'const recoveryRequested = process.env.ALLOW_FAILED_PUBLISH_RECOVERY === "true";',
|
|
);
|
|
expect(workflow).toContain("Failed-publish recovery requires conclusion=failure");
|
|
expect(workflow).toContain('--allow-failed-publish-recovery "$ALLOW_FAILED_PUBLISH_RECOVERY"');
|
|
expect(workflow).toContain('"Publish Docker images / Publish prepared Docker images"');
|
|
expect(workflow).toContain(
|
|
'existing_manifest_args=(--existing-manifest "$CLOSEOUT_DIR/$closeout_asset"',
|
|
);
|
|
expect(workflow).toContain(
|
|
"Stable closeout requires repository variables RELEASE_ROLLBACK_DRILL_ID and RELEASE_ROLLBACK_DRILL_DATE, or explicit manual overrides.",
|
|
);
|
|
expect(workflow).toContain(
|
|
"REPAIR_PARTIAL_CLOSEOUT: ${{ needs.resolve.outputs.repair_partial_closeout }}",
|
|
);
|
|
expect(workflow).toContain('--allow-stale-rollback-drill "$REPAIR_PARTIAL_CLOSEOUT"');
|
|
expect(workflow).toContain(
|
|
'awk -v asset="openclaw-${release_version}-stable-main-closeout.json"',
|
|
);
|
|
expect(workflow).toContain("attach_or_verify \\");
|
|
expect(attachStep.run).toContain('cp -- "$source_path" "$existing_dir/$asset_name"');
|
|
expect(attachStep.run).toContain(
|
|
'"$existing_dir/$asset_name#$asset_name" --repo "$GITHUB_REPOSITORY"',
|
|
);
|
|
expect(attachStep.run).not.toContain('"$source_path#$asset_name"');
|
|
expect(workflow).toContain(
|
|
"full_release_validation_run_attempt: ${{ steps.inputs.outputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(workflow).toContain("(.[0].runAttempt == null) or");
|
|
expect(workflow).toContain(
|
|
'release_manifest_asset="openclaw-${evidence_version}-release-manifest.json"',
|
|
);
|
|
expect(workflow).toContain('sha256sum --strict --status -c "$release_manifest_checksum_asset"');
|
|
expect(workflow).toContain(
|
|
'"$existing_closeout_full_release_validation_run_attempt" != "$full_release_validation_run_attempt"',
|
|
);
|
|
expect(evidenceStep.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe(
|
|
"${{ needs.resolve.outputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(evidenceStep.run).toContain(
|
|
"actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}",
|
|
);
|
|
expect(evidenceStep.run).toContain(
|
|
'String(run.run_attempt ?? "") !== process.env.FULL_RELEASE_VALIDATION_RUN_ATTEMPT',
|
|
);
|
|
expect(evidenceStep.run).toContain(
|
|
'manifest_asset="openclaw-${evidence_version}-release-manifest.json"',
|
|
);
|
|
expect(evidenceStep.run).toContain('gh_with_retry release download "$EVIDENCE_TAG"');
|
|
expect(evidenceStep.run).toContain(".runId == $run_id");
|
|
expect(evidenceStep.run).toContain(".runAttempt == $run_attempt");
|
|
expect(workflow).toContain(
|
|
'--full-release-validation-run-attempt "$FULL_RELEASE_VALIDATION_RUN_ATTEMPT"',
|
|
);
|
|
expect(checksumIndex).toBeGreaterThan(-1);
|
|
expect(evidenceReadIndex).toBeGreaterThan(checksumIndex);
|
|
expect(existingCloseoutEvidenceMatchIndex).toBeGreaterThan(evidenceReadIndex);
|
|
expect(workflow.slice(checksumIndex, existingCloseoutEvidenceMatchIndex)).not.toContain(
|
|
'echo "should_closeout=false"',
|
|
);
|
|
expect(releaseVersionGateIndex).toBeGreaterThan(-1);
|
|
expect(partialRepairIndex).toBeGreaterThan(-1);
|
|
expect(partialRepairIndex).toBeLessThan(releaseVersionGateIndex);
|
|
expect(evidenceDownloadIndex).toBeGreaterThan(releaseVersionGateIndex);
|
|
expect(rollbackDrillGateIndex).toBeGreaterThan(existingCloseoutEvidenceMatchIndex);
|
|
expect(rollbackDrillPushSkipIndex).toBeGreaterThan(rollbackDrillGateIndex);
|
|
});
|
|
|
|
it("keeps pnpm version selection sourced from packageManager", () => {
|
|
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")) as {
|
|
packageManager?: string;
|
|
};
|
|
const setupPnpmAction = readFileSync(SETUP_PNPM_STORE_CACHE_ACTION, "utf8");
|
|
|
|
expect(packageJson.packageManager).toMatch(/^pnpm@\d+\.\d+\.\d+\+sha512\.[a-f0-9]+$/u);
|
|
expect(setupPnpmAction).toContain("Setup pnpm from packageManager");
|
|
expect(setupPnpmAction).toContain("PACKAGE_MANAGER_FILE: ${{ inputs.package-manager-file }}");
|
|
expect(setupPnpmAction).toContain('case "$package_manager" in');
|
|
expect(setupPnpmAction).toContain('corepack prepare "$package_manager" --activate');
|
|
expect(setupPnpmAction).toContain(
|
|
"if: ${{ inputs.cache-mode != 'off' && runner.os != 'Windows' }}",
|
|
);
|
|
const action = parse(setupPnpmAction) as { runs: { steps: WorkflowStep[] } };
|
|
const setup = { steps: action.runs.steps };
|
|
const pin = workflowStep(setup, "Validate pnpm setup inputs");
|
|
const cache = workflowStep(setup, "Restore pnpm store cache");
|
|
const root = tempDirs.make("pnpm-cache-key-");
|
|
const manifest = join(root, "selected-package.json");
|
|
const lockfile = join(root, "selected-lock.yaml");
|
|
const output = join(root, "outputs");
|
|
writeFileSync(manifest, JSON.stringify({ packageManager: packageJson.packageManager }));
|
|
writeFileSync(lockfile, "lockfileVersion: '9.0'\n");
|
|
const resolved = spawnSync("bash", ["-eu", "-c", pin.run ?? ""], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
CACHE_MODE: "restore",
|
|
PACKAGE_MANAGER_FILE: manifest,
|
|
REQUESTED_NODE_VERSION: "",
|
|
GITHUB_ACTION_PATH: resolve(dirname(SETUP_PNPM_STORE_CACHE_ACTION)),
|
|
GITHUB_OUTPUT: output,
|
|
},
|
|
});
|
|
expect(resolved.status, resolved.stderr).toBe(0);
|
|
const outputs = Object.fromEntries(
|
|
readFileSync(output, "utf8")
|
|
.trim()
|
|
.split("\n")
|
|
.map((line) => line.split("=")),
|
|
);
|
|
expect(outputs["package-manager"]).toBe(packageJson.packageManager);
|
|
for (const runner of [
|
|
{ os: "Linux", arch: "X64" },
|
|
{ os: "macOS", arch: "ARM64" },
|
|
]) {
|
|
const lockHash = createHash("sha256").update(readFileSync(lockfile)).digest("hex");
|
|
const key = cache.with?.key?.replace(/\$\{\{\s*(.*?)\s*\}\}/gu, (_, expression: string) =>
|
|
String(
|
|
runInNewContext(expression.replace(/\.([\w-]+)/gu, '["$1"]'), {
|
|
runner,
|
|
inputs: {
|
|
"node-version": "24.x",
|
|
"lockfile-path": lockfile,
|
|
"package-manager-file": manifest,
|
|
},
|
|
steps: { "setup-pnpm": { outputs } },
|
|
hashFiles: (file: string) => {
|
|
expect(file).toBe(lockfile);
|
|
return lockHash;
|
|
},
|
|
}),
|
|
),
|
|
);
|
|
expect(key).toContain(`-${runner.os}-${runner.arch}-24.x-`);
|
|
expect(key).toContain(outputs["package-manager"]);
|
|
expect(key?.endsWith(`-${lockHash}`)).toBe(true);
|
|
}
|
|
expect(setupPnpmAction).not.toContain("pnpm/action-setup");
|
|
expect(setupPnpmAction).not.toContain("shasum");
|
|
expect(setupPnpmAction).not.toContain("PNPM_VERSION_INPUT");
|
|
expect(setupPnpmAction).not.toContain("version: ${{ inputs.pnpm-version }}");
|
|
expect(setupPnpmAction).toContain('corepack enable --install-directory "$PNPM_HOME"');
|
|
expect(setupPnpmAction).toContain('echo "PNPM_HOME=$PNPM_HOME" >> "$GITHUB_ENV"');
|
|
|
|
const setupReleaseHarnessAction = readFileSync(SETUP_RELEASE_HARNESS_ACTION, "utf8");
|
|
const setupHarnessPackageManagerIndex = setupReleaseHarnessAction.indexOf(
|
|
"Setup trusted release harness package manager",
|
|
);
|
|
const installHarnessDependenciesIndex = setupReleaseHarnessAction.indexOf(
|
|
"Install trusted release harness dependencies",
|
|
);
|
|
expect(setupHarnessPackageManagerIndex).toBeGreaterThan(-1);
|
|
expect(installHarnessDependenciesIndex).toBeGreaterThan(setupHarnessPackageManagerIndex);
|
|
expect(setupReleaseHarnessAction).toContain(
|
|
"uses: ./.release-harness/.github/actions/setup-pnpm-store-cache",
|
|
);
|
|
expect(setupReleaseHarnessAction).toContain(
|
|
"package-manager-file: .release-harness/package.json",
|
|
);
|
|
expect(setupReleaseHarnessAction).toContain("working-directory: .release-harness");
|
|
expect(setupReleaseHarnessAction).toContain(
|
|
"pnpm install --frozen-lockfile --prefer-offline --ignore-scripts",
|
|
);
|
|
|
|
const setupNodeAction = readFileSync(".github/actions/setup-node-env/action.yml", "utf8");
|
|
expect(setupNodeAction).toContain("Normalize container toolcache");
|
|
expect(setupNodeAction).toContain("ln -s /__t /opt/hostedtoolcache");
|
|
|
|
for (const workflowPath of workflowPaths()) {
|
|
const workflowText = readFileSync(workflowPath, "utf8");
|
|
// Observed versions such as REPLAY_PNPM_VERSION are not a competing pin.
|
|
expect(workflowText, workflowPath).not.toMatch(/\bPNPM_VERSION\b/u);
|
|
expect(workflowText, workflowPath).not.toContain("pnpm-version:");
|
|
expect(workflowText, workflowPath).not.toContain("pnpm/action-setup");
|
|
}
|
|
});
|
|
|
|
it("runs trusted npm preflight pnpm commands from the tooling checkout", () => {
|
|
const root = tempDirs.make("npm-preflight-tooling-pnpm-");
|
|
const toolingDir = join(root, ".artifacts/plugin-sdk-release-tooling");
|
|
const binDir = join(root, "bin");
|
|
const logPath = join(root, "pnpm-cwds.log");
|
|
mkdirSync(toolingDir, { recursive: true });
|
|
mkdirSync(binDir);
|
|
writeFileSync(
|
|
join(root, "package.json"),
|
|
JSON.stringify({ private: true, packageManager: "pnpm@11.2.2" }),
|
|
);
|
|
writeFileSync(
|
|
join(toolingDir, "package.json"),
|
|
JSON.stringify({ private: true, packageManager: "pnpm@12.1.0" }),
|
|
);
|
|
const pnpmPath = join(binDir, "pnpm");
|
|
writeFileSync(
|
|
pnpmPath,
|
|
`#!/bin/sh
|
|
set -eu
|
|
package_manager="$(node -p 'require("./package.json").packageManager')"
|
|
printf '%s\\t%s\\n' "$PWD" "$package_manager" >> "$PNPM_CWD_LOG"
|
|
test "$package_manager" = "pnpm@12.1.0"
|
|
`,
|
|
);
|
|
chmodSync(pnpmPath, 0o755);
|
|
|
|
const sdkStep = workflowStep(
|
|
workflowJob(OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "check_sdk_npm"),
|
|
"Verify Plugin SDK API changes",
|
|
);
|
|
const sdkCommandBlock = (sdkStep.run ?? "").match(
|
|
/\(\n\s+cd "\$tooling_dir"\n\s+pnpm install --frozen-lockfile --ignore-scripts --filter openclaw\n\s+pnpm run plugin-sdk:api:diff -- "\$\{diff_args\[@\]\}"\n\s*\)/u,
|
|
)?.[0];
|
|
expect(sdkCommandBlock).toBeDefined();
|
|
const installSteps = ["check_dependencies_npm", "check_contents_npm"].map((jobName) =>
|
|
workflowStep(
|
|
workflowJob(OPENCLAW_NPM_PREFLIGHT_WORKFLOW, jobName),
|
|
"Install trusted qualification dependencies",
|
|
),
|
|
);
|
|
|
|
for (const { trustedPnpmCommand, cwd } of [
|
|
{ trustedPnpmCommand: sdkCommandBlock ?? "", cwd: root },
|
|
...installSteps.map((step) => ({
|
|
trustedPnpmCommand: step.run ?? "",
|
|
cwd: resolve(root, step["working-directory"] ?? "."),
|
|
})),
|
|
]) {
|
|
const result = spawnSync("bash", ["-c", trustedPnpmCommand], {
|
|
cwd,
|
|
encoding: "utf8",
|
|
env: {
|
|
...process.env,
|
|
GITHUB_WORKSPACE: root,
|
|
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
PNPM_CWD_LOG: logPath,
|
|
tooling_dir: toolingDir,
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
}
|
|
|
|
expect(readFileSync(logPath, "utf8").trim().split("\n")).toEqual(
|
|
Array.from({ length: 4 }, () => `${toolingDir}\tpnpm@12.1.0`),
|
|
);
|
|
});
|
|
|
|
it("keeps Crabbox hydration compatible with local Actions replay", () => {
|
|
const crabboxConfig = parse(readFileSync(CRABBOX_CONFIG, "utf8")) as {
|
|
actions?: { job?: string };
|
|
};
|
|
const workflowText = readFileSync(CRABBOX_HYDRATE_WORKFLOW, "utf8");
|
|
const hydrate = workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate");
|
|
const hydrateWindowsDaemon = workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate-windows-daemon");
|
|
const hydrateGithub = workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate-github");
|
|
|
|
expect(crabboxConfig.actions?.job).toBe("hydrate");
|
|
expect(hydrate.if).toBe(
|
|
"${{ inputs.crabbox_job != 'hydrate-github' && inputs.crabbox_job != 'hydrate-windows-daemon' }}",
|
|
);
|
|
const hydrateNode = workflowStep(hydrate, "Setup Node.js");
|
|
expect(hydrateNode.shell).toBe("bash");
|
|
expect(hydrateNode.run).toContain(
|
|
"source .github/actions/setup-pnpm-store-cache/ensure-node.sh",
|
|
);
|
|
expect(hydrateNode.run).toContain('openclaw_ensure_node "24.x"');
|
|
const hydratePnpm = workflowStep(hydrate, "Setup pnpm and dependencies");
|
|
expect(hydratePnpm.if).toBeUndefined();
|
|
expect(hydratePnpm.run).toContain('corepack enable --install-directory "$PNPM_HOME"');
|
|
expect(hydratePnpm.run).toContain("COREPACK_HOME");
|
|
expect(workflowText).not.toContain('PNPM_CONFIG_STORE_DIR: "/var/cache/crabbox/pnpm/store"');
|
|
expect(hydratePnpm.run).toContain('preferred_pnpm_store="/var/cache/crabbox/pnpm/store"');
|
|
expect(hydratePnpm.run).toContain('mkdir -p "$preferred_pnpm_store" 2>/dev/null');
|
|
expect(hydratePnpm.run).toContain('[ -w "$preferred_pnpm_store" ]');
|
|
expect(hydratePnpm.run).toContain(
|
|
'export PNPM_CONFIG_STORE_DIR="$GITHUB_WORKSPACE/.cache/openclaw-pnpm-store"',
|
|
);
|
|
expect(hydratePnpm.run).toContain('export PNPM_CONFIG_PACKAGE_IMPORT_METHOD="hardlink"');
|
|
expect(hydratePnpm.run).toContain('echo "PNPM_CONFIG_STORE_DIR=$PNPM_CONFIG_STORE_DIR"');
|
|
expect(hydratePnpm.run).toContain(
|
|
'echo "PNPM_CONFIG_PACKAGE_IMPORT_METHOD=${PNPM_CONFIG_PACKAGE_IMPORT_METHOD:-}"',
|
|
);
|
|
expect(hydratePnpm.run).toContain('} >> "$GITHUB_ENV"');
|
|
expect(hydratePnpm.run).toContain(
|
|
"append_pnpm_option_arg PNPM_CONFIG_PACKAGE_IMPORT_METHOD package-import-method",
|
|
);
|
|
expect(hydratePnpm.run).toContain("Refusing unsafe pnpm directory");
|
|
expect(hydratePnpm.run).toContain("pnpm_install_artifacts_ready");
|
|
expect(hydratePnpm.run).toContain("run_pnpm_install || run_pnpm_install");
|
|
expect(hydratePnpm.run).toContain('setsid pnpm "${install_args[@]}"');
|
|
expect(hydratePnpm.run).toContain("grep -qE '^Done in .+ using pnpm v'");
|
|
expect(hydratePnpm.run).toContain("https://github.com/pnpm/pnpm/issues/12297");
|
|
expect(hydratePnpm.run).toContain('kill -TERM -- "-$pnpm_pid"');
|
|
expect(hydratePnpm.run).toContain('kill -KILL -- "-$pnpm_pid"');
|
|
expect(workflowStep(hydrate, "Fetch main ref").run).toContain(
|
|
"timeout --signal=TERM --kill-after=10s 30s git",
|
|
);
|
|
expect(workflowStep(hydrate, "Fetch main ref").run).toContain(
|
|
"fetch --no-tags --prune --no-recurse-submodules --depth=50 origin",
|
|
);
|
|
expect(workflowStep(hydrate, "Fetch main ref").run).toContain(
|
|
'"+refs/heads/main:refs/remotes/origin/main"',
|
|
);
|
|
expect(workflowStep(hydrate, "Prepare Crabbox shell").if).toBeUndefined();
|
|
const prepareCrabboxShell = workflowStep(hydrate, "Prepare Crabbox shell").run;
|
|
expect(prepareCrabboxShell).toContain("link_node_tool()");
|
|
expect(prepareCrabboxShell).toContain('readlink -f "$source"');
|
|
expect(prepareCrabboxShell).toContain('readlink -f "$target"');
|
|
expect(prepareCrabboxShell).toContain("link_node_tool corepack");
|
|
const ensureDocker = workflowStep(hydrate, "Ensure Docker is running");
|
|
expect(ensureDocker.if).toBeUndefined();
|
|
expect(ensureDocker.env).toEqual({
|
|
CRABBOX_JOB: "${{ inputs.crabbox_job }}",
|
|
});
|
|
expect(ensureDocker.run).toContain("docker_required=false");
|
|
expect(ensureDocker.run).toContain('if [ "${CRABBOX_JOB:-hydrate}" = "hydrate-docker" ]; then');
|
|
expect(ensureDocker.run).toContain("other marker names do not");
|
|
expect(ensureDocker.run).toContain('if [ "$docker_required" = true ]; then');
|
|
expect(ensureDocker.run).toContain(
|
|
"Docker is unavailable for ${CRABBOX_JOB:-hydrate}; route this workload to a Docker-capable provider",
|
|
);
|
|
expect(ensureDocker.run).toContain(
|
|
"Docker is unavailable; standard hydration will continue without Docker",
|
|
);
|
|
expect(ensureDocker.run).toContain(
|
|
'echo "OPENCLAW_CRABBOX_DOCKER_AVAILABLE=0" >> "$GITHUB_ENV"',
|
|
);
|
|
expect(ensureDocker.run).toContain(
|
|
'echo "OPENCLAW_CRABBOX_DOCKER_AVAILABLE=1" >> "$GITHUB_ENV"',
|
|
);
|
|
expect(workflowStep(hydrate, "Ensure SSH is available").if).toBeUndefined();
|
|
expect(workflowStep(hydrate, "Hydrate provider env helper").if).toBeUndefined();
|
|
const markCrabboxReady = workflowStep(hydrate, "Mark Crabbox ready").run;
|
|
expect(markCrabboxReady).toContain("COREPACK_HOME");
|
|
expect(markCrabboxReady).toContain("OPENCLAW_CRABBOX_DOCKER_AVAILABLE");
|
|
expect(markCrabboxReady).toContain("PNPM_CONFIG_PACKAGE_IMPORT_METHOD");
|
|
expect(markCrabboxReady).not.toContain("PNPM_CONFIG_MODULES_DIR");
|
|
expect(markCrabboxReady).not.toContain("PNPM_CONFIG_VIRTUAL_STORE_DIR");
|
|
expect(workflowStep(hydrate, "Hydrate provider env helper").env).toBeUndefined();
|
|
|
|
expect(hydrateWindowsDaemon.if).toBe("${{ inputs.crabbox_job == 'hydrate-windows-daemon' }}");
|
|
expect(workflowStep(hydrateWindowsDaemon, "Setup Node.js").uses).toBe(SETUP_NODE_V6);
|
|
const hydrateWindowsPnpm = workflowStep(hydrateWindowsDaemon, "Setup pnpm and dependencies");
|
|
expect(hydrateWindowsPnpm.shell).toBe("powershell");
|
|
expect(hydrateWindowsPnpm.run).toContain(
|
|
'$env:PNPM_CONFIG_MODULES_DIR = Join-Path $pnpmCacheRoot "node_modules"',
|
|
);
|
|
expect(hydrateWindowsPnpm.run).toContain(
|
|
'$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $pnpmCacheRoot "virtual-store"',
|
|
);
|
|
expect(hydrateWindowsPnpm.run).not.toContain("PNPM_CONFIG_PACKAGE_IMPORT_METHOD");
|
|
expect(hydrateWindowsPnpm.run).toContain("--config.side-effects-cache=false");
|
|
expect(hydrateWindowsPnpm.run).toContain('"--ignore-scripts"');
|
|
expect(hydrateWindowsPnpm.run).toContain('$env:PNPM_CONFIG_CHILD_CONCURRENCY = "4"');
|
|
expect(hydrateWindowsPnpm.run).toContain('$env:PNPM_CONFIG_NETWORK_CONCURRENCY = "8"');
|
|
expect(hydrateWindowsPnpm.run).toContain('$env:PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN = "false"');
|
|
expect(hydrateWindowsPnpm.run).toContain(
|
|
"$Value | Out-File -FilePath $Path -Encoding utf8 -Append",
|
|
);
|
|
expect(hydrateWindowsPnpm.run).toContain('"--filter",');
|
|
expect(hydrateWindowsPnpm.run).toContain('"openclaw",');
|
|
expect(hydrateWindowsPnpm.run).toContain(
|
|
"New-Item -ItemType Junction -Path $workspaceNodeModules -Target $env:PNPM_CONFIG_MODULES_DIR",
|
|
);
|
|
expect(hydrateWindowsPnpm.run).toContain(".pnpm-workspace-state-v1.json");
|
|
expect(hydrateWindowsPnpm.run).not.toContain("Remove-Item -Recurse -Force");
|
|
expect(hydrateWindowsPnpm.run).not.toContain("Add-Content -Path $env:GITHUB_ENV");
|
|
expect(hydrateWindowsPnpm.run).not.toContain("Add-Content -Path $env:GITHUB_PATH");
|
|
expect(hydrateWindowsPnpm.run).toContain("corepack enable --install-directory $env:PNPM_HOME");
|
|
expect(hydrateWindowsPnpm.run).toContain("pnpm @installArgs");
|
|
expect(hydrateWindowsPnpm.run).toContain(
|
|
'$corepackShimDir = Join-Path $nodeBin "node_modules\\corepack\\shims"',
|
|
);
|
|
const hydrateWindowsFetch = workflowStep(hydrateWindowsDaemon, "Fetch main ref");
|
|
expect(hydrateWindowsFetch.shell).toBe("powershell");
|
|
expect(hydrateWindowsFetch.run).toContain(
|
|
"$fetchInfo = New-Object System.Diagnostics.ProcessStartInfo",
|
|
);
|
|
expect(hydrateWindowsFetch.run).toContain('$fetchInfo.FileName = "git"');
|
|
expect(hydrateWindowsFetch.run).toContain("$fetchInfo.WorkingDirectory = $repo");
|
|
expect(hydrateWindowsFetch.run).toContain("$fetchInfo.UseShellExecute = $false");
|
|
expect(hydrateWindowsFetch.run).not.toContain("$fetchInfo.RedirectStandardOutput = $true");
|
|
expect(hydrateWindowsFetch.run).not.toContain("$fetchInfo.RedirectStandardError = $true");
|
|
expect(hydrateWindowsFetch.run).toContain("$fetch = New-Object System.Diagnostics.Process");
|
|
expect(hydrateWindowsFetch.run).toContain("$fetch.StartInfo = $fetchInfo");
|
|
expect(hydrateWindowsFetch.run).toContain("$fetch.WaitForExit(30000)");
|
|
expect(hydrateWindowsFetch.run).toContain("$fetch.Kill()");
|
|
expect(hydrateWindowsFetch.run).not.toContain("StandardOutput.ReadToEnd()");
|
|
expect(hydrateWindowsFetch.run).not.toContain("StandardError.ReadToEnd()");
|
|
expect(hydrateWindowsFetch.run).toContain("git fetch failed with exit code $($fetch.ExitCode)");
|
|
expect(hydrateWindowsFetch.run).toContain(
|
|
"--no-tags --no-progress --prune --no-recurse-submodules --depth=50",
|
|
);
|
|
expect(hydrateWindowsFetch.run).toContain('"+refs/heads/main:refs/remotes/origin/main"');
|
|
expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").shell).toBe("powershell");
|
|
const markWindowsCrabboxReady = workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").run;
|
|
expect(markWindowsCrabboxReady).toContain('"NODE_BIN"');
|
|
expect(markWindowsCrabboxReady).toContain('"PNPM_HOME"');
|
|
expect(markWindowsCrabboxReady).toContain('"CRABBOX_PNPM_MODULES_DIR"');
|
|
expect(markWindowsCrabboxReady).not.toContain('"PNPM_CONFIG_MODULES_DIR"');
|
|
expect(markWindowsCrabboxReady).not.toContain('"PNPM_CONFIG_VIRTUAL_STORE_DIR"');
|
|
expect(markWindowsCrabboxReady).toContain('"PATH"');
|
|
expect(workflowText).toContain("OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_TIMEOUT_SECONDS:-300");
|
|
expect(workflowText).toContain("OPENCLAW_CRABBOX_HYDRATE_DOWNLOAD_RETRIES:-3");
|
|
expect(workflowText).toContain("--retry-all-errors");
|
|
expect(workflowText).not.toContain("curl -fsSL https://get.docker.com | sudo sh");
|
|
|
|
expect(hydrateGithub.if).toBe("${{ inputs.crabbox_job == 'hydrate-github' }}");
|
|
expect(workflowStep(hydrateGithub, "Setup Node environment").uses).toBe(
|
|
"./.github/actions/setup-node-env",
|
|
);
|
|
expect(workflowStep(hydrateGithub, "Setup Node environment").env?.PNPM_HOME).toBe(
|
|
"${{ runner.temp }}/pnpm-home",
|
|
);
|
|
const hydrateGithubCrabboxShell = workflowStep(hydrateGithub, "Prepare Crabbox shell").run;
|
|
expect(hydrateGithubCrabboxShell).toContain("link_node_tool()");
|
|
expect(hydrateGithubCrabboxShell).toContain('readlink -f "$source"');
|
|
expect(hydrateGithubCrabboxShell).toContain('readlink -f "$target"');
|
|
expect(hydrateGithubCrabboxShell).toContain("link_node_tool corepack");
|
|
const markHydrateGithubReady = workflowStep(hydrateGithub, "Mark Crabbox ready").run;
|
|
expect(markHydrateGithubReady).toContain("OPENCLAW_CRABBOX_DOCKER_AVAILABLE");
|
|
expect(markHydrateGithubReady).toContain("PNPM_CONFIG_PACKAGE_IMPORT_METHOD");
|
|
expect(workflowStep(hydrateGithub, "Hydrate provider env helper").env?.FACTORY_API_KEY).toBe(
|
|
"${{ secrets.FACTORY_API_KEY }}",
|
|
);
|
|
});
|
|
|
|
it("defaults Crabbox proof to Blacksmith while keeping direct jobs on Azure", () => {
|
|
const crabboxConfig = parse(readFileSync(CRABBOX_CONFIG, "utf8")) as {
|
|
aws?: { region?: string };
|
|
capacity?: {
|
|
availabilityZones?: string[];
|
|
fallback?: string;
|
|
market?: string;
|
|
regions?: string[];
|
|
};
|
|
jobs?: {
|
|
changed?: {
|
|
command?: string;
|
|
market?: string;
|
|
provider?: string;
|
|
shell?: boolean;
|
|
type?: string;
|
|
};
|
|
prewarm?: { market?: string; provider?: string; type?: string };
|
|
};
|
|
provider?: string;
|
|
ssh?: { port?: string; user?: string };
|
|
};
|
|
|
|
expect(crabboxConfig.provider).toBe("blacksmith-testbox");
|
|
expect(crabboxConfig.capacity?.market).toBe("on-demand");
|
|
expect(crabboxConfig.capacity?.fallback).toBeUndefined();
|
|
expect(crabboxConfig.capacity?.regions).toBeUndefined();
|
|
expect(crabboxConfig.capacity?.availabilityZones).toBeUndefined();
|
|
expect(crabboxConfig.aws?.region).toBe("eu-west-1");
|
|
expect(crabboxConfig.jobs?.prewarm?.market).toBe("on-demand");
|
|
expect(crabboxConfig.jobs?.prewarm?.provider).toBe("azure");
|
|
expect(crabboxConfig.jobs?.prewarm?.type).toBe("Standard_D4ads_v6");
|
|
expect(crabboxConfig.jobs?.changed?.market).toBe("on-demand");
|
|
expect(crabboxConfig.jobs?.changed?.provider).toBe("azure");
|
|
expect(crabboxConfig.jobs?.changed?.type).toBe("Standard_D4ads_v6");
|
|
expect(crabboxConfig.jobs?.changed?.shell).toBe(true);
|
|
expect(crabboxConfig.jobs?.changed?.command).toContain("set -euo pipefail");
|
|
expect(crabboxConfig.jobs?.changed?.command).toContain("git init -q");
|
|
expect(crabboxConfig.jobs?.changed?.command).toContain(
|
|
"commit -q --no-gpg-sign -m remote-check-tree",
|
|
);
|
|
expect(crabboxConfig.jobs?.changed?.command).toContain("env CI=1 corepack pnpm check --timed");
|
|
expect(crabboxConfig.ssh?.user).toBe("crabbox");
|
|
expect(crabboxConfig.ssh?.port).toBe("22");
|
|
});
|
|
|
|
it("resolves candidate package sources before reusing Docker E2E lanes", () => {
|
|
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
|
|
|
expect(workflow).toContain("name: Package Acceptance");
|
|
expect(workflow).toContain("workflow_call:");
|
|
expect(workflow).toContain("workflow_ref:");
|
|
expect(workflow).toContain("package_ref:");
|
|
expect(workflow).toContain("source:");
|
|
expect(workflow).toContain("- npm");
|
|
expect(workflow).toContain("- ref");
|
|
expect(workflow).toContain("- url");
|
|
expect(workflow).toContain("- trusted-url");
|
|
expect(workflow).toContain("- artifact");
|
|
expect(workflow).toContain("trusted_source_id:");
|
|
expect(workflow).toContain("TRUSTED_SOURCE_ID: ${{ inputs.trusted_source_id }}");
|
|
expect(workflow).toContain('--trusted-source-id "$TRUSTED_SOURCE_ID"');
|
|
expect(workflow).toContain("scripts/resolve-openclaw-package-candidate.mts");
|
|
expect(workflow).toContain('--package-ref "$PACKAGE_REF"');
|
|
expect(workflow).toContain("artifact-ids: ${{ inputs.artifact_id }}");
|
|
expect(workflow).toContain("actions/artifacts/${ARTIFACT_ID}");
|
|
expect(workflow).toContain("name: ${{ env.PACKAGE_ARTIFACT_NAME }}");
|
|
expect(workflow).toContain("pull-requests: read");
|
|
expect(workflow).toContain(
|
|
"uses: ./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
|
);
|
|
expect(workflow).toContain("ref: ${{ needs.resolve_package.outputs.package_source_sha }}");
|
|
expect(workflow).toContain(
|
|
"package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}",
|
|
);
|
|
expect(workflow).toContain("package_integrity:");
|
|
expect(workflow).toContain("name: Package integrity");
|
|
expect(workflow).toContain('node scripts/check-openclaw-package-tarball.mjs "$package"');
|
|
expect(workflow).toContain('[[ "$actual_sha256" == "$EXPECTED_PACKAGE_SHA256" ]]');
|
|
expect(workflow).toContain("needs: [resolve_package, package_integrity]");
|
|
expect(workflow).toContain("package_integrity=${PACKAGE_INTEGRITY_RESULT}");
|
|
const npm12Job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "npm_12_install_sh");
|
|
expect(jobNeeds(npm12Job)).toEqual(["resolve_package", "package_integrity"]);
|
|
expect(npm12Job.permissions).toEqual({ actions: "read", contents: "read" });
|
|
const npm12Step = workflowStep(npm12Job, "Run install.sh with npm 12");
|
|
expect(npm12Step.run).toContain("npm@12.0.2");
|
|
expect(npm12Step.run).toContain("bash scripts/install.sh");
|
|
expect(npm12Step.run).toContain("scripts/docker/install-sh-common/version-parse.sh");
|
|
expect(npm12Step.run).toContain("extract_openclaw_semver");
|
|
expect(npm12Step.run).toContain(".openclaw-lifecycle-pending");
|
|
expect(JSON.stringify(npm12Job)).not.toContain("secrets.");
|
|
});
|
|
|
|
it("binds npm 12 installation to the supplied prerelease dependency artifact", () => {
|
|
const job = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "npm_12_install_sh");
|
|
const validate = workflowStep(job, "Validate prerelease plugin registry artifact identity");
|
|
const download = workflowStep(job, "Download prerelease plugin registry artifact");
|
|
const install = workflowStep(job, "Run install.sh with npm 12");
|
|
const tuple = "needs.resolve_package.outputs.prepublish_plugin_registry_json";
|
|
const field = (name: string) =>
|
|
`\${{ fromJSON(${tuple} || '{}').prepublishPluginRegistry${name} || '' }}`;
|
|
|
|
expect(validate.if).toBe(`${tuple} != ''`);
|
|
expect(validate.env).toMatchObject({
|
|
ARTIFACT_DIGEST: field("ArtifactDigest"),
|
|
ARTIFACT_ID: field("ArtifactId"),
|
|
ARTIFACT_NAME: field("ArtifactName"),
|
|
ARTIFACT_RUN_ATTEMPT: field("ArtifactRunAttempt"),
|
|
ARTIFACT_RUN_ID: field("ArtifactRunId"),
|
|
});
|
|
expect(validate.run).toContain('verify-upload "Prerelease plugin registry"');
|
|
expect(download.if).toBe(validate.if);
|
|
expect(download.uses).toBe(DOWNLOAD_ARTIFACT_V8);
|
|
expect(download.with).toMatchObject({
|
|
"artifact-ids": field("ArtifactId"),
|
|
"run-id": field("ArtifactRunId"),
|
|
path: ".artifacts/prepublish-plugin-registry",
|
|
});
|
|
expect(install.env).toMatchObject({
|
|
OPENCLAW_DOCKER_E2E_SELECTED_SHA: "${{ needs.resolve_package.outputs.package_source_sha }}",
|
|
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_CANDIDATE_VERSION:
|
|
"${{ needs.resolve_package.outputs.package_version }}",
|
|
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR: `\${{ ${tuple} != '' && format('{0}/.artifacts/prepublish-plugin-registry', github.workspace) || '' }}`,
|
|
OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: field("ManifestSha256"),
|
|
});
|
|
expect(install.run).toMatch(
|
|
/bash scripts\/e2e\/lib\/prepublish-plugin-registry\.sh\s*\\\s*bash scripts\/install\.sh/u,
|
|
);
|
|
const steps = job.steps ?? [];
|
|
expect(steps.indexOf(validate)).toBeLessThan(steps.indexOf(download));
|
|
expect(steps.indexOf(download)).toBeLessThan(steps.indexOf(install));
|
|
});
|
|
|
|
it("keeps ref packaging independent of workflow-checkout dependencies", () => {
|
|
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
|
const resolveJob = workflow.slice(
|
|
workflow.indexOf(" resolve_package:"),
|
|
workflow.indexOf(" package_integrity:"),
|
|
);
|
|
|
|
expect(resolveJob).toContain("scripts/resolve-openclaw-package-candidate.mts");
|
|
expect(resolveJob).not.toContain("pnpm install");
|
|
});
|
|
|
|
it("offers bounded product profiles and can run Telegram against the resolved artifact", () => {
|
|
const parsedWorkflow = readWorkflow(PACKAGE_ACCEPTANCE_WORKFLOW);
|
|
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
|
const npmTelegramWorkflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");
|
|
const packageTelegram = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "package_telegram");
|
|
const dockerAcceptance = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance");
|
|
const dockerAcceptanceRegistry = workflowJob(
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
"docker_acceptance_registry",
|
|
);
|
|
const npm12Install = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "npm_12_install_sh");
|
|
const npmTelegram = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e");
|
|
const buildPrivateQa = workflowStep(npmTelegram, "Build private QA harness runtime");
|
|
|
|
expect(workflow).toContain("suite_profile:");
|
|
expect(parsedWorkflow.on?.workflow_dispatch?.inputs?.suite_profile).toMatchObject({
|
|
default: "package",
|
|
description: "Acceptance profile: smoke, package, telegram, product, full, or custom",
|
|
options: ["smoke", "package", "telegram", "product", "full", "custom"],
|
|
});
|
|
const dispatchInputs = parsedWorkflow.on?.workflow_dispatch?.inputs;
|
|
const callInputs = parsedWorkflow.on?.workflow_call?.inputs;
|
|
expect(dispatchInputs?.prepublish_plugin_registry_json).toBeUndefined();
|
|
expect(dispatchInputs?.advisory).toBeUndefined();
|
|
expect(callInputs?.advisory).toBeUndefined();
|
|
expect(callInputs?.telegram_advisory).toBeUndefined();
|
|
expect(Object.keys(dispatchInputs ?? {})).toHaveLength(25);
|
|
expect(parsedWorkflow.on?.workflow_dispatch?.inputs?.telegram_advisory).toBeUndefined();
|
|
expect(parsedWorkflow.on?.workflow_call?.inputs?.suite_profile).toMatchObject({
|
|
default: "package",
|
|
description: "Acceptance profile: smoke, package, telegram, product, full, or custom",
|
|
});
|
|
expect(workflow).toContain("published_upgrade_survivor_baseline:");
|
|
expect(workflow).toContain("published_upgrade_survivor_baselines:");
|
|
expect(workflow).toContain("last-stable-4");
|
|
expect(workflow).toContain("all-since-2026.6.1");
|
|
expect(workflow).toContain("published_upgrade_survivor_scenarios:");
|
|
expect(workflow).toContain("scripts/resolve-upgrade-survivor-baselines.mts");
|
|
expect(workflow).toContain('"last-stable-"');
|
|
expect(workflow).toContain('"all-since-"');
|
|
const smoke = runPackageAcceptanceProfile({ suiteProfile: "smoke" });
|
|
expect(smoke.result.status, smoke.result.stderr).toBe(0);
|
|
expect(smoke.outputs.docker_lanes).toBe(
|
|
"npm-onboard-channel-agent gateway-network config-reload",
|
|
);
|
|
const packageProfile = runPackageAcceptanceProfile({ suiteProfile: "package" });
|
|
expect(packageProfile.result.status, packageProfile.result.stderr).toBe(0);
|
|
expect(packageProfile.outputs.docker_lanes?.split(" ")).toEqual([
|
|
"npm-onboard-channel-agent",
|
|
"doctor-switch",
|
|
"update-channel-switch",
|
|
"skill-install",
|
|
"update-corrupt-plugin",
|
|
"upgrade-survivor",
|
|
"update-first-hop-compat",
|
|
"published-upgrade-survivor",
|
|
"root-managed-vps-upgrade",
|
|
"update-restart-auth",
|
|
"plugins-offline",
|
|
"plugin-update",
|
|
]);
|
|
expect(
|
|
runPackageAcceptanceProfile({ suiteProfile: "full" }).outputs.include_release_path_suites,
|
|
).toBe("true");
|
|
expect(workflow).not.toContain("telegram_mode requires source=npm");
|
|
expect(workflow).toContain("uses: ./.github/workflows/npm-telegram-beta-e2e.yml");
|
|
expect(workflow).toContain(
|
|
"package_artifact_name: ${{ needs.resolve_package.outputs.package_artifact_name }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_artifact_digest: ${{ needs.resolve_package.outputs.package_artifact_digest }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_artifact_id: ${{ needs.resolve_package.outputs.package_artifact_id }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_artifact_run_attempt: ${{ needs.resolve_package.outputs.package_artifact_run_attempt }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_artifact_run_id: ${{ needs.resolve_package.outputs.package_artifact_run_id }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_file_name: ${{ needs.resolve_package.outputs.package_file_name }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_sha256: ${{ needs.resolve_package.outputs.package_sha256 }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_source_sha: ${{ needs.resolve_package.outputs.package_source_sha }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_version: ${{ needs.resolve_package.outputs.package_version }}",
|
|
);
|
|
expect(workflow).toContain("telegram_scenarios:");
|
|
expect(packageTelegram.with?.scenario).toBe(
|
|
"${{ needs.resolve_package.outputs.telegram_scenarios }}",
|
|
);
|
|
expect(packageTelegram.with?.advisory).toBeUndefined();
|
|
expect(packageTelegram.with).not.toHaveProperty("allow_older_binary_destructive_actions");
|
|
expect(workflow).toContain(
|
|
"package_label: openclaw@${{ needs.resolve_package.outputs.package_version }}",
|
|
);
|
|
expect(npmTelegramWorkflow).toContain("package_artifact_run_id:");
|
|
expect(npmTelegramWorkflow).toContain("Download package-under-test artifact from release run");
|
|
expect(npmTelegramWorkflow).toContain("run-id: ${{ inputs.package_artifact_run_id }}");
|
|
expect(npmTelegramWorkflow).toContain("github-token: ${{ github.token }}");
|
|
expect(workflow).toContain(
|
|
"package_source_sha: ${{ steps.resolve.outputs.package_source_sha }}",
|
|
);
|
|
expect(packageTelegram.with?.harness_ref).toBe(
|
|
"${{ needs.resolve_package.outputs.tooling_sha }}",
|
|
);
|
|
expect(packageTelegram.with?.package_source_sha).toBe(
|
|
"${{ needs.resolve_package.outputs.package_source_sha }}",
|
|
);
|
|
const registryInputs = {
|
|
prepublish_plugin_registry_artifact_name:
|
|
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '' }}",
|
|
prepublish_plugin_registry_artifact_id:
|
|
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactId || '' }}",
|
|
prepublish_plugin_registry_artifact_digest:
|
|
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}",
|
|
prepublish_plugin_registry_artifact_run_id:
|
|
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}",
|
|
prepublish_plugin_registry_artifact_run_attempt:
|
|
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}",
|
|
prepublish_plugin_registry_manifest_sha256:
|
|
"${{ fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
|
};
|
|
expect(packageTelegram.with).toMatchObject(registryInputs);
|
|
const registryInputSchema = Object.fromEntries(
|
|
Object.keys(registryInputs).map((name) => [
|
|
name,
|
|
expect.objectContaining({ default: "", required: false, type: "string" }),
|
|
]),
|
|
);
|
|
const npmTelegramInputs = readWorkflow(NPM_TELEGRAM_WORKFLOW).on;
|
|
expect(npmTelegramInputs?.workflow_call?.inputs).toMatchObject(registryInputSchema);
|
|
expect(npmTelegramInputs?.workflow_dispatch?.inputs).toMatchObject(registryInputSchema);
|
|
expect(npmTelegramWorkflow).toContain(
|
|
"Artifact-backed Telegram E2E requires the complete prerelease plugin registry tuple.",
|
|
);
|
|
expect(npmTelegramWorkflow).toContain(
|
|
"Prerelease plugin registry inputs require an artifact-backed OpenClaw package.",
|
|
);
|
|
expect(npmTelegramWorkflow).toContain(
|
|
'expected_registry_suffix="-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID}-${PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT}"',
|
|
);
|
|
expect(npmTelegramWorkflow).toContain(
|
|
'"docker-e2e-prepublish-plugin-registry${expected_registry_suffix}" | \\\n' +
|
|
' "package-acceptance-telegram-plugin-registry${expected_registry_suffix}"',
|
|
);
|
|
expect(npmTelegramWorkflow).not.toContain(
|
|
"Prerelease plugin registry and package artifacts must come from the same workflow run attempt.",
|
|
);
|
|
expect(npmTelegramWorkflow).toContain('verify-upload "Prerelease plugin registry"');
|
|
expect(npmTelegramWorkflow).toContain("Download prerelease plugin registry artifact");
|
|
expect(npmTelegramWorkflow).toContain("--required-packages-json '[\"@openclaw/codex\"]'");
|
|
expect(packageTelegram.secrets).toEqual({
|
|
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
|
|
OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
|
|
OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}",
|
|
});
|
|
expect(dockerAcceptance.with?.ref).toBe(
|
|
"${{ needs.resolve_package.outputs.package_source_sha }}",
|
|
);
|
|
expect(dockerAcceptance.with?.advisory).toBeUndefined();
|
|
expect(dockerAcceptanceRegistry.with?.advisory).toBeUndefined();
|
|
expect(dockerAcceptance.with?.prepublish_plugin_registry_artifact_name).toContain(
|
|
"startsWith(",
|
|
);
|
|
expect(dockerAcceptance.with?.prepublish_plugin_registry_artifact_name).toContain(
|
|
"'docker-e2e-prepublish-plugin-registry-'",
|
|
);
|
|
expect(packageTelegram.with?.prepublish_plugin_registry_artifact_name).not.toContain(
|
|
"startsWith(",
|
|
);
|
|
expect(npm12Install.if).toBe("inputs.suite_profile != 'telegram'");
|
|
expect(dockerAcceptance.if).toBe(
|
|
"inputs.suite_profile != 'telegram' && inputs.shared_image_policy == 'no-push-artifact'",
|
|
);
|
|
expect(dockerAcceptanceRegistry.if).toBe(
|
|
"inputs.suite_profile != 'telegram' && inputs.shared_image_policy == 'existing-only'",
|
|
);
|
|
expect(parsedWorkflow.permissions).toEqual({
|
|
actions: "read",
|
|
contents: "read",
|
|
packages: "read",
|
|
"pull-requests": "read",
|
|
});
|
|
expect(workflow).not.toContain("NPM_TOKEN");
|
|
expect(workflow).not.toContain("contents: write");
|
|
expect(workflow).not.toContain("packages: write");
|
|
expect(workflow).not.toContain("id-token: write");
|
|
expect(buildPrivateQa.env).toMatchObject({
|
|
NODE_OPTIONS: "--max-old-space-size=8192",
|
|
OPENCLAW_BUILD_PRIVATE_QA: "1",
|
|
});
|
|
expectTextToIncludeAll(buildPrivateQa.run, [
|
|
"pnpm build qaRuntime",
|
|
"test -f dist/plugin-sdk/qa-channel-protocol.js",
|
|
"test -f dist/plugin-sdk/qa-runtime.js",
|
|
"test -f dist/extensions/qa-lab/runtime-api.js",
|
|
]);
|
|
expect(workflow).toContain('echo "baseline=$fallback_baseline" >> "$GITHUB_OUTPUT"');
|
|
expect(workflow).toContain(
|
|
"published_upgrade_survivor_baseline: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baseline }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"published_upgrade_survivor_baselines: ${{ needs.resolve_package.outputs.published_upgrade_survivor_baselines }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"published_upgrade_survivor_scenarios: ${{ needs.resolve_package.outputs.published_upgrade_survivor_scenarios }}",
|
|
);
|
|
expect(workflow).toContain("Published upgrade survivor baseline:");
|
|
expect(workflow).toContain("Published upgrade survivor baselines:");
|
|
expect(workflow).toContain("Published upgrade survivor scenarios:");
|
|
});
|
|
|
|
it("normalizes one closed prerelease registry tuple before child workflows", () => {
|
|
const tuple = packageAcceptanceRegistryTuple();
|
|
const direct = runPackageAcceptanceRegistryInputValidation({
|
|
prepublishPluginRegistryJson: JSON.stringify(tuple),
|
|
});
|
|
expect(direct.result.status, direct.result.stderr).toBe(0);
|
|
expect(direct.output).toContain(`json=${JSON.stringify(tuple)}\n`);
|
|
|
|
const candidate = runPackageAcceptanceRegistryInputValidation({
|
|
candidateArtifactJson: JSON.stringify({
|
|
imageArtifactName: "image-123-2",
|
|
...tuple,
|
|
}),
|
|
});
|
|
expect(candidate.result.status, candidate.result.stderr).toBe(0);
|
|
expect(candidate.output).toContain(`json=${JSON.stringify(tuple)}\n`);
|
|
|
|
const identical = runPackageAcceptanceRegistryInputValidation({
|
|
candidateArtifactJson: JSON.stringify(tuple),
|
|
prepublishPluginRegistryJson: JSON.stringify(tuple),
|
|
});
|
|
expect(identical.result.status, identical.result.stderr).toBe(0);
|
|
expect(identical.output).toContain(`json=${JSON.stringify(tuple)}\n`);
|
|
});
|
|
|
|
it("rejects partial or ambiguous prerelease registry tuples before child workflows", () => {
|
|
const tuple = packageAcceptanceRegistryTuple();
|
|
const partial = runPackageAcceptanceRegistryInputValidation({
|
|
prepublishPluginRegistryJson: JSON.stringify({
|
|
prepublishPluginRegistryArtifactId: tuple.prepublishPluginRegistryArtifactId,
|
|
}),
|
|
});
|
|
expect(partial.result.status).toBe(1);
|
|
expect(partial.result.stderr).toContain(
|
|
"Prerelease plugin registry JSON must contain one complete immutable tuple.",
|
|
);
|
|
|
|
const ambiguous = runPackageAcceptanceRegistryInputValidation({
|
|
candidateArtifactJson: JSON.stringify(tuple),
|
|
prepublishPluginRegistryJson: JSON.stringify(
|
|
packageAcceptanceRegistryTuple({
|
|
prepublishPluginRegistryArtifactId: "789",
|
|
}),
|
|
),
|
|
});
|
|
expect(ambiguous.result.status).toBe(1);
|
|
expect(ambiguous.result.stderr).toContain("Prerelease plugin registry inputs disagree.");
|
|
});
|
|
|
|
it("generates a Telegram-only registry only for direct ref or artifact candidates", () => {
|
|
for (const source of ["ref", "artifact"] as const) {
|
|
const generated = runPackageAcceptanceResolveScript({
|
|
source,
|
|
telegramMode: "mock-openai",
|
|
});
|
|
expect(generated.result.status, generated.result.stderr).toBe(0);
|
|
expect(generated.args).toContain("--plugin-registry-output-dir\n");
|
|
expect(generated.args).toContain(".artifacts/package-acceptance-telegram-plugin-registry\n");
|
|
expect(generated.args).toContain('--required-plugin-packages-json\n["@openclaw/codex"]\n');
|
|
}
|
|
|
|
for (const source of ["npm", "url", "trusted-url"] as const) {
|
|
const skipped = runPackageAcceptanceResolveScript({
|
|
source,
|
|
telegramMode: "mock-openai",
|
|
});
|
|
expect(skipped.result.status, skipped.result.stderr).toBe(0);
|
|
expect(skipped.args).not.toContain("--plugin-registry-output-dir");
|
|
}
|
|
|
|
const telegramDisabled = runPackageAcceptanceResolveScript({
|
|
source: "ref",
|
|
telegramMode: "none",
|
|
});
|
|
expect(telegramDisabled.result.status, telegramDisabled.result.stderr).toBe(0);
|
|
expect(telegramDisabled.args).not.toContain("--plugin-registry-output-dir");
|
|
|
|
const publishedArtifact = runPackageAcceptanceResolveScript({
|
|
candidateArtifactJson: releaseCandidateArtifactJson("a".repeat(40), true),
|
|
source: "artifact",
|
|
telegramMode: "mock-openai",
|
|
});
|
|
expect(publishedArtifact.result.status, publishedArtifact.result.stderr).toBe(0);
|
|
expect(publishedArtifact.args).not.toContain("--plugin-registry-output-dir");
|
|
});
|
|
|
|
it.each([
|
|
{ candidateVersion: "2026.8.1", targetContextRef: "", expected: "2026.7.1-2" },
|
|
{
|
|
candidateVersion: "2026.6.35",
|
|
targetContextRef: "extended-stable/2026.6.33",
|
|
expected: "2026.6.34",
|
|
},
|
|
])(
|
|
"derives the default baseline from resolved candidate $candidateVersion, not a moving latest tag",
|
|
({ candidateVersion, targetContextRef, expected }) => {
|
|
const result = runPackageAcceptanceBaselineStep({
|
|
candidateVersion,
|
|
targetContextRef,
|
|
source: "npm",
|
|
});
|
|
|
|
expect(result.result.status, result.result.stderr).toBe(0);
|
|
expect(result.output).toContain(`baseline=openclaw@${expected}\n`);
|
|
expect(result.npmCalls).toHaveLength(1);
|
|
expect(result.npmCalls[0]).toContain("view openclaw versions");
|
|
expect(result.npmCalls[0]).not.toContain("openclaw@latest");
|
|
},
|
|
);
|
|
|
|
it("reuses a propagated artifact baseline without resolving npm metadata again", () => {
|
|
const result = runPackageAcceptanceBaselineStep({
|
|
candidateVersion: "2026.8.1",
|
|
fallbackBaseline: "openclaw@2026.7.1-2",
|
|
source: "artifact",
|
|
});
|
|
|
|
expect(result.result.status, result.result.stderr).toBe(0);
|
|
expect(result.output).toContain("baseline=openclaw@2026.7.1-2\n");
|
|
expect(result.npmCalls).toEqual([]);
|
|
});
|
|
|
|
it("reuses a supplied registry and keeps generated artifact names distinct from Docker", () => {
|
|
const tuple = packageAcceptanceRegistryTuple();
|
|
const reused = runPackageAcceptanceResolveScript({
|
|
prepublishPluginRegistryJson: JSON.stringify(tuple),
|
|
source: "ref",
|
|
telegramMode: "mock-openai",
|
|
});
|
|
expect(reused.result.status, reused.result.stderr).toBe(0);
|
|
expect(reused.args).not.toContain("--plugin-registry-output-dir");
|
|
|
|
const workflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
|
expect(workflow).toContain(
|
|
"package-acceptance-telegram-plugin-registry-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(workflow).toContain('"docker-e2e-prepublish-plugin-registry-" +');
|
|
});
|
|
|
|
it("schedules updater first-hop compatibility in the product acceptance profile", () => {
|
|
const { outputs, result } = runPackageAcceptanceProfile({ suiteProfile: "product" });
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect((outputs.docker_lanes ?? "").split(/\s+/u)).toContain("update-first-hop-compat");
|
|
});
|
|
|
|
it("selects one normalized Telegram scenario without enabling broad acceptance lanes", () => {
|
|
const { outputs, result } = runPackageAcceptanceProfile({
|
|
suiteProfile: "telegram",
|
|
telegramMode: "mock-openai",
|
|
telegramScenarios: " telegram-commands-command ",
|
|
});
|
|
|
|
expect(result.status).toBe(0);
|
|
expect(outputs).toMatchObject({
|
|
docker_lanes: "",
|
|
include_live_suites: "false",
|
|
include_openwebui: "false",
|
|
include_release_path_suites: "false",
|
|
telegram_enabled: "true",
|
|
telegram_mode: "mock-openai",
|
|
telegram_scenarios: "telegram-commands-command",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
["telegram_mode must not be none", "none", "telegram-commands-command"],
|
|
["telegram_scenarios must contain exactly one scenario", "mock-openai", ""],
|
|
[
|
|
"telegram_scenarios must contain exactly one scenario",
|
|
"mock-openai",
|
|
"telegram-help-command, telegram-commands-command",
|
|
],
|
|
])(
|
|
"rejects an invalid Telegram-only profile: %s",
|
|
(expected, telegramMode, telegramScenarios) => {
|
|
const { result } = runPackageAcceptanceProfile({
|
|
suiteProfile: "telegram",
|
|
telegramMode,
|
|
telegramScenarios,
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(expected);
|
|
},
|
|
);
|
|
|
|
it("requires full release child workflows to run at the parent workflow SHA", () => {
|
|
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
|
|
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
|
const performanceJob = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"),
|
|
"Dispatch OpenClaw Performance",
|
|
).run;
|
|
|
|
expect(workflow).toContain("TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}");
|
|
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
|
|
expect(workflow).toContain("PARENT_WORKFLOW_SHA: ${{ github.sha }}");
|
|
expect(workflow).toContain("release_package_spec:");
|
|
expect(workflow).toContain('args+=(-f release_package_spec="$RELEASE_PACKAGE_SPEC")');
|
|
expect(workflow).toContain("package_acceptance_package_spec:");
|
|
expect(workflow).toContain(
|
|
'args+=(-f package_acceptance_package_spec="$PACKAGE_ACCEPTANCE_PACKAGE_SPEC")',
|
|
);
|
|
expect(workflow).toContain("codex_plugin_spec:");
|
|
expect(workflow).toContain('args+=(-f codex_plugin_spec="$CODEX_PLUGIN_SPEC")');
|
|
expect(releaseChecksWorkflow).toContain(
|
|
'codex_plugin_spec="npm:@openclaw/codex@${BASH_REMATCH[1]}"',
|
|
);
|
|
expect(releaseChecksWorkflow.match(/run: pnpm build qaRuntime/gu)).toHaveLength(6);
|
|
expect(releaseChecksWorkflow).not.toContain(
|
|
"node --import tsx scripts/build-all.mts qaRuntime",
|
|
);
|
|
expect(releaseChecksWorkflow).toContain(
|
|
"codex_plugin_spec: ${{ needs.resolve_target.outputs.codex_plugin_spec }}",
|
|
);
|
|
expect(workflow).toContain("full-release-validation-state.mjs verify");
|
|
expect(workflow).toContain(
|
|
'gh_with_retry api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}" --jq .sha',
|
|
);
|
|
expect(workflow).toContain(
|
|
"Child workflow ref ${CHILD_WORKFLOW_REF} moved to ${current_workflow_sha}, expected ${PARENT_WORKFLOW_SHA}; refusing dispatch.",
|
|
);
|
|
expect(workflow).toContain('if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then');
|
|
expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1');
|
|
expect(performanceJob).toContain(
|
|
'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"',
|
|
);
|
|
expect(performanceJob).toContain('-f dispatch_id="$dispatch_id"');
|
|
expect(performanceJob).toContain(
|
|
'DISPATCH_RUN_NAME="$dispatch_run_name" CHILD_WORKFLOW_REF="$CHILD_WORKFLOW_REF"',
|
|
);
|
|
expect(performanceJob).toContain(".display_title == env.DISPATCH_RUN_NAME");
|
|
expect(performanceJob).toContain("Could not find exact dispatched run ${dispatch_run_name}");
|
|
expect(performanceJob).not.toContain("BEFORE_IDS=");
|
|
expect(performanceJob).not.toContain(
|
|
"did not return an Actions run URL; refusing to guess from recent workflow_dispatch runs",
|
|
);
|
|
expect(workflow).toContain(
|
|
"full-release-decision-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"full-release-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(releaseChecksWorkflow).toContain("refs/heads/release-ci/[0-9a-f]{12}-[0-9]+");
|
|
expect(releaseChecksWorkflow).toContain(
|
|
"source: ${{ needs.resolve_target.outputs.package_acceptance_package_spec != '' && 'npm' || 'artifact' }}",
|
|
);
|
|
expect(releaseChecksWorkflow).toContain(
|
|
"package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || 'openclaw@beta' }}",
|
|
);
|
|
});
|
|
|
|
it("reports beta performance regressions while blocking selected execution failures", () => {
|
|
const performanceStep = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance"),
|
|
"Dispatch OpenClaw Performance",
|
|
);
|
|
const summaryStep = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"),
|
|
"Verify exact release state artifacts",
|
|
);
|
|
|
|
expect(performanceStep.env?.RELEASE_PROFILE).toBe("${{ inputs.release_profile }}");
|
|
expectTextToIncludeAll(performanceStep.run, [
|
|
"fail_on_regression=true",
|
|
'if [[ "$RELEASE_PROFILE" == "beta" ]]',
|
|
"fail_on_regression=false",
|
|
'-f fail_on_regression="$fail_on_regression"',
|
|
"Release impact: selected execution failures are blocking",
|
|
]);
|
|
expect(summaryStep.env?.RELEASE_PROFILE).toBe("${{ inputs.release_profile }}");
|
|
expect(summaryStep.run).toBe("node scripts/full-release-validation-state.mjs verify");
|
|
const manifestStep = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"),
|
|
"Write release validation manifest",
|
|
);
|
|
expect(manifestStep.env?.RELEASE_PROFILE).toBe("${{ inputs.release_profile }}");
|
|
expect(manifestStep.env?.STABLE_SOAK_WAIVER).toBeUndefined();
|
|
expect(manifestStep.env?.GH_TOKEN).toBe("${{ github.token }}");
|
|
expect(manifestStep.run).toBe("node scripts/full-release-validation-state.mjs write-manifest");
|
|
});
|
|
|
|
it("routes publication controls through the trusted shared gate", () => {
|
|
const validationStep = workflowStep(
|
|
workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target"),
|
|
"Validate full release validation manifest",
|
|
);
|
|
const npmValidationStep = workflowStep(
|
|
workflowJob(".github/workflows/openclaw-npm-release.yml", "publish_openclaw_npm"),
|
|
"Verify full release validation target",
|
|
);
|
|
|
|
expectTextToIncludeAll(validationStep.run, [
|
|
'node "${GITHUB_WORKSPACE}/.release-validation-tooling/scripts/lib/release-publish-gates.mts"',
|
|
'--consumer publisher --manifest "$manifest"',
|
|
]);
|
|
expectTextToIncludeAll(npmValidationStep.run, [
|
|
"node trusted-workflow/scripts/lib/release-publish-gates.mts",
|
|
'--consumer core-npm --manifest "$MANIFEST_FILE"',
|
|
]);
|
|
for (const step of [validationStep, npmValidationStep]) {
|
|
expect(step.env).toMatchObject({
|
|
RELEASE_TAG: "${{ inputs.tag }}",
|
|
RELEASE_NPM_DIST_TAG: "${{ inputs.npm_dist_tag }}",
|
|
});
|
|
}
|
|
expect(validationStep.env?.EXPECTED_RELEASE_PROFILE).toBe("${{ inputs.release_profile }}");
|
|
expect(validationStep.env?.PLUGIN_SDK_API_ACKNOWLEDGEMENT).toBe(
|
|
"${{ inputs.plugin_sdk_api_acknowledgement }}",
|
|
);
|
|
});
|
|
|
|
it("dispatches exact child identities without owning child completion", () => {
|
|
const dispatchScripts = FULL_RELEASE_CHILD_DISPATCHES.map((child) => {
|
|
const job = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, child.jobName);
|
|
const step = workflowStep(job, child.stepName);
|
|
expect(step.env?.CHILD_WORKFLOW_KIND ?? job.env?.CHILD_WORKFLOW_KIND).toBe(child.kind);
|
|
if (child.kind.startsWith("artifact-")) {
|
|
expect(step.if).toBe("github.run_attempt == 1");
|
|
} else {
|
|
expect(job["timeout-minutes"]).toBe(15);
|
|
expect(job.outputs).toMatchObject({
|
|
run_attempt: "${{ steps.dispatch.outputs.run_attempt }}",
|
|
run_id: "${{ steps.dispatch.outputs.run_id }}",
|
|
url: "${{ steps.dispatch.outputs.url }}",
|
|
});
|
|
}
|
|
return step.run ?? "";
|
|
});
|
|
expect(new Set(dispatchScripts).size).toBe(1);
|
|
for (const script of dispatchScripts) {
|
|
expect(script.match(/gh workflow run/gu)).toHaveLength(1);
|
|
expectTextToIncludeAll(script, [
|
|
"The dispatch POST is one-shot",
|
|
'validate_child_run "$run_id"',
|
|
'child_run_attempt="$(jq -r \'.run_attempt // ""\' <<< "$run_json")"',
|
|
'echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"',
|
|
'echo "run_attempt=${child_run_attempt}" >> "$GITHUB_OUTPUT"',
|
|
'echo "url=${url}" >> "$GITHUB_OUTPUT"',
|
|
]);
|
|
expect(script).not.toContain("while true");
|
|
expect(script).not.toContain("gh run cancel");
|
|
expect(script).not.toContain("fail_fast_failed_jobs");
|
|
}
|
|
});
|
|
|
|
it.each(
|
|
FULL_RELEASE_CHILD_DISPATCHES.filter(
|
|
(child, index, children) =>
|
|
children.findIndex((entry) => entry.kind === child.kind) === index,
|
|
),
|
|
)("adopts and validates the exact $jobName run without waiting for terminal status", (child) => {
|
|
const { calls, result } = runFullReleaseChildDispatch(child, {
|
|
MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
MOCK_GH_STATUSES: '["in_progress"]',
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1);
|
|
expect(calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel")).toHaveLength(0);
|
|
expect(result.stdout).toContain("Dispatched");
|
|
});
|
|
|
|
it.each([
|
|
{ exactTitleAt: 2, label: "inside the former window" },
|
|
{ exactTitleAt: 13, label: "after the former window" },
|
|
])("adopts a returned child whose title converges $label", ({ exactTitleAt }) => {
|
|
const child = fullReleaseChild("artifact-docker");
|
|
const expectedTitle = `${child.runName} full-release-validation-77-2${child.nonceSuffix}`;
|
|
const titles = Array.from({ length: exactTitleAt }, (_, index) =>
|
|
index + 1 === exactTitleAt ? expectedTitle : child.runName,
|
|
);
|
|
|
|
const { calls, result } = runFullReleaseChildDispatch(child, {
|
|
MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
MOCK_GH_RUN_TITLES: JSON.stringify(titles),
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1);
|
|
expect(
|
|
calls.filter(({ args }) => args.some((value) => value.endsWith("/runs/101"))),
|
|
).toHaveLength(exactTitleAt);
|
|
});
|
|
|
|
it("bounds title convergence without reposting the dispatch", () => {
|
|
const child = fullReleaseChild("artifact-docker");
|
|
const nodeGuard = join(tempDirs.make("dispatch-node-guard-"), "reject-node.cjs");
|
|
writeFileSync(nodeGuard, 'throw new Error("dispatch fixture must not start Node");\n');
|
|
const { calls, result } = runFullReleaseChildDispatch(child, {
|
|
MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
MOCK_GH_RUN_TITLES: JSON.stringify([child.runName]),
|
|
// Guard the startup dependency directly instead of asserting elapsed time.
|
|
NODE_OPTIONS: `--require=${JSON.stringify(nodeGuard)}`,
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("run never became readable with display title");
|
|
expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1);
|
|
expect(
|
|
calls.filter(({ args }) => args.some((value) => value.endsWith("/runs/101"))),
|
|
).toHaveLength(60);
|
|
});
|
|
|
|
it("reports a child startup failure immediately without reposting", () => {
|
|
const child = fullReleaseChild("artifact-candidate");
|
|
const { calls, result } = runFullReleaseChildDispatch(child, {
|
|
MOCK_GH_CONCLUSION: "startup_failure",
|
|
MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
MOCK_GH_RUN_TITLES: JSON.stringify([child.runName]),
|
|
MOCK_GH_STATUSES: '["completed"]',
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("before any jobs started (startup_failure)");
|
|
expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1);
|
|
expect(
|
|
calls.filter(({ args }) => args.some((value) => value.endsWith("/runs/101"))),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it("refuses an immutable child mismatch immediately without reposting", () => {
|
|
const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], {
|
|
MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
MOCK_GH_RUN_EVENT: "push",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain("Refusing to adopt unvalidated");
|
|
expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1);
|
|
expect(
|
|
calls.filter(({ args }) => args.some((value) => value.endsWith("/runs/101"))),
|
|
).toHaveLength(1);
|
|
});
|
|
|
|
it.each([
|
|
["full", "v2026.8.1", "", "historical_target_tag=v2026.8.1"],
|
|
["npm-beta", "refs/tags/v2026.8.1-beta.3", "", "historical_target_tag=v2026.8.1-beta.3"],
|
|
["npm-stable", "v2026.8.1", "", "historical_target_tag=v2026.8.1"],
|
|
["npm-stable", "main", "release/2026.8.1-1", "target_context_ref=release/2026.8.1-1"],
|
|
["full", "main", "release/2026.8.1", "target_context_ref=release/2026.8.1"],
|
|
["full", "main", "release/2026.8.1-1", "target_context_ref=release/2026.8.1-1"],
|
|
["npm-beta", "main", "refs/heads/release/2026.8.1", "target_context_ref=release/2026.8.1"],
|
|
["full", "main", "v2026.8.1", "historical_target_tag=v2026.8.1"],
|
|
["full", "main", "v2026.8.1-1", "historical_target_tag=v2026.8.1-1"],
|
|
["npm-beta", "main", "refs/tags/v2026.8.1-beta.3", "historical_target_tag=v2026.8.1-beta.3"],
|
|
])(
|
|
"dispatches %s CI scope for target=%s context=%s",
|
|
(scope, targetRef, targetContextRef, contextInput) => {
|
|
const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], {
|
|
CI_RELEASE_SCOPE: scope,
|
|
TARGET_CONTEXT_REF: targetContextRef,
|
|
TARGET_REF: targetRef,
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
const dispatches = calls.filter(({ args }) => args[0] === "workflow");
|
|
expect(dispatches).toHaveLength(1);
|
|
const args = dispatches[0]!.args;
|
|
expect(args).toEqual(
|
|
expect.arrayContaining([
|
|
`target_ref=${"b".repeat(40)}`,
|
|
`release_scope=${scope}`,
|
|
`include_android=${scope === "full"}`,
|
|
]),
|
|
);
|
|
expect(
|
|
args.filter((arg) => /^(historical_target_tag|target_context_ref)=/u.test(arg)),
|
|
).toEqual([contextInput]);
|
|
expect(args.some((arg) => arg.startsWith("release_candidate_ref="))).toBe(false);
|
|
},
|
|
);
|
|
|
|
it("recovers one ambiguous dispatch by exact name without reposting", () => {
|
|
const { calls, result } = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], {
|
|
MOCK_GH_DISPATCH_ERROR: "HTTP 500: Failed to run workflow dispatch",
|
|
});
|
|
|
|
expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
|
|
expect(calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(1);
|
|
expect(calls.some(({ args }) => args.includes("-X"))).toBe(true);
|
|
expect(result.stderr).toContain("adopted exact run 101");
|
|
});
|
|
|
|
it("keeps dispatch provenance failures separate from cancellation policy", () => {
|
|
const moved = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], {
|
|
MOCK_GH_CURRENT_SHA: "c".repeat(40),
|
|
});
|
|
expect(moved.result.status).toBe(1);
|
|
expect(moved.result.stderr).toContain("refusing dispatch");
|
|
expect(moved.calls.filter(({ args }) => args[0] === "workflow")).toHaveLength(0);
|
|
|
|
const mismatched = runFullReleaseChildDispatch(FULL_RELEASE_CHILD_DISPATCHES[0], {
|
|
MOCK_GH_CHILD_SHA: "c".repeat(40),
|
|
MOCK_GH_DISPATCH_OUTPUT: "https://github.com/openclaw/openclaw/actions/runs/101",
|
|
});
|
|
expect(mismatched.result.status).toBe(1);
|
|
expect(mismatched.result.stderr).toContain("expected parent workflow SHA");
|
|
expect(
|
|
mismatched.calls.filter(({ args }) => args.some((value) => value.endsWith("/runs/101"))),
|
|
).toHaveLength(1);
|
|
expect(
|
|
mismatched.calls.filter(({ args }) => args[0] === "run" && args[1] === "cancel"),
|
|
).toHaveLength(0);
|
|
});
|
|
|
|
it("runs Release Decision and Diagnostic Drain in parallel from exact child tuples", () => {
|
|
const executionPlan = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_execution_plan");
|
|
const decision = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_decision");
|
|
const drain = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "diagnostic_drain");
|
|
const summary = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary");
|
|
const decisionStep = workflowStep(decision, "Evaluate release decision");
|
|
const drainStep = workflowStep(drain, "Drain child diagnostics");
|
|
const decisionUpload = workflowStep(decision, "Upload release decision");
|
|
const drainUpload = workflowStep(drain, "Upload diagnostic drain manifest");
|
|
const planStep = workflowStep(executionPlan, "Seal immutable release execution plan");
|
|
const planCache = workflowStep(executionPlan, "Cache immutable release execution plan");
|
|
const planRestore = workflowStep(
|
|
executionPlan,
|
|
"Restore immutable release execution plan artifact",
|
|
);
|
|
const planUpload = workflowStep(executionPlan, "Upload immutable release execution plan");
|
|
const manifestStep = workflowStep(summary, "Write release validation manifest");
|
|
const selectState = workflowStep(summary, "Select newest compatible release state artifacts");
|
|
const decisionDownloads = workflowStep(summary, "Download release decision attempts");
|
|
const drainDownloads = workflowStep(summary, "Download diagnostic drain attempts");
|
|
|
|
expect(decision.needs).toEqual(["resolve_target", "release_execution_plan"]);
|
|
expect(drain.needs).toEqual(["resolve_target", "release_execution_plan"]);
|
|
expect(decision.if).toBe("always()");
|
|
expect(drain.if).toBe("always()");
|
|
expect(decisionStep.run).toBe(drainStep.run);
|
|
expect(decisionStep.env).toMatchObject({
|
|
FAIL_FAST: "${{ inputs.fail_fast }}",
|
|
FULL_RELEASE_STATE_MODE: "decision",
|
|
});
|
|
expect(drainStep.env).toMatchObject({
|
|
FAIL_FAST: "false",
|
|
FULL_RELEASE_STATE_MODE: "drain",
|
|
});
|
|
expectTextToIncludeAll(decisionStep.run, [
|
|
'node scripts/full-release-validation-state.mjs "$FULL_RELEASE_STATE_MODE"',
|
|
]);
|
|
expect(planStep.run).toContain("FULL_RELEASE_PLAN_INPUTS_JSON");
|
|
expect(planStep.env).toMatchObject({
|
|
CANDIDATE_EVIDENCE_JSON: "${{ needs.candidate_acquisition.outputs.binding_json }}",
|
|
CANDIDATE_REQUEST_JSON: "${{ needs.resolve_target.outputs.candidate_request_json }}",
|
|
EVIDENCE_CHANGED_PATHS: "${{ needs.evidence_reuse.outputs.changed_paths || '[]' }}",
|
|
EVIDENCE_RUN_ID: "${{ needs.evidence_reuse.outputs.evidence_run_id }}",
|
|
TRUSTED_WORKFLOW_JSON: "${{ needs.resolve_target.outputs.trusted_workflow_json }}",
|
|
});
|
|
expect(planStep.env).not.toHaveProperty("EVIDENCE_MANIFEST");
|
|
expect(planStep.run).not.toContain("EVIDENCE_MANIFEST");
|
|
expect(planStep.run).toContain('--arg evidenceRunId "$EVIDENCE_RUN_ID"');
|
|
expect(planStep.run).toContain(
|
|
'--argjson candidateEvidence "${CANDIDATE_EVIDENCE_JSON:-null}"',
|
|
);
|
|
expect(planStep.run).toContain('--argjson candidateRequestInput "$CANDIDATE_REQUEST_JSON"');
|
|
expect(planStep.run).toContain("candidateRequestInput: $candidateRequestInput");
|
|
expect(planStep.run).not.toContain("candidateRequestInput: {");
|
|
expect(planStep.run).toContain('--argjson trustedWorkflow "$TRUSTED_WORKFLOW_JSON"');
|
|
expect(planCache.uses).toBe(
|
|
ACTIONS_CACHE_V6.replace("actions/cache@", "actions/cache/restore@"),
|
|
);
|
|
expect(planCache["continue-on-error"]).toBe(true);
|
|
expect(planCache.with).toMatchObject({
|
|
key: "full-release-execution-plan-v2-${{ github.run_id }}",
|
|
});
|
|
expect(planCache.with).not.toHaveProperty("fail-on-cache-miss");
|
|
expect(planRestore.if).toBe(
|
|
"${{ always() && github.run_attempt != 1 && steps.plan_cache.outputs.cache-hit != 'true' }}",
|
|
);
|
|
expect(planRestore.with).toMatchObject({
|
|
"github-token": "${{ github.token }}",
|
|
name: "full-release-execution-plan-${{ github.run_id }}",
|
|
"run-id": "${{ github.run_id }}",
|
|
});
|
|
expect(planUpload.if).toBe(
|
|
"${{ always() && github.run_attempt == 1 && steps.plan.outputs.sha256 != '' && steps.plan.outputs.source_parent_attempt == '1' }}",
|
|
);
|
|
expect(planUpload.with?.name).toBe("full-release-execution-plan-${{ github.run_id }}");
|
|
expect(planUpload.with?.overwrite).toBe(false);
|
|
expect(manifestStep.env).not.toHaveProperty("EVIDENCE_MANIFEST");
|
|
expect(manifestStep.run).not.toContain("needs.evidence_reuse.outputs");
|
|
expect(decisionUpload.with?.name).toBe(
|
|
"full-release-decision-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(drainUpload.with?.name).toBe(
|
|
"full-release-diagnostics-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(decisionDownloads.with).toMatchObject({
|
|
path: "${{ runner.temp }}/full-release-decision-attempts",
|
|
pattern: "full-release-decision-${{ github.run_id }}-*",
|
|
});
|
|
expect(drainDownloads.with).toMatchObject({
|
|
path: "${{ runner.temp }}/full-release-diagnostic-attempts",
|
|
pattern: "full-release-diagnostics-${{ github.run_id }}-*",
|
|
});
|
|
expect(selectState.run).toBe("node scripts/full-release-validation-state.mjs select");
|
|
expect(summary.needs).toEqual(expect.arrayContaining(["release_decision", "diagnostic_drain"]));
|
|
for (const jobId of [
|
|
"normal_ci",
|
|
"plugin_prerelease_independent",
|
|
"plugin_prerelease_candidate",
|
|
"release_checks_independent",
|
|
"release_checks_candidate",
|
|
"npm_telegram",
|
|
"performance",
|
|
]) {
|
|
expect(String(workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, jobId).if)).toContain(
|
|
"github.run_attempt == 1",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("builds a rerun-all manifest from sealed plan A instead of retry-B job outputs", () => {
|
|
const summary = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary");
|
|
const manifest = workflowStep(summary, "Write release validation manifest");
|
|
const evidenceDispatch = workflowStep(summary, "Request release evidence update");
|
|
|
|
expect(manifest.env).not.toHaveProperty("TARGET_SHA");
|
|
expect(manifest.env).not.toHaveProperty("NORMAL_CI_RUN_ID");
|
|
expect(manifest.env).not.toHaveProperty("EVIDENCE_REUSE");
|
|
expect(manifest.run).not.toContain("needs.evidence_reuse.outputs");
|
|
expect(evidenceDispatch.run).toContain(
|
|
'EVIDENCE_REUSE="$(jq -r \'.evidenceReuse.requested\' "$RELEASE_EXECUTION_PLAN_PATH")"',
|
|
);
|
|
expect(evidenceDispatch.env).not.toHaveProperty("EVIDENCE_REUSE");
|
|
});
|
|
|
|
it("pins every Full Release Validation artifact download to the canonical action", () => {
|
|
const workflow = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW);
|
|
const downloadSteps = Object.entries(workflow.jobs ?? {}).flatMap(([jobId, job]) =>
|
|
(job.steps ?? [])
|
|
.filter((step) => step.uses?.startsWith("actions/download-artifact@"))
|
|
.map((step) => ({ jobId, step })),
|
|
);
|
|
|
|
expect(downloadSteps).toHaveLength(8);
|
|
expect(
|
|
downloadSteps.map(({ jobId, step }) => [
|
|
jobId,
|
|
step.name,
|
|
step.with?.name ?? step.with?.pattern,
|
|
step.with?.path,
|
|
]),
|
|
).toEqual([
|
|
[
|
|
"evidence_reuse",
|
|
"Download publication admission for reuse budgeting",
|
|
"full-release-publication-admission-${{ github.run_id }}-1",
|
|
"${{ runner.temp }}/full-release-publication-admission",
|
|
],
|
|
[
|
|
"release_execution_plan",
|
|
"Restore immutable release execution plan artifact",
|
|
"full-release-execution-plan-${{ github.run_id }}",
|
|
"${{ github.workspace }}/full-release-execution-plan",
|
|
],
|
|
[
|
|
"release_execution_plan",
|
|
"Download immutable publication admission",
|
|
"full-release-publication-admission-${{ github.run_id }}-1",
|
|
"${{ runner.temp }}/full-release-publication-admission",
|
|
],
|
|
[
|
|
"release_decision",
|
|
"Download immutable release execution plan",
|
|
"full-release-execution-plan-${{ github.run_id }}",
|
|
"${{ runner.temp }}/full-release-execution-plan",
|
|
],
|
|
[
|
|
"diagnostic_drain",
|
|
"Download immutable release execution plan",
|
|
"full-release-execution-plan-${{ github.run_id }}",
|
|
"${{ runner.temp }}/full-release-execution-plan",
|
|
],
|
|
[
|
|
"summary",
|
|
"Download immutable release execution plan",
|
|
"full-release-execution-plan-${{ github.run_id }}",
|
|
"${{ runner.temp }}/full-release-execution-plan",
|
|
],
|
|
[
|
|
"summary",
|
|
"Download release decision attempts",
|
|
"full-release-decision-${{ github.run_id }}-*",
|
|
"${{ runner.temp }}/full-release-decision-attempts",
|
|
],
|
|
[
|
|
"summary",
|
|
"Download diagnostic drain attempts",
|
|
"full-release-diagnostics-${{ github.run_id }}-*",
|
|
"${{ runner.temp }}/full-release-diagnostic-attempts",
|
|
],
|
|
]);
|
|
for (const { step } of downloadSteps) {
|
|
expect(step.uses).toBe(DOWNLOAD_ARTIFACT_V8);
|
|
}
|
|
});
|
|
|
|
it("runs secretless weekly supported-line migration with optional manual historical replays", () => {
|
|
const workflow = readFileSync(UPDATE_MIGRATION_WORKFLOW, "utf8");
|
|
const packageWorkflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
|
const job = workflowJob(UPDATE_MIGRATION_WORKFLOW, "update_migration");
|
|
|
|
expect(workflow).toContain("name: Update Migration");
|
|
expect(workflow).toContain("uses: ./.github/workflows/package-acceptance.yml");
|
|
expect(workflow).toContain("source: ref");
|
|
expect(workflow).toContain("suite_profile: custom");
|
|
expect(workflow).toContain("docker_lanes: update-migration");
|
|
expect(
|
|
readWorkflow(UPDATE_MIGRATION_WORKFLOW).on?.workflow_dispatch?.inputs?.baselines,
|
|
).toMatchObject({
|
|
default: "supported-lines",
|
|
required: false,
|
|
});
|
|
expect(
|
|
readWorkflow(UPDATE_MIGRATION_WORKFLOW).on?.workflow_dispatch?.inputs,
|
|
).not.toHaveProperty("allow_frozen_target_scenario_omissions");
|
|
expect(readWorkflow(UPDATE_MIGRATION_WORKFLOW).on?.schedule).toEqual([{ cron: "17 3 * * 0" }]);
|
|
expect(job.with).toMatchObject({
|
|
workflow_ref: "${{ inputs.workflow_ref || 'main' }}",
|
|
package_ref: "${{ inputs.package_ref || 'main' }}",
|
|
published_upgrade_survivor_baselines: "${{ inputs.baselines || 'supported-lines' }}",
|
|
published_upgrade_survivor_scenarios:
|
|
"${{ inputs.scenarios || 'plugin-deps-cleanup legacy-operator-state' }}",
|
|
});
|
|
expect(job.with).not.toHaveProperty("allow_frozen_target_scenario_omissions");
|
|
expect(workflow).toContain("telegram_mode: none");
|
|
expect(job.secrets).toBeUndefined();
|
|
expect(packageWorkflow).toContain("supported-lines for latest/previous/extended/floor");
|
|
});
|
|
});
|
|
|
|
describe("package artifact reuse", () => {
|
|
it("binds package acceptance input artifacts to the complete producer tuple", () => {
|
|
const resolvePackage = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package");
|
|
expect(workflowStep(resolvePackage, "Setup Node environment").with).toMatchObject({
|
|
"install-deps": "true",
|
|
});
|
|
expect(
|
|
workflowStep(resolvePackage, "Checkout package workflow ref").with?.["persist-credentials"],
|
|
).toBe(false);
|
|
const identity = workflowStep(resolvePackage, "Validate package artifact input identity");
|
|
expect(identity.env).toMatchObject({
|
|
ARTIFACT_DIGEST: "${{ inputs.artifact_digest }}",
|
|
ARTIFACT_ID: "${{ inputs.artifact_id }}",
|
|
ARTIFACT_NAME: "${{ inputs.artifact_name }}",
|
|
ARTIFACT_RUN_ATTEMPT: "${{ inputs.artifact_run_attempt }}",
|
|
ARTIFACT_RUN_ID: "${{ inputs.artifact_run_id }}",
|
|
EXPECTED_PACKAGE_FILE_NAME: "${{ inputs.package_file_name }}",
|
|
EXPECTED_PACKAGE_SHA256: "${{ inputs.package_sha256 }}",
|
|
EXPECTED_PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha }}",
|
|
EXPECTED_PACKAGE_VERSION: "${{ inputs.package_version }}",
|
|
});
|
|
expectTextToIncludeAll(identity.run, [
|
|
"source=artifact requires the complete immutable artifact and package identity tuple.",
|
|
'[[ "$ARTIFACT_NAME" == *"-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" ]]',
|
|
'--arg digest "sha256:${ARTIFACT_DIGEST}"',
|
|
"actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}",
|
|
]);
|
|
expect(workflowStep(resolvePackage, "Download package artifact input").with).toMatchObject({
|
|
"artifact-ids": "${{ inputs.artifact_id }}",
|
|
"github-token": "${{ github.token }}",
|
|
"run-id": "${{ inputs.artifact_run_id }}",
|
|
});
|
|
const resolveStep = workflowStep(resolvePackage, "Resolve package candidate");
|
|
expect(resolveStep.env).toMatchObject({
|
|
PACKAGE_FILE_NAME: "${{ inputs.package_file_name }}",
|
|
PACKAGE_SHA256: "${{ inputs.package_sha256 }}",
|
|
PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha }}",
|
|
PACKAGE_VERSION: "${{ inputs.package_version }}",
|
|
});
|
|
expectTextToIncludeAll(resolveStep.run, [
|
|
'artifact_tarball="${artifact_dir}/${PACKAGE_FILE_NAME}"',
|
|
"Selected artifact package SHA-256 differs from package_sha256.",
|
|
"Resolved package identity differs from the declared immutable tuple.",
|
|
]);
|
|
|
|
const packageIntegrity = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "package_integrity");
|
|
expect(
|
|
workflowStep(packageIntegrity, "Setup package validation dependencies").with,
|
|
).toMatchObject({
|
|
"install-deps": "true",
|
|
});
|
|
expect(
|
|
workflowStep(packageIntegrity, "Download package-under-test artifact").with,
|
|
).toMatchObject({
|
|
"artifact-ids": "${{ needs.resolve_package.outputs.package_artifact_id }}",
|
|
"github-token": "${{ github.token }}",
|
|
"run-id": "${{ needs.resolve_package.outputs.package_artifact_run_id }}",
|
|
});
|
|
});
|
|
|
|
it("lets reusable Docker E2E consume an already resolved package artifact", () => {
|
|
const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
const parsedWorkflow = parse(workflow) as {
|
|
jobs?: Record<string, WorkflowJob>;
|
|
on?: { workflow_call?: { inputs?: Record<string, unknown> } };
|
|
};
|
|
const packageJson = readFileSync(PACKAGE_JSON, "utf8");
|
|
const scheduler = readFileSync("scripts/test-docker-all.mts", "utf8");
|
|
const publishedUpgradeSurvivor = readFileSync(UPGRADE_SURVIVOR_RUN_SCRIPT, "utf8");
|
|
|
|
expect(workflow).toContain("package_artifact_name:");
|
|
expect(workflow).toContain("package_artifact_digest:");
|
|
expect(workflow).toContain("package_artifact_id:");
|
|
expect(workflow).toContain("package_artifact_run_attempt:");
|
|
expect(workflow).toContain("package_artifact_run_id:");
|
|
expect(workflow).toContain("package_file_name:");
|
|
expect(workflow).toContain("package_source_sha:");
|
|
expect(workflow).toContain("package_sha256:");
|
|
expect(workflow).toContain("package_version:");
|
|
expect(workflow).toContain("published_upgrade_survivor_baseline:");
|
|
expect(workflow).toContain("published_upgrade_survivor_baselines:");
|
|
expect(workflow).toContain("published_upgrade_survivor_scenarios:");
|
|
expect(parsedWorkflow.on?.workflow_call?.inputs).toHaveProperty(
|
|
"allow_frozen_target_scenario_omissions",
|
|
);
|
|
expect(workflow).toContain("docker_e2e_bare_image:");
|
|
expect(workflow).toContain("docker_e2e_functional_image:");
|
|
expect(workflow).toContain("OPENCLAW_DOCKER_E2E_SELECTED_SHA:");
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC: ${{ needs.validate_selected_ref.outputs.upgrade_baseline }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ matrix.group.published_upgrade_survivor_baselines || needs.validate_selected_ref.outputs.upgrade_baselines }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: ${{ inputs.published_upgrade_survivor_scenarios }}",
|
|
);
|
|
expect(workflow).toContain("OPENCLAW_UPGRADE_SURVIVOR_TARGET_ROOT: ${{ github.workspace }}");
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_ALLOW_FROZEN_TARGET_SCENARIO_OMISSIONS: ${{ inputs.allow_frozen_target_scenario_omissions && '1' || '0' }}",
|
|
);
|
|
expect(workflow).toContain("Download current-run OpenClaw Docker E2E package");
|
|
expect(workflow).toContain("Download previous-run OpenClaw Docker E2E package");
|
|
expect(workflow).toContain(
|
|
"needs.validate_selected_ref.outputs.package_artifact_present == 'true'",
|
|
);
|
|
expect(workflow).toContain(
|
|
'bare_image="${PROVIDED_BARE_IMAGE:-ghcr.io/${repository}-docker-e2e-bare:${image_tag}}"',
|
|
);
|
|
expect(workflow).toContain(
|
|
'functional_image="${PROVIDED_FUNCTIONAL_IMAGE:-ghcr.io/${repository}-docker-e2e-functional:${image_tag}}"',
|
|
);
|
|
expect(workflow).toContain("artifact-ids: ${{ inputs.package_artifact_id }}");
|
|
expect(workflow).toContain(
|
|
'[[ "$ARTIFACT_NAME" == *"-${ARTIFACT_RUN_ID}-${ARTIFACT_RUN_ATTEMPT}" ]]',
|
|
);
|
|
expect(workflow).toContain('--arg digest "sha256:${ARTIFACT_DIGEST}"');
|
|
expect(workflow).toContain("actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}");
|
|
expect(workflow).not.toContain("uses: ./.github/actions/docker-e2e-plan");
|
|
expect(workflow).toContain("Checkout trusted release harness");
|
|
expect(workflow).toContain("OPENCLAW_DOCKER_E2E_REPO_ROOT:");
|
|
expect(workflow).toContain("node .release-harness/scripts/test-docker-all.mjs --plan-json");
|
|
expect(workflow).toContain("node .release-harness/scripts/docker-e2e.mjs github-outputs");
|
|
expect(parsedWorkflow.on?.workflow_call?.inputs).toHaveProperty(
|
|
"enable_prepublish_plugin_registry",
|
|
);
|
|
expect(workflow).toContain("Pack prerelease plugin registry artifact");
|
|
expect(workflow).toContain("Validate prerelease plugin registry artifact");
|
|
expect(workflow).toContain("Download targeted prerelease plugin registry artifact");
|
|
expect(workflow).toContain("OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR");
|
|
expect(workflow).toContain("prepublishPluginRegistryManifestSha256");
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"),
|
|
"Pack prerelease plugin registry artifact",
|
|
).id,
|
|
).toBe("create_prepublish_plugin_registry");
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"),
|
|
"Validate prerelease plugin registry artifact",
|
|
).env?.EXPECTED_MANIFEST_SHA256,
|
|
).toBe(
|
|
"${{ steps.create_prepublish_plugin_registry.outputs.manifest_sha256 || inputs.prepublish_plugin_registry_manifest_sha256 }}",
|
|
);
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"),
|
|
"Pack prerelease plugin registry artifact",
|
|
).if,
|
|
).toBe(
|
|
"steps.plan.outputs.needs_package == '1' && (inputs.prepared_npm_bundle_json != '' || (inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1')) && inputs.prepublish_plugin_registry_artifact_id == ''",
|
|
);
|
|
expect(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image").outputs
|
|
?.prepublish_plugin_registry_artifact_id,
|
|
).toContain("steps.prepublish_plugin_registry.outputs.manifest_sha256 != ''");
|
|
for (const jobId of ["validate_docker_e2e", "validate_docker_lanes"]) {
|
|
const job = workflowJob(LIVE_E2E_WORKFLOW, jobId);
|
|
expect(job.env).toMatchObject({
|
|
OPENCLAW_SELECTED_SHA: "${{ needs.validate_selected_ref.outputs.selected_sha }}",
|
|
OPENCLAW_TOOLING_SHA: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
|
|
});
|
|
const registrySteps = (job.steps ?? []).filter((step) =>
|
|
/^(Validate|Download) .*prerelease plugin registry/u.test(step.name ?? ""),
|
|
);
|
|
expect(registrySteps).toHaveLength(3);
|
|
for (const step of registrySteps) {
|
|
expect(step.if).toBe(
|
|
"steps.plan.outputs.needs_package == '1' && needs.prepare_docker_e2e_image.outputs.prepublish_plugin_registry_artifact_id != ''",
|
|
);
|
|
}
|
|
expect(registrySteps.at(-1)?.env?.REQUIRED_PACKAGES_JSON).toBe(
|
|
"${{ steps.plan.outputs.required_prepublish_plugin_packages }}",
|
|
);
|
|
const runStep = (job.steps ?? []).find((step) =>
|
|
step.run?.includes("export OPENCLAW_PREPUBLISH_PLUGIN_REGISTRY_DIR="),
|
|
);
|
|
expect(runStep).toBeDefined();
|
|
expect(runStep?.run).toContain("openclaw_resolve_frozen_update_channel_dry_run_mode");
|
|
expect(`${runStep?.run}\n${JSON.stringify(runStep?.env)}`).toContain(
|
|
"steps.plan.outputs.needs_package",
|
|
);
|
|
expect(`${runStep?.run}\n${JSON.stringify(runStep?.env)}`).not.toContain(
|
|
"steps.plan.outputs.needs_prepublish_plugin_registry",
|
|
);
|
|
}
|
|
expect(workflow).toContain("bash .release-harness/scripts/ci-docker-pull-retry.sh");
|
|
const setupHarnessStepName = "Setup trusted release harness";
|
|
const harnessJobCases = [
|
|
{
|
|
jobId: "validate_docker_e2e",
|
|
planStepName: "Plan Docker E2E chunk",
|
|
setupIf: "contains(matrix.profiles, inputs.release_test_profile)",
|
|
},
|
|
{
|
|
jobId: "validate_docker_lanes",
|
|
planStepName: "Plan targeted Docker E2E lanes",
|
|
setupIf: undefined,
|
|
},
|
|
{
|
|
jobId: "validate_docker_openwebui",
|
|
planStepName: "Plan Open WebUI Docker E2E chunk",
|
|
setupIf: undefined,
|
|
},
|
|
{
|
|
jobId: "prepare_docker_e2e_image",
|
|
planStepName: "Plan Docker E2E images",
|
|
setupIf: undefined,
|
|
},
|
|
] as const;
|
|
const typedHarnessJobIds = Object.entries(parsedWorkflow.jobs ?? {})
|
|
.filter(([, job]) =>
|
|
(job.steps ?? []).some(
|
|
(step) =>
|
|
step.run?.includes("node .release-harness/scripts/test-docker-all.mjs") ||
|
|
step.run?.includes("node .release-harness/scripts/docker-e2e.mjs"),
|
|
),
|
|
)
|
|
.map(([jobId]) => jobId)
|
|
.toSorted();
|
|
expect(typedHarnessJobIds).toEqual(harnessJobCases.map(({ jobId }) => jobId).toSorted());
|
|
|
|
for (const { jobId, planStepName, setupIf } of harnessJobCases) {
|
|
const harnessJob = workflowJob(LIVE_E2E_WORKFLOW, jobId);
|
|
const harnessJobSteps = harnessJob.steps ?? [];
|
|
const harnessJobStepNames = harnessJobSteps.map((step) => step.name);
|
|
const checkoutIndex = harnessJobStepNames.indexOf("Checkout trusted release harness");
|
|
const setupIndex = harnessJobStepNames.indexOf(setupHarnessStepName);
|
|
const typedHarnessIndex = harnessJobSteps.findIndex(
|
|
(step) =>
|
|
step.run?.includes("node .release-harness/scripts/test-docker-all.mjs") ||
|
|
step.run?.includes("node .release-harness/scripts/docker-e2e.mjs"),
|
|
);
|
|
expect(harnessJobStepNames.filter((name) => name === setupHarnessStepName)).toHaveLength(1);
|
|
expect(checkoutIndex).toBeGreaterThan(-1);
|
|
expect(setupIndex).toBeGreaterThan(checkoutIndex);
|
|
expect(typedHarnessIndex).toBeGreaterThan(setupIndex);
|
|
expect(workflowStep(harnessJob, planStepName)).toBeDefined();
|
|
const setupHarnessStep = workflowStep(harnessJob, setupHarnessStepName);
|
|
expect(setupHarnessStep).toMatchObject({
|
|
uses: "./.release-harness/.github/actions/setup-release-harness",
|
|
with: { "node-version": "${{ env.NODE_VERSION }}" },
|
|
});
|
|
expect(setupHarnessStep.if).toBe(setupIf);
|
|
}
|
|
|
|
const prepareDockerImage = workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image");
|
|
const prepareDockerImageStepNames = (prepareDockerImage.steps ?? []).map((step) => step.name);
|
|
const planStepName = "Plan Docker E2E images";
|
|
const setupCandidateStepName = "Setup Node environment";
|
|
expect(
|
|
prepareDockerImageStepNames.filter((name) => name === setupCandidateStepName),
|
|
).toHaveLength(1);
|
|
expect(prepareDockerImageStepNames.indexOf(setupCandidateStepName)).toBeGreaterThan(
|
|
prepareDockerImageStepNames.indexOf(planStepName),
|
|
);
|
|
expect(workflowStep(prepareDockerImage, setupCandidateStepName)).toMatchObject({
|
|
if: "(steps.plan.outputs.needs_package == '1' && steps.package_source.outputs.required == 'true') || (inputs.enable_prepublish_plugin_registry && steps.plan.outputs.needs_prepublish_plugin_registry == '1' && inputs.prepublish_plugin_registry_artifact_id == '')",
|
|
uses: "./.github/actions/setup-node-env",
|
|
});
|
|
expect(workflowStep(prepareDockerImage, planStepName).env).toEqual({
|
|
INCLUDE_OPENWEBUI: "${{ inputs.include_openwebui }}",
|
|
INCLUDE_RELEASE_PATH_SUITES: "${{ inputs.include_release_path_suites }}",
|
|
LANES: "${{ inputs.docker_lanes }}",
|
|
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:
|
|
"${{ needs.validate_selected_ref.outputs.upgrade_baseline }}",
|
|
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS:
|
|
"${{ needs.validate_selected_ref.outputs.upgrade_baselines }}",
|
|
OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS: "${{ inputs.published_upgrade_survivor_scenarios }}",
|
|
PREPARE_ONLY: "${{ inputs.prepare_only }}",
|
|
RELEASE_TEST_PROFILE: "${{ inputs.release_test_profile }}",
|
|
});
|
|
expect(workflow).toContain("plan_docker_lane_groups:");
|
|
const reusable = readWorkflow(LIVE_E2E_WORKFLOW);
|
|
expect(reusable.on?.workflow_dispatch?.inputs).not.toHaveProperty(
|
|
"published_upgrade_survivor_baseline_scope",
|
|
);
|
|
expect(reusable.on?.workflow_call?.inputs).toHaveProperty(
|
|
"published_upgrade_survivor_baseline_scope",
|
|
);
|
|
const groupPlanner = workflowStep(
|
|
workflowJob(LIVE_E2E_WORKFLOW, "plan_docker_lane_groups"),
|
|
"Build targeted Docker lane groups",
|
|
);
|
|
expect(groupPlanner.env).toMatchObject({
|
|
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC:
|
|
"${{ needs.validate_selected_ref.outputs.upgrade_baseline }}",
|
|
OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SCOPE:
|
|
"${{ needs.validate_selected_ref.outputs.upgrade_baseline_scope }}",
|
|
});
|
|
expect(workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_lanes").strategy?.["max-parallel"]).toBe(
|
|
32,
|
|
);
|
|
expect(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance").with).toMatchObject({
|
|
published_upgrade_survivor_baseline_scope:
|
|
"${{ needs.resolve_package.outputs.published_upgrade_survivor_baseline_scope }}",
|
|
});
|
|
expect(workflow).toContain("targeted_docker_lane_group_size:");
|
|
expect(workflow).toContain("scripts/plan-targeted-docker-lane-groups.mjs");
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS: ${{ needs.validate_selected_ref.outputs.upgrade_baselines }}",
|
|
);
|
|
expect(workflow).toContain("Docker E2E targeted lanes (${{ matrix.group.label }})");
|
|
expect(workflow).toContain("LANES: ${{ matrix.group.docker_lanes }}");
|
|
expect(workflow).toContain("GROUP_LABEL: ${{ matrix.group.label }}");
|
|
expect(workflow).toContain("DOCKER_E2E_LANES: ${{ matrix.group.docker_lanes }}");
|
|
expect(workflow).toContain("name: docker-e2e-${{ steps.plan.outputs.artifact_suffix }}");
|
|
expect(scheduler).toContain(
|
|
"published_upgrade_survivor_baseline=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC)}",
|
|
);
|
|
expect(scheduler).toContain(
|
|
"published_upgrade_survivor_baselines=${shellQuote(env.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS)}",
|
|
);
|
|
expect(scheduler).toContain(
|
|
'["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC", baseEnv.OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPEC]',
|
|
);
|
|
expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_BASELINE_SPECS",');
|
|
expect(scheduler).toContain('["OPENCLAW_UPGRADE_SURVIVOR_SCENARIOS",');
|
|
expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_PUBLISHED_BASELINE=1");
|
|
expect(packageJson).toContain("test:docker:update-restart-auth");
|
|
expect(packageJson).toContain("OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE=auto-auth");
|
|
expect(publishedUpgradeSurvivor).toContain("OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE");
|
|
expect(publishedUpgradeSurvivor).toContain("write_update_restart_service_env");
|
|
expect(publishedUpgradeSurvivor).toContain("GATEWAY_AUTH_TOKEN_REF=%s");
|
|
expect(publishedUpgradeSurvivor).toContain("OPENCLAW_CLAWHUB_URL=%s");
|
|
expect(publishedUpgradeSurvivor).toContain("assert-no-requests");
|
|
expect(publishedUpgradeSurvivor).toContain("phase prepare-update-restart-probe");
|
|
expect(publishedUpgradeSurvivor).toContain("configure_watchos_tls_fixture");
|
|
expect(publishedUpgradeSurvivor).toContain('"publicUrl":"wss://localhost:18789"');
|
|
expect(publishedUpgradeSurvivor).toContain('export NODE_EXTRA_CA_CERTS="$WATCH_TLS_CA_CERT"');
|
|
expect(publishedUpgradeSurvivor).not.toContain(
|
|
"--base-url http://127.0.0.1:18789/api/nodes/watch",
|
|
);
|
|
expect(publishedUpgradeSurvivor).toContain(
|
|
"source scripts/e2e/lib/upgrade-survivor/plugin-dependency-fixtures.sh",
|
|
);
|
|
expect(publishedUpgradeSurvivor).toContain("probe_gateway_endpoint");
|
|
const preDoctorCleanupIndex = publishedUpgradeSurvivor.indexOf(
|
|
"run_plugin_fixture_phase assert-package-local-dependency-cleanup assert_legacy_plugin_dependency_debris_cleaned",
|
|
);
|
|
const doctorIndex = publishedUpgradeSurvivor.indexOf("phase doctor run_doctor");
|
|
const postDoctorCleanupIndex = publishedUpgradeSurvivor.indexOf(
|
|
"run_plugin_fixture_phase assert-legacy-plugin-dependency-debris-cleaned assert_legacy_plugin_dependency_debris_cleaned",
|
|
);
|
|
expect(preDoctorCleanupIndex).toBeGreaterThan(-1);
|
|
expect(doctorIndex).toBeGreaterThan(preDoctorCleanupIndex);
|
|
expect(postDoctorCleanupIndex).toBeGreaterThan(doctorIndex);
|
|
expect(publishedUpgradeSurvivor.indexOf("phase seed-source-only-plugin-shadow")).toBeLessThan(
|
|
publishedUpgradeSurvivor.indexOf("phase assert-baseline"),
|
|
);
|
|
expect(publishedUpgradeSurvivor).toContain('"id": "opik-openclaw"');
|
|
expect(publishedUpgradeSurvivor).toContain('"configSchema": {');
|
|
});
|
|
|
|
it("reuses a content-addressed bare image for prepared E2E images", () => {
|
|
const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
|
|
expect(workflow).toContain("bare_context_sha=");
|
|
expect(workflow).toContain("-docker-e2e-bare:base-${bare_context_sha:0:32}");
|
|
expect(workflow).toContain('docker manifest inspect "$CACHE_IMAGE_REF"');
|
|
expect(workflow).toContain('cache=(--cache-from "$CACHE_IMAGE_REF")');
|
|
expect(workflow).not.toContain('docker tag "$CACHE_IMAGE_REF" "$IMAGE_REF"');
|
|
expect(workflow).toContain(
|
|
"Shared release candidate preparation requires both Docker image variants.",
|
|
);
|
|
expect(workflow).toContain(
|
|
"inputs.shared_image_artifact_id != '' && '1' || steps.plan.outputs.needs_bare_image",
|
|
);
|
|
expect(workflow).toContain("env DOCKER_BUILDKIT=1 docker build");
|
|
expect(workflow).toContain(
|
|
'if bash .release-harness/scripts/ci-docker-pull-retry.sh "$CACHE_IMAGE_REF"; then',
|
|
);
|
|
expect(workflow).toContain("Bare image cache pull failed; continuing with a cold build.");
|
|
expect(workflow).toContain('--build-context "openclaw_package=$package_context"');
|
|
expect(workflow).toContain('cache=(--cache-from "$BARE_IMAGE_REF")');
|
|
expect(workflow).not.toContain('docker push "$CACHE_IMAGE_REF"');
|
|
expect(workflow).toContain("uses: useblacksmith/setup-docker-builder@");
|
|
expect(workflow).toContain("uses: useblacksmith/build-push-action@");
|
|
expect(workflow).not.toContain("cache-from: type=gha,scope=docker-e2e");
|
|
expect(workflow).not.toContain("cache-to: type=gha,mode=max,scope=docker-e2e");
|
|
});
|
|
|
|
it("qualifies prepared publication bytes without serializing source, package, and Docker preparation", () => {
|
|
const prepare = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "prepare_npm_package");
|
|
const docker = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "prepare_docker_release");
|
|
const qualify = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "qualify_npm_package");
|
|
const candidate = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "candidate_acquisition");
|
|
for (const job of [prepare, docker]) {
|
|
expect(jobNeeds(job)).toEqual([
|
|
"resolve_target",
|
|
"plugin_compatibility_readiness",
|
|
"evidence_reuse",
|
|
]);
|
|
}
|
|
expect(jobNeeds(qualify)).toEqual(["resolve_target", "prepare_npm_package"]);
|
|
expect(qualify.if).toBe(
|
|
"${{ always() && needs.resolve_target.result == 'success' && inputs.rerun_group == 'all' && needs.prepare_npm_package.result == 'success' }}",
|
|
);
|
|
expect(qualify.env?.ARTIFACT_RUN_ID).toBe("${{ needs.prepare_npm_package.outputs.run_id }}");
|
|
expect(workflowStepById(prepare, "bundle").env?.ARTIFACT_OUTPUT).toBe("raw");
|
|
expect(qualify.env?.ARTIFACT_OUTPUT).toBe("receipt");
|
|
for (const job of [prepare, docker, candidate]) {
|
|
expect(job.if).not.toContain("github.run_attempt == 1");
|
|
expect(workflowStepById(job, "dispatch").if).toBe("github.run_attempt == 1");
|
|
expect(workflowStepById(job, "producer").run).toBe(
|
|
"node scripts/full-release-artifacts.mjs resolve",
|
|
);
|
|
expect(workflowStepById(job, "bundle").env?.ARTIFACT_RUN_ID).toBe(
|
|
"${{ steps.producer.outputs.run_id }}",
|
|
);
|
|
}
|
|
const producer = readWorkflow(FULL_RELEASE_ARTIFACTS_WORKFLOW);
|
|
const upload = workflowStep(
|
|
workflowJob(FULL_RELEASE_ARTIFACTS_WORKFLOW, "admit"),
|
|
"Preserve immutable artifact dispatch",
|
|
);
|
|
expect(upload.with?.overwrite).not.toBe(true);
|
|
expect(upload.with?.path).toBe("${{ steps.request.outputs.directory }}/dispatch.json");
|
|
expect(producer.permissions).toEqual({
|
|
actions: "read",
|
|
contents: "read",
|
|
packages: "read",
|
|
"pull-requests": "read",
|
|
});
|
|
expect(jobNeeds(workflowJob(FULL_RELEASE_ARTIFACTS_WORKFLOW, "npm"))).toEqual(["admit"]);
|
|
expect(workflowJob(FULL_RELEASE_ARTIFACTS_WORKFLOW, "npm").with?.preflight_phase).toBe(
|
|
"${{ inputs.preflight_phase }}",
|
|
);
|
|
expect(workflowJob(FULL_RELEASE_ARTIFACTS_WORKFLOW, "docker").uses).toBe(
|
|
"./.github/workflows/docker-release-prepare.yml",
|
|
);
|
|
const gate = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"),
|
|
"Require qualified publication artifacts",
|
|
);
|
|
const env = {
|
|
...process.env,
|
|
NPM_PREPARE_RESULT: "success",
|
|
NPM_QUALIFY_RESULT: "success",
|
|
QUALIFIED_NPM_BUNDLE_JSON: "{}",
|
|
DOCKER_PREPARE_RESULT: "success",
|
|
PREPARED_DOCKER_MANIFEST_SHA256: "a".repeat(64),
|
|
TARGET_VERSION: "2026.8.1",
|
|
};
|
|
for (const targetVersion of ["2026.8.1", "2026.8.1-1", "2026.8.1-beta.1", "2026.8.33"]) {
|
|
const releaseEnv = { ...env, TARGET_VERSION: targetVersion };
|
|
expect(spawnSync("bash", ["-c", gate.run ?? ""], { env: releaseEnv }).status).toBe(0);
|
|
for (const failure of [
|
|
{ NPM_PREPARE_RESULT: "failure" },
|
|
{ NPM_QUALIFY_RESULT: "failure" },
|
|
{ DOCKER_PREPARE_RESULT: "failure" },
|
|
{ DOCKER_PREPARE_RESULT: "skipped" },
|
|
{ PREPARED_DOCKER_MANIFEST_SHA256: "" },
|
|
]) {
|
|
expect(
|
|
spawnSync("bash", ["-c", gate.run ?? ""], {
|
|
env: { ...releaseEnv, ...failure },
|
|
}).status,
|
|
).not.toBe(0);
|
|
}
|
|
}
|
|
});
|
|
|
|
it("prepares one immutable candidate for release validation children", () => {
|
|
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
|
|
const reusableWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
const candidateWorkflow = readWorkflow(FULL_RELEASE_CANDIDATE_WORKFLOW);
|
|
const acquisition = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "candidate_acquisition");
|
|
const discovery = workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "discover");
|
|
const prepare = workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "prepare");
|
|
const candidateBinding = workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "resolve_candidate");
|
|
const summary = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary");
|
|
const producer = workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image");
|
|
const binder = workflowJob(LIVE_E2E_WORKFLOW, "bind_full_release_candidate_evidence");
|
|
const producerIdentity = workflowStepById(producer, "producer_identity");
|
|
const request = workflowStep(producer, "Build full release candidate request");
|
|
const legacyTuple = workflowStep(producer, "Emit immutable release candidate tuple");
|
|
const workflowRevision = workflowStepById(binder, "workflow");
|
|
const evidence = workflowStepById(binder, "candidate_evidence");
|
|
const upload = workflowStepById(binder, "upload_candidate_evidence");
|
|
const binding = workflowStep(binder, "Bind full release candidate evidence");
|
|
const pluginDispatch = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "plugin_prerelease_candidate"),
|
|
"Dispatch plugin prerelease candidate phase",
|
|
);
|
|
const releaseDispatch = workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "release_checks_candidate"),
|
|
"Dispatch release checks candidate phase",
|
|
);
|
|
const pluginDocker = workflowJob(PLUGIN_PRERELEASE_WORKFLOW, "plugin-prerelease-docker-suite");
|
|
|
|
expect(candidateWorkflow.jobs).not.toHaveProperty("finalize");
|
|
expect(candidateWorkflow.concurrency).toEqual({
|
|
group: "full-release-candidate-${{ inputs.request_sha256 }}",
|
|
"cancel-in-progress": false,
|
|
queue: "max",
|
|
});
|
|
for (const checkout of [
|
|
workflowStep(discovery, "Checkout trusted candidate discovery"),
|
|
workflowStep(candidateBinding, "Checkout candidate binding authority"),
|
|
workflowStep(summary, "Checkout release state verifier"),
|
|
]) {
|
|
expect(checkout.with?.["sparse-checkout"]).toBe("scripts");
|
|
}
|
|
expect(discovery.outputs?.state).toBe("${{ steps.discover.outputs.state }}");
|
|
expect(workflowStep(discovery, "Discover trusted release candidate").run).toContain(
|
|
"full-release-candidate-reuse.mjs discover",
|
|
);
|
|
expect(jobNeeds(prepare)).toEqual(["discover"]);
|
|
expect(prepare.if).toContain("needs.discover.outputs.state == 'miss'");
|
|
expect(prepare.if).not.toContain("outputs.reused != 'true'");
|
|
expect(jobNeeds(candidateBinding)).toEqual(["discover", "prepare"]);
|
|
expect(workflowStep(candidateBinding, "Resolve candidate binding").run).toContain(
|
|
"full-release-candidate-reuse.mjs resolve",
|
|
);
|
|
expect(candidateBinding.if).toBe("always()");
|
|
const resultStep = workflowStep(candidateBinding, "Record candidate acquisition result");
|
|
expect(resultStep.if).toBe("always()");
|
|
expect(resultStep.env?.RESOLVE_RESULT).toBe("${{ steps.resolve.outcome }}");
|
|
expect(resultStep.env?.JOB_STATUS).toBe("${{ job.status }}");
|
|
expect(candidateBinding.outputs?.state).toBe("${{ steps.result.outputs.state }}");
|
|
expect(workflowStep(candidateBinding, "Enforce candidate acquisition").if).toBe("always()");
|
|
expect(workflowStep(candidateBinding, "Record candidate acquisition result").run).toContain(
|
|
"state=unavailable",
|
|
);
|
|
expect(workflowStep(candidateBinding, "Enforce candidate acquisition").run).toContain(
|
|
'if [[ "$STATE" == "ready" ]]',
|
|
);
|
|
expect(upload.with?.overwrite).toBe(false);
|
|
expect(acquisition.env).toMatchObject({
|
|
TARGET_SHA: "${{ needs.resolve_target.outputs.sha }}",
|
|
CANDIDATE_REQUEST_JSON: "${{ needs.resolve_target.outputs.candidate_request_json }}",
|
|
PREPARED_NPM_BUNDLE_JSON: "${{ needs.prepare_npm_package.outputs.prepared_bundle_json }}",
|
|
});
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
|
|
"Build canonical release candidate request",
|
|
).env,
|
|
).toMatchObject({
|
|
PACKAGE_PUBLISHED: "${{ inputs.release_package_spec != '' }}",
|
|
});
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target"),
|
|
"Build canonical release candidate request",
|
|
).run,
|
|
).toContain('--argjson packagePublished "$PACKAGE_PUBLISHED"');
|
|
expect(workflowJob(FULL_RELEASE_ARTIFACTS_WORKFLOW, "candidate").uses).toBe(
|
|
"./.github/workflows/full-release-candidate.yml",
|
|
);
|
|
expect(prepare.uses).toBe("./.github/workflows/openclaw-live-and-e2e-checks-reusable.yml");
|
|
expect(prepare.with).toMatchObject({
|
|
enable_prepublish_plugin_registry: true,
|
|
emit_candidate_evidence: true,
|
|
package_published: "${{ fromJSON(inputs.request_json).packagePublished }}",
|
|
prepare_only: true,
|
|
release_soak: "${{ fromJSON(inputs.request_json).releaseSoak }}",
|
|
shared_image_policy: "${{ fromJSON(inputs.request_json).sharedImagePolicy }}",
|
|
});
|
|
expect(
|
|
readWorkflow(LIVE_E2E_WORKFLOW).on?.workflow_call?.inputs?.package_published,
|
|
).toMatchObject({
|
|
default: false,
|
|
required: false,
|
|
type: "boolean",
|
|
});
|
|
expect(request.env?.PACKAGE_PUBLISHED).toBe("${{ inputs.package_published }}");
|
|
expect(request.run).toContain('--argjson packagePublished "$PACKAGE_PUBLISHED"');
|
|
expect(prepare.with?.published_upgrade_survivor_scenarios).toBe(
|
|
"${{ join(fromJSON(inputs.request_json).upgradeSurvivorScenarios, ',') }}",
|
|
);
|
|
expect(prepare.with?.allow_frozen_target_scenario_omissions).toBe(
|
|
"${{ fromJSON(inputs.request_json).allowFrozenTargetScenarioOmissions }}",
|
|
);
|
|
expect(pluginDispatch.run).toContain(
|
|
'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")',
|
|
);
|
|
expect(releaseDispatch.run).toContain(
|
|
'args+=(-f candidate_artifact_json="$CANDIDATE_ARTIFACT_JSON")',
|
|
);
|
|
expect(releaseDispatch.env?.CANDIDATE_ARTIFACT_JSON).toBe(
|
|
"${{ needs.resolve_target.outputs.release_candidate_artifact_required == 'true' && needs.candidate_acquisition.outputs.candidate_artifact_json || '' }}",
|
|
);
|
|
expect(pluginDocker.with).toMatchObject({
|
|
prepublish_plugin_registry_artifact_digest:
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactDigest || '' }}",
|
|
prepublish_plugin_registry_artifact_id:
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactId || '' }}",
|
|
prepublish_plugin_registry_artifact_name:
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactName || '' }}",
|
|
prepublish_plugin_registry_artifact_run_attempt:
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunAttempt || '' }}",
|
|
prepublish_plugin_registry_artifact_run_id:
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryArtifactRunId || '' }}",
|
|
prepublish_plugin_registry_manifest_sha256:
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
|
});
|
|
expect(workflow).toContain("candidateAcquisitionResult: $candidateAcquisitionResult");
|
|
expect(request.run).toContain("full-release-candidate-contract.mjs request");
|
|
expect(evidence.run).toContain("full-release-candidate-contract.mjs manifest");
|
|
expect(evidence.run).toContain("verify-full-release-producer-job.mjs");
|
|
expect(evidence.run).toContain('--job-id "$PRODUCER_JOB_ID"');
|
|
expect(evidence.run).toContain('--job-name "$PRODUCER_JOB_NAME"');
|
|
expect(evidence.run).toContain('--run-id "$PRODUCER_RUN_ID"');
|
|
expect(evidence.run).toContain('--run-attempt "$PRODUCER_RUN_ATTEMPT"');
|
|
expect(evidence.run).toContain("publisher: {");
|
|
expect(evidence.run).toContain("requiredPrepublishPluginPackages");
|
|
expect(evidence.run).toContain('verify-upload "$label"');
|
|
expect(binding.run).toContain("full-release-candidate-contract.mjs binding");
|
|
expect(binding.run).toContain("for attempt in 1 2 3");
|
|
expect(upload.with).toMatchObject({
|
|
name: "full-release-candidate-v2-${{ needs.prepare_docker_e2e_image.outputs.candidate_request_sha256 }}",
|
|
"retention-days": 7,
|
|
});
|
|
expect(workflowRevision.env).toMatchObject({
|
|
EXPECTED_WORKFLOW_PATH: ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
|
JOB_CONTEXT: "${{ toJSON(job) }}",
|
|
RUN_ATTEMPT: "${{ github.run_attempt }}",
|
|
RUN_ID: "${{ github.run_id }}",
|
|
});
|
|
expect(workflowRevision.run).toContain("job.check_run_id");
|
|
expect(workflowRevision.run).toContain("job.workflow_file_path");
|
|
expect(workflowRevision.run).toContain("actions/jobs/${jobId}");
|
|
expect(producer.outputs).toMatchObject({
|
|
candidate_artifact_json: "${{ steps.candidate_manifest.outputs.json }}",
|
|
candidate_request_json: "${{ steps.candidate_request.outputs.json }}",
|
|
candidate_request_sha256: "${{ steps.candidate_request.outputs.sha256 }}",
|
|
plan_sha256: "${{ steps.plan.outputs.plan_sha256 }}",
|
|
producer_job_id: "${{ steps.producer_identity.outputs.job_id }}",
|
|
producer_job_name: "${{ steps.producer_identity.outputs.job_name }}",
|
|
producer_run_attempt: "${{ steps.producer_identity.outputs.run_attempt }}",
|
|
producer_run_id: "${{ steps.producer_identity.outputs.run_id }}",
|
|
producer_workflow_path: "${{ steps.producer_identity.outputs.workflow_path }}",
|
|
producer_workflow_repository: "${{ steps.producer_identity.outputs.workflow_repository }}",
|
|
producer_workflow_sha: "${{ steps.producer_identity.outputs.workflow_sha }}",
|
|
});
|
|
expect(producer.outputs).not.toHaveProperty("candidate_evidence_json");
|
|
expect(binder).toMatchObject({
|
|
if: "inputs.emit_candidate_evidence && needs.prepare_docker_e2e_image.result == 'success'",
|
|
needs: ["validate_selected_ref", "prepare_docker_e2e_image"],
|
|
outputs: {
|
|
candidate_evidence_json: "${{ steps.candidate_binding.outputs.json }}",
|
|
},
|
|
permissions: {
|
|
actions: "read",
|
|
contents: "read",
|
|
},
|
|
});
|
|
expect(evidence.env).toMatchObject({
|
|
CANDIDATE_ARTIFACT_JSON:
|
|
"${{ needs.prepare_docker_e2e_image.outputs.candidate_artifact_json }}",
|
|
CANDIDATE_REQUEST_JSON:
|
|
"${{ needs.prepare_docker_e2e_image.outputs.candidate_request_json }}",
|
|
CANDIDATE_REQUEST_SHA256:
|
|
"${{ needs.prepare_docker_e2e_image.outputs.candidate_request_sha256 }}",
|
|
PLAN_SHA256: "${{ needs.prepare_docker_e2e_image.outputs.plan_sha256 }}",
|
|
PRODUCER_JOB_ID: "${{ needs.prepare_docker_e2e_image.outputs.producer_job_id }}",
|
|
PRODUCER_JOB_NAME: "${{ needs.prepare_docker_e2e_image.outputs.producer_job_name }}",
|
|
PRODUCER_RUN_ATTEMPT: "${{ needs.prepare_docker_e2e_image.outputs.producer_run_attempt }}",
|
|
PRODUCER_RUN_ID: "${{ needs.prepare_docker_e2e_image.outputs.producer_run_id }}",
|
|
PRODUCER_WORKFLOW_PATH:
|
|
"${{ needs.prepare_docker_e2e_image.outputs.producer_workflow_path }}",
|
|
PRODUCER_WORKFLOW_REPOSITORY:
|
|
"${{ needs.prepare_docker_e2e_image.outputs.producer_workflow_repository }}",
|
|
PRODUCER_WORKFLOW_SHA: "${{ needs.prepare_docker_e2e_image.outputs.producer_workflow_sha }}",
|
|
PUBLISHER_JOB_ID: "${{ steps.workflow.outputs.job_id }}",
|
|
PUBLISHER_JOB_NAME: "${{ steps.workflow.outputs.job_name }}",
|
|
PUBLISHER_RUN_ATTEMPT: "${{ steps.workflow.outputs.run_attempt }}",
|
|
PUBLISHER_RUN_ID: "${{ steps.workflow.outputs.run_id }}",
|
|
PUBLISHER_WORKFLOW_PATH: "${{ steps.workflow.outputs.workflow_path }}",
|
|
PUBLISHER_WORKFLOW_REPOSITORY: "${{ steps.workflow.outputs.repository }}",
|
|
PUBLISHER_WORKFLOW_SHA: "${{ steps.workflow.outputs.sha }}",
|
|
REQUIRED_PACKAGES_JSON:
|
|
"${{ needs.prepare_docker_e2e_image.outputs.required_prepublish_plugin_packages }}",
|
|
});
|
|
const checkoutAction = "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1";
|
|
const producerSteps = producer.steps ?? [];
|
|
const binderSteps = binder.steps ?? [];
|
|
const producerCheckouts = producerSteps.filter((step) =>
|
|
step.uses?.startsWith("actions/checkout@"),
|
|
);
|
|
const binderCheckouts = binderSteps.filter((step) =>
|
|
step.uses?.startsWith("actions/checkout@"),
|
|
);
|
|
const binderSetup = binderSteps.find(
|
|
(step) => step.uses === "./.release-harness/.github/actions/setup-release-harness",
|
|
);
|
|
expect(producerCheckouts.map(({ uses, with: inputs }) => ({ inputs, uses }))).toEqual([
|
|
{
|
|
inputs: {
|
|
ref: "${{ needs.validate_selected_ref.outputs.selected_sha }}",
|
|
"fetch-depth": 1,
|
|
"persist-credentials": false,
|
|
},
|
|
uses: checkoutAction,
|
|
},
|
|
{
|
|
inputs: {
|
|
repository: "${{ needs.validate_selected_ref.outputs.workflow_repository }}",
|
|
ref: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
|
|
"fetch-depth": 1,
|
|
path: ".release-harness",
|
|
"persist-credentials": false,
|
|
},
|
|
uses: checkoutAction,
|
|
},
|
|
]);
|
|
expect(binderCheckouts.map(({ uses, with: inputs }) => ({ inputs, uses }))).toEqual([
|
|
{
|
|
inputs: {
|
|
repository: "openclaw/openclaw",
|
|
ref: "main",
|
|
"fetch-depth": 1,
|
|
path: ".release-harness",
|
|
"persist-credentials": false,
|
|
},
|
|
uses: checkoutAction,
|
|
},
|
|
]);
|
|
expect(producerSteps.indexOf(producerIdentity)).toBeLessThan(
|
|
producerSteps.indexOf(producerCheckouts[0]!),
|
|
);
|
|
expect(workflowRevision.env).toEqual({
|
|
EXPECTED_WORKFLOW_PATH: ".github/workflows/openclaw-live-and-e2e-checks-reusable.yml",
|
|
EXPECTED_WORKFLOW_REPOSITORY: "${{ github.repository }}",
|
|
GH_TOKEN: "${{ github.token }}",
|
|
HARNESS_PATH: ".release-harness",
|
|
JOB_CONTEXT: "${{ toJSON(job) }}",
|
|
RUN_ATTEMPT: "${{ github.run_attempt }}",
|
|
RUN_ID: "${{ github.run_id }}",
|
|
});
|
|
expect(workflowRevision.run).toContain('repository !== "openclaw/openclaw"');
|
|
expect(workflowRevision.run).toContain("job.workflow_repository !== repository");
|
|
expect(workflowRevision.run).toContain("job.workflow_sha");
|
|
expect(workflowRevision.run).toContain('"fetch"');
|
|
expect(workflowRevision.run).toContain('"checkout", "--detach", job.workflow_sha');
|
|
expect(binderSetup).toBeDefined();
|
|
expect(binderSteps.indexOf(workflowRevision)).toBeLessThan(binderSteps.indexOf(binderSetup!));
|
|
expect(binderSteps.indexOf(workflowRevision)).toBeLessThan(binderSteps.indexOf(evidence));
|
|
expect(binderSteps.indexOf(workflowRevision)).toBeLessThan(binderSteps.indexOf(upload));
|
|
expect(producerIdentity.env).toMatchObject({
|
|
JOB_CONTEXT: "${{ toJSON(job) }}",
|
|
TOOLING_REPOSITORY: "${{ needs.validate_selected_ref.outputs.workflow_repository }}",
|
|
TOOLING_SHA: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
|
|
});
|
|
expect(producerIdentity.run).toContain('.status == "in_progress"');
|
|
expect(producerIdentity.run).toContain("(.run_attempt | tostring) == $run_attempt");
|
|
expect(reusableWorkflow).toContain(
|
|
"value: ${{ jobs.bind_full_release_candidate_evidence.outputs.candidate_evidence_json }}",
|
|
);
|
|
expect(legacyTuple.run).not.toContain("candidate_request");
|
|
expect(legacyTuple.run).not.toContain("expiresAt");
|
|
});
|
|
|
|
it.each(["fresh", "reused"])(
|
|
"resolves and admits a %s candidate without a second job",
|
|
(source) => {
|
|
const manifest = fullReleaseCandidateManifestFixture();
|
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
for (const artifact of [
|
|
manifest.package.artifact,
|
|
manifest.prepublishPluginRegistry.artifact,
|
|
manifest.sharedImage.artifact,
|
|
]) {
|
|
artifact.expiresAt = expiresAt;
|
|
}
|
|
const binding = buildFullReleaseCandidateBinding({
|
|
manifest,
|
|
artifact: fullReleaseCandidateArtifact(
|
|
`full-release-candidate-v2-${manifest.requestSha256}`,
|
|
{ expiresAt },
|
|
),
|
|
});
|
|
const resolved = runCandidateAcquisitionStep("Resolve candidate binding", {
|
|
CANDIDATE_REQUEST_JSON: JSON.stringify(manifest.request),
|
|
EXPECTED_PACKAGE_SHA256: binding.package.packageSha256,
|
|
DISCOVERY_STATE: source === "fresh" ? "miss" : "hit",
|
|
FRESH_CANDIDATE_BINDING_JSON: source === "fresh" ? JSON.stringify(binding) : "",
|
|
REUSED_CANDIDATE_BINDING_JSON: source === "reused" ? JSON.stringify(binding) : "",
|
|
});
|
|
expect(resolved.result.status, resolved.result.stderr).toBe(0);
|
|
const recorded = runCandidateAcquisitionStep("Record candidate acquisition result", {
|
|
JOB_STATUS: "success",
|
|
DISCOVERY_RESULT: "success",
|
|
DISCOVERY_STATE: source === "fresh" ? "miss" : "hit",
|
|
PREPARE_RESULT: source === "fresh" ? "success" : "skipped",
|
|
RESOLVE_RESULT: "success",
|
|
RESOLVED_STATE: resolved.output.state ?? "",
|
|
RESOLVED_SOURCE: resolved.output.source ?? "",
|
|
BINDING_JSON: resolved.output.binding_json ?? "",
|
|
CANDIDATE_ARTIFACT_JSON: resolved.output.candidate_artifact_json ?? "",
|
|
});
|
|
expect(recorded.result.status, recorded.result.stderr).toBe(0);
|
|
expect(recorded.output).toEqual(resolved.output);
|
|
expect(JSON.parse(recorded.output.binding_json ?? "")).toEqual(binding);
|
|
expect(recorded.output.source).toBe(source);
|
|
expect(JSON.parse(recorded.output.candidate_artifact_json ?? "")).toMatchObject({
|
|
packageArtifactId: binding.package.artifact.id,
|
|
packageSha256: binding.package.packageSha256,
|
|
imageArtifactId: binding.sharedImage.artifact.id,
|
|
prepublishPluginRegistryArtifactId: binding.prepublishPluginRegistry.artifact.id,
|
|
});
|
|
expect(
|
|
runCandidateAcquisitionStep("Enforce candidate acquisition", {
|
|
STATE: recorded.output.state ?? "",
|
|
}).result.status,
|
|
).toBe(0);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["discovery failure", { DISCOVERY_RESULT: "failure" }],
|
|
["unavailable discovery", { DISCOVERY_STATE: "unavailable" }],
|
|
["prepare failure", { PREPARE_RESULT: "failure" }],
|
|
["prepare cancellation", { PREPARE_RESULT: "cancelled" }],
|
|
["checkout failure", { JOB_STATUS: "failure", RESOLVE_RESULT: "skipped" }],
|
|
["resolution failure", { JOB_STATUS: "failure", RESOLVE_RESULT: "failure" }],
|
|
["resolution cancellation", { JOB_STATUS: "cancelled", RESOLVE_RESULT: "cancelled" }],
|
|
["cancellation after resolution", { JOB_STATUS: "cancelled" }],
|
|
["missing resolution", { RESOLVE_RESULT: "" }],
|
|
] satisfies Array<[string, Record<string, string>]>)(
|
|
"keeps candidate acquisition unavailable after %s",
|
|
(_label, failure) => {
|
|
const recorded = runCandidateAcquisitionStep("Record candidate acquisition result", {
|
|
JOB_STATUS: "success",
|
|
DISCOVERY_RESULT: "success",
|
|
DISCOVERY_STATE: "hit",
|
|
PREPARE_RESULT: "skipped",
|
|
RESOLVE_RESULT: "success",
|
|
RESOLVED_STATE: "ready",
|
|
RESOLVED_SOURCE: "reused",
|
|
BINDING_JSON: "must-not-forward",
|
|
CANDIDATE_ARTIFACT_JSON: "must-not-forward",
|
|
...failure,
|
|
});
|
|
expect(recorded.result.status, recorded.result.stderr).toBe(0);
|
|
expect(recorded.output).toEqual({
|
|
state: "unavailable",
|
|
source: "",
|
|
binding_json: "",
|
|
candidate_artifact_json: "",
|
|
});
|
|
expect(
|
|
runCandidateAcquisitionStep("Enforce candidate acquisition", {
|
|
STATE: recorded.output.state ?? "",
|
|
}).result.status,
|
|
).toBe(1);
|
|
},
|
|
);
|
|
|
|
it("rejects malformed candidate resolution and missing terminal output", () => {
|
|
const resolved = runCandidateAcquisitionStep("Resolve candidate binding", {
|
|
CANDIDATE_REQUEST_JSON: "{}",
|
|
DISCOVERY_STATE: "miss",
|
|
FRESH_CANDIDATE_BINDING_JSON: "",
|
|
REUSED_CANDIDATE_BINDING_JSON: "",
|
|
});
|
|
expect(resolved.result.status).toBe(1);
|
|
expect(resolved.result.stderr).toContain("exactly one fresh or reused");
|
|
expect(resolved.output).toEqual({});
|
|
expect(
|
|
runCandidateAcquisitionStep("Enforce candidate acquisition", { STATE: "" }).result.status,
|
|
).toBe(1);
|
|
});
|
|
|
|
it("separates daily live checks from the secretless weekly upgrade survivors", () => {
|
|
const workflow = readWorkflow(SCHEDULED_LIVE_CHECKS_WORKFLOW);
|
|
const daily = workflowJob(SCHEDULED_LIVE_CHECKS_WORKFLOW, "live_and_openwebui_checks");
|
|
const weekly = workflowJob(SCHEDULED_LIVE_CHECKS_WORKFLOW, "weekly_upgrade_survivors");
|
|
|
|
expect(workflow.on?.schedule).toEqual([{ cron: "23 4 * * *" }, { cron: "41 6 * * 1" }]);
|
|
expect(daily.if).toBe(
|
|
"github.repository == 'openclaw/openclaw' && (github.event_name == 'workflow_dispatch' || github.event.schedule == '23 4 * * *')",
|
|
);
|
|
expect(daily.with).toMatchObject({
|
|
enable_prepublish_plugin_registry: true,
|
|
ref: "${{ github.sha }}",
|
|
});
|
|
expect(weekly.if).toBe(
|
|
"github.repository == 'openclaw/openclaw' && github.event_name == 'schedule' && github.event.schedule == '41 6 * * 1'",
|
|
);
|
|
expect(weekly.permissions).toEqual({
|
|
actions: "read",
|
|
contents: "read",
|
|
packages: "read",
|
|
"pull-requests": "read",
|
|
});
|
|
expect(weekly.with).toMatchObject({
|
|
ref: "${{ github.sha }}",
|
|
include_repo_e2e: false,
|
|
include_release_path_suites: false,
|
|
include_openwebui: false,
|
|
include_live_suites: false,
|
|
allow_unreleased_changelog: true,
|
|
docker_lanes: "update-migration",
|
|
published_upgrade_survivor_baselines: "2026.7.1 2026.8.1",
|
|
published_upgrade_survivor_scenarios: "mobile-pairing-reconnect watchos-direct-node",
|
|
shared_image_artifact_namespace: "scheduled-upgrade-survivors",
|
|
shared_image_policy: "no-push-artifact",
|
|
});
|
|
expect(weekly.secrets).toBeUndefined();
|
|
expect(weekly.with).not.toHaveProperty("docker_e2e_bare_image");
|
|
expect(weekly.with).not.toHaveProperty("docker_e2e_functional_image");
|
|
expect(weekly.with).not.toHaveProperty("allow_frozen_target_scenario_omissions");
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"reconstructs packagePublished=%s in the produced candidate request",
|
|
(packagePublished) => {
|
|
const { output, result } = runFullReleaseCandidateRequest(packagePublished);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(JSON.parse(output.json ?? "{}")).toMatchObject({ packagePublished });
|
|
},
|
|
);
|
|
|
|
it("gives memory extension shards enough CPU without lowering their planner cost", () => {
|
|
const workflow = readFileSync(PLUGIN_PRERELEASE_WORKFLOW, "utf8");
|
|
|
|
expect(workflow).toContain('extensionId.startsWith("memory-")');
|
|
expect(workflow).toContain('"blacksmith-16vcpu-ubuntu-2404"');
|
|
expect(workflow).toContain("vitest_max_workers:");
|
|
expect(workflow).toContain("OPENCLAW_VITEST_MAX_WORKERS: ${{ matrix.vitest_max_workers }}");
|
|
expect(readFileSync("scripts/lib/extension-test-plan.mts", "utf8")).toContain(
|
|
'"test/vitest/vitest.extension-memory.config.ts": 1',
|
|
);
|
|
});
|
|
|
|
it.each(["beta", "minimum", "stable", "full"])(
|
|
"accepts every runnable focused live suite for the %s profile",
|
|
(profile) => {
|
|
const suiteIds = new Set(["openshell-e2e", "live-cache", "docker-live-models"]);
|
|
for (const jobName of [
|
|
"validate_live_provider_suites",
|
|
"validate_live_docker_provider_suites",
|
|
"validate_live_media_provider_suites",
|
|
]) {
|
|
const entries =
|
|
jobName === "validate_live_docker_provider_suites"
|
|
? createReleaseWorkflowMatrixPlan({ releaseProfile: profile, includeLiveSuites: true })
|
|
.liveDocker.matrix.include
|
|
: workflowJob(LIVE_E2E_WORKFLOW, jobName).strategy?.matrix?.include;
|
|
expect(entries?.length, jobName).toBeGreaterThan(0);
|
|
for (const entry of entries ?? []) {
|
|
if (!entry.profiles?.split(/\s+/u).includes(profile)) {
|
|
continue;
|
|
}
|
|
for (const suiteId of [entry.suite_id, entry.suite_group]) {
|
|
if (suiteId) {
|
|
suiteIds.add(suiteId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const suiteId of suiteIds) {
|
|
const result = runFocusedLiveSuiteValidation(suiteId, { RELEASE_TEST_PROFILE: profile });
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(`Focused live suite filter is valid: ${suiteId}`);
|
|
}
|
|
},
|
|
);
|
|
|
|
it("fails focused live suite validation when Docker matrix planning fails", () => {
|
|
const result = runFocusedLiveSuiteValidation("openshell-e2e", {
|
|
ADMISSION_TOOLING_ROOT: resolve(tempDirs.make("missing-admission-tooling-"), "missing"),
|
|
});
|
|
|
|
expect(result.status).not.toBe(0);
|
|
});
|
|
|
|
it("accepts the OpenCode Go aggregate for its stable smoke lane", () => {
|
|
const result = runFocusedLiveSuiteValidation("native-live-src-gateway-profiles-opencode-go", {
|
|
RELEASE_TEST_PROFILE: "stable",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it.each<{ suiteId: string; env?: Record<string, string> }>([
|
|
{ suiteId: "native-live-src-gateway-profiles-opencode-go-unknown" },
|
|
{ suiteId: "native-live-extensions-media-video-e" },
|
|
{ suiteId: "unknown-live-suite" },
|
|
{
|
|
suiteId: "native-live-src-gateway-profiles-opencode-go-kimi",
|
|
env: { RELEASE_TEST_PROFILE: "stable" },
|
|
},
|
|
{
|
|
suiteId: "native-live-extensions-media-video-a",
|
|
env: { RELEASE_TEST_PROFILE: "beta" },
|
|
},
|
|
{
|
|
suiteId: "native-live-src-gateway-profiles-opencode-go-kimi",
|
|
env: { INCLUDE_LIVE_SUITES: "false" },
|
|
},
|
|
{
|
|
suiteId: "native-live-extensions-media-video-a",
|
|
env: { LIVE_MODELS_ONLY: "true" },
|
|
},
|
|
{ suiteId: "openshell-e2e", env: { INCLUDE_REPO_E2E: "false" } },
|
|
{ suiteId: "docker-live-models", env: { INCLUDE_LIVE_SUITES: "false" } },
|
|
])("rejects unavailable focused suite $suiteId with $env", ({ suiteId, env }) => {
|
|
const result = runFocusedLiveSuiteValidation(suiteId, env);
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
`live_suite_filter '${suiteId}' does not match any runnable suite`,
|
|
);
|
|
});
|
|
|
|
it("shards broad native live tests instead of one serial live-all job", () => {
|
|
const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
const nativeLiveJob = workflowJob(LIVE_E2E_WORKFLOW, "validate_live_provider_suites");
|
|
const selection = workflowStep(nativeLiveJob, "Select native live suite");
|
|
|
|
expect(workflow).not.toContain("suite_id: live-all");
|
|
expect(workflow).not.toContain("command: pnpm test:live\n");
|
|
expect(workflow).toContain("suite_id: native-live-src-agents");
|
|
expect(workflow).toContain("Checkout trusted live shard harness");
|
|
expect(selection.if).toContain("contains(matrix.profiles, inputs.release_test_profile)");
|
|
expect(selection.if).toContain("inputs.live_suite_filter == matrix.suite_id");
|
|
for (const stepName of [
|
|
"Checkout selected ref",
|
|
"Checkout trusted live shard harness",
|
|
"Setup Node environment",
|
|
"Setup trusted release harness",
|
|
"Hydrate live auth/profile inputs",
|
|
"Configure suite-specific env",
|
|
"Run ${{ matrix.label }}",
|
|
]) {
|
|
expect(workflowStep(nativeLiveJob, stepName).if).toBe(
|
|
"steps.selection.outputs.run == 'true'",
|
|
);
|
|
}
|
|
for (const job of [
|
|
nativeLiveJob,
|
|
workflowJob(LIVE_E2E_WORKFLOW, "validate_live_media_provider_suites"),
|
|
]) {
|
|
const setup = workflowStep(job, "Setup trusted release harness");
|
|
expect(setup).toMatchObject({
|
|
uses: "./.release-harness/.github/actions/setup-release-harness",
|
|
if: workflowStep(job, "Run ${{ matrix.label }}").if,
|
|
with: { "node-version": "${{ env.NODE_VERSION }}" },
|
|
});
|
|
const names = job.steps?.map((step) => step.name) ?? [];
|
|
expect(names.indexOf(setup.name)).toBeGreaterThan(names.indexOf("Setup Node environment"));
|
|
expect(names.indexOf(setup.name)).toBeLessThan(
|
|
names.indexOf("Hydrate live auth/profile inputs"),
|
|
);
|
|
}
|
|
expect(
|
|
workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_provider_suites",
|
|
"native-live-src-agents",
|
|
),
|
|
).toMatchObject({
|
|
command:
|
|
"OPENCLAW_LIVE_OPENAI_COMPACTION=1 OPENCLAW_LIVE_OPENAI_COMPACTION_FULL=0 node .release-harness/scripts/test-live-shard.mjs native-live-src-agents",
|
|
profiles: "stable full",
|
|
});
|
|
expect(workflow).toContain("suite_id: native-live-src-agents-zai-coding");
|
|
expect(workflow).toContain(
|
|
"command: ZAI_CODING_LIVE_TEST=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-agents-zai-coding",
|
|
);
|
|
expect(workflow).toContain("OPENCLAW_LIVE_COMMAND: ${{ matrix.command }}");
|
|
expect(workflow).toContain("live_suite_filter:");
|
|
const admissionSteps = workflowJob(LIVE_E2E_WORKFLOW, "validate_selected_ref").steps ?? [];
|
|
expect(
|
|
admissionSteps.findIndex((step) => step.name === "Validate focused live suite filter"),
|
|
).toBeLessThan(
|
|
admissionSteps.findIndex((step) => step.name === "Admit frozen source contracts"),
|
|
);
|
|
expect(workflow).toContain("LIVE_SUITE_FILTER: ${{ inputs.live_suite_filter }}");
|
|
expect(workflow).toContain("timeout --foreground --kill-after=30s 8m pnpm test:live:cache");
|
|
expect(workflow).toContain(
|
|
"live_suite_filter '${LIVE_SUITE_FILTER}' does not match any runnable suite",
|
|
);
|
|
expect(workflow).toContain(
|
|
"inputs.live_suite_filter == '' || inputs.live_suite_filter == matrix.suite_id",
|
|
);
|
|
expect(workflow).not.toContain("openai-ws-stream-live-e2e");
|
|
expect(workflow).not.toContain("src/agents/openai-ws-stream.e2e.test.ts");
|
|
expect(
|
|
createReleaseWorkflowMatrixPlan({ releaseProfile: "full", includeLiveSuites: true })
|
|
.liveDocker.matrix.include,
|
|
).toContainEqual(
|
|
expect.objectContaining({ suite_id: "live-gateway-advisory-docker-deepseek-fireworks" }),
|
|
);
|
|
const dockerSuiteIds = createReleaseWorkflowMatrixPlan({
|
|
releaseProfile: "full",
|
|
includeLiveSuites: true,
|
|
}).liveDocker.matrix.include.map((row: { suite_id: string }) => row.suite_id);
|
|
expect(dockerSuiteIds).toEqual(
|
|
expect.arrayContaining([
|
|
"live-gateway-advisory-docker-opencode-openrouter",
|
|
"live-gateway-advisory-docker-xai-zai",
|
|
"live-subagent-announce-docker",
|
|
"live-cli-cache-docker",
|
|
]),
|
|
);
|
|
const advisoryRows = createReleaseWorkflowMatrixPlan({
|
|
releaseProfile: "full",
|
|
includeLiveSuites: true,
|
|
liveSuiteFilter: "live-gateway-advisory-docker",
|
|
}).liveDocker.matrix.include;
|
|
expect(advisoryRows.map((row: { suite_group?: string }) => row.suite_group)).toEqual(
|
|
Array(3).fill("live-gateway-advisory-docker"),
|
|
);
|
|
for (const providers of ["deepseek,fireworks", "opencode-go,openrouter", "xai,zai"]) {
|
|
expect(
|
|
advisoryRows.some((row: { command: string }) =>
|
|
row.command.includes(`OPENCLAW_LIVE_GATEWAY_PROVIDERS=${providers}`),
|
|
),
|
|
).toBe(true);
|
|
}
|
|
expect(workflow).toContain("inputs.live_suite_filter == matrix.suite_group");
|
|
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_MODEL=claude-cli/claude-sonnet-4-6");
|
|
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_AUTH=api-key");
|
|
expect(workflow).toContain("OPENCLAW_LIVE_CLI_BACKEND_CACHE_PROBE=1");
|
|
expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_USE_CI_SAFE_CODEX_CONFIG=1");
|
|
expect(workflow).not.toContain('service_tier=\\"fast\\"');
|
|
expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_ARGS=");
|
|
expect(workflow).not.toContain("OPENCLAW_LIVE_CLI_BACKEND_RESUME_ARGS=");
|
|
expect(workflow).not.toContain(
|
|
'OPENCLAW_LIVE_CLI_BACKEND_ARGS=["exec","--json","--color","never","--sandbox","danger-full-access","--skip-git-repo-check"]',
|
|
);
|
|
expect(workflow).toContain('bash -o pipefail -c "$OPENCLAW_LIVE_COMMAND"');
|
|
expect(workflow).toContain("use_github_hosted_runners:");
|
|
for (const [jobName, runner] of [
|
|
["validate_special_e2e", "blacksmith-32vcpu-ubuntu-2404"],
|
|
["validate_live_provider_suites", "blacksmith-8vcpu-ubuntu-2404"],
|
|
] as const) {
|
|
expect(evaluateWorkflowRunner(workflowJob(LIVE_E2E_WORKFLOW, jobName)["runs-on"])).toBe(
|
|
runner,
|
|
);
|
|
expect(
|
|
evaluateWorkflowRunner(workflowJob(LIVE_E2E_WORKFLOW, jobName)["runs-on"], {
|
|
useGithubHostedRunners: true,
|
|
}),
|
|
).toBe("ubuntu-24.04");
|
|
}
|
|
for (const jobName of ["build", "test"]) {
|
|
const selector = workflowJob(".github/workflows/openclaw-repo-e2e-reusable.yml", jobName)[
|
|
"runs-on"
|
|
];
|
|
expect(evaluateWorkflowRunner(selector)).toBe("blacksmith-32vcpu-ubuntu-2404");
|
|
expect(evaluateWorkflowRunner(selector, { useGithubHostedRunners: true })).toBe(
|
|
"ubuntu-24.04",
|
|
);
|
|
}
|
|
const repoE2eHarnessCheckout = workflowStep(
|
|
workflowJob(".github/workflows/openclaw-repo-e2e-reusable.yml", "build"),
|
|
"Checkout trusted artifact harness",
|
|
);
|
|
expect(repoE2eHarnessCheckout.with).toMatchObject({
|
|
"sparse-checkout": "/package.json\n/scripts/\n/src/shared/non-packaged-plugin-dirs.ts\n",
|
|
"sparse-checkout-cone-mode": false,
|
|
});
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-core");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-backends");
|
|
expect(workflow).toContain(
|
|
"command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-core",
|
|
);
|
|
expect(workflow).toContain(
|
|
"command: OPENCLAW_LIVE_CODEX_HARNESS=1 OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-backends",
|
|
);
|
|
expect(workflow).toContain("suite_id: native-live-src-infra");
|
|
expect(workflow).toContain(
|
|
"command: OPENCLAW_LIVE_APNS_REACHABILITY=1 OPENCLAW_LIVE_SESSION_EVENT_WAKE=1 node .release-harness/scripts/test-live-shard.mjs native-live-src-infra",
|
|
);
|
|
expect(workflow).toContain(
|
|
"command: OPENCLAW_LIVE_CODEX_NODE_EXEC_TIMEOUT=1 node .release-harness/scripts/test-live-shard.mjs native-live-test",
|
|
);
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-anthropic-smoke");
|
|
expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_SETUP_TIMEOUT_MS=300000");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-anthropic-opus");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-anthropic-sonnet-haiku");
|
|
expect(workflow).toContain("suite_group: native-live-src-gateway-profiles-anthropic");
|
|
expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-opus-5");
|
|
expect(workflow).toContain("anthropic/claude-sonnet-4-6,anthropic/claude-haiku-4-5");
|
|
expect(workflow).toMatch(
|
|
/suite_id: native-live-src-gateway-profiles-openai[\s\S]*?timeout_minutes: 60[\s\S]*?profiles: beta minimum stable full/u,
|
|
);
|
|
expect(workflow).toContain(
|
|
"command: OPENCLAW_LIVE_GATEWAY_SETUP_TIMEOUT_MS=300000 OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=180000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000",
|
|
);
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_MODELS=google/gemini-3.1-pro-preview node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles",
|
|
);
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2",
|
|
);
|
|
expect(workflow).toMatch(
|
|
/suite_id: native-live-src-gateway-profiles-fireworks[\s\S]*?timeout_minutes: 30/u,
|
|
);
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-deepseek");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-opencode-go");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-openrouter");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-xai");
|
|
expect(workflow).toContain("suite_id: native-live-src-gateway-profiles-zai");
|
|
expect(workflow).not.toContain("Z.AI API Platform validation is temporarily disabled");
|
|
expect(workflow).not.toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek,opencode-go,openrouter,xai,zai",
|
|
);
|
|
const dockerRows = ["stable", "full"].flatMap(
|
|
(releaseProfile) =>
|
|
createReleaseWorkflowMatrixPlan({ releaseProfile, includeLiveSuites: true }).liveDocker
|
|
.matrix.include,
|
|
);
|
|
const dockerCommands = dockerRows.map((row: { command: string }) => row.command).join("\n");
|
|
expect(dockerRows).toContainEqual(
|
|
expect.objectContaining({ suite_id: "live-gateway-anthropic-docker" }),
|
|
);
|
|
expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2");
|
|
expect(dockerCommands).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1 OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=90000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000",
|
|
);
|
|
expect(dockerCommands).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_MODELS=anthropic/claude-sonnet-4-6,anthropic/claude-haiku-4-5 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2",
|
|
);
|
|
expect(workflow).toContain("OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000");
|
|
expect(workflow).toContain("timeout --foreground --kill-after=30s 35m");
|
|
expect(dockerRows).toContainEqual(
|
|
expect.objectContaining({ suite_id: "live-gateway-docker", timeout_minutes: 40 }),
|
|
);
|
|
expect(workflow).toContain("suite_id: native-live-extensions-a-k");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-l-n");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-moonshot");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-openai");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-o-z-other");
|
|
expect(workflow).toContain("validate_live_media_provider_suites:");
|
|
const mediaRunner = workflowJob(LIVE_E2E_WORKFLOW, "validate_live_media_provider_suites")[
|
|
"runs-on"
|
|
];
|
|
expect(evaluateWorkflowRunner(mediaRunner)).toBe("blacksmith-8vcpu-ubuntu-2404");
|
|
expect(evaluateWorkflowRunner(mediaRunner, { useGithubHostedRunners: true })).toBe(
|
|
"ubuntu-24.04",
|
|
);
|
|
expect(workflow).toContain(`image: ${LIVE_MEDIA_RUNNER_IMAGE}`);
|
|
expect(workflow).toContain("ffmpeg -version | head -1");
|
|
expect(workflow).toContain("ffprobe -version | head -1");
|
|
const imageDockerfile = readFileSync(LIVE_MEDIA_RUNNER_DOCKERFILE, "utf8");
|
|
const imageWorkflow = readFileSync(LIVE_MEDIA_RUNNER_IMAGE_WORKFLOW, "utf8");
|
|
const buildJob = workflowJob(LIVE_MEDIA_RUNNER_IMAGE_WORKFLOW, "build");
|
|
const buildStep = workflowStep(buildJob, "Build and push live media runner image");
|
|
expect(imageDockerfile).toMatch(/^FROM ubuntu:24\.04$/m);
|
|
expect(imageDockerfile).toContain("apt-get install -y --no-install-recommends");
|
|
for (const packageName of ["bash", "curl", "ffmpeg", "git", "openssh-client", "zstd"]) {
|
|
expect(imageDockerfile).toContain(` ${packageName} \\`);
|
|
}
|
|
expect(imageDockerfile).toContain("rm -rf /var/lib/apt/lists/*");
|
|
expect(imageWorkflow).toContain(`- "${LIVE_MEDIA_RUNNER_DOCKERFILE}"`);
|
|
expect(buildStep.with?.context).toBe(".github/images/live-media-runner");
|
|
expect(buildStep.with?.file).toBe(LIVE_MEDIA_RUNNER_DOCKERFILE);
|
|
expect(buildStep.with?.tags).toContain(LIVE_MEDIA_RUNNER_IMAGE);
|
|
expect(workflow).toContain("suite_id: native-live-extensions-media-audio");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-media-music-google");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-media-music-minimax");
|
|
expect(workflow).toContain("suite_id: native-live-extensions-media-video");
|
|
expect(workflow).toContain("suite_group: native-live-extensions-media-video");
|
|
for (const suffix of ["a", "b", "c", "d"]) {
|
|
const shard = workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_media_provider_suites",
|
|
`native-live-extensions-media-video-${suffix}`,
|
|
);
|
|
const providers = shard.command?.match(
|
|
/OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=(\S+)/u,
|
|
)?.[1];
|
|
if (!providers) {
|
|
throw new Error(`Missing video providers for shard ${suffix}`);
|
|
}
|
|
expect(providers.split(",")).toHaveLength(4);
|
|
expect(shard.command).toContain("OPENCLAW_LIVE_VIDEO_GENERATION_TIMEOUT_MS=600000");
|
|
// Four serial ten-minute operations plus test overhead leave eight minutes for setup.
|
|
expect(shard.timeout_minutes).toBeGreaterThanOrEqual(4 * 10.5 + 8);
|
|
}
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=google,kie,minimax,pixverse OPENCLAW_LIVE_VIDEO_GENERATION_TIMEOUT_MS=600000",
|
|
);
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_LIVE_VIDEO_GENERATION_PROVIDERS=novita,openrouter,xai,zai",
|
|
);
|
|
expect(workflow).toContain(
|
|
"inputs.live_suite_filter == 'native-live-src-gateway-profiles-anthropic'",
|
|
);
|
|
expect(workflow).toContain(
|
|
"inputs.live_suite_filter == 'native-live-src-gateway-profiles-opencode-go'",
|
|
);
|
|
expect(workflow).toContain("inputs.live_suite_filter == 'native-live-extensions-media-video'");
|
|
expect(workflow).not.toContain("needs_ffmpeg: true");
|
|
expect(
|
|
workflow.match(/moonshot\) require_any Moonshot MOONSHOT_API_KEY KIMI_API_KEY ;;/gu),
|
|
).toHaveLength(2);
|
|
});
|
|
|
|
it("pins DeepSeek live profiles to both current V4 model refs", () => {
|
|
const deepSeek = workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_provider_suites",
|
|
"native-live-src-gateway-profiles-deepseek",
|
|
);
|
|
const openCodeGo = workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_provider_suites",
|
|
"native-live-src-gateway-profiles-opencode-go-deepseek-glm",
|
|
);
|
|
|
|
expect(deepSeek).toMatchObject({
|
|
command:
|
|
"OPENCLAW_LIVE_GATEWAY_PROVIDERS=deepseek OPENCLAW_LIVE_GATEWAY_MODELS=deepseek/deepseek-v4-flash,deepseek/deepseek-v4-pro node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles",
|
|
profiles: "full",
|
|
});
|
|
expect(openCodeGo.command).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/deepseek-v4-flash,opencode-go/deepseek-v4-pro",
|
|
);
|
|
});
|
|
|
|
it("pins OpenCode Go MiMo live profiles to both current V2.5 model refs", () => {
|
|
const mimo = workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_provider_suites",
|
|
"native-live-src-gateway-profiles-opencode-go-mimo",
|
|
);
|
|
|
|
expect(mimo).toMatchObject({
|
|
command:
|
|
"OPENCLAW_LIVE_GATEWAY_PROVIDERS=opencode-go OPENCLAW_LIVE_GATEWAY_MODELS=opencode-go/mimo-v2.5,opencode-go/mimo-v2.5-pro node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles",
|
|
profiles: "full",
|
|
suite_group: "native-live-src-gateway-profiles-opencode-go",
|
|
});
|
|
expect(mimo.command).not.toContain("opencode-go/mimo-v2-omni");
|
|
expect(mimo.command).not.toContain("opencode-go/mimo-v2-pro");
|
|
});
|
|
|
|
it("runs the fresh OpenAI API-key default without hard-coding a model filter", () => {
|
|
const openaiDefault = workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_provider_suites",
|
|
"native-live-src-gateway-profiles-openai-api-default",
|
|
);
|
|
|
|
expect(openaiDefault).toMatchObject({ profiles: "stable full" });
|
|
expect(openaiDefault.command).toContain("OPENCLAW_LIVE_GATEWAY_OPENAI_API_DEFAULT=1");
|
|
expect(openaiDefault.command).toContain("OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai");
|
|
expect(openaiDefault.command).not.toContain("OPENCLAW_LIVE_GATEWAY_MODELS=");
|
|
});
|
|
|
|
it("retains the full OpenAI Ultra model coverage independently of the fresh default", () => {
|
|
const ultra = workflowMatrixEntry(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_live_provider_suites",
|
|
"native-live-src-gateway-profiles-openai-gpt56-ultra",
|
|
);
|
|
expect(ultra).toMatchObject({
|
|
profiles: "stable full",
|
|
timeout_minutes: 75,
|
|
profile_env_only: false,
|
|
command:
|
|
"OPENCLAW_LIVE_GATEWAY_THINKING=ultra OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-sol,openai/gpt-5.6-terra,openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=300000 OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=900000 node .release-harness/scripts/test-live-shard.mjs native-live-src-gateway-profiles",
|
|
});
|
|
});
|
|
|
|
it("runs Docker live harnesses from trusted helper scripts", () => {
|
|
const workflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
const providerSuites = workflowJob(LIVE_E2E_WORKFLOW, "validate_live_docker_provider_suites");
|
|
const scenarios = readFileSync("scripts/lib/docker-e2e-scenarios.mts", "utf8");
|
|
const harness = readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8");
|
|
const codexLiveTest = readFileSync("src/gateway/gateway-codex-harness.live.test.ts", "utf8");
|
|
const liveDockerAuth = readFileSync("scripts/lib/live-docker-auth.sh", "utf8");
|
|
const sharedLiveScripts = [
|
|
readFileSync("scripts/test-live-models-docker.sh", "utf8"),
|
|
readFileSync("scripts/test-live-gateway-models-docker.sh", "utf8"),
|
|
readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8"),
|
|
readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8"),
|
|
readFileSync("scripts/test-live-subagent-announce-docker.sh", "utf8"),
|
|
];
|
|
const build = readFileSync("scripts/test-live-build-docker.sh", "utf8");
|
|
const stage = readFileSync("scripts/lib/live-docker-stage.sh", "utf8");
|
|
const dockerRows = createReleaseWorkflowMatrixPlan({
|
|
releaseProfile: "full",
|
|
includeLiveSuites: true,
|
|
}).liveDocker.matrix.include;
|
|
const commands = dockerRows.map((row: { command: string }) => row.command).join("\n");
|
|
|
|
expect(workflow).toContain(
|
|
'run: OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-models-docker.sh',
|
|
);
|
|
expect(commands).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_THINKING=off OPENCLAW_LIVE_GATEWAY_PROVIDERS=openai OPENCLAW_LIVE_GATEWAY_MODELS=openai/gpt-5.6-luna OPENCLAW_LIVE_GATEWAY_MAX_MODELS=1",
|
|
);
|
|
expect(commands).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_PROVIDERS=minimax,minimax-portal OPENCLAW_LIVE_GATEWAY_MODELS=minimax/MiniMax-M3,minimax-portal/MiniMax-M3 OPENCLAW_LIVE_GATEWAY_MAX_MODELS=2",
|
|
);
|
|
expect(commands).toContain(
|
|
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-cli-backend-docker.sh',
|
|
);
|
|
expect(commands).toContain(
|
|
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 45m bash .release-harness/scripts/test-live-acp-bind-docker.sh',
|
|
);
|
|
expect(commands).toContain(
|
|
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 35m bash .release-harness/scripts/test-live-codex-harness-docker.sh',
|
|
);
|
|
const codexCompatibility = workflowStep(
|
|
providerSuites,
|
|
"Resolve frozen Codex live compatibility",
|
|
);
|
|
expect(codexCompatibility).toMatchObject({
|
|
id: "codex_compat",
|
|
env: {
|
|
OPENCLAW_FROZEN_CODEX_SUITE_ID: "${{ matrix.suite_id }}",
|
|
OPENCLAW_FROZEN_TARGET_ROOT: "${{ github.workspace }}",
|
|
OPENCLAW_SELECTED_SHA: "${{ needs.validate_selected_ref.outputs.selected_sha }}",
|
|
OPENCLAW_WORKFLOW_SHA: "${{ needs.validate_selected_ref.outputs.workflow_sha }}",
|
|
},
|
|
run: "node .release-harness/scripts/resolve-frozen-codex-live-suite.mjs",
|
|
});
|
|
for (const stepName of [
|
|
"Validate live-test image artifact binding",
|
|
"Download live-test image artifact",
|
|
"Verify and load live-test image artifact",
|
|
"Setup Node environment",
|
|
"Hydrate live auth/profile inputs",
|
|
"Log in to GHCR",
|
|
"Configure suite-specific env",
|
|
]) {
|
|
expect(workflowStep(providerSuites, stepName).if, stepName).toContain(
|
|
"steps.codex_compat.outputs.run_lane != 'false'",
|
|
);
|
|
}
|
|
const runCodexSuite = providerSuites.steps?.find((candidate) =>
|
|
candidate.name?.startsWith("Run ${{ matrix.label }}"),
|
|
);
|
|
expect(runCodexSuite?.if).toContain("steps.codex_compat.outputs.run_lane != 'false'");
|
|
for (const [model, thinking] of [
|
|
["sol", "ultra"],
|
|
["terra", "ultra"],
|
|
["luna", "max"],
|
|
]) {
|
|
expect(commands).toContain(
|
|
`OPENCLAW_LIVE_CODEX_HARNESS_TARGETS=openai/gpt-5.6-${model}=${thinking}`,
|
|
);
|
|
}
|
|
expect(workflow.match(/live-codex-harness\*-docker\)/gu)).toHaveLength(2);
|
|
for (const suiteId of [
|
|
"native-live-src-gateway-profiles-openai-api-default",
|
|
"native-live-src-gateway-profiles-openai-gpt56-ultra",
|
|
]) {
|
|
expect(workflow).toContain(`add_profile_suite ${suiteId} "stable full"`);
|
|
}
|
|
expect(codexLiveTest).toContain("command: `/model ${modelKey} --runtime codex`");
|
|
expect(codexLiveTest).toContain("thinkingLevel: CODEX_HARNESS_THINKING");
|
|
expect(commands).toContain(
|
|
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$GITHUB_WORKSPACE" timeout --foreground --kill-after=30s 20m bash .release-harness/scripts/test-live-subagent-announce-docker.sh',
|
|
);
|
|
expect(scenarios).toContain("function liveDockerScriptCommand");
|
|
expect(scenarios).toContain("const LIVE_DOCKER_DEFAULT_HARNESS_DIR");
|
|
expect(scenarios).toContain("fileURLToPath(import.meta.url)");
|
|
expect(scenarios).toContain('? ".release-harness"');
|
|
expect(scenarios).toContain("process.env.OPENCLAW_DOCKER_E2E_REPO_ROOT");
|
|
expect(scenarios).toContain(
|
|
'harness="\\${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-${LIVE_DOCKER_DEFAULT_HARNESS_DIR}}"',
|
|
);
|
|
expect(scenarios).not.toContain("harness=.release-harness");
|
|
expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-models-docker\.sh"/u);
|
|
expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-gateway-models-docker\.sh"/u);
|
|
expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-cli-backend-docker\.sh"/u);
|
|
expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-acp-bind-docker\.sh"/u);
|
|
expect(scenarios).toMatch(/liveDockerScriptCommand\(\s*"test-live-codex-harness-docker\.sh"/u);
|
|
expect(scenarios).toMatch(
|
|
/liveDockerScriptCommand\(\s*"e2e\/codex-npm-plugin-live-docker\.sh"/u,
|
|
);
|
|
expect(scenarios).toMatch(
|
|
/liveDockerScriptCommand\(\s*"test-live-subagent-announce-docker\.sh"/u,
|
|
);
|
|
expect(liveDockerAuth).toContain("codex-cli | openai)");
|
|
expect(liveDockerAuth).toContain("openclaw_live_init_docker_run_args()");
|
|
expect(liveDockerAuth).toContain("openclaw_live_stage_profile_into_home()");
|
|
expect(liveDockerAuth).toContain("openclaw_live_chown_bind_dirs_for_container_user()");
|
|
expect(liveDockerAuth).toContain("openclaw_live_uses_managed_bind_dirs()");
|
|
expect(liveDockerAuth).toContain('openclaw_live_truthy "${OPENCLAW_TESTBOX:-}"');
|
|
expect(liveDockerAuth).toContain('[[ -n "${OPENCLAW_DOCKER_CACHE_HOME_DIR:-}" ]]');
|
|
expect(liveDockerAuth).toContain(
|
|
'timeout_value="${2:-${OPENCLAW_LIVE_DOCKER_RUN_TIMEOUT:-2700s}}"',
|
|
);
|
|
expect(harness).toContain('source "$TRUSTED_HARNESS_DIR/scripts/lib/live-docker-auth.sh"');
|
|
expect(harness).not.toContain('source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"');
|
|
expect(harness).toContain(
|
|
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR" "$TRUSTED_HARNESS_DIR/scripts/test-live-build-docker.sh"',
|
|
);
|
|
expect(harness).toContain(
|
|
'-e OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR="${DOCKER_TRUSTED_HARNESS_CONTAINER_DIR}/scripts"',
|
|
);
|
|
expect(harness).toContain('node --import tsx "$trusted_scripts_dir/prepare-codex-ci-auth.ts"');
|
|
expect(harness).toContain('source "$trusted_scripts_dir/lib/live-docker-stage.sh"');
|
|
for (const script of [harness, ...sharedLiveScripts]) {
|
|
expect(script).toContain('source "$TRUSTED_HARNESS_DIR/scripts/lib/live-docker-auth.sh"');
|
|
expect(script).not.toContain('source "$ROOT_DIR/scripts/lib/live-docker-auth.sh"');
|
|
expect(script).toContain("openclaw_live_init_docker_run_args DOCKER_RUN_ARGS");
|
|
expect(script).toContain("DOCKER_RUN_ARGS+=(--rm -t \\");
|
|
expect(script).not.toContain("DOCKER_RUN_ARGS=(docker run --rm -t \\");
|
|
}
|
|
expect(liveDockerAuth).toContain("openclaw_live_prepare_bind_dir_for_container_user");
|
|
for (const script of sharedLiveScripts) {
|
|
expect(script).toContain("openclaw_live_uses_managed_bind_dirs");
|
|
expect(script).toContain(
|
|
'OPENCLAW_LIVE_DOCKER_REPO_ROOT="$ROOT_DIR" "$TRUSTED_HARNESS_DIR/scripts/test-live-build-docker.sh"',
|
|
);
|
|
expect(script).toContain('source "$trusted_scripts_dir/lib/live-docker-stage.sh"');
|
|
expect(script).toContain(
|
|
'-e OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR="${DOCKER_TRUSTED_HARNESS_CONTAINER_DIR}/scripts"',
|
|
);
|
|
expect(script).toContain(
|
|
"openclaw_live_append_array DOCKER_RUN_ARGS DOCKER_TRUSTED_HARNESS_MOUNT",
|
|
);
|
|
}
|
|
for (const [file, helper] of [
|
|
["scripts/test-live-cli-backend-docker.sh", "openclaw_live_prepare_cli_backend"],
|
|
["scripts/test-live-acp-bind-docker.sh", "openclaw_live_run_setup_command"],
|
|
["scripts/test-live-codex-harness-docker.sh", "openclaw_live_run_setup_command"],
|
|
] as const) {
|
|
const script = readFileSync(file, "utf8");
|
|
expect(script).toContain(helper);
|
|
expect(script).not.toContain('timeout --kill-after=30s "${OPENCLAW_LIVE_');
|
|
}
|
|
expect(stage).toContain("elif command -v gtimeout >/dev/null 2>&1; then");
|
|
expect(stage).toContain('if "$timeout_bin" --kill-after=1s 1s true');
|
|
expect(stage).toContain('"$timeout_bin" --kill-after=30s "${timeout_seconds}s" "$@"');
|
|
expect(stage).toContain(
|
|
'echo "timeout command not found; cannot bound ${label} after ${timeout_seconds}s"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-models-docker.sh", "utf8")).toContain(
|
|
"OPENCLAW_LIVE_MODELS_DOCKER_RUN_TIMEOUT:-2100s",
|
|
);
|
|
expect(readFileSync("scripts/test-live-gateway-models-docker.sh", "utf8")).toContain(
|
|
"OPENCLAW_LIVE_GATEWAY_DOCKER_RUN_TIMEOUT:-2100s",
|
|
);
|
|
expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain(
|
|
"OPENCLAW_LIVE_CLI_BACKEND_DOCKER_RUN_TIMEOUT:-2700s",
|
|
);
|
|
expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain(
|
|
'CLI_SETUP_TIMEOUT_SECONDS="$(openclaw_live_read_positive_int_env OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS 180)"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-cli-backend-docker.sh", "utf8")).toContain(
|
|
'"$docker_package" "$OPENCLAW_LIVE_CLI_BACKEND_SETUP_TIMEOUT_SECONDS"',
|
|
);
|
|
expect(stage).toContain('"live CLI backend setup"');
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
"OPENCLAW_LIVE_ACP_BIND_DOCKER_RUN_TIMEOUT:-2700s",
|
|
);
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
'ACP_SETUP_TIMEOUT_SECONDS="$(openclaw_live_read_positive_int_env OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS 180)"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
'"${OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS:?missing live ACP bind setup timeout seconds}"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
'-e OPENCLAW_LIVE_ACP_BIND_SETUP_TIMEOUT_SECONDS="$ACP_SETUP_TIMEOUT_SECONDS"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
'-e OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON="${OPENCLAW_LIVE_ACP_BIND_REQUIRE_CRON:-}"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
'"live ACP bind setup"',
|
|
);
|
|
const acpBindScript = readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8");
|
|
expect(acpBindScript).toContain(
|
|
"OPENCLAW_LIVE_ACP_BIND_CLAUDE_AUTH must be one of: auto, api-key, subscription.",
|
|
);
|
|
expect(acpBindScript).toContain(
|
|
'if [[ "$ACP_AGENT" == "claude" && "$CLAUDE_AUTH_MODE" == "subscription" ]]; then',
|
|
);
|
|
expect(acpBindScript).toContain(
|
|
"unset ANTHROPIC_API_KEY ANTHROPIC_API_KEY_OLD ANTHROPIC_API_TOKEN",
|
|
);
|
|
expect(acpBindScript).toContain('-e CLAUDE_CODE_OAUTH_TOKEN="${CLAUDE_CODE_OAUTH_TOKEN:-}"');
|
|
expect(acpBindScript).not.toContain(" -e ANTHROPIC_API_KEY \\\n");
|
|
expect(workflow.match(/OPENCLAW_LIVE_ACP_BIND_CLAUDE_AUTH=subscription/g)).toHaveLength(2);
|
|
expect(workflow.match(/OPENCLAW_LIVE_ACP_BIND_CLAUDE_AUTH=api-key/g)).toHaveLength(2);
|
|
expect(readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8")).toContain(
|
|
"run_setup_command bash -lc 'curl -fsSL https://app.factory.ai/cli | sh'",
|
|
);
|
|
expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain(
|
|
"OPENCLAW_LIVE_CODEX_HARNESS_DOCKER_RUN_TIMEOUT:-$((2100 * CODEX_HARNESS_TARGET_COUNT))s",
|
|
);
|
|
expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain(
|
|
'CODEX_HARNESS_SETUP_TIMEOUT_SECONDS="$(openclaw_live_read_positive_int_env OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS 180)"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain(
|
|
'"${OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS:?missing live Codex harness setup timeout seconds}"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain(
|
|
'-e OPENCLAW_LIVE_CODEX_HARNESS_SETUP_TIMEOUT_SECONDS="$CODEX_HARNESS_SETUP_TIMEOUT_SECONDS"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain(
|
|
'"live Codex harness setup"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-codex-harness-docker.sh", "utf8")).toContain(
|
|
'run_setup_command npm install -g "$OPENCLAW_LIVE_CODEX_CLI_PACKAGE_SPEC"',
|
|
);
|
|
expect(readFileSync("scripts/test-live-subagent-announce-docker.sh", "utf8")).toContain(
|
|
"OPENCLAW_LIVE_SUBAGENT_DOCKER_RUN_TIMEOUT:-1200s",
|
|
);
|
|
expect(build).toContain('ROOT_DIR="${OPENCLAW_LIVE_DOCKER_REPO_ROOT:-$SCRIPT_ROOT_DIR}"');
|
|
expect(build).toContain('source "$SCRIPT_ROOT_DIR/scripts/lib/docker-build.sh"');
|
|
expect(build).toContain('source "$SCRIPT_ROOT_DIR/scripts/lib/docker-e2e-container.sh"');
|
|
expect(build).toContain(
|
|
'DOCKER_COMMAND_TIMEOUT="${DOCKER_COMMAND_TIMEOUT:-${OPENCLAW_LIVE_DOCKER_PULL_TIMEOUT:-600s}}"',
|
|
);
|
|
expect(build).toContain('LIVE_IMAGE_PULL_ATTEMPTS="${OPENCLAW_LIVE_DOCKER_PULL_ATTEMPTS:-3}"');
|
|
expect(build).toContain('docker_e2e_docker_cmd pull "$LIVE_IMAGE_NAME"');
|
|
expect(build).not.toContain('docker pull "$LIVE_IMAGE_NAME"');
|
|
expect(stage).toContain(
|
|
'local scripts_dir="${OPENCLAW_LIVE_DOCKER_SCRIPTS_DIR:-/src/scripts}"',
|
|
);
|
|
expect(stage).toContain('node --import tsx "$scripts_dir/live-docker-normalize-config.ts"');
|
|
});
|
|
|
|
it("fails Droid ACP Docker live proof when Factory auth is missing", () => {
|
|
const script = readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8");
|
|
|
|
expect(script).toContain("openclaw_live_acp_bind_load_factory_api_key_from_profile");
|
|
expect(script).not.toContain('source "$PROFILE_FILE"');
|
|
expect(script.indexOf("openclaw_live_acp_bind_load_factory_api_key_from_profile")).toBeLessThan(
|
|
script.indexOf('if [[ "$ACP_AGENT" == "droid" && -z "${FACTORY_API_KEY:-}" ]]; then'),
|
|
);
|
|
expect(script).toContain(
|
|
"ERROR: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container.",
|
|
);
|
|
expect(script).not.toContain(
|
|
"SKIP: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container.",
|
|
);
|
|
expect(script).not.toMatch(
|
|
/Droid Docker ACP bind requires FACTORY_API_KEY[\s\S]{0,160}(exit 0|continue)/u,
|
|
);
|
|
});
|
|
|
|
it("plumbs live credentials through planned Docker E2E live lanes", () => {
|
|
const reusableWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
|
const scheduledWorkflow = readFileSync(SCHEDULED_LIVE_CHECKS_WORKFLOW, "utf8");
|
|
const packageAcceptanceWorkflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
|
|
const testboxWorkflow = readFileSync(CI_CHECK_TESTBOX_WORKFLOW, "utf8");
|
|
const hydrateScript = readFileSync(CI_HYDRATE_LIVE_AUTH_SCRIPT, "utf8");
|
|
const providerVerifier = readFileSync(VERIFY_PROVIDER_SECRETS_SCRIPT, "utf8");
|
|
const testboxProviderSecretKeys = [
|
|
"OPENAI_API_KEY",
|
|
"OPENAI_BASE_URL",
|
|
"ANTHROPIC_API_KEY",
|
|
"ANTHROPIC_API_KEY_OLD",
|
|
"ANTHROPIC_API_TOKEN",
|
|
"FACTORY_API_KEY",
|
|
"BYTEPLUS_API_KEY",
|
|
"CEREBRAS_API_KEY",
|
|
"DEEPINFRA_API_KEY",
|
|
"DEEPSEEK_API_KEY",
|
|
"DASHSCOPE_API_KEY",
|
|
"GROQ_API_KEY",
|
|
"KIMI_API_KEY",
|
|
"MODELSTUDIO_API_KEY",
|
|
"MOONSHOT_API_KEY",
|
|
"MISTRAL_API_KEY",
|
|
"MINIMAX_API_KEY",
|
|
"OPENCODE_API_KEY",
|
|
"OPENCODE_ZEN_API_KEY",
|
|
"OPENCLAW_LIVE_BROWSER_CDP_URL",
|
|
"OPENCLAW_LIVE_SETUP_TOKEN",
|
|
"OPENCLAW_LIVE_SETUP_TOKEN_MODEL",
|
|
"OPENCLAW_LIVE_SETUP_TOKEN_PROFILE",
|
|
"OPENCLAW_LIVE_SETUP_TOKEN_VALUE",
|
|
"GEMINI_API_KEY",
|
|
"GOOGLE_API_KEY",
|
|
"OPENROUTER_API_KEY",
|
|
"QWEN_API_KEY",
|
|
"FAL_KEY",
|
|
"RUNWAY_API_KEY",
|
|
"DEEPGRAM_API_KEY",
|
|
"TOGETHER_API_KEY",
|
|
"VYDRA_API_KEY",
|
|
"XAI_API_KEY",
|
|
"ZAI_API_KEY",
|
|
"Z_AI_API_KEY",
|
|
"BYTEPLUS_ACCESS_KEY_ID",
|
|
"BYTEPLUS_SECRET_ACCESS_KEY",
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
"OPENCLAW_CODEX_AUTH_JSON",
|
|
"OPENCLAW_CODEX_CONFIG_TOML",
|
|
"OPENCLAW_CLAUDE_JSON",
|
|
"OPENCLAW_CLAUDE_CREDENTIALS_JSON",
|
|
"OPENCLAW_CLAUDE_SETTINGS_JSON",
|
|
"OPENCLAW_CLAUDE_SETTINGS_LOCAL_JSON",
|
|
"OPENCLAW_GEMINI_SETTINGS_JSON",
|
|
"FIREWORKS_API_KEY",
|
|
];
|
|
const githubBackedTestboxProviderSteps = [
|
|
workflowStep(
|
|
workflowJob(CI_CHECK_TESTBOX_WORKFLOW, "check"),
|
|
"Hydrate Testbox provider env helper",
|
|
),
|
|
workflowStep(
|
|
workflowJob(CI_CHECK_ARM_TESTBOX_WORKFLOW, "check-arm"),
|
|
"Hydrate Testbox provider env helper",
|
|
),
|
|
workflowStep(
|
|
workflowJob(CI_BUILD_ARTIFACTS_TESTBOX_WORKFLOW, "build-artifacts"),
|
|
"Hydrate Testbox provider env helper",
|
|
),
|
|
workflowStep(
|
|
workflowJob(CRABBOX_HYDRATE_WORKFLOW, "hydrate-github"),
|
|
"Hydrate provider env helper",
|
|
),
|
|
];
|
|
|
|
expect(hydrateScript).toContain(" FACTORY_API_KEY \\");
|
|
expect(providerVerifier).toContain('url: "https://api.anthropic.com/v1/messages"');
|
|
expect(providerVerifier).toContain('model: "claude-haiku-4-5"');
|
|
expect(providerVerifier).toContain("validateResponse:");
|
|
expect(providerVerifier).not.toContain("ANTHROPIC_OAUTH_TOKEN");
|
|
for (const workflow of [
|
|
reusableWorkflow,
|
|
releaseChecksWorkflow,
|
|
scheduledWorkflow,
|
|
packageAcceptanceWorkflow,
|
|
testboxWorkflow,
|
|
]) {
|
|
expect(workflow).toContain("FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}");
|
|
}
|
|
for (const step of githubBackedTestboxProviderSteps) {
|
|
for (const key of testboxProviderSecretKeys) {
|
|
expect(step.env?.[key]).toBe("${{ secrets." + key + " }}");
|
|
}
|
|
}
|
|
for (const workflowPath of [LIVE_E2E_WORKFLOW, PACKAGE_ACCEPTANCE_WORKFLOW]) {
|
|
expect(readWorkflow(workflowPath).on?.workflow_call).toMatchObject({
|
|
secrets: { DEEPSEEK_API_KEY: { required: false } },
|
|
});
|
|
}
|
|
for (const [jobName, job] of Object.entries(readWorkflow(LIVE_E2E_WORKFLOW).jobs ?? {})) {
|
|
if (job.steps?.some((step) => step.run === `bash ${CI_HYDRATE_LIVE_AUTH_SCRIPT}`)) {
|
|
expect(job.env?.DEEPSEEK_API_KEY, jobName).toBe("${{ secrets.DEEPSEEK_API_KEY }}");
|
|
}
|
|
}
|
|
for (const workflowPath of [RELEASE_CHECKS_WORKFLOW, PACKAGE_ACCEPTANCE_WORKFLOW]) {
|
|
for (const [jobName, job] of Object.entries(readWorkflow(workflowPath).jobs ?? {})) {
|
|
if (
|
|
job.uses === `./${LIVE_E2E_WORKFLOW}` ||
|
|
job.uses === `./${PACKAGE_ACCEPTANCE_WORKFLOW}`
|
|
) {
|
|
expect(job.secrets, `${workflowPath}:${jobName}`).toMatchObject({
|
|
DEEPSEEK_API_KEY: "${{ secrets.DEEPSEEK_API_KEY }}",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
expect(
|
|
workflowJob(SCHEDULED_LIVE_CHECKS_WORKFLOW, "live_and_openwebui_checks").secrets,
|
|
).toMatchObject({
|
|
DEEPSEEK_API_KEY: "${{ secrets.DEEPSEEK_API_KEY }}",
|
|
});
|
|
expect(
|
|
workflowJob(SCHEDULED_LIVE_CHECKS_WORKFLOW, "weekly_upgrade_survivors").secrets,
|
|
).toBeUndefined();
|
|
const hydrationHome = tempDirs.make("live-auth-hydration-");
|
|
const hydrated = spawnSync(
|
|
"bash",
|
|
[
|
|
"-euc",
|
|
`bash "$1" "$2"
|
|
unset DEEPSEEK_API_KEY DEEPINFRA_API_KEY
|
|
source "$2"
|
|
printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|
"hydrate-live-auth",
|
|
CI_HYDRATE_LIVE_AUTH_SCRIPT,
|
|
resolve(hydrationHome, "live.profile"),
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
timeout: 10_000,
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HOME: hydrationHome,
|
|
DEEPSEEK_API_KEY: "deepseek-sentinel",
|
|
DEEPINFRA_API_KEY: "deepinfra-sentinel",
|
|
},
|
|
},
|
|
);
|
|
expect(hydrated.status, hydrated.stderr).toBe(0);
|
|
expect(hydrated.stdout).toBe("deepseek-sentinel\ndeepinfra-sentinel\n");
|
|
expect(reusableWorkflow).toContain("FACTORY_API_KEY:\n required: false");
|
|
expect(packageAcceptanceWorkflow).toContain("FACTORY_API_KEY:\n required: false");
|
|
expectTextToIncludeAll(reusableWorkflow, [
|
|
'if [[ "$credentials" == *",openai,"* ]]; then',
|
|
"require_any OpenAI OPENAI_API_KEY",
|
|
'if [[ "$credentials" == *",codex,"* ]]; then',
|
|
"require_any Codex OPENCLAW_CODEX_AUTH_JSON",
|
|
'if [[ "$credentials" == *",gemini,"* ]]; then',
|
|
"require_any Gemini GEMINI_API_KEY GOOGLE_API_KEY OPENCLAW_GEMINI_SETTINGS_JSON",
|
|
'if [[ "$credentials" == *",opencode,"* ]]; then',
|
|
"require_any OpenCode OPENCODE_API_KEY OPENCODE_ZEN_API_KEY",
|
|
]);
|
|
expect(reusableWorkflow.match(/OPENCLAW_LIVE_CLI_BACKEND_AUTH=subscription/g)).toHaveLength(2);
|
|
expect(
|
|
reusableWorkflow.match(
|
|
/if \[\[ -n "\$\{OPENCLAW_CLAUDE_CREDENTIALS_JSON:-\}" \|\| -n "\$\{CLAUDE_CODE_OAUTH_TOKEN:-\}" \]\]; then/g,
|
|
),
|
|
).toHaveLength(4);
|
|
});
|
|
|
|
it("finalizes dispatched Testbox delegation even when setup or the remote command fails", () => {
|
|
const workflow = readFileSync(CI_CHECK_TESTBOX_WORKFLOW, "utf8");
|
|
const checkTestboxJob = workflowJob(CI_CHECK_TESTBOX_WORKFLOW, "check");
|
|
const setupNodeStep = workflowStep(checkTestboxJob, "Setup Node environment");
|
|
const runTestboxStep = workflowStep(checkTestboxJob, "Run Testbox");
|
|
const closeTestboxSshStep = workflowStep(checkTestboxJob, "Close Testbox SSH sessions");
|
|
const setupNodeWith = setupNodeStep.with ?? {};
|
|
const checkTestboxSteps = checkTestboxJob.steps ?? [];
|
|
const runArmTestboxStep = workflowStep(
|
|
workflowJob(CI_CHECK_ARM_TESTBOX_WORKFLOW, "check-arm"),
|
|
"Run Testbox",
|
|
);
|
|
const runBuildArtifactsTestboxStep = workflowStep(
|
|
workflowJob(CI_BUILD_ARTIFACTS_TESTBOX_WORKFLOW, "build-artifacts"),
|
|
"Run Testbox",
|
|
);
|
|
const windowsTestboxJob = workflowJob(WINDOWS_BLACKSMITH_TESTBOX_WORKFLOW, "windows");
|
|
const runWindowsTestboxStep = workflowStep(windowsTestboxJob, "Run Testbox");
|
|
const windowsTestboxActionMarker = workflowStep(windowsTestboxJob, "Testbox action marker");
|
|
|
|
expect(workflow).not.toContain('PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"');
|
|
expect(workflow).not.toContain("PNPM_CONFIG_MODULES_DIR");
|
|
expect(workflow).not.toContain("PNPM_CONFIG_VIRTUAL_STORE_DIR");
|
|
expect(setupNodeWith).not.toHaveProperty("dependency-cache");
|
|
expect(setupNodeWith).not.toHaveProperty("sticky-disk");
|
|
expect(setupNodeWith["cache-mode"]).toBe("restore");
|
|
for (const [minutes, expected] of [
|
|
["45", 45],
|
|
["", 60],
|
|
] as const) {
|
|
expect(
|
|
evaluateWorkflowExpression(checkTestboxJob["timeout-minutes"], {
|
|
eventName: "workflow_dispatch",
|
|
repository: "openclaw/openclaw",
|
|
runAttempt: 1,
|
|
additionalNeeds: { admission: { outputs: { minutes }, result: "success" } },
|
|
}),
|
|
).toBe(expected);
|
|
}
|
|
for (const step of [runTestboxStep, runArmTestboxStep, runBuildArtifactsTestboxStep]) {
|
|
expect(step.uses).toBe(RUN_TESTBOX_WITH_FAILURE_REPORTING);
|
|
}
|
|
expect(windowsTestboxActionMarker.uses).toBe(
|
|
"useblacksmith/run-testbox@3f60ff9ceb2c10c3feefa87dc0c6490cffae059d",
|
|
);
|
|
expect(windowsTestboxActionMarker.if).toBe("${{ false }}");
|
|
for (const step of [
|
|
runTestboxStep,
|
|
runArmTestboxStep,
|
|
runBuildArtifactsTestboxStep,
|
|
closeTestboxSshStep,
|
|
]) {
|
|
for (const [eventName, expected] of [
|
|
["workflow_dispatch", true],
|
|
["pull_request", false],
|
|
] as const) {
|
|
expect(
|
|
evaluateWorkflowExpression(`\${{ ${step.if} }}`, {
|
|
eventName,
|
|
repository: "openclaw/openclaw",
|
|
runAttempt: 1,
|
|
failed: true,
|
|
cancelled: true,
|
|
}),
|
|
).toBe(expected);
|
|
}
|
|
}
|
|
expect(closeTestboxSshStep.run).toContain(
|
|
`sudo sshd -T 2>/dev/null | awk '$1 == "port" { print $2; exit }'`,
|
|
);
|
|
expect(closeTestboxSshStep.run).toContain(
|
|
'ss -K state established \\\n "( sport = :${runner_ssh_local_port} )"',
|
|
);
|
|
expect(checkTestboxSteps.indexOf(closeTestboxSshStep)).toBe(
|
|
checkTestboxSteps.indexOf(runTestboxStep) + 1,
|
|
);
|
|
expect(runWindowsTestboxStep.if).toBe("always()");
|
|
expect(runWindowsTestboxStep.env?.JOB_STATUS).toBe("${{ job.status }}");
|
|
expect(runWindowsTestboxStep.env?.NATIVE_SSH_USER).toBe(
|
|
"${{ steps.prepare_windows.outputs.ssh_user }}",
|
|
);
|
|
expect(runWindowsTestboxStep.run).toContain("${NATIVE_SSH_USER}@${runner_host}");
|
|
expect(runTestboxStep["continue-on-error"]).toBeUndefined();
|
|
});
|
|
|
|
it("allows the Telegram lane to run from reusable package acceptance artifacts", () => {
|
|
const workflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");
|
|
|
|
expect(workflow).toContain("workflow_call:");
|
|
expect(workflow).toContain("package_artifact_name:");
|
|
expect(workflow).toContain("Download package-under-test artifact");
|
|
expect(workflow).toContain("harness_ref:");
|
|
expect(workflow).toContain("ref: ${{ inputs.harness_ref || github.sha }}");
|
|
expect(workflow).toContain("OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ");
|
|
expect(workflow).toContain("provider_mode:");
|
|
expect(workflow).toContain("provider_mode must be mock-openai or live-frontier");
|
|
expect(workflow).toContain("run_package_telegram_e2e:");
|
|
});
|
|
|
|
it("forwards optional RTT scenario selection from manual and reusable Telegram inputs", () => {
|
|
const workflow = readWorkflow(NPM_TELEGRAM_WORKFLOW);
|
|
const expectedInput = {
|
|
default: "",
|
|
description: "Optional Telegram QA scenario id for repeated RTT sampling",
|
|
required: false,
|
|
type: "string",
|
|
};
|
|
|
|
expect(workflow.on?.workflow_dispatch?.inputs?.rtt_scenario).toEqual(expectedInput);
|
|
expect(workflow.on?.workflow_call?.inputs?.rtt_scenario).toEqual(expectedInput);
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"),
|
|
"Run package Telegram E2E",
|
|
).env?.OPENCLAW_NPM_TELEGRAM_RTT_CHECKS,
|
|
).toBe("${{ inputs.rtt_scenario }}");
|
|
});
|
|
|
|
it("requires explicit approval for historical package destructive actions", () => {
|
|
const workflow = readWorkflow(NPM_TELEGRAM_WORKFLOW);
|
|
const expectedInput = {
|
|
default: false,
|
|
description:
|
|
"Allow destructive actions for intentional historical downgrade or recovery proof",
|
|
required: false,
|
|
type: "boolean",
|
|
};
|
|
|
|
expect(workflow.on?.workflow_dispatch?.inputs?.allow_older_binary_destructive_actions).toEqual(
|
|
expectedInput,
|
|
);
|
|
expect(workflow.on?.workflow_call?.inputs?.allow_older_binary_destructive_actions).toEqual(
|
|
expectedInput,
|
|
);
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"),
|
|
"Run package Telegram E2E",
|
|
).env?.OPENCLAW_ALLOW_OLDER_BINARY_DESTRUCTIVE_ACTIONS,
|
|
).toBe("${{ inputs.allow_older_binary_destructive_actions && '1' || '' }}");
|
|
});
|
|
|
|
it.each(["stable", "full"])(
|
|
"rejects Package Acceptance Telegram deferral for direct %s release checks",
|
|
(releaseProfile) => {
|
|
const { result } = runReleaseChecksInputValidation(releaseProfile, "true");
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"skip_package_telegram_e2e is allowed only for release_profile=beta.",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(["stable", "full"])(
|
|
"rejects Package Acceptance Telegram deferral for umbrella %s validation",
|
|
(releaseProfile) => {
|
|
const result = runFullReleaseInputValidation(releaseProfile, "true");
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"skip_package_telegram_e2e is allowed only for release_profile=beta.",
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each(
|
|
["2026.8.1", "2026.9.1", "2026.9.5"].flatMap((version) => [
|
|
["stable", version],
|
|
["full", version],
|
|
]),
|
|
)("waives only reviewed integration lanes for approved %s %s", (profile, version) => {
|
|
const options = { telegramWaiver: `${version}-owner-approved`, version };
|
|
const direct = runReleaseChecksInputValidation(profile, "false", "all", "false", "", options);
|
|
const umbrella = runFullReleaseInputValidation(profile, "false", options);
|
|
expect(direct.result.status, direct.result.stderr).toBe(0);
|
|
expect(umbrella.status, umbrella.stderr).toBe(0);
|
|
const output = readFileSync(direct.outputPath, "utf8");
|
|
expect(output).toContain(`telegram_waiver=${version}-owner-approved`);
|
|
expect(output).toContain("qa_live_telegram_enabled=false");
|
|
expect(output).toContain(`qa_live_matrix_enabled=${version !== "2026.9.5"}`);
|
|
expect(output).toContain("qa_live_buzz_enabled=true");
|
|
expect(output).toContain("run_release_soak=true");
|
|
expect(output).toContain("skip_package_telegram_e2e=false");
|
|
});
|
|
|
|
it.each([
|
|
{ version: "2026.8.2" },
|
|
{ telegramWaiver: "2026.9.1-owner-approved" },
|
|
{ rerunGroup: "npm-telegram" },
|
|
{ liveSuiteFilter: "qa-telegram" },
|
|
])("rejects a conflicting Telegram waiver request: %j", (override) => {
|
|
const options = { telegramWaiver: "2026.8.1-owner-approved", ...override };
|
|
const umbrella = runFullReleaseInputValidation("stable", "false", options);
|
|
const direct = runReleaseChecksInputValidation(
|
|
"stable",
|
|
"false",
|
|
options.rerunGroup ?? "all",
|
|
"false",
|
|
options.liveSuiteFilter ?? "",
|
|
options,
|
|
);
|
|
expect(umbrella.status).not.toBe(0);
|
|
expect(direct.result.status).not.toBe(0);
|
|
expect(umbrella.stderr).toContain("Telegram waiver");
|
|
expect(direct.result.stderr).toContain("Telegram waiver");
|
|
});
|
|
|
|
it("allows Package Acceptance Telegram deferral only for beta validation", () => {
|
|
const direct = runReleaseChecksInputValidation("beta", "true");
|
|
const umbrella = runFullReleaseInputValidation("beta", "true");
|
|
|
|
expect(direct.result.status, direct.result.stderr).toBe(0);
|
|
expect(readFileSync(direct.outputPath, "utf8")).toContain("skip_package_telegram_e2e=true");
|
|
expect(umbrella.status, umbrella.stderr).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
{ label: "canonical beta", scope: "npm-beta" },
|
|
{
|
|
label: "explicit all-OS beta",
|
|
crossOsSuiteFilter: "ubuntu,windows,macos",
|
|
scope: "npm-beta",
|
|
},
|
|
{ label: "beta soak", runReleaseSoak: "true", scope: "full" },
|
|
{ label: "focused beta CI", rerunGroup: "ci", scope: "full" },
|
|
{ label: "stable profile", releaseProfile: "stable", scope: "full" },
|
|
{ label: "stable version", version: "2026.8.1", scope: "full" },
|
|
{ label: "main beta profile", targetRef: "main", scope: "full" },
|
|
{
|
|
label: "canonical stable with explicit suites",
|
|
crossOsSuiteFilter: "packaged-fresh,installer-fresh,packaged-upgrade",
|
|
releaseProfile: "stable",
|
|
version: "2026.8.1",
|
|
runReleaseSoak: "true",
|
|
scope: "npm-stable",
|
|
},
|
|
{
|
|
label: "stable correction",
|
|
releaseProfile: "stable",
|
|
targetRef: "v2026.8.1-1",
|
|
version: "2026.8.1",
|
|
runReleaseSoak: "true",
|
|
scope: "npm-stable",
|
|
},
|
|
{
|
|
label: "extended stable",
|
|
releaseProfile: "stable",
|
|
targetRef: "extended-stable/2026.8.33",
|
|
version: "2026.8.33",
|
|
runReleaseSoak: "true",
|
|
scope: "full",
|
|
},
|
|
{
|
|
label: "full stable",
|
|
releaseProfile: "full",
|
|
version: "2026.8.1",
|
|
runReleaseSoak: "true",
|
|
scope: "full",
|
|
},
|
|
{
|
|
label: "main stable",
|
|
releaseProfile: "stable",
|
|
targetRef: "main",
|
|
version: "2026.8.1",
|
|
runReleaseSoak: "true",
|
|
scope: "full",
|
|
},
|
|
])(
|
|
"resolves $label qualification scope before dispatch",
|
|
({ label: _label, scope, ...overrides }) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
targetRef: "release/2026.8.1",
|
|
version: "2026.8.1-beta.3",
|
|
...overrides,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.output).toContain(`ci_release_scope=${scope}\n`);
|
|
expect(result.output).toContain(`coverage_policy=${scope === "full" ? "" : `${scope}-v1`}\n`);
|
|
expect(result.output).toContain(
|
|
`skip_package_telegram_e2e=${overrides.runReleaseSoak === "true" || overrides.rerunGroup === "ci" || overrides.releaseProfile === "stable" ? "false" : "true"}\n`,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["beta", "2026.8.1-beta.3", "false"],
|
|
["stable", "2026.8.1", "true"],
|
|
])(
|
|
"rejects %s qualification when the cross-OS selection omits Linux coverage",
|
|
(releaseProfile, version, runReleaseSoak) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
targetRef: "release/2026.8.1",
|
|
releaseProfile,
|
|
version,
|
|
runReleaseSoak,
|
|
crossOsSuiteFilter: "windows,macos,ubuntu/packaged-fresh",
|
|
});
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("Linux");
|
|
expect(result.output).not.toContain("coverage_policy=");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
["release/2026.8.1", "2026.8.1"],
|
|
["release/2026.8.1", "2026.8.1-beta.3"],
|
|
["extended-stable/2026.7.33", "2026.7.33"],
|
|
["extended-stable/2026.7.33", "2026.7.35"],
|
|
["v2026.8.1", "2026.8.1"],
|
|
["v2026.8.1-beta.3", "2026.8.1-beta.3"],
|
|
])("accepts direct Full Release Validation identity %s at package %s", (targetRef, version) => {
|
|
const result = runFullReleaseTargetIdentityValidation({ targetRef, version });
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
["release/2026.8.1", "2026.8.2", "does not belong to release branch"],
|
|
["release/2026.8.1", "2026.8.1-alpha.2", "Alpha releases are retired;"],
|
|
["extended-stable/2026.7.33", "2026.7.32", "PATCH >= 33"],
|
|
["extended-stable/2026.7.33", "2026.8.35", "does not belong to extended-stable branch"],
|
|
["extended-stable/2026.7.33", "2026.7.35-beta.1", "does not belong to extended-stable branch"],
|
|
["v2026.8.1", "2026.8.1-beta.1", "does not match release tag"],
|
|
["v2026.8.1-alpha.2", "2026.8.1-alpha.3", "Alpha releases are retired;"],
|
|
])(
|
|
"rejects direct Full Release Validation identity %s at package %s",
|
|
(targetRef, version, error) => {
|
|
const result = runFullReleaseTargetIdentityValidation({ targetRef, version });
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(error);
|
|
},
|
|
);
|
|
|
|
it.each(["v2026.8.1-alpha.2", "a".repeat(40)])(
|
|
"rejects an alpha Full Release Validation candidate at %s",
|
|
(targetRef) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
targetRef,
|
|
version: "2026.8.1-alpha.2",
|
|
});
|
|
expect(result.status, result.stderr).toBe(1);
|
|
expect(result.stderr).toContain("Alpha releases are retired;");
|
|
},
|
|
);
|
|
|
|
it("validates an exact-SHA helper target against its canonical release context", () => {
|
|
const accepted = runFullReleaseTargetIdentityValidation({
|
|
targetContextRef: "release/2026.8.1",
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1-beta.3",
|
|
});
|
|
const rejected = runFullReleaseTargetIdentityValidation({
|
|
targetContextRef: "release/2026.8.1",
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1-alpha.3",
|
|
});
|
|
|
|
expect(accepted.status, accepted.stderr).toBe(0);
|
|
expect(rejected.status).toBe(1);
|
|
expect(rejected.stderr).toContain("Alpha releases are retired;");
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "branch head",
|
|
targetContextRef: "refs/heads/release/2026.8.1",
|
|
comparisonStatus: "identical",
|
|
},
|
|
{
|
|
label: "branch ancestor",
|
|
targetContextRef: "release/2026.8.1",
|
|
remoteSha: "b".repeat(40),
|
|
comparisonStatus: "ahead",
|
|
},
|
|
{ label: "release tag commit", targetContextRef: "refs/tags/v2026.8.1" },
|
|
{ label: "correction base commit", targetContextRef: "release/2026.8.1-1" },
|
|
])("validates $label through authenticated refs when anonymous Git is unavailable", (entry) => {
|
|
const { label: _label, ...identity } = entry;
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
...identity,
|
|
anonymousGitUnavailable: true,
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.output).toContain("ci_release_scope=full\n");
|
|
});
|
|
|
|
it.each([
|
|
["context", "release/2026.8.1"],
|
|
["comparison", "release/2026.8.1"],
|
|
["base", "release/2026.8.1-1"],
|
|
] as const)("fails closed on a %s API error", (apiError, targetContextRef) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
anonymousGitUnavailable: true,
|
|
apiError,
|
|
targetContextRef,
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1",
|
|
});
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("HTTP 503");
|
|
expect(result.output).not.toContain("ci_release_scope=");
|
|
});
|
|
|
|
it.each(["refs/tags/release/2026.8.1", "refs/heads/v2026.8.1", "release-ci/2026.8.1"])(
|
|
"rejects the wrong release context namespace %s before ref lookup",
|
|
(targetContextRef) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
anonymousGitUnavailable: true,
|
|
targetContextRef,
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1",
|
|
});
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("must be a canonical OpenClaw release branch or tag");
|
|
expect(result.output).not.toContain("ci_release_scope=");
|
|
},
|
|
);
|
|
|
|
it("validates an exact-SHA extended-stable successor against its canonical branch", () => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
targetContextRef: "extended-stable/2026.6.33",
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.6.35",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
["release/2026.8.1-1", "2026.8.1"],
|
|
["refs/heads/release/2026.8.1-1", "2026.8.1-1"],
|
|
["v2026.8.1-1", "2026.8.1"],
|
|
])("preserves the correction publication tag for %s with package %s", (context, version) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
targetContextRef: context,
|
|
targetRef: "a".repeat(40),
|
|
version,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.output).toContain("release_tag=v2026.8.1-1\n");
|
|
expect(result.output).toContain(`target_version=${version}\n`);
|
|
});
|
|
|
|
it.each(["", "b".repeat(40)])(
|
|
"rejects a correction with unproven base source %s",
|
|
(baseTagSha) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
baseTagSha,
|
|
targetContextRef: "release/2026.8.1-1",
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1",
|
|
});
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain("must match base release tag");
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
[
|
|
"refs/heads/extended-stable/2026.6.33",
|
|
"2026.6.35",
|
|
"extended-stable",
|
|
"extended-stable/2026.6.33",
|
|
],
|
|
["refs/tags/v2026.6.35", "2026.6.35", "extended-stable", "extended-stable/2026.6.33"],
|
|
["refs/heads/release/2026.8.1", "2026.8.1-beta.3", "beta", ""],
|
|
["v2026.8.1", "2026.8.1", "beta", ""],
|
|
])("records the npm publication channel for %s", (context, version, distTag, branch) => {
|
|
const result = runFullReleaseTargetIdentityValidation({
|
|
targetContextRef: context,
|
|
targetRef: "a".repeat(40),
|
|
version,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.output).toContain(`npm_dist_tag=${distTag}\n`);
|
|
expect(result.output).toContain(`release_candidate_branch=${branch}\n`);
|
|
});
|
|
|
|
it("rejects exact-SHA release contexts outside the named branch or tag", () => {
|
|
const divergedBranch = runFullReleaseTargetIdentityValidation({
|
|
comparisonStatus: "diverged",
|
|
remoteSha: "b".repeat(40),
|
|
targetContextRef: "release/2026.8.1",
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1-beta.3",
|
|
});
|
|
const mismatchedTag = runFullReleaseTargetIdentityValidation({
|
|
remoteSha: "b".repeat(40),
|
|
targetContextRef: "v2026.8.1-beta.2",
|
|
targetRef: "a".repeat(40),
|
|
version: "2026.8.1-beta.2",
|
|
});
|
|
|
|
expect(divergedBranch.status).toBe(1);
|
|
expect(divergedBranch.stderr).toContain("is not reachable from release context branch");
|
|
expect(mismatchedTag.status).toBe(1);
|
|
expect(mismatchedTag.stderr).toContain("does not match release tag");
|
|
});
|
|
|
|
it.each(["stable", "full"])(
|
|
"preserves normal %s validation when Telegram deferral is false",
|
|
(releaseProfile) => {
|
|
const direct = runReleaseChecksInputValidation(releaseProfile, "false");
|
|
const umbrella = runFullReleaseInputValidation(releaseProfile, "false");
|
|
|
|
expect(direct.result.status, direct.result.stderr).toBe(0);
|
|
expect(readFileSync(direct.outputPath, "utf8")).toContain("skip_package_telegram_e2e=false");
|
|
expect(umbrella.status, umbrella.stderr).toBe(0);
|
|
},
|
|
);
|
|
|
|
it("declares isolated release-check phases in dispatch and concurrency", () => {
|
|
const workflow = readWorkflow(RELEASE_CHECKS_WORKFLOW);
|
|
|
|
expect(workflow.on?.workflow_dispatch?.inputs?.phase).toEqual({
|
|
default: "all",
|
|
description: "Release check phase to run",
|
|
options: ["all", "independent", "candidate"],
|
|
required: false,
|
|
type: "choice",
|
|
});
|
|
expect(workflow.concurrency).toEqual({
|
|
group:
|
|
"openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}-${{ inputs.phase }}-${{ inputs.release_profile == 'minimum' && 'beta' || inputs.release_profile }}-${{ inputs.run_release_soak || inputs.release_profile == 'stable' || inputs.release_profile == 'full' }}",
|
|
"cancel-in-progress": false,
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"all",
|
|
{
|
|
cross_os_scheduled: "true",
|
|
docker_required: "true",
|
|
install_smoke_scheduled: "true",
|
|
live_e2e_scheduled: "true",
|
|
package_acceptance_scheduled: "true",
|
|
package_required: "true",
|
|
qa_live_scheduled: "true",
|
|
qa_parity_scheduled: "true",
|
|
run_maturity_scorecard: "true",
|
|
},
|
|
],
|
|
[
|
|
"independent",
|
|
{
|
|
cross_os_scheduled: "false",
|
|
docker_required: "false",
|
|
install_smoke_scheduled: "true",
|
|
live_e2e_scheduled: "true",
|
|
package_acceptance_scheduled: "false",
|
|
package_required: "false",
|
|
qa_live_scheduled: "true",
|
|
qa_parity_scheduled: "true",
|
|
run_maturity_scorecard: "true",
|
|
},
|
|
],
|
|
[
|
|
"candidate",
|
|
{
|
|
cross_os_scheduled: "true",
|
|
docker_required: "true",
|
|
install_smoke_scheduled: "false",
|
|
live_e2e_scheduled: "false",
|
|
package_acceptance_scheduled: "true",
|
|
package_required: "true",
|
|
qa_live_scheduled: "false",
|
|
qa_parity_scheduled: "false",
|
|
run_maturity_scorecard: "false",
|
|
},
|
|
],
|
|
] as const)("routes only the %s release-check phase", (phase, expected) => {
|
|
const { outputPath, result } = runReleaseChecksInputValidation(
|
|
"stable",
|
|
"false",
|
|
"all",
|
|
"false",
|
|
"",
|
|
{
|
|
candidateArtifactJson: releaseCandidateArtifactJson(),
|
|
phase,
|
|
runMaturityScorecard: "true",
|
|
},
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(outputPath, "utf8");
|
|
expect(output).toContain(`phase=${phase}\n`);
|
|
for (const [lane, scheduled] of Object.entries(expected)) {
|
|
expect(output).toContain(`${lane}=${scheduled}\n`);
|
|
}
|
|
});
|
|
|
|
it("fails closed when a candidate phase would rebuild an FRV artifact", () => {
|
|
const missing = runReleaseChecksInputValidation("beta", "false", "package", "false", "", {
|
|
phase: "candidate",
|
|
});
|
|
const publishedAcceptance = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"package",
|
|
"false",
|
|
"",
|
|
{
|
|
packageAcceptancePackageSpec: "openclaw@beta",
|
|
phase: "candidate",
|
|
},
|
|
);
|
|
const publishedCrossOs = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"cross-os",
|
|
"false",
|
|
"",
|
|
{
|
|
phase: "candidate",
|
|
releasePackageSpec: "openclaw@beta",
|
|
},
|
|
);
|
|
const conflictingPublishedRelease = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"all",
|
|
"false",
|
|
"",
|
|
{
|
|
candidateArtifactJson: releaseCandidateArtifactJson(),
|
|
phase: "candidate",
|
|
releasePackageSpec: "openclaw@beta",
|
|
},
|
|
);
|
|
const missingProvenance = runReleaseChecksInputValidation("beta", "false", "all", "false", "", {
|
|
candidateArtifactJson: JSON.stringify({
|
|
...JSON.parse(releaseCandidateArtifactJson()),
|
|
packagePublished: undefined,
|
|
}),
|
|
phase: "candidate",
|
|
});
|
|
const malformedProvenance = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"all",
|
|
"false",
|
|
"",
|
|
{
|
|
candidateArtifactJson: releaseCandidateArtifactJson().replace(
|
|
'"packagePublished":false',
|
|
'"packagePublished":"false"',
|
|
),
|
|
phase: "candidate",
|
|
},
|
|
);
|
|
|
|
expect(missing.result.status).toBe(1);
|
|
expect(missing.result.stderr).toContain(
|
|
"phase=candidate requires candidate_artifact_json unless every selected package path uses a published package spec.",
|
|
);
|
|
expect(publishedAcceptance.result.status, publishedAcceptance.result.stderr).toBe(0);
|
|
expect(publishedCrossOs.result.status, publishedCrossOs.result.stderr).toBe(0);
|
|
expect(conflictingPublishedRelease.result.status).toBe(1);
|
|
expect(conflictingPublishedRelease.result.stderr).toContain(
|
|
"candidate_artifact_json cannot be combined with release_package_spec.",
|
|
);
|
|
expect(missingProvenance.result.status).not.toBe(0);
|
|
expect(malformedProvenance.result.status).not.toBe(0);
|
|
});
|
|
|
|
it("uses a candidate for release lanes with a separate Package Acceptance override", () => {
|
|
const { outputPath, result } = runReleaseChecksInputValidation(
|
|
"stable",
|
|
"false",
|
|
"all",
|
|
"false",
|
|
"",
|
|
{
|
|
candidateArtifactJson: releaseCandidateArtifactJson(),
|
|
packageAcceptancePackageSpec: "openclaw@next",
|
|
phase: "candidate",
|
|
},
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(outputPath, "utf8");
|
|
expect(output).toContain("package_mode=artifact\n");
|
|
expect(output).toContain("package_acceptance_package_spec=openclaw@next\n");
|
|
expect(output).toContain("cross_os_scheduled=true\n");
|
|
expect(output).toContain("docker_required=true\n");
|
|
expect(output).toContain("package_acceptance_scheduled=true\n");
|
|
});
|
|
|
|
it("preserves published provenance when an immutable candidate is reused", () => {
|
|
const { outputPath, result } = runReleaseChecksInputValidation(
|
|
"stable",
|
|
"false",
|
|
"all",
|
|
"false",
|
|
"",
|
|
{
|
|
candidateArtifactJson: releaseCandidateArtifactJson("a".repeat(40), true),
|
|
phase: "candidate",
|
|
},
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(outputPath, "utf8");
|
|
expect(output).toContain("package_mode=artifact\n");
|
|
expect(output).toContain("candidate_published=true\n");
|
|
});
|
|
|
|
it.each([
|
|
["beta", "all", "false", "false", "false"],
|
|
["beta", "all", "true", "true", "true"],
|
|
["stable", "all", "false", "true", "true"],
|
|
["full", "all", "false", "true", "true"],
|
|
["beta", "qa", "false", "false", "true"],
|
|
["beta", "qa-live", "false", "false", "true"],
|
|
])(
|
|
"normalizes QA-live scheduling for profile=%s group=%s soak=%s",
|
|
(releaseProfile, rerunGroup, runReleaseSoak, expectedSoak, expectedScheduled) => {
|
|
const { outputPath, result } = runReleaseChecksInputValidation(
|
|
releaseProfile,
|
|
"false",
|
|
rerunGroup,
|
|
runReleaseSoak,
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(outputPath, "utf8");
|
|
expect(output).toContain(`run_release_soak=${expectedSoak}\n`);
|
|
expect(output).toContain(`qa_live_scheduled=${expectedScheduled}\n`);
|
|
},
|
|
);
|
|
|
|
it("schedules only the selected QA-live lane for a QA-group filter", () => {
|
|
const { outputPath, result } = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"qa",
|
|
"false",
|
|
"qa-live-telegram",
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(outputPath, "utf8");
|
|
expect(output).toContain("qa_live_scheduled=true\n");
|
|
expect(output).toContain("qa_live_telegram_enabled=true\n");
|
|
for (const lane of ["matrix", "buzz", "discord", "whatsapp", "slack"]) {
|
|
expect(output).toContain(`qa_live_${lane}_enabled=false\n`);
|
|
}
|
|
});
|
|
|
|
it("keeps a focused repo-live filter within the live-E2E group", () => {
|
|
const { outputPath, result } = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"live-e2e",
|
|
"false",
|
|
"repo-e2e",
|
|
);
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const output = readFileSync(outputPath, "utf8");
|
|
expect(output).toContain("qa_live_scheduled=false\n");
|
|
expect(output).toContain("repo_live_suite_filter=repo-e2e\n");
|
|
expect(output).toContain("package_required=false\n");
|
|
expect(output).toContain("docker_required=false\n");
|
|
});
|
|
|
|
it("rejects a QA-live filter for an unrelated rerun group", () => {
|
|
const { result } = runReleaseChecksInputValidation(
|
|
"beta",
|
|
"false",
|
|
"install-smoke",
|
|
"false",
|
|
"qa-live-telegram",
|
|
);
|
|
|
|
expect(result.status).not.toBe(0);
|
|
expect(result.stderr).toContain(
|
|
"QA live_suite_filter selectors require rerun_group=qa or qa-live",
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "deferred Package Acceptance",
|
|
rerunGroup: "package",
|
|
skipTelegram: "true",
|
|
overrides: {},
|
|
expected: "Package Telegram E2E: deferred by `skip_package_telegram_e2e`",
|
|
},
|
|
{
|
|
label: "unrelated CI group",
|
|
rerunGroup: "ci",
|
|
skipTelegram: "true",
|
|
overrides: {},
|
|
expected: "Package Telegram E2E: skipped by rerun group",
|
|
},
|
|
{
|
|
label: "selected Package Acceptance",
|
|
rerunGroup: "package",
|
|
skipTelegram: "false",
|
|
overrides: {},
|
|
expected: "Package Telegram E2E: OpenClaw Release Checks Package Acceptance",
|
|
},
|
|
{
|
|
label: "focused Telegram without a package",
|
|
rerunGroup: "npm-telegram",
|
|
skipTelegram: "false",
|
|
overrides: {},
|
|
expected:
|
|
"Package Telegram E2E: focused rerun requires `release_package_spec` or `npm_telegram_package_spec`",
|
|
},
|
|
...["RELEASE_PACKAGE_SPEC", "NPM_TELEGRAM_PACKAGE_SPEC"].map((input) => ({
|
|
label: `published package from ${input}`,
|
|
rerunGroup: "npm-telegram",
|
|
skipTelegram: "false",
|
|
overrides: { [input]: "openclaw@2026.8.1-beta.3" },
|
|
expected: "Published-package Telegram E2E: `openclaw@2026.8.1-beta.3`",
|
|
})),
|
|
])(
|
|
"summarizes Telegram package outcome for $label",
|
|
({ rerunGroup, skipTelegram, expected, overrides }) => {
|
|
const { result, summary } = runFullReleaseTargetSummary(rerunGroup, skipTelegram, overrides);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(
|
|
summary
|
|
.split("\n")
|
|
.filter((line) => /^- (Published-package|Package) Telegram E2E:/u.test(line)),
|
|
).toEqual([`- ${expected}`]);
|
|
},
|
|
);
|
|
|
|
it.each([false, true])(
|
|
"selects the candidate-compatible upgrade survivor profile for current source (soak=%s)",
|
|
(soak) => {
|
|
const { result, output, calls } = runReleaseSurvivorProfileStep({ soak });
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(output).toEqual({
|
|
baselines: "supported-lines",
|
|
scenarios: soak
|
|
? parseUpgradeSurvivorScenarios("reported-issues").join(" ")
|
|
: "base legacy-operator-state custom-plugin-siblings",
|
|
});
|
|
expect(calls).toEqual([
|
|
{
|
|
tool: "gh",
|
|
args: [
|
|
"api",
|
|
`repos/openclaw/openclaw/contents/scripts/e2e/lib/upgrade-survivor?ref=${"a".repeat(40)}`,
|
|
"--jq",
|
|
"[.[].name]",
|
|
],
|
|
},
|
|
]);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{
|
|
name: "frozen June published candidate",
|
|
candidateVersion: "2026.6.35",
|
|
published: true,
|
|
context: "extended-stable/2026.6.33",
|
|
},
|
|
{ name: "historical source candidate", candidateVersion: "2026.6.35" },
|
|
{
|
|
name: "older published candidate on the current line",
|
|
candidateVersion: "2026.9.3-beta.1",
|
|
published: true,
|
|
},
|
|
{ name: "npm package override", override: "openclaw@2026.6.35" },
|
|
{ name: "source before the new scenario", supportsScenario: false },
|
|
{
|
|
name: "frozen source with the new scenario",
|
|
candidateVersion: "2026.9.35",
|
|
context: "refs/heads/extended-stable/2026.9.33",
|
|
},
|
|
{
|
|
name: "frozen source branch without separate context",
|
|
candidateVersion: "2026.9.35",
|
|
ref: "extended-stable/2026.9.33",
|
|
},
|
|
])("keeps the candidate-compatible upgrade survivor predecessor for $name", (params) => {
|
|
const { result, output } = runReleaseSurvivorProfileStep(params);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(output).toEqual({ baselines: "", scenarios: "base" });
|
|
});
|
|
|
|
it("preserves the historical candidate-compatible upgrade survivor soak inventory without the new scenario", () => {
|
|
const { result, output } = runReleaseSurvivorProfileStep({
|
|
candidateVersion: "2026.6.35",
|
|
soak: true,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(output.baselines).toBe("");
|
|
expect(output.scenarios?.split(" ")).toEqual([
|
|
"base",
|
|
"acpx-openclaw-tools-bridge",
|
|
"feishu-channel",
|
|
"bootstrap-persona",
|
|
"channel-post-core-restore",
|
|
"plugin-deps-cleanup",
|
|
"configured-plugin-installs",
|
|
"stale-source-plugin-shadow",
|
|
"tilde-log-path",
|
|
"meeting-transcripts-sqlite",
|
|
"versioned-runtime-deps",
|
|
"cron-scheduled-authority",
|
|
]);
|
|
});
|
|
|
|
it.each([false, true])(
|
|
"keeps published candidate-compatible upgrade survivor fixtures on the predecessor (soak=%s)",
|
|
(soak) => {
|
|
const { result, output, calls } = runReleaseSurvivorProfileStep({
|
|
published: true,
|
|
soak,
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(output.baselines).toBe("");
|
|
expect(output.scenarios?.split(" ")).toEqual(
|
|
soak
|
|
? [
|
|
"base",
|
|
"acpx-openclaw-tools-bridge",
|
|
"feishu-channel",
|
|
"bootstrap-persona",
|
|
"channel-post-core-restore",
|
|
"plugin-deps-cleanup",
|
|
"configured-plugin-installs",
|
|
"stale-source-plugin-shadow",
|
|
"tilde-log-path",
|
|
"meeting-transcripts-sqlite",
|
|
"versioned-runtime-deps",
|
|
"cron-scheduled-authority",
|
|
]
|
|
: ["base"],
|
|
);
|
|
expect(calls).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it("fails candidate-compatible upgrade survivor source qualification when metadata cannot be read", () => {
|
|
const { result, output } = runReleaseSurvivorProfileStep({ metadataError: true });
|
|
expect(result.status).not.toBe(0);
|
|
expect(output).toEqual({});
|
|
});
|
|
|
|
it("includes package acceptance in release checks", () => {
|
|
const workflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
|
const filterValidator = readFileSync(RELEASE_FILTER_VALIDATOR, "utf8");
|
|
const packageAcceptanceWorkflow = parse(readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8")) as {
|
|
on?: {
|
|
workflow_call?: { inputs?: Record<string, unknown> };
|
|
};
|
|
};
|
|
const packageAcceptanceJob = workflowJob(
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"package_acceptance_release_checks",
|
|
);
|
|
const releaseChecksTargetSummary = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target"),
|
|
"Summarize validated ref",
|
|
);
|
|
const packageAcceptanceResolve = workflowStep(
|
|
workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"),
|
|
"Resolve package candidate",
|
|
);
|
|
const packageAcceptanceBaseline = workflowStep(
|
|
workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package"),
|
|
"Resolve published upgrade survivor baselines",
|
|
);
|
|
const dockerAcceptanceJob = workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance");
|
|
|
|
expect(workflow).toContain("package_acceptance_release_checks:");
|
|
expect(packageAcceptanceWorkflow.on?.workflow_call?.inputs).toHaveProperty(
|
|
"allow_frozen_target_scenario_omissions",
|
|
);
|
|
expect(packageAcceptanceWorkflow.on?.workflow_call?.inputs).not.toHaveProperty(
|
|
"published_artifact",
|
|
);
|
|
expect(packageAcceptanceJob.with).toMatchObject({
|
|
allow_frozen_target_scenario_omissions:
|
|
"${{ inputs.allow_frozen_target_scenario_omissions }}",
|
|
artifact_digest: "${{ needs.prepare_release_package.outputs.artifact_digest }}",
|
|
artifact_id: "${{ needs.prepare_release_package.outputs.artifact_id }}",
|
|
artifact_name: "${{ needs.prepare_release_package.outputs.artifact_name }}",
|
|
artifact_run_attempt: "${{ needs.prepare_release_package.outputs.artifact_run_attempt }}",
|
|
artifact_run_id: "${{ needs.prepare_release_package.outputs.artifact_run_id }}",
|
|
package_file_name: "${{ needs.prepare_release_package.outputs.package_file_name }}",
|
|
package_sha256:
|
|
"${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.prepare_release_package.outputs.package_sha256 || '' }}",
|
|
package_source_sha: "${{ needs.prepare_release_package.outputs.source_sha }}",
|
|
package_version: "${{ needs.prepare_release_package.outputs.package_version }}",
|
|
prepublish_plugin_registry_json:
|
|
"${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.prepare_release_package.outputs.prepublish_plugin_registry_json || '' }}",
|
|
suite_profile: "custom",
|
|
target_context_ref:
|
|
"${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && inputs.target_context_ref || '' }}",
|
|
published_upgrade_survivor_baseline:
|
|
"${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && format('openclaw@{0}', needs.prepare_release_package.outputs.upgrade_baseline) || 'openclaw@latest' }}",
|
|
});
|
|
expect(packageAcceptanceJob.with?.candidate_artifact_json).toBe(
|
|
"${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.prepare_release_package.outputs.candidate_artifact_json || '' }}",
|
|
);
|
|
expect(packageAcceptanceResolve.env?.PUBLISHED_ARTIFACT).toBe(
|
|
"${{ fromJSON(inputs.candidate_artifact_json || '{}').packagePublished == true }}",
|
|
);
|
|
expect(packageAcceptanceBaseline.env).toMatchObject({
|
|
CANDIDATE_PUBLISHED:
|
|
"${{ inputs.source == 'npm' || fromJSON(inputs.candidate_artifact_json || '{}').packagePublished == true }}",
|
|
CANDIDATE_VERSION: "${{ steps.resolve.outputs.package_version }}",
|
|
TARGET_CONTEXT_REF: "${{ inputs.target_context_ref }}",
|
|
});
|
|
expect(releaseChecksTargetSummary.env).toMatchObject({
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "${{ steps.inputs.outputs.skip_package_telegram_e2e }}",
|
|
});
|
|
expect(releaseChecksTargetSummary.run).toContain("Package Acceptance Telegram E2E deferred:");
|
|
expect(dockerAcceptanceJob.with).toMatchObject({
|
|
allow_frozen_target_scenario_omissions:
|
|
"${{ inputs.allow_frozen_target_scenario_omissions || false }}",
|
|
enable_prepublish_plugin_registry:
|
|
"${{ contains(fromJSON('[\"artifact\",\"ref\"]'), inputs.source) && fromJSON(inputs.candidate_artifact_json || '{}').packagePublished != true }}",
|
|
prepublish_plugin_registry_manifest_sha256:
|
|
"${{ startsWith(fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryArtifactName || '', 'docker-e2e-prepublish-plugin-registry-') && fromJSON(needs.resolve_package.outputs.prepublish_plugin_registry_json || '{}').prepublishPluginRegistryManifestSha256 || '' }}",
|
|
});
|
|
expect(workflow).toContain(
|
|
"candidate_artifact_json cannot be combined with release_package_spec.",
|
|
);
|
|
expect(workflow).toContain(
|
|
"live_repo_e2e_release_checks:\n name: Run repo/live E2E validation\n needs: [resolve_target]",
|
|
);
|
|
expect(workflow).toContain(
|
|
"docker_e2e_release_checks:\n name: Run Docker release-path validation\n needs: [resolve_target, prepare_release_package]",
|
|
);
|
|
expect(workflow).toContain("include_release_path_suites: false");
|
|
expect(workflow).toContain("include_release_path_suites: true");
|
|
expect(workflow).toContain(
|
|
"allow_frozen_target_scenario_omissions: ${{ inputs.allow_frozen_target_scenario_omissions }}",
|
|
);
|
|
expect(workflow).toContain("uses: ./.github/workflows/package-acceptance.yml");
|
|
expect(workflow).toContain(
|
|
"source: ${{ needs.resolve_target.outputs.package_acceptance_package_spec != '' && 'npm' || 'artifact' }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_spec: ${{ needs.resolve_target.outputs.package_acceptance_package_spec || 'openclaw@beta' }}",
|
|
);
|
|
expect(workflow).toContain(".artifacts/docker-e2e-package/package-candidate.json");
|
|
expect(workflow).toContain(
|
|
"artifact_name: ${{ needs.prepare_release_package.outputs.artifact_name }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"package_sha256: ${{ needs.resolve_target.outputs.package_acceptance_package_spec == '' && needs.prepare_release_package.outputs.package_sha256 || '' }}",
|
|
);
|
|
expect(workflow).toContain("suite_profile: custom");
|
|
expect(packageAcceptanceJob.with?.docker_lanes).toBe(
|
|
"${{ needs.resolve_target.outputs.package_acceptance_lanes }}",
|
|
);
|
|
const selection = runReleaseChecksInputValidation("full", "false", "package");
|
|
expect(selection.result.status, selection.result.stderr).toBe(0);
|
|
const selectedLanes = readFileSync(selection.outputPath, "utf8")
|
|
.split("\n")
|
|
.find((line) => line.startsWith("package_acceptance_lanes="))
|
|
?.slice("package_acceptance_lanes=".length);
|
|
expect(selectedLanes?.split(/\s+/u)).toEqual([
|
|
"release-typed-onboarding",
|
|
"doctor-switch",
|
|
"update-channel-switch",
|
|
"skill-install",
|
|
"update-corrupt-plugin",
|
|
"upgrade-survivor",
|
|
"update-first-hop-compat",
|
|
"published-upgrade-survivor",
|
|
"root-managed-vps-upgrade",
|
|
"update-restart-auth",
|
|
"plugins-offline",
|
|
"plugin-update",
|
|
"plugin-binding-command-escape",
|
|
]);
|
|
expect(packageAcceptanceJob.with?.published_upgrade_survivor_baselines).toBe(
|
|
"${{ needs.prepare_release_package.outputs.upgrade_survivor_baselines }}",
|
|
);
|
|
expect(packageAcceptanceJob.with?.published_upgrade_survivor_scenarios).toBe(
|
|
"${{ needs.prepare_release_package.outputs.upgrade_survivor_scenarios }}",
|
|
);
|
|
expect(readWorkflow(RELEASE_CHECKS_WORKFLOW).on?.workflow_dispatch?.inputs).toMatchObject({
|
|
skip_package_telegram_e2e: {
|
|
default: false,
|
|
type: "boolean",
|
|
},
|
|
});
|
|
expect(packageAcceptanceJob.with).toMatchObject({
|
|
telegram_mode:
|
|
"${{ (needs.resolve_target.outputs.telegram_waiver != '' || needs.resolve_target.outputs.skip_package_telegram_e2e == 'true') && 'none' || 'mock-openai' }}",
|
|
});
|
|
expect(packageAcceptanceJob.with?.telegram_advisory).toBeUndefined();
|
|
expect(workflow).not.toContain("telegram_scenarios:");
|
|
expect(workflow).toContain("ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}");
|
|
expect(workflow).toContain("ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}");
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_QA_CONVEX_SITE_URL: ${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
|
|
);
|
|
expect(workflow).toContain("rerun_group:");
|
|
expect(workflow).toContain("live_suite_filter:");
|
|
expect(workflow).toContain("repo_live_suite_filter:");
|
|
expect(workflow).toContain(
|
|
"RELEASE_FILTER_VALIDATOR: workflow/scripts/github/validate-release-suite-filters.sh",
|
|
);
|
|
expect(workflow).toContain('source "$RELEASE_FILTER_VALIDATOR"');
|
|
expect(filterValidator).toContain('repo_filter_tokens+=("$token")');
|
|
expect(filterValidator).toContain(
|
|
'RELEASE_FILTER_REPO_LIVE_SUITE_FILTER="$(IFS=,; printf \'%s\' "${repo_filter_tokens[*]-}")"',
|
|
);
|
|
expect(workflow).toContain("cross_os_suite_filter:");
|
|
expect(workflow).not.toContain("advisory:");
|
|
expect(workflow).toContain(
|
|
"suite_filter: ${{ needs.resolve_target.outputs.cross_os_suite_filter }}",
|
|
);
|
|
expect(workflow).toContain(
|
|
"live_suite_filter: ${{ needs.resolve_target.outputs.repo_live_suite_filter }}",
|
|
);
|
|
expect(workflow).toContain("if: needs.resolve_target.outputs.package_required == 'true'");
|
|
expect(workflow).toContain("if: needs.resolve_target.outputs.docker_required == 'true'");
|
|
expect(workflow).toContain(
|
|
'if [[ "$release_profile" == "stable" || "$release_profile" == "full" ]]; then\n run_release_soak=true',
|
|
);
|
|
expect(workflow).toContain("forced on for release_profile=stable and full");
|
|
expect(workflow).toContain("- live-e2e");
|
|
expect(workflow).toContain("- qa-live");
|
|
expect(workflow).toContain("disabled_required_lanes=()");
|
|
expect(filterValidator).toContain(
|
|
"QA live_suite_filter selectors require rerun_group=qa or qa-live",
|
|
);
|
|
expect(filterValidator).toContain(
|
|
"Repo live_suite_filter selectors require rerun_group=live-e2e",
|
|
);
|
|
expect(filterValidator).toContain("cross_os_suite_filter requires rerun_group=all or cross-os");
|
|
expect(workflow).toContain("live_suite_filter explicitly requested disabled QA live lane(s)");
|
|
expect(workflow).toContain("OPENCLAW_RELEASE_QA_*_LIVE_CI_ENABLED");
|
|
expect(workflow).not.toContain(
|
|
"QA release-check lanes are advisory and do not block release validation.",
|
|
);
|
|
});
|
|
|
|
it("prefers fresh Claude OAuth credentials for direct Anthropic live provider lanes", () => {
|
|
const hydrateScript = readFileSync(CI_HYDRATE_LIVE_AUTH_SCRIPT, "utf8");
|
|
|
|
expect(hydrateScript).toContain(" ANTHROPIC_OAUTH_TOKEN \\");
|
|
expect(hydrateScript).toContain("access_token=\"$(jq -r '.claudeAiOauth.accessToken // empty'");
|
|
expect(hydrateScript).toContain('export ANTHROPIC_OAUTH_TOKEN="$access_token"');
|
|
expect(hydrateScript).toContain('local min_remaining_ms="$(( 90 * 60 * 1000 ))"');
|
|
expect(hydrateScript).toContain(
|
|
'printf \'ANTHROPIC_OAUTH_TOKEN=%s\\n\' "$access_token" >>"$GITHUB_ENV"',
|
|
);
|
|
for (const jobName of [
|
|
"validate_live_models_docker",
|
|
"validate_live_models_docker_targeted",
|
|
"validate_live_provider_suites",
|
|
]) {
|
|
expect(workflowJob(LIVE_E2E_WORKFLOW, jobName).env?.ANTHROPIC_OAUTH_TOKEN).toBe(
|
|
"${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}",
|
|
);
|
|
}
|
|
});
|
|
|
|
it("routes release Matrix through the QA Lab selector", () => {
|
|
const releaseWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
|
const releaseTelegramWorkflow = readFileSync(RELEASE_TELEGRAM_QA_WORKFLOW, "utf8");
|
|
const qaWorkflow = readFileSync(".github/workflows/qa-live-transports-convex.yml", "utf8");
|
|
const releaseJob = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_release_checks");
|
|
|
|
expect(releaseJob.uses).toBe("./.github/workflows/qa-live-transports-convex.yml");
|
|
expect(releaseJob.secrets).toEqual({
|
|
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
|
|
OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
|
|
OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}",
|
|
});
|
|
expect(releaseJob.permissions).toEqual({ contents: "read", "pull-requests": "read" });
|
|
expect(releaseJob.if).toBe(
|
|
"needs.resolve_target.outputs.qa_live_scheduled == 'true' && needs.resolve_target.outputs.qa_live_matrix_enabled == 'true'",
|
|
);
|
|
expect(releaseJob.with).toMatchObject({
|
|
expected_sha: "${{ needs.resolve_target.outputs.revision }}",
|
|
fail_fast: "${{ fromJSON(needs.resolve_target.outputs.fail_fast) }}",
|
|
run_matrix: true,
|
|
});
|
|
for (const lane of ["mock_parity", "buzz", "telegram", "discord", "whatsapp", "slack"]) {
|
|
expect(releaseJob.with?.[`run_${lane}`]).toBeUndefined();
|
|
}
|
|
const manualScenarioGuard =
|
|
"(github.event_name != 'workflow_dispatch' || (inputs.scenario == '' && inputs.matrix_scenario == ''))";
|
|
expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_mock_parity").if).toBe(
|
|
`(inputs.expected_sha == '' || inputs.run_mock_parity) && ${manualScenarioGuard}`,
|
|
);
|
|
expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix").if).toBe(
|
|
"(inputs.expected_sha == '' || inputs.run_matrix) && (github.event_name != 'workflow_dispatch' || inputs.scenario == '')",
|
|
);
|
|
expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_telegram").if).toBe(
|
|
"(inputs.expected_sha == '' || inputs.run_telegram) && (github.event_name != 'workflow_dispatch' || inputs.matrix_scenario == '')",
|
|
);
|
|
for (const channel of ["discord", "whatsapp", "slack"]) {
|
|
expect(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, `run_live_${channel}`).if).toBe(
|
|
`(inputs.expected_sha == '' || inputs.run_${channel}) && ${manualScenarioGuard}`,
|
|
);
|
|
}
|
|
expect(releaseWorkflow).not.toContain("qa_live_matrix_release_checks");
|
|
expect(releaseWorkflow).not.toContain("Run QA Lab live Matrix lane");
|
|
expect(releaseWorkflow).not.toContain("pnpm openclaw qa matrix");
|
|
expect(qaWorkflow).toContain("pnpm openclaw qa matrix");
|
|
expect(qaWorkflow).toContain('if [[ "$FAIL_FAST" == "true" ]]');
|
|
expect(qaWorkflow).toContain('trusted_reason="repository-branch"');
|
|
expect(qaWorkflow).toContain('"${selected_revision}" != "${EXPECTED_SHA}"');
|
|
expect(qaWorkflow).toContain("EXPECTED_SHA: ${{ inputs.expected_sha }}");
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "authorize_actor"),
|
|
"Require maintainer-level repository access",
|
|
).env?.EXPECTED_SHA,
|
|
).toBe("${{ inputs.expected_sha }}");
|
|
expect(qaWorkflow).toContain('(process.env.EXPECTED_SHA ?? "") !== ""');
|
|
expect(qaWorkflow).not.toContain('"${{ inputs.expected_sha }}" !== ""');
|
|
expect(qaWorkflow).toContain('if [[ -n "${EXPECTED_SHA}" ]]; then');
|
|
const matrixJob = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix");
|
|
expect(matrixJob["timeout-minutes"]).toBe(90);
|
|
expect(workflowStep(matrixJob, "Run Matrix live lane").run).toContain(
|
|
"--provider-mode mock-openai",
|
|
);
|
|
const matrixSpecificInputs = Object.keys(
|
|
readWorkflow(QA_LIVE_TRANSPORTS_WORKFLOW).on?.workflow_call?.inputs ?? {},
|
|
).filter((input) => input.startsWith("matrix_"));
|
|
expect(matrixSpecificInputs).toEqual(["matrix_scenario"]);
|
|
expect(workflowStep(matrixJob, "Upload Matrix QA artifacts").with?.name).toBe(
|
|
"${{ inputs.expected_sha != '' && format('release-qa-live-matrix-{0}', inputs.expected_sha) || format('qa-live-matrix-{0}-{1}', github.run_id, github.run_attempt) }}",
|
|
);
|
|
expect(matrixJob["continue-on-error"]).toBeUndefined();
|
|
expect(matrixJob.strategy).toBeUndefined();
|
|
expect(workflowStep(matrixJob, "Run Matrix live lane").env).toEqual({
|
|
FAIL_FAST: "${{ inputs.fail_fast }}",
|
|
INPUT_SCENARIO: "${{ inputs.matrix_scenario || '' }}",
|
|
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
|
|
OPENCLAW_LIVE_OPENAI_KEY: "${{ secrets.OPENAI_API_KEY }}",
|
|
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1",
|
|
});
|
|
expect(workflowStep(matrixJob, "Run Matrix live lane").run).toContain(
|
|
'matrix_args+=(--scenario "${scenario}")',
|
|
);
|
|
expect(releaseTelegramWorkflow).not.toContain("retrying once");
|
|
});
|
|
|
|
it.each([
|
|
["discord", "Discord"],
|
|
["whatsapp", "WhatsApp"],
|
|
["slack", "Slack"],
|
|
])("preserves the first failed %s release QA attempt", (channel, label) => {
|
|
const job = workflowJob(RELEASE_CHECKS_WORKFLOW, `qa_live_${channel}_release_checks`);
|
|
const run = workflowStep(job, `Run ${label} live lane`).run;
|
|
const workdir = tempDirs.make(`release-qa-${channel}-first-failure-`);
|
|
const attempts = join(workdir, "attempts");
|
|
const pnpm = join(workdir, "pnpm");
|
|
writeFileSync(
|
|
pnpm,
|
|
'#!/bin/sh\nif [ -f "$ATTEMPTS" ]; then printf "retried\\n" >> "$ATTEMPTS"; exit 0; fi\nprintf "first\\n" > "$ATTEMPTS"\nexit 17\n',
|
|
{ mode: 0o755 },
|
|
);
|
|
writeFileSync(join(workdir, "sleep"), "#!/bin/sh\nexit 0\n", { mode: 0o755 });
|
|
const result = spawnSync("bash", ["-c", run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
ATTEMPTS: attempts,
|
|
GITHUB_OUTPUT: join(workdir, "outputs"),
|
|
GITHUB_RUN_ID: "123",
|
|
GITHUB_RUN_ATTEMPT: "1",
|
|
PATH: `${workdir}:${process.env.PATH}`,
|
|
},
|
|
});
|
|
|
|
expect(job["continue-on-error"]).toBeUndefined();
|
|
expect(result.status, result.stderr).toBe(17);
|
|
expect(readFileSync(attempts, "utf8")).toBe("first\n");
|
|
});
|
|
|
|
it("routes release Buzz through the QA Lab selector", () => {
|
|
const releaseJob = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_buzz_release_checks");
|
|
|
|
expect(releaseJob.uses).toBe("./.github/workflows/qa-live-transports-convex.yml");
|
|
expect(releaseJob.secrets).toEqual({
|
|
OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}",
|
|
OPENCLAW_QA_CONVEX_SECRET_CI: "${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}",
|
|
OPENCLAW_QA_CONVEX_SITE_URL: "${{ secrets.OPENCLAW_QA_CONVEX_SITE_URL }}",
|
|
});
|
|
expect(releaseJob.permissions).toEqual({ contents: "read", "pull-requests": "read" });
|
|
expect(releaseJob.if).toBe(
|
|
"needs.resolve_target.outputs.qa_live_scheduled == 'true' && needs.resolve_target.outputs.qa_live_buzz_enabled == 'true'",
|
|
);
|
|
expect(releaseJob.with).toMatchObject({
|
|
buzz_scenario: "channel-canary,channel-mention-gating",
|
|
expected_sha: "${{ needs.resolve_target.outputs.revision }}",
|
|
run_buzz: true,
|
|
});
|
|
const buzzJob = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_buzz");
|
|
expect(buzzJob.if).toBe("inputs.run_buzz");
|
|
const resolveBuzz = workflowStep(buzzJob, "Resolve Buzz QA runner");
|
|
expect(resolveBuzz.run).toContain('runner?.commandName === "buzz"');
|
|
expect(resolveBuzz.run).toContain("selected ref does not declare the Buzz QA runner");
|
|
expect(workflowStep(buzzJob, "Validate required Buzz QA credential env").if).toBe(
|
|
"steps.resolve_buzz.outputs.available == 'true'",
|
|
);
|
|
expect(workflowStep(buzzJob, "Build private QA runtime").if).toBe(
|
|
"steps.resolve_buzz.outputs.available == 'true'",
|
|
);
|
|
expect(workflowStep(buzzJob, "Run Buzz live lane").if).toBe(
|
|
"steps.resolve_buzz.outputs.available == 'true'",
|
|
);
|
|
expect(workflowStep(buzzJob, "Upload Buzz QA artifacts").with?.name).toBe(
|
|
"${{ inputs.expected_sha != '' && format('release-qa-live-buzz-{0}-{1}', inputs.expected_sha, github.run_attempt) || format('qa-live-buzz-{0}-{1}', github.run_id, github.run_attempt) }}",
|
|
);
|
|
expect(workflowStep(buzzJob, "Upload Buzz QA artifacts").with?.path).toBe(
|
|
"${{ steps.resolve_buzz.outputs.output_dir }}",
|
|
);
|
|
const requireBuzz = workflowStep(buzzJob, "Require requested Buzz QA runner");
|
|
expect(requireBuzz.if).toBe(
|
|
"always() && inputs.expected_sha == '' && steps.resolve_buzz.outcome == 'success' && steps.resolve_buzz.outputs.available != 'true'",
|
|
);
|
|
expect(requireBuzz.run).toContain(
|
|
"The selected ref does not declare the requested Buzz QA runner.",
|
|
);
|
|
expect(requireBuzz.run).toContain("exit 1");
|
|
});
|
|
|
|
it("runs QA-live on soak or explicit QA groups, not beta all by default", () => {
|
|
const workflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
|
const resolveTarget = workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target");
|
|
const liveJobs = [
|
|
["qa_live_release_checks", "qa_live_matrix_enabled"],
|
|
["qa_live_buzz_release_checks", "qa_live_buzz_enabled"],
|
|
["qa_live_telegram_release_checks", "qa_live_telegram_enabled"],
|
|
["qa_live_discord_release_checks", "qa_live_discord_enabled"],
|
|
["qa_live_whatsapp_release_checks", "qa_live_whatsapp_enabled"],
|
|
["qa_live_slack_release_checks", "qa_live_slack_enabled"],
|
|
] as const;
|
|
const selection = "needs.resolve_target.outputs.qa_live_scheduled == 'true'";
|
|
|
|
expect(resolveTarget.outputs?.qa_live_scheduled).toBe(
|
|
"${{ steps.inputs.outputs.qa_live_scheduled }}",
|
|
);
|
|
|
|
for (const [jobName, enabledOutput] of liveJobs) {
|
|
expect(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName).if).toBe(
|
|
`${selection} && needs.resolve_target.outputs.${enabledOutput} == 'true'`,
|
|
);
|
|
}
|
|
|
|
const verifyStep = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"),
|
|
"Verify release check results",
|
|
);
|
|
expect(verifyStep.env?.QA_LIVE_TELEGRAM_SELECTED).toBe(
|
|
`\${{ ${selection} && needs.resolve_target.outputs.qa_live_telegram_enabled == 'true' }}`,
|
|
);
|
|
const kickoffSummary = workflowStep(resolveTarget, "Summarize validated ref");
|
|
expect(kickoffSummary.env?.QA_LIVE_SCHEDULED).toBe(
|
|
"${{ steps.inputs.outputs.qa_live_scheduled }}",
|
|
);
|
|
expect(kickoffSummary.run).toContain("- QA-live scheduled:");
|
|
expect(kickoffSummary.run).toContain("- QA-live lane eligibility:");
|
|
expect(kickoffSummary.run).not.toContain("- QA live lanes:");
|
|
expect(workflow).not.toContain('contains(fromJSON(\'["qa","qa-live"]\')');
|
|
});
|
|
|
|
it("runs live transport lanes nightly while release checks stay gated", () => {
|
|
const releaseWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
|
|
const qaWorkflow = readFileSync(QA_LIVE_TRANSPORTS_WORKFLOW, "utf8");
|
|
|
|
for (const channel of ["DISCORD", "WHATSAPP", "SLACK"]) {
|
|
const lower = channel.toLowerCase();
|
|
expect(releaseWorkflow).toContain(
|
|
`RELEASE_QA_${channel}_LIVE_CI_ENABLED: \${{ vars.OPENCLAW_RELEASE_QA_${channel}_LIVE_CI_ENABLED || 'false' }}`,
|
|
);
|
|
expect(releaseWorkflow).toContain(`qa_live_${lower}_enabled="$qa_live_${lower}_ci_enabled"`);
|
|
expect(releaseWorkflow).toContain(
|
|
`needs.resolve_target.outputs.qa_live_${lower}_enabled == 'true'`,
|
|
);
|
|
expect(releaseWorkflow).not.toContain(
|
|
`vars.OPENCLAW_RELEASE_QA_${channel}_LIVE_CI_ENABLED == 'true'`,
|
|
);
|
|
expect(qaWorkflow).not.toContain(`OPENCLAW_QA_${channel}_LIVE_CI_ENABLED`);
|
|
}
|
|
});
|
|
|
|
it("requires QA live evidence artifacts when lanes run", () => {
|
|
const cases = [
|
|
["run_mock_parity", "Upload parity artifacts", "always()"],
|
|
[
|
|
"run_live_runtime_token_efficiency",
|
|
"Upload live runtime token-efficiency artifacts",
|
|
"always() && steps.run_lane.outputs.output_dir != ''",
|
|
],
|
|
["run_live_matrix", "Upload Matrix QA artifacts", "always()"],
|
|
["run_live_buzz", "Upload Buzz QA artifacts", "always()"],
|
|
["run_live_telegram", "Upload Telegram QA artifacts", "always()"],
|
|
["run_live_discord", "Upload Discord QA artifacts", "always()"],
|
|
["run_live_whatsapp", "Upload WhatsApp QA artifacts", "always()"],
|
|
["run_live_slack", "Upload Slack QA artifacts", "always()"],
|
|
] as const;
|
|
|
|
for (const [jobName, stepName, uploadCondition] of cases) {
|
|
const uploadStep = workflowStep(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, jobName), stepName);
|
|
|
|
expect(uploadStep.if, jobName).toBe(uploadCondition);
|
|
expect(uploadStep.with?.["if-no-files-found"], jobName).toBe("error");
|
|
}
|
|
});
|
|
|
|
it("preserves the primary runtime token-efficiency failure", () => {
|
|
const job = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_runtime_token_efficiency");
|
|
const runStep = workflowStep(job, "Run live core runtime-pair lane");
|
|
const reportStep = workflowStep(job, "Generate live runtime token-efficiency report");
|
|
|
|
expect(runStep.run).toContain('mkdir -p "${output_dir}"');
|
|
expect(runStep.run).toContain(
|
|
"printf 'Runtime token-efficiency lane started.\\n' > \"${output_dir}/runtime-lane-started.txt\"",
|
|
);
|
|
expect(reportStep.if).toBe("steps.run_lane.outcome == 'success'");
|
|
});
|
|
|
|
it("overlays only trusted Anthropic mock tooling for frozen-target QA parity", () => {
|
|
const resolveTarget = workflowJob(RELEASE_CHECKS_WORKFLOW, "resolve_target");
|
|
const contextValidation = workflowStep(resolveTarget, "Validate trusted QA tooling context");
|
|
const eligibility = workflowStep(resolveTarget, "Validate trusted QA tooling eligibility");
|
|
const resolveStepNames = resolveTarget.steps?.map((step) => step.name) ?? [];
|
|
const job = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_lab_parity_lane_release_checks");
|
|
const trustedCheckout = workflowStep(job, "Checkout trusted QA Anthropic mock tooling");
|
|
const installTooling = workflowStep(job, "Install trusted QA Anthropic mock tooling");
|
|
const stepNames = job.steps?.map((step) => step.name) ?? [];
|
|
const targetSha = "a".repeat(40);
|
|
const eligibilityCondition =
|
|
"needs.resolve_target.outputs.trusted_qa_tooling_eligible == 'true'";
|
|
|
|
expect(resolveTarget.outputs?.trusted_qa_tooling_eligible).toBe(
|
|
"${{ steps.trusted_qa_tooling.outputs.eligible }}",
|
|
);
|
|
expect(contextValidation.if).toBe("inputs.target_context_ref != ''");
|
|
expect(
|
|
resolveTarget.steps?.find((step) => step.name === "Checkout trusted QA tooling context"),
|
|
).toBeUndefined();
|
|
expect(eligibility.if).toBe("steps.trusted_qa_context.outputs.fetch_ref != ''");
|
|
expect(resolveStepNames.indexOf("Validate trusted QA tooling context")).toBeLessThan(
|
|
resolveStepNames.indexOf("Validate trusted QA tooling eligibility"),
|
|
);
|
|
expect(eligibility.env?.TRUSTED_REPOSITORY_URL).toBe(
|
|
"https://github.com/${{ github.repository }}.git",
|
|
);
|
|
expect(resolveStepNames.indexOf("Checkout trusted workflow helper")).toBeLessThan(
|
|
resolveStepNames.indexOf("Validate trusted QA tooling eligibility"),
|
|
);
|
|
|
|
for (const contextRef of [
|
|
"release/2026.8.1",
|
|
"release/2026.8.1-1",
|
|
"refs/heads/extended-stable/2026.8.33",
|
|
"v2026.8.1",
|
|
"refs/tags/v2026.8.1-1",
|
|
"refs/tags/v2026.8.1-alpha.2",
|
|
"v2026.8.1-beta.3",
|
|
]) {
|
|
const { output, result } = runReleaseChecksShellStep("Validate trusted QA tooling context", {
|
|
TARGET_CONTEXT_REF: contextRef,
|
|
TARGET_REF: targetSha,
|
|
});
|
|
expect(result.status, `${contextRef}: ${result.stderr}`).toBe(0);
|
|
const normalizedRef = contextRef.replace(/^refs\/(heads|tags)\//u, "");
|
|
expect(output, contextRef).toBe(
|
|
`fetch_ref=refs/${normalizedRef.startsWith("v") ? "tags" : "heads"}/${normalizedRef}\n`,
|
|
);
|
|
}
|
|
|
|
for (const contextRef of [
|
|
"main",
|
|
"release-ci/2026.8.1-frozen",
|
|
"release/2026.8",
|
|
"v2026.8.1-rc.1",
|
|
"refs/heads/refs/tags/v2026.8.1",
|
|
]) {
|
|
const { result } = runReleaseChecksShellStep("Validate trusted QA tooling context", {
|
|
TARGET_CONTEXT_REF: contextRef,
|
|
TARGET_REF: targetSha,
|
|
});
|
|
expect(result.status, contextRef).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"target_context_ref must be a canonical OpenClaw release branch or tag.",
|
|
);
|
|
}
|
|
|
|
for (const targetRef of ["release/2026.8.1", "a".repeat(39)]) {
|
|
const { result } = runReleaseChecksShellStep("Validate trusted QA tooling context", {
|
|
TARGET_CONTEXT_REF: "release/2026.8.1",
|
|
TARGET_REF: targetRef,
|
|
});
|
|
expect(result.status, targetRef).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"target_context_ref requires ref to be a full 40-character commit SHA.",
|
|
);
|
|
}
|
|
|
|
const fixture = createReleaseChecksContextFixture();
|
|
const relationshipCases = [
|
|
["tag", "refs/tags/v2026.8.1", fixture.branchHeadSha, 0, ""],
|
|
["tag", "refs/tags/v2026.8.1", fixture.candidateSha, 1, "does not match target"],
|
|
["branch", "refs/heads/release/2026.8.1", fixture.branchHeadSha, 0, ""],
|
|
["branch", "refs/heads/release/2026.8.1", fixture.candidateSha, 0, ""],
|
|
[
|
|
"branch",
|
|
"refs/heads/release/2026.8.1",
|
|
fixture.unrelatedSha,
|
|
1,
|
|
"is not reachable from branch",
|
|
],
|
|
[
|
|
"tag",
|
|
"refs/tags/v2026.8.1-beta.99",
|
|
fixture.branchHeadSha,
|
|
1,
|
|
"Failed to fetch trusted QA tooling context",
|
|
],
|
|
] as const;
|
|
for (const [contextKind, fetchRef, targetRef, expectedStatus, error] of relationshipCases) {
|
|
const { output, result } = runReleaseChecksShellStep(
|
|
"Validate trusted QA tooling eligibility",
|
|
{
|
|
CONTEXT_FETCH_REF: fetchRef,
|
|
TARGET_REF: targetRef,
|
|
TRUSTED_REPOSITORY_URL: fixture.repoUrl,
|
|
},
|
|
);
|
|
expect(result.status, `${contextKind} ${targetRef}: ${result.stderr}`).toBe(expectedStatus);
|
|
expect(output).toBe(expectedStatus === 0 ? "eligible=true\n" : "");
|
|
if (error) {
|
|
expect(result.stderr).toContain(error);
|
|
}
|
|
}
|
|
|
|
expect(trustedCheckout).toMatchObject({
|
|
if: eligibilityCondition,
|
|
uses: "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1",
|
|
with: {
|
|
"persist-credentials": false,
|
|
ref: "${{ github.workflow_sha }}",
|
|
path: ".release-qa-tooling-trusted",
|
|
"sparse-checkout":
|
|
"extensions/qa-lab/src/providers/mock-openai/mock-anthropic-messages.ts\nextensions/qa-lab/src/providers/mock-openai/mock-anthropic-wire.ts\n",
|
|
"sparse-checkout-cone-mode": false,
|
|
},
|
|
});
|
|
expect(installTooling.if).toBe(eligibilityCondition);
|
|
expect(installTooling.run).toContain("trap 'rm -rf -- \"$trusted_checkout\"' EXIT");
|
|
const installLines = (installTooling.run ?? "").split("\n").map((line) => line.trim());
|
|
for (const file of ["mock-anthropic-messages.ts", "mock-anthropic-wire.ts"]) {
|
|
const sourceArgument = `"$trusted_checkout/extensions/qa-lab/src/providers/mock-openai/${file}" \\`;
|
|
const sourceArgumentIndex = installLines.indexOf(sourceArgument);
|
|
expect(sourceArgumentIndex).toBeGreaterThanOrEqual(0);
|
|
expect(installLines[sourceArgumentIndex + 1]).toBe(
|
|
`extensions/qa-lab/src/providers/mock-openai/${file}`,
|
|
);
|
|
}
|
|
expect(installTooling.run).toContain('rm -rf -- "$trusted_checkout"');
|
|
expect(stepNames.indexOf("Checkout selected ref")).toBeLessThan(
|
|
stepNames.indexOf("Checkout trusted QA Anthropic mock tooling"),
|
|
);
|
|
expect(stepNames.indexOf("Install trusted QA Anthropic mock tooling")).toBeLessThan(
|
|
stepNames.indexOf("Setup Node environment"),
|
|
);
|
|
expect(stepNames.indexOf("Install trusted QA Anthropic mock tooling")).toBeLessThan(
|
|
stepNames.indexOf("Build private QA runtime"),
|
|
);
|
|
});
|
|
|
|
it("runs the core gateway restart pair on its pinned live model", () => {
|
|
const job = workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_runtime_token_efficiency");
|
|
const credentialStep = workflowStep(job, "Validate required QA credential env");
|
|
const runStep = workflowStep(job, "Run pinned GPT-5.4 gateway restart runtime pair");
|
|
const stepNames = job.steps?.map((step) => step.name) ?? [];
|
|
|
|
expect(credentialStep.run).toContain('if [[ -z "${OPENAI_API_KEY:-}" ]]');
|
|
expect(credentialStep.run).toContain("exit 1");
|
|
expect(runStep.run).toContain("--provider-mode live-frontier");
|
|
expect(runStep.run).toContain("--scenario gateway-restart-multi-live");
|
|
expect(runStep.run).toContain("--model openai/gpt-5.4");
|
|
expect(runStep.run).toContain("--alt-model openai/gpt-5.4");
|
|
expect(runStep.run).toContain("--runtime-pair openclaw,codex");
|
|
expect(runStep.run).toContain(
|
|
"steps.run_lane.outputs.output_dir }}/gateway-restart-gpt-5.4-runtime-pair",
|
|
);
|
|
expect(runStep.run).not.toContain("--allow-failures");
|
|
expect(stepNames.indexOf("Run pinned GPT-5.4 gateway restart runtime pair")).toBeLessThan(
|
|
stepNames.indexOf("Generate live runtime token-efficiency report"),
|
|
);
|
|
});
|
|
|
|
it("requires release-check QA evidence artifacts when lanes run", () => {
|
|
const cases = [
|
|
["qa_lab_parity_lane_release_checks", "Upload parity lane artifacts"],
|
|
["qa_lab_parity_report_release_checks", "Upload parity artifacts"],
|
|
["qa_lab_runtime_parity_release_checks", "Upload runtime parity artifacts"],
|
|
["qa_live_discord_release_checks", "Upload Discord QA artifacts"],
|
|
["qa_live_whatsapp_release_checks", "Upload WhatsApp QA artifacts"],
|
|
["qa_live_slack_release_checks", "Upload Slack QA artifacts"],
|
|
] as const;
|
|
|
|
for (const [jobName, stepName] of cases) {
|
|
const uploadStep = workflowStep(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName), stepName);
|
|
|
|
expect(uploadStep.if, jobName).toBe("always()");
|
|
expect(uploadStep.uses, jobName).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(uploadStep.with?.["if-no-files-found"], jobName).toBe("error");
|
|
}
|
|
|
|
const telegramUpload = workflowStep(
|
|
workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, "run_telegram"),
|
|
"Upload Telegram QA artifacts",
|
|
);
|
|
expect(telegramUpload.if).toContain("always()");
|
|
expect(telegramUpload.uses).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(telegramUpload.with?.["if-no-files-found"]).toBe("error");
|
|
|
|
const runtimeCoverageUpload = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "runtime_tool_coverage_release_checks"),
|
|
"Upload runtime tool coverage artifacts",
|
|
);
|
|
expect(runtimeCoverageUpload.if).toContain("always()");
|
|
expect(runtimeCoverageUpload.if).toContain(
|
|
"steps.verify_runtime_parity_status.outputs.ready == 'true'",
|
|
);
|
|
expect(runtimeCoverageUpload.uses).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(runtimeCoverageUpload.with?.["if-no-files-found"]).toBe("error");
|
|
});
|
|
|
|
it("runs canonical runtime-pair lanes in parallel and preserves one gate", () => {
|
|
const laneJob = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_lab_runtime_pair_lane_release_checks");
|
|
const collectorJob = workflowJob(
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"qa_lab_runtime_parity_release_checks",
|
|
);
|
|
|
|
expect(laneJob.strategy?.["fail-fast"]).toBe(false);
|
|
expect(laneJob.strategy?.matrix?.lane).toContain('["core","soak"]');
|
|
expect(laneJob.strategy?.matrix?.lane).toContain('["core"]');
|
|
const runtimePairRun = workflowStep(laneJob, "Run runtime-pair lane").run;
|
|
expect(runtimePairRun).toContain('--runtime-pair-lane "$RUNTIME_PAIR_LANE"');
|
|
expect(runtimePairRun).toContain("--runtime-parity-tier standard");
|
|
expect(runtimePairRun).toContain("--runtime-parity-tier soak");
|
|
expect(runtimePairRun).toContain("Frozen candidate cannot select runtime-pair lane");
|
|
expect(workflowStep(laneJob, "Run runtime-pair lane")["continue-on-error"]).toBeUndefined();
|
|
const runtimePairValidation = workflowStep(laneJob, "Validate runtime-pair lane").run;
|
|
expect(runtimePairValidation).toContain("validator_args+=(--require-explicit-gap)");
|
|
expect(runtimePairValidation).toContain('--target-sha "$RELEASE_CHECK_TARGET_SHA"');
|
|
expect(runtimePairValidation).toContain('--lane "$RUNTIME_PAIR_LANE"');
|
|
expect(runtimePairValidation).toContain(
|
|
'node --import tsx trusted-suite-validator/scripts/validate-qa-runtime-pair-summary.mts "${validator_args[@]}"',
|
|
);
|
|
const coreRestartRun = workflowStep(laneJob, "Run OpenClaw core restart proof").run;
|
|
expect(coreRestartRun).toContain("--scenario gateway-restart-inflight-run");
|
|
expect(coreRestartRun).toContain('--output-dir ".artifacts/qa-e2e/openclaw-core-restart"');
|
|
const trustedValidatorCheckout = workflowStep(
|
|
laneJob,
|
|
"Checkout trusted validator after candidate suite",
|
|
);
|
|
expect(trustedValidatorCheckout.with).toMatchObject({
|
|
ref: "${{ github.sha }}",
|
|
path: "trusted-suite-validator",
|
|
"persist-credentials": false,
|
|
});
|
|
const runtimePairStepNames = (laneJob.steps ?? []).map((step) => step.name);
|
|
expect(runtimePairStepNames.indexOf("Run runtime-pair lane")).toBeLessThan(
|
|
runtimePairStepNames.indexOf("Checkout trusted validator after candidate suite"),
|
|
);
|
|
expect(workflowStep(laneJob, "Generate runtime-pair lane report")["continue-on-error"]).toBe(
|
|
true,
|
|
);
|
|
const runtimePairReport = workflowStep(laneJob, "Validate runtime-pair lane report").run;
|
|
expect(runtimePairReport).toContain(
|
|
'--report-summary "$report_dir/qa-runtime-parity-summary.json"',
|
|
);
|
|
expect(runtimePairReport).toContain(
|
|
'--report-markdown "$report_dir/qa-runtime-parity-report.md"',
|
|
);
|
|
expect(runtimePairReport).toContain("validator_args+=(--require-explicit-gap)");
|
|
expect(runtimePairReport).toContain(
|
|
"node --import tsx trusted-report-validator/scripts/validate-qa-runtime-pair-summary.mts",
|
|
);
|
|
expect(runtimePairStepNames.indexOf("Generate runtime-pair lane report")).toBeLessThan(
|
|
runtimePairStepNames.indexOf("Checkout trusted validator after candidate report"),
|
|
);
|
|
const recordedOutcomes = workflowStep(laneJob, "Record runtime-pair lane status").env?.[
|
|
"RELEASE_CHECK_STEP_OUTCOMES"
|
|
];
|
|
expect(recordedOutcomes).toContain("steps.runtime_parity_validation.outcome");
|
|
expect(recordedOutcomes).toContain("steps.generate_runtime_parity_report.outcome");
|
|
expect(recordedOutcomes).not.toContain("steps.candidate_runtime_pair.outcome");
|
|
expect(recordedOutcomes).not.toContain("steps.candidate_runtime_parity_report.outcome");
|
|
expect(workflowStep(laneJob, "Upload runtime-pair lane artifacts").with?.name).toContain(
|
|
"${{ matrix.lane }}",
|
|
);
|
|
expect(collectorJob.needs).toEqual([
|
|
"resolve_target",
|
|
"qa_lab_runtime_pair_lane_release_checks",
|
|
]);
|
|
expect(collectorJob.name).toBe("Verify QA Lab runtime-pair lanes");
|
|
expect(workflowStep(collectorJob, "Resolve runtime-pair lane artifacts").run).toContain(
|
|
"qa_lab_runtime_pair_lane_release_checks|core",
|
|
);
|
|
expect(workflowStep(collectorJob, "Resolve runtime-pair lane artifacts").run).toContain(
|
|
"qa_lab_runtime_pair_lane_release_checks|soak",
|
|
);
|
|
expect(workflowStep(collectorJob, "Download runtime-pair lane artifacts").with).toMatchObject({
|
|
"artifact-ids": "${{ steps.resolve_runtime_pair_artifacts.outputs.payload_ids }}",
|
|
"merge-multiple": true,
|
|
});
|
|
expect(workflowStep(collectorJob, "Download runtime-pair lane artifacts").if).toBe(
|
|
"always() && steps.resolve_runtime_pair_artifacts.outcome == 'success'",
|
|
);
|
|
expect(workflowStep(collectorJob, "Download runtime-pair lane statuses").if).toBe(
|
|
"always() && steps.resolve_runtime_pair_artifacts.outcome == 'success'",
|
|
);
|
|
expect(workflowStep(collectorJob, "Verify runtime-pair lane statuses").run).toContain(
|
|
"resolve-release-check-artifacts.sh validate",
|
|
);
|
|
expect(workflowStep(collectorJob, "Upload runtime parity artifacts").with?.name).toBe(
|
|
"release-qa-runtime-parity-${{ needs.resolve_target.outputs.revision }}-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
});
|
|
|
|
it("requires live proof evidence artifacts when proof jobs run", () => {
|
|
const cases = [
|
|
{
|
|
workflowPath: MANTIS_DISCORD_SMOKE_WORKFLOW,
|
|
jobName: "run_discord_smoke",
|
|
stepName: "Upload Mantis artifacts",
|
|
},
|
|
{
|
|
workflowPath: MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW,
|
|
jobName: "run_status_reactions",
|
|
stepName: "Upload Mantis status reaction artifacts",
|
|
},
|
|
{
|
|
workflowPath: MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW,
|
|
jobName: "run_thread_attachment",
|
|
stepName: "Upload Mantis thread attachment artifacts",
|
|
},
|
|
{
|
|
workflowPath: MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW,
|
|
jobName: "run_slack_desktop",
|
|
stepName: "Upload Mantis Slack desktop artifacts",
|
|
},
|
|
{
|
|
workflowPath: MANTIS_WEB_UI_CHAT_PROOF_WORKFLOW,
|
|
jobName: "run_web_ui_chat",
|
|
stepName: "Upload Mantis web UI chat artifacts",
|
|
},
|
|
{
|
|
workflowPath: NPM_TELEGRAM_WORKFLOW,
|
|
jobName: "run_package_telegram_e2e",
|
|
stepName: "Upload npm Telegram E2E artifacts",
|
|
},
|
|
];
|
|
|
|
for (const item of cases) {
|
|
const label = `${item.workflowPath} ${item.jobName}`;
|
|
const uploadStep = workflowStep(workflowJob(item.workflowPath, item.jobName), item.stepName);
|
|
|
|
expect(uploadStep.if, label).toContain("always()");
|
|
expect(uploadStep.uses, label).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(uploadStep.with?.["if-no-files-found"], label).toBe("error");
|
|
}
|
|
});
|
|
|
|
it("pins Mantis worktree ownership and candidate dependency installation", () => {
|
|
const cases = [
|
|
[MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW, "run_status_reactions", 2],
|
|
[MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW, "run_thread_attachment", 2],
|
|
[MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW, "run_slack_desktop", 1],
|
|
[MANTIS_WEB_UI_CHAT_PROOF_WORKFLOW, "run_web_ui_chat", 1],
|
|
] as const;
|
|
const owner = 'python3 -I -S "$CI_GIT_OWNER"';
|
|
let worktrees = 0;
|
|
|
|
for (const [workflowPath, jobName, count] of cases) {
|
|
const job = workflowJob(workflowPath, jobName);
|
|
const initialSteps = [
|
|
"Checkout harness ref",
|
|
...(jobName === "run_web_ui_chat" ? ["Allocate invocation evidence directory"] : []),
|
|
"Prepare Git owner",
|
|
"Setup Node environment",
|
|
];
|
|
expect(
|
|
job.steps?.slice(0, initialSteps.length).map(({ name }) => name),
|
|
workflowPath,
|
|
).toEqual(initialSteps);
|
|
expect(job.steps?.filter(({ name }) => name === "Prepare Git owner")).toEqual([
|
|
{
|
|
name: "Prepare Git owner",
|
|
uses: "openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475",
|
|
},
|
|
]);
|
|
const prepare =
|
|
workflowStep(
|
|
job,
|
|
count === 2 ? "Prepare baseline and candidate worktrees" : "Prepare candidate worktree",
|
|
).run ?? "";
|
|
const calls = prepare
|
|
.split("\n")
|
|
.map((line) => line.trim())
|
|
.filter((line) => line.includes(" worktree add "));
|
|
expect(calls, workflowPath).toEqual([
|
|
...(count === 2
|
|
? [
|
|
`${owner} --checkout-git 0 worktree add --detach "$worktree_root/baseline" "$${jobName === "run_status_reactions" ? "BASELINE_SHA" : "CANDIDATE_SHA"}"`,
|
|
]
|
|
: []),
|
|
`${owner} --checkout-git 0 worktree add --detach "$worktree_root/candidate" "$CANDIDATE_SHA"`,
|
|
]);
|
|
worktrees += calls.length;
|
|
expect(prepare.startsWith("set -euo pipefail\n")).toBe(true);
|
|
if (count === 2) {
|
|
expect(prepare).toContain('pnpm --dir "$lane_dir" install --frozen-lockfile');
|
|
expect(prepare).toContain('pnpm --dir "$lane_dir" build');
|
|
} else {
|
|
expect(prepare).toContain(
|
|
'pnpm --dir "$worktree_root/candidate" install --frozen-lockfile --prefer-offline',
|
|
);
|
|
expect(prepare.includes('pnpm --dir "$worktree_root/candidate" build')).toBe(
|
|
jobName === "run_slack_desktop",
|
|
);
|
|
}
|
|
}
|
|
expect(worktrees).toBe(6);
|
|
for (const workflowPath of workflowPaths().filter((file) => file.includes("/mantis-"))) {
|
|
expect(readFileSync(workflowPath, "utf8"), workflowPath).not.toMatch(
|
|
/\btimeout\b[^\n]*\bgit\b|\bgit\s+(?:-C\s+\S+\s+)?(?:clone|fetch|worktree\s+add)\b/u,
|
|
);
|
|
}
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"run_status_reactions",
|
|
MANTIS_DISCORD_STATUS_REACTIONS_WORKFLOW,
|
|
"Prepare baseline and candidate worktrees",
|
|
],
|
|
[
|
|
"run_thread_attachment",
|
|
MANTIS_DISCORD_THREAD_ATTACHMENT_WORKFLOW,
|
|
"Prepare baseline and candidate worktrees",
|
|
],
|
|
["run_slack_desktop", MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW, "Prepare candidate worktree"],
|
|
[
|
|
"run_telegram_proof",
|
|
MANTIS_TELEGRAM_BOT_E2E_PROOF_WORKFLOW,
|
|
"Build exact candidate without credentials or network",
|
|
],
|
|
])(
|
|
"sets up plugin-owned Crabbox before candidate execution in %s",
|
|
(jobName, workflowPath, candidateStep) => {
|
|
const workflow = readWorkflow(workflowPath);
|
|
const job = workflowJob(workflowPath, jobName);
|
|
const telegram = jobName === "run_telegram_proof";
|
|
const checkout = workflowStep(
|
|
job,
|
|
telegram ? "Checkout trusted harness" : "Checkout harness ref",
|
|
);
|
|
const tooling = workflowStep(
|
|
job,
|
|
telegram ? "Setup trusted tooling" : "Setup Node environment",
|
|
);
|
|
const install = workflowStep(
|
|
job,
|
|
telegram ? "Install Crabbox for disposable candidate lifecycle" : "Install Crabbox CLI",
|
|
);
|
|
const steps = job.steps ?? [];
|
|
|
|
expect(checkout.with?.["persist-credentials"]).toBe(false);
|
|
expect(tooling.uses).toBe("./.github/actions/setup-node-env");
|
|
expect(String(tooling.with?.["install-deps"] ?? true)).toBe("true");
|
|
expect(steps.indexOf(tooling)).toBeGreaterThan(steps.indexOf(checkout));
|
|
expect(steps.indexOf(install)).toBeGreaterThan(steps.indexOf(tooling));
|
|
expect(steps.indexOf(install)).toBeLessThan(steps.indexOf(workflowStep(job, candidateStep)));
|
|
expect(install.run).toBe("node scripts/crabbox-setup.mjs");
|
|
expect(install.env).toEqual({
|
|
OPENCLAW_STATE_DIR: "${{ runner.temp }}/openclaw-crabbox-setup",
|
|
});
|
|
expect(job.env?.OPENCLAW_STATE_DIR).toBeUndefined();
|
|
expect(workflow.env?.OPENCLAW_STATE_DIR).toBeUndefined();
|
|
expect(install.if).toBe(
|
|
telegram ? "${{ inputs.scenario == 'telegram-bot-e2e-proof' }}" : undefined,
|
|
);
|
|
if (telegram) {
|
|
expect(checkout.with?.ref).toBe("${{ github.workflow_sha }}");
|
|
}
|
|
},
|
|
);
|
|
|
|
it("maps every supported Slack approval checkpoint scenario family", () => {
|
|
const workflow = readFileSync(MANTIS_SLACK_DESKTOP_SMOKE_WORKFLOW, "utf8");
|
|
|
|
expectTextToIncludeAll(workflow, [
|
|
'endswith("-exec-native")',
|
|
'endswith("-plugin-native")',
|
|
'startswith("slack-codex-")',
|
|
'expected_result="Slack approval checkpoint passes for $scenario_label"',
|
|
]);
|
|
});
|
|
|
|
it("fails Docker E2E release lanes when summary artifacts are missing", () => {
|
|
const cases = [
|
|
{
|
|
jobName: "validate_docker_e2e",
|
|
summaryStep: "Summarize Docker E2E chunk",
|
|
uploadStep: "Upload Docker E2E chunk artifacts",
|
|
},
|
|
{
|
|
jobName: "validate_docker_lanes",
|
|
summaryStep: "Summarize targeted Docker E2E lanes",
|
|
uploadStep: "Upload targeted Docker E2E artifacts",
|
|
},
|
|
{
|
|
jobName: "validate_docker_openwebui",
|
|
summaryStep: "Summarize Open WebUI Docker E2E chunk",
|
|
uploadStep: "Upload Open WebUI Docker E2E artifacts",
|
|
},
|
|
];
|
|
|
|
for (const item of cases) {
|
|
const job = workflowJob(LIVE_E2E_WORKFLOW, item.jobName);
|
|
const summaryStep = workflowStep(job, item.summaryStep);
|
|
const uploadStep = workflowStep(job, item.uploadStep);
|
|
|
|
expect(summaryStep.run, item.jobName).toContain("summary missing:");
|
|
expect(summaryStep.run, item.jobName).toContain("exit 1");
|
|
expect(uploadStep.with?.["if-no-files-found"], item.jobName).toBe("error");
|
|
}
|
|
});
|
|
|
|
it("isolates Open WebUI release coverage with lean runtime setup", () => {
|
|
const job = workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_openwebui");
|
|
const setupNode = workflowStep(job, "Setup Node environment");
|
|
|
|
expect(job.if).toBe(
|
|
"(!inputs.prepare_only) && inputs.include_openwebui && inputs.docker_lanes == '' && (inputs.release_test_profile == 'stable' || inputs.release_test_profile == 'full')",
|
|
);
|
|
expect(job.env?.OPENCLAW_DOCKER_ALL_RELEASE_PROFILE).toBe("${{ inputs.release_test_profile }}");
|
|
expect(setupNode.with).toMatchObject({
|
|
"cache-mode": "off",
|
|
"install-bun": "false",
|
|
"install-deps": "false",
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
"accepts Telegram result success when enabled=true",
|
|
undefined,
|
|
0,
|
|
{ telegramEnabled: true, telegramResult: "success" },
|
|
],
|
|
[
|
|
"accepts Telegram result skipped when enabled=false",
|
|
undefined,
|
|
0,
|
|
{ telegramEnabled: false, telegramResult: "skipped" },
|
|
],
|
|
[
|
|
"rejects a skipped Telegram lane when package acceptance enabled it",
|
|
"::error::package_telegram ended with skipped",
|
|
1,
|
|
{ telegramEnabled: true, telegramResult: "skipped" },
|
|
],
|
|
[
|
|
"rejects package acceptance when no Docker transport ran",
|
|
"::error::No Docker acceptance transport ran",
|
|
1,
|
|
{
|
|
dockerArtifactResult: "skipped",
|
|
dockerRegistryResult: "skipped",
|
|
telegramEnabled: false,
|
|
telegramResult: "skipped",
|
|
},
|
|
],
|
|
[
|
|
"rejects a failed npm 12 installer acceptance lane",
|
|
"::error::npm_12_install_sh ended with failure",
|
|
1,
|
|
{
|
|
npm12InstallResult: "failure",
|
|
telegramEnabled: false,
|
|
telegramResult: "skipped",
|
|
},
|
|
],
|
|
[
|
|
"accepts Telegram-only profile when broad lanes skip and Telegram succeeds",
|
|
undefined,
|
|
0,
|
|
{
|
|
dockerArtifactResult: "skipped",
|
|
dockerRegistryResult: "skipped",
|
|
npm12InstallResult: "skipped",
|
|
suiteProfile: "telegram",
|
|
telegramEnabled: true,
|
|
telegramResult: "success",
|
|
},
|
|
],
|
|
[
|
|
"rejects Telegram-only profile when npm 12 acceptance runs",
|
|
"::error::npm_12_install_sh ran for suite_profile=telegram",
|
|
1,
|
|
{
|
|
dockerArtifactResult: "skipped",
|
|
dockerRegistryResult: "skipped",
|
|
suiteProfile: "telegram",
|
|
telegramEnabled: true,
|
|
telegramResult: "success",
|
|
},
|
|
],
|
|
[
|
|
"rejects Telegram-only profile when a Docker transport runs",
|
|
"::error::Docker acceptance ran for suite_profile=telegram",
|
|
1,
|
|
{
|
|
dockerRegistryResult: "skipped",
|
|
npm12InstallResult: "skipped",
|
|
suiteProfile: "telegram",
|
|
telegramEnabled: true,
|
|
telegramResult: "success",
|
|
},
|
|
],
|
|
] as const)("%s", (_name, expectedOutput, expectedStatus, params) => {
|
|
const result = runPackageAcceptanceSummary(params);
|
|
|
|
expect(result.status).toBe(expectedStatus);
|
|
if (expectedOutput) {
|
|
expect(result.stdout).toContain(expectedOutput);
|
|
} else {
|
|
expect(result.stderr).toBe("");
|
|
}
|
|
});
|
|
|
|
it("keeps Telegram and Docker package acceptance failures blocking", () => {
|
|
const telegramResult = runPackageAcceptanceSummary({
|
|
telegramEnabled: true,
|
|
telegramResult: "failure",
|
|
});
|
|
const dockerResult = runPackageAcceptanceSummary({
|
|
dockerArtifactResult: "failure",
|
|
telegramEnabled: true,
|
|
telegramResult: "success",
|
|
});
|
|
|
|
expect(telegramResult.status).toBe(1);
|
|
expect(telegramResult.stdout).toContain("::error::package_telegram ended with failure");
|
|
expect(dockerResult.status).toBe(1);
|
|
expect(dockerResult.stdout).toContain("::error::docker_acceptance ended with failure");
|
|
});
|
|
|
|
it.each(["failure", "skipped"] as const)(
|
|
"reports a package Telegram %s even when no suite report exists",
|
|
(outcome) => {
|
|
const report = workflowStep(
|
|
workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e"),
|
|
"Summarize Telegram attempt",
|
|
);
|
|
expect(report.if).toBe("always()");
|
|
const workdir = tempDirs.make("npm-telegram-attempt-summary-");
|
|
const summary = resolve(workdir, "summary.md");
|
|
const result = spawnSync("bash", ["-c", report.run ?? ""], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
GITHUB_STEP_SUMMARY: summary,
|
|
LANE_OUTCOME: outcome,
|
|
},
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stdout).toContain(`::error::Package Telegram attempt: ${outcome}`);
|
|
expect(readFileSync(summary, "utf8")).toContain(`Telegram attempt: ${outcome}`);
|
|
expect(readFileSync(summary, "utf8")).not.toContain("passed");
|
|
},
|
|
);
|
|
|
|
it("gives release build steps enough Node heap", () => {
|
|
for (const workflowPath of [LIVE_E2E_WORKFLOW, RELEASE_CHECKS_WORKFLOW]) {
|
|
const jobs = readWorkflow(workflowPath).jobs ?? {};
|
|
for (const [jobName, job] of Object.entries(jobs)) {
|
|
for (const step of job.steps ?? []) {
|
|
if (step.run === "pnpm build") {
|
|
expect(step.env, `${workflowPath}:${jobName}:${step.name}`).toMatchObject({
|
|
NODE_OPTIONS: "--max-old-space-size=8192",
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("runs full release children from the trusted workflow ref", () => {
|
|
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
|
|
const workflowInputs = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).on?.workflow_dispatch
|
|
?.inputs;
|
|
const resolveTargetJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "resolve_target");
|
|
const resolveTargetSteps = resolveTargetJob.steps ?? [];
|
|
const evidenceReuseJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse");
|
|
const releaseChecksJob = workflowJob(
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"release_checks_candidate",
|
|
);
|
|
const npmTelegramJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "npm_telegram");
|
|
const performanceJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance");
|
|
const summaryJob = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary");
|
|
const targetSummaryStep = workflowStep(resolveTargetJob, "Summarize target");
|
|
const targetManifestCheckout = workflowStep(
|
|
resolveTargetJob,
|
|
"Checkout target package manifest",
|
|
);
|
|
const detectPluginCompatibility = workflowStep(
|
|
resolveTargetJob,
|
|
"Detect target plugin compatibility gate",
|
|
);
|
|
const pluginCompatibilityJob = workflowJob(
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"plugin_compatibility_readiness",
|
|
);
|
|
const enforcePluginCompatibility = workflowStep(
|
|
pluginCompatibilityJob,
|
|
"Enforce target compatibility readiness",
|
|
);
|
|
const toolingIdentity = workflowStep(resolveTargetJob, "Resolve trusted workflow identity");
|
|
const releaseInputValidation = workflowStep(resolveTargetJob, "Validate release inputs");
|
|
const evidenceReuseStep = workflowStep(evidenceReuseJob, "Find reusable validation evidence");
|
|
const releaseChecksDispatchStep = workflowStep(
|
|
releaseChecksJob,
|
|
"Dispatch release checks candidate phase",
|
|
);
|
|
const dispatchStep = workflowStep(npmTelegramJob, "Dispatch npm Telegram E2E");
|
|
const verificationStep = workflowStep(summaryJob, "Verify exact release state artifacts");
|
|
const manifestStep = workflowStep(summaryJob, "Write release validation manifest");
|
|
|
|
expect(workflowInputs).toMatchObject({
|
|
skip_package_telegram_e2e: {
|
|
default: false,
|
|
type: "boolean",
|
|
},
|
|
trusted_workflow_json: {
|
|
default: "",
|
|
required: true,
|
|
type: "string",
|
|
},
|
|
});
|
|
expect(readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW).env).toMatchObject({
|
|
RELEASE_ISOLATION_TOOLING_CONTRACT: "2",
|
|
});
|
|
expect(workflow).toContain("CHILD_WORKFLOW_REF: ${{ github.ref_name }}");
|
|
expect(workflow).toContain('gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1');
|
|
expect(targetManifestCheckout.with).toMatchObject({
|
|
ref: "${{ steps.resolve.outputs.sha }}",
|
|
path: "target",
|
|
"sparse-checkout": "package.json",
|
|
"sparse-checkout-cone-mode": false,
|
|
"persist-credentials": false,
|
|
});
|
|
expect(resolveTargetSteps.indexOf(targetManifestCheckout)).toBeLessThan(
|
|
resolveTargetSteps.indexOf(releaseInputValidation),
|
|
);
|
|
expect(resolveTargetSteps.indexOf(targetManifestCheckout)).toBeLessThan(
|
|
resolveTargetSteps.indexOf(detectPluginCompatibility),
|
|
);
|
|
expect(detectPluginCompatibility.run).toContain('.scripts["plugins:boundary-report:ci"]');
|
|
expect(detectPluginCompatibility.run).toContain("38ba27834dd3f98c19d5833e0598dfef3abb7587");
|
|
expect(detectPluginCompatibility.run).toContain("Current target is missing");
|
|
expect(detectPluginCompatibility.run).toContain("required=false");
|
|
expect(resolveTargetJob.outputs?.plugin_compatibility_required).toBe(
|
|
"${{ steps.plugin_compatibility.outputs.required }}",
|
|
);
|
|
expect(pluginCompatibilityJob.needs).toEqual(["resolve_target"]);
|
|
expect(pluginCompatibilityJob.if).toBe(
|
|
"needs.resolve_target.outputs.plugin_compatibility_required == 'true'",
|
|
);
|
|
expect(enforcePluginCompatibility.run).toBe("pnpm plugins:boundary-report:ci");
|
|
for (const jobName of [
|
|
"normal_ci",
|
|
"plugin_prerelease_independent",
|
|
"release_checks_independent",
|
|
"release_checks_candidate",
|
|
"npm_telegram",
|
|
"performance",
|
|
"prepare_npm_package",
|
|
"prepare_docker_release",
|
|
]) {
|
|
const job = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, jobName);
|
|
expect(jobNeeds(job), jobName).toContain("plugin_compatibility_readiness");
|
|
expect(String(job.if), jobName).toContain("needs.plugin_compatibility_readiness.result");
|
|
}
|
|
const candidateCondition = String(releaseChecksJob.if).replace(
|
|
/^\$\{\{\s*([\s\S]*?)\s*\}\}$/u,
|
|
"$1",
|
|
);
|
|
for (const [compatibilityResult, admitted] of [
|
|
["success", true],
|
|
["skipped", true],
|
|
["failure", false],
|
|
["cancelled", false],
|
|
] as const) {
|
|
const result = runInNewContext(candidateCondition, {
|
|
github: { run_attempt: 1 },
|
|
inputs: { release_package_spec: "openclaw@next", rerun_group: "cross-os" },
|
|
needs: {
|
|
resolve_target: {
|
|
result: "success",
|
|
outputs: { live_suite_filter: "", release_candidate_artifact_required: "false" },
|
|
},
|
|
plugin_compatibility_readiness: { result: compatibilityResult },
|
|
evidence_reuse: { result: "success", outputs: { reuse: "false" } },
|
|
candidate_acquisition: { result: "skipped", outputs: {} },
|
|
},
|
|
always: () => true,
|
|
contains: (values: string | string[], value: string) => values.includes(value),
|
|
fromJSON: JSON.parse,
|
|
});
|
|
expect(Boolean(result), compatibilityResult).toBe(admitted);
|
|
}
|
|
expect(jobNeeds(evidenceReuseJob)).toContain("plugin_compatibility_readiness");
|
|
expect(evidenceReuseJob.if).toContain("always()");
|
|
expect(evidenceReuseJob.if).toContain("needs.resolve_target.result == 'success'");
|
|
expect(evidenceReuseJob.if).toContain("needs.plugin_compatibility_readiness.result");
|
|
expect(resolveTargetJob.outputs?.trusted_workflow_json).toBe(
|
|
"${{ steps.tooling_identity.outputs.json }}",
|
|
);
|
|
expect(toolingIdentity.env).toMatchObject({
|
|
GH_TOKEN: "${{ github.token }}",
|
|
REQUESTED_IDENTITY_JSON: "${{ steps.publication_dispatch.outputs.trusted_workflow_json }}",
|
|
WORKFLOW_CONTRACT: "${{ env.RELEASE_ISOLATION_TOOLING_CONTRACT }}",
|
|
WORKFLOW_FULL_REF: "${{ github.ref }}",
|
|
WORKFLOW_REF: "${{ github.ref_name }}",
|
|
WORKFLOW_SHA: "${{ github.sha }}",
|
|
});
|
|
expectTextToIncludeAll(toolingIdentity.run, [
|
|
"node workflow/scripts/release-tooling-identity.mjs resolve",
|
|
'--workflow-contract "$WORKFLOW_CONTRACT"',
|
|
'--requested-identity-json "$REQUESTED_IDENTITY_JSON"',
|
|
'echo "json=${identity}"',
|
|
]);
|
|
expectTextToIncludeAll(releaseInputValidation.run, [
|
|
'target_version="$(jq -er',
|
|
"is not reachable from release context branch",
|
|
"does not match release tag",
|
|
"target_context_ref must be a canonical OpenClaw release branch or tag.",
|
|
]);
|
|
expect(npmTelegramJob.name).toBe("Run package Telegram E2E");
|
|
expect(npmTelegramJob.needs).toEqual([
|
|
"resolve_target",
|
|
"plugin_compatibility_readiness",
|
|
"evidence_reuse",
|
|
]);
|
|
expect(npmTelegramJob["timeout-minutes"]).toBe(15);
|
|
expect(performanceJob["timeout-minutes"]).toBe(15);
|
|
expect(npmTelegramJob.if).toContain(
|
|
'contains(fromJSON(\'["all","npm-telegram"]\'), inputs.rerun_group)',
|
|
);
|
|
expect(npmTelegramJob.if).toContain("needs.evidence_reuse.outputs.reuse != 'true'");
|
|
expect(evidenceReuseStep.env).toMatchObject({
|
|
ALLOW_UNRELEASED_CHANGELOG:
|
|
"${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}",
|
|
NPM_TELEGRAM_PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec }}",
|
|
NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}",
|
|
NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}",
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}",
|
|
TRUSTED_WORKFLOW_JSON: "${{ needs.resolve_target.outputs.trusted_workflow_json }}",
|
|
});
|
|
expectTextToIncludeAll(evidenceReuseStep.run, [
|
|
"npmTelegramPackageSpec: $npmTelegramPackageSpec",
|
|
"npmTelegramProviderMode: $npmTelegramProviderMode",
|
|
"npmTelegramScenario: $npmTelegramScenario",
|
|
"skipPackageTelegramE2e: $skipPackageTelegramE2e",
|
|
"allowUnreleasedChangelog: $allowUnreleasedChangelog",
|
|
'trusted_workflow_ref="$(jq -er',
|
|
'trusted_workflow_full_ref="$(jq -er',
|
|
'trusted_workflow_sha="$(jq -er',
|
|
'--trusted-workflow-ref "$trusted_workflow_ref"',
|
|
'--trusted-workflow-full-ref "$trusted_workflow_full_ref"',
|
|
'--trusted-workflow-sha "$trusted_workflow_sha"',
|
|
]);
|
|
expect(targetSummaryStep.env).toMatchObject({
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "${{ steps.release_inputs.outputs.skip_package_telegram_e2e }}",
|
|
});
|
|
expectTextToIncludeAll(targetSummaryStep.run, [
|
|
"Validation SHA:",
|
|
"Frozen tuple:",
|
|
"Package Acceptance Telegram E2E deferred:",
|
|
"Package Telegram E2E: deferred by \\`skip_package_telegram_e2e\\`",
|
|
]);
|
|
expect(releaseChecksDispatchStep.env).toMatchObject({
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}",
|
|
});
|
|
expect(releaseChecksDispatchStep.run).toContain(
|
|
'-f skip_package_telegram_e2e="$SKIP_PACKAGE_TELEGRAM_E2E"',
|
|
);
|
|
expect(dispatchStep.env).toEqual({
|
|
CHILD_WORKFLOW_KIND: "npm-telegram",
|
|
CHILD_WORKFLOW_REF: "${{ github.ref_name }}",
|
|
GH_TOKEN: "${{ github.token }}",
|
|
PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec || inputs.release_package_spec }}",
|
|
PARENT_WORKFLOW_SHA: "${{ github.sha }}",
|
|
PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}",
|
|
SCENARIO: "${{ inputs.npm_telegram_scenario }}",
|
|
TARGET_SHA: "${{ needs.resolve_target.outputs.sha }}",
|
|
});
|
|
expect(manifestStep.env).toMatchObject({
|
|
ALLOW_UNRELEASED_CHANGELOG:
|
|
"${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}",
|
|
TARGET_REF:
|
|
"${{ startsWith(github.ref, 'refs/heads/release-ci/') && needs.resolve_target.outputs.sha || inputs.ref }}",
|
|
NPM_TELEGRAM_PACKAGE_SPEC: "${{ inputs.npm_telegram_package_spec }}",
|
|
NPM_TELEGRAM_PROVIDER_MODE: "${{ inputs.npm_telegram_provider_mode }}",
|
|
NPM_TELEGRAM_SCENARIO: "${{ inputs.npm_telegram_scenario }}",
|
|
SKIP_PACKAGE_TELEGRAM_E2E: "${{ needs.resolve_target.outputs.skip_package_telegram_e2e }}",
|
|
});
|
|
expect(manifestStep.run).toBe("node scripts/full-release-validation-state.mjs write-manifest");
|
|
expect(verificationStep.env).toMatchObject({
|
|
RELEASE_PROFILE: "${{ inputs.release_profile }}",
|
|
RERUN_GROUP: "${{ inputs.rerun_group }}",
|
|
});
|
|
expect(verificationStep.run).toBe("node scripts/full-release-validation-state.mjs verify");
|
|
expectTextToIncludeAll(dispatchStep.run, [
|
|
'dispatch_id="full-release-validation-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-npm-telegram"',
|
|
'dispatch_output="$(gh workflow run "$workflow" --ref "$CHILD_WORKFLOW_REF" "$@" 2>&1)"',
|
|
'dispatch_child npm-telegram-beta-e2e.yml "$dispatch_run_name" "${args[@]}"',
|
|
".display_title == env.DISPATCH_RUN_NAME and .head_branch == env.CHILD_WORKFLOW_REF",
|
|
"The dispatch was not retried to avoid creating a duplicate child.",
|
|
'if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then',
|
|
'echo "run_attempt=${child_run_attempt}" >> "$GITHUB_OUTPUT"',
|
|
'-f harness_ref="$TARGET_SHA"',
|
|
'args=(-f package_spec="$PACKAGE_SPEC"',
|
|
'args+=(-f scenario="$SCENARIO")',
|
|
]);
|
|
expect(dispatchStep.run).not.toContain("package_artifact");
|
|
expect(dispatchStep.run).not.toContain("allow_older_binary_destructive_actions");
|
|
expectTextToIncludeAll(workflow, [
|
|
'-f rerun_group="$RERUN_GROUP"',
|
|
'args+=(-f live_suite_filter="$LIVE_SUITE_FILTER")',
|
|
'args+=(-f cross_os_suite_filter="$CROSS_OS_SUITE_FILTER")',
|
|
"cancel-in-progress: false",
|
|
"FULL_RELEASE_PLAN_INPUTS_JSON",
|
|
"full-release-validation-policy.mjs",
|
|
"full-release-validation-state.mjs verify",
|
|
'FAIL_FAST: "false"',
|
|
"NORMAL_CI_RESULT: ${{ needs.normal_ci.result }}",
|
|
]);
|
|
expect(workflow).not.toContain("force-cancel");
|
|
expect(workflow).not.toContain("workflow_ref:");
|
|
expect(workflow).not.toContain("inputs.workflow_ref");
|
|
});
|
|
|
|
it("lets npm Telegram consume current-run or release-run package artifacts", () => {
|
|
const job = workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e");
|
|
const currentRunDownload = workflowStep(job, "Download package-under-test artifact");
|
|
const releaseRunDownload = workflowStep(
|
|
job,
|
|
"Download package-under-test artifact from release run",
|
|
);
|
|
const validateStep = workflowStep(job, "Validate inputs and secrets");
|
|
const identityStep = workflowStep(job, "Validate package artifact identity");
|
|
const runStep = workflowStep(job, "Run package Telegram E2E");
|
|
|
|
expect(currentRunDownload).toEqual({
|
|
if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id == github.run_id",
|
|
name: "Download package-under-test artifact",
|
|
uses: DOWNLOAD_ARTIFACT_V8,
|
|
with: {
|
|
"artifact-ids": "${{ inputs.package_artifact_id }}",
|
|
"github-token": "${{ github.token }}",
|
|
path: ".artifacts/telegram-package-under-test",
|
|
"run-id": "${{ inputs.package_artifact_run_id }}",
|
|
},
|
|
});
|
|
expect(releaseRunDownload).toEqual({
|
|
if: "inputs.package_artifact_name != '' && inputs.package_artifact_run_id != github.run_id",
|
|
name: "Download package-under-test artifact from release run",
|
|
uses: DOWNLOAD_ARTIFACT_V8,
|
|
with: {
|
|
"artifact-ids": "${{ inputs.package_artifact_id }}",
|
|
"github-token": "${{ github.token }}",
|
|
path: ".artifacts/telegram-package-under-test",
|
|
"run-id": "${{ inputs.package_artifact_run_id }}",
|
|
},
|
|
});
|
|
expectTextToIncludeAll(validateStep.run, [
|
|
'if [[ -z "${PACKAGE_ARTIFACT_NAME// }" ]]; then',
|
|
"Artifact-backed Telegram E2E requires all artifact identity fields or none.",
|
|
"package_spec must be openclaw@beta",
|
|
"Artifact-backed Telegram E2E requires the complete immutable artifact and package identity tuple.",
|
|
]);
|
|
expect(identityStep.env).toMatchObject({
|
|
ARTIFACT_DIGEST: "${{ inputs.package_artifact_digest }}",
|
|
ARTIFACT_ID: "${{ inputs.package_artifact_id }}",
|
|
ARTIFACT_NAME: "${{ inputs.package_artifact_name }}",
|
|
ARTIFACT_RUN_ATTEMPT: "${{ inputs.package_artifact_run_attempt }}",
|
|
ARTIFACT_RUN_ID: "${{ inputs.package_artifact_run_id }}",
|
|
});
|
|
expectTextToIncludeAll(identityStep.run, [
|
|
"actions/artifacts/${ARTIFACT_ID}",
|
|
'--arg digest "sha256:${ARTIFACT_DIGEST}"',
|
|
"actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}",
|
|
'if [[ "$ARTIFACT_RUN_ID" == "$GITHUB_RUN_ID" ]]',
|
|
'.status == "pending" or .status == "queued" or .status == "requested" or .status == "waiting" or .status == "in_progress"',
|
|
".conclusion == null",
|
|
"Package Telegram artifact predates the active producer run attempt.",
|
|
'.status == "completed"',
|
|
'.conclusion == "success"',
|
|
"artifact_created_at <= attempt_started_at",
|
|
"artifact_created_at > attempt_completed_at",
|
|
"Package Telegram artifact creation time is outside the declared producer run attempt.",
|
|
"Package Telegram artifact producer run attempt does not match the requested tuple.",
|
|
"actions/runs/${ARTIFACT_RUN_ID}/attempts/${ARTIFACT_RUN_ATTEMPT}/jobs?per_page=100",
|
|
"Resolve package candidate",
|
|
"Prior-attempt Package Telegram artifact lacks one exact successful producer job.",
|
|
"Package Telegram artifact creation time is outside the successful producer job.",
|
|
]);
|
|
expect(runStep.env).toMatchObject({
|
|
PACKAGE_FILE_NAME: "${{ inputs.package_file_name || '' }}",
|
|
PACKAGE_SHA256: "${{ inputs.package_sha256 || '' }}",
|
|
PACKAGE_SOURCE_SHA: "${{ inputs.package_source_sha || '' }}",
|
|
PACKAGE_VERSION: "${{ inputs.package_version || '' }}",
|
|
});
|
|
expectTextToIncludeAll(runStep.run, [
|
|
'declared_package_tgz="${package_dir}/${PACKAGE_FILE_NAME}"',
|
|
'manifest="${package_dir}/preflight-manifest.json"',
|
|
'candidate_manifest="${package_dir}/package-candidate.json"',
|
|
'find "${package_dir}" -type f -name "*.tgz"',
|
|
"package artifact manifest contains duplicate package metadata",
|
|
"Array.isArray(manifest.corePackageTarballs)",
|
|
"manifest.corePackageTarballs === undefined",
|
|
"package artifact tarball set does not match preflight manifest",
|
|
"package candidate manifest does not match the OpenClaw tarball",
|
|
"Package Telegram artifact SHA-256 differs from package_sha256.",
|
|
"package candidate digest mismatch",
|
|
"Package Telegram artifact tarball differs from package_file_name.",
|
|
"Package Telegram artifact source SHA/version differs from the declared identity.",
|
|
'export OPENCLAW_NPM_TELEGRAM_PACKAGE_DIR="${package_dir}"',
|
|
'export OPENCLAW_NPM_TELEGRAM_PACKAGE_TGZ="${package_tgz}"',
|
|
]);
|
|
});
|
|
|
|
it("accepts immutable artifacts produced earlier in the active workflow attempt", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunId: "123",
|
|
producerConclusion: null,
|
|
producerRunId: "123",
|
|
producerStatus: "in_progress",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it("accepts active artifacts while GitHub still reports the workflow as queued", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunId: "123",
|
|
producerConclusion: null,
|
|
producerRunId: "123",
|
|
producerStatus: "queued",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it("accepts active artifacts while GitHub still reports the workflow as pending", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunId: "123",
|
|
producerConclusion: null,
|
|
producerRunId: "123",
|
|
producerStatus: "pending",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it("accepts a prior-attempt artifact from the exact successful package producer", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunAttempt: "3",
|
|
currentRunId: "123",
|
|
producerConclusion: "failure",
|
|
producerRunId: "123",
|
|
producerStatus: "completed",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it("rejects a prior-attempt artifact when the package producer failed", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunAttempt: "3",
|
|
currentRunId: "123",
|
|
producerConclusion: "failure",
|
|
producerJobConclusion: "failure",
|
|
producerRunId: "123",
|
|
producerStatus: "completed",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"Prior-attempt Package Telegram artifact lacks one exact successful producer job.",
|
|
);
|
|
});
|
|
|
|
it("rejects queued artifacts after GitHub assigns a conclusion", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunId: "123",
|
|
producerConclusion: "success",
|
|
producerRunId: "123",
|
|
producerStatus: "queued",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"Current-run Package Telegram artifact is not from the active workflow attempt.",
|
|
);
|
|
});
|
|
|
|
it("keeps completed external producer attempts success-gated", () => {
|
|
const result = runNpmTelegramArtifactValidation({
|
|
currentRunId: "456",
|
|
producerConclusion: "success",
|
|
producerRunId: "123",
|
|
producerStatus: "completed",
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it("rejects partial npm Telegram artifact identity instead of falling back to npm", () => {
|
|
const result = runNpmTelegramInputValidation({
|
|
PACKAGE_ARTIFACT_ID: "123",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"Artifact-backed Telegram E2E requires all artifact identity fields or none.",
|
|
);
|
|
});
|
|
|
|
it("accepts direct package artifacts and validates an optional registry tuple", () => {
|
|
const packageTuple = {
|
|
PACKAGE_ARTIFACT_DIGEST: "a".repeat(64),
|
|
PACKAGE_ARTIFACT_ID: "123",
|
|
PACKAGE_ARTIFACT_NAME: "package-under-test",
|
|
PACKAGE_ARTIFACT_RUN_ATTEMPT: "2",
|
|
PACKAGE_ARTIFACT_RUN_ID: "456",
|
|
PACKAGE_FILE_NAME: "openclaw-2026.8.1.tgz",
|
|
PACKAGE_SHA256: "b".repeat(64),
|
|
PACKAGE_SOURCE_SHA: "c".repeat(40),
|
|
PACKAGE_VERSION: "2026.8.1",
|
|
};
|
|
const registryTuple = {
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_DIGEST: "d".repeat(64),
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID: "789",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME: "docker-e2e-prepublish-plugin-registry-123-1",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ATTEMPT: "1",
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_RUN_ID: "123",
|
|
PREPUBLISH_PLUGIN_REGISTRY_MANIFEST_SHA256: "e".repeat(64),
|
|
};
|
|
|
|
expect(runNpmTelegramInputValidation(packageTuple).status).toBe(0);
|
|
expect(runNpmTelegramInputValidation({ ...packageTuple, ...registryTuple }).status).toBe(0);
|
|
expect(
|
|
runNpmTelegramInputValidation({
|
|
...packageTuple,
|
|
...registryTuple,
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME:
|
|
"package-acceptance-telegram-plugin-registry-123-1",
|
|
}).status,
|
|
).toBe(0);
|
|
|
|
const partial = runNpmTelegramInputValidation({
|
|
...packageTuple,
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID: "789",
|
|
});
|
|
expect(partial.status).toBe(1);
|
|
expect(partial.stderr).toContain(
|
|
"Artifact-backed Telegram E2E requires the complete prerelease plugin registry tuple.",
|
|
);
|
|
|
|
const wrongName = runNpmTelegramInputValidation({
|
|
...packageTuple,
|
|
...registryTuple,
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_NAME: "wrong-name",
|
|
});
|
|
expect(wrongName.status).toBe(1);
|
|
expect(wrongName.stderr).toContain(
|
|
"Prerelease plugin registry artifact name does not match its producer run.",
|
|
);
|
|
});
|
|
|
|
it("rejects prerelease plugin registry inputs without a package artifact", () => {
|
|
const result = runNpmTelegramInputValidation({
|
|
PREPUBLISH_PLUGIN_REGISTRY_ARTIFACT_ID: "789",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stderr).toContain(
|
|
"Prerelease plugin registry inputs require an artifact-backed OpenClaw package.",
|
|
);
|
|
});
|
|
|
|
it("uses bounded Convex lease waits instead of GitHub concurrency for CI Telegram consumers", () => {
|
|
const telegramJobs = [
|
|
[NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e", "Run package Telegram E2E", undefined],
|
|
[RELEASE_TELEGRAM_QA_WORKFLOW, "run_telegram", "Run Telegram live lane", undefined],
|
|
[QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_telegram", "Run Telegram live lane", "1800000"],
|
|
] as const;
|
|
|
|
for (const [workflowPath, jobName, stepName, acquireTimeoutMs] of telegramJobs) {
|
|
const job = workflowJob(workflowPath, jobName);
|
|
expect(job.concurrency).toBeUndefined();
|
|
const step = workflowStep(job, stepName);
|
|
expect(step.env?.OPENCLAW_QA_CREDENTIAL_ACQUIRE_TIMEOUT_MS).toBe(acquireTimeoutMs);
|
|
}
|
|
});
|
|
|
|
it("keeps release QA and repo E2E lanes off scarce 32-core runners", () => {
|
|
const liveE2eWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
|
|
|
|
for (const jobName of [
|
|
"qa_lab_parity_lane_release_checks",
|
|
"qa_lab_parity_report_release_checks",
|
|
]) {
|
|
expect(evaluateWorkflowRunner(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName)["runs-on"])).toBe(
|
|
"ubuntu-24.04",
|
|
);
|
|
}
|
|
for (const jobName of [
|
|
"trusted_identity",
|
|
"build_candidate",
|
|
"attest_candidate",
|
|
"run_telegram",
|
|
"advisory_status",
|
|
]) {
|
|
expect(
|
|
evaluateWorkflowRunner(workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, jobName)["runs-on"]),
|
|
).toBe("ubuntu-24.04");
|
|
}
|
|
|
|
expectTextToIncludeAll(liveE2eWorkflow, [
|
|
"OPENCLAW_LIVE_GATEWAY_STEP_TIMEOUT_MS=180000",
|
|
"OPENCLAW_LIVE_GATEWAY_MODEL_TIMEOUT_MS=600000",
|
|
]);
|
|
});
|
|
|
|
describe("release check artifact resolver", () => {
|
|
const runId = "123456";
|
|
const targetSha = "a".repeat(40);
|
|
const pair = (job: string, variant: string, slug: string): ReleaseCheckArtifactPair => ({
|
|
job,
|
|
payloadBase: `release-payload-${slug}-${targetSha}-${runId}`,
|
|
statusBase: `release-status-${slug}-${targetSha}-${runId}`,
|
|
variant,
|
|
});
|
|
const artifactsFor = (
|
|
artifactPair: ReleaseCheckArtifactPair,
|
|
attempt: number,
|
|
firstId: number,
|
|
options: { expiredPayload?: boolean; expiredStatus?: boolean } = {},
|
|
): ReleaseCheckArtifact[] => [
|
|
releaseCheckArtifact({
|
|
expired: options.expiredStatus,
|
|
id: firstId,
|
|
name: `${artifactPair.statusBase}-${attempt}`,
|
|
runId,
|
|
}),
|
|
releaseCheckArtifact({
|
|
expired: options.expiredPayload,
|
|
id: firstId + 1,
|
|
name: `${artifactPair.payloadBase}-${attempt}`,
|
|
runId,
|
|
}),
|
|
];
|
|
|
|
it.each([
|
|
[
|
|
"selects the current producer attempt",
|
|
[1, 2].flatMap((attempt, index) =>
|
|
artifactsFor(pair("qa_job", "candidate", "candidate"), attempt, index * 10 + 1),
|
|
),
|
|
"2",
|
|
2,
|
|
],
|
|
[
|
|
"carries attempt 1 into consumer attempt 2",
|
|
artifactsFor(pair("qa_job", "candidate", "candidate"), 1, 1),
|
|
"2",
|
|
1,
|
|
],
|
|
[
|
|
"orders producer attempts numerically",
|
|
[2, 10].flatMap((attempt, index) =>
|
|
artifactsFor(pair("qa_job", "candidate", "candidate"), attempt, index * 10 + 1),
|
|
),
|
|
"10",
|
|
10,
|
|
],
|
|
[
|
|
"excludes future producer attempts",
|
|
[2, 3].flatMap((attempt, index) =>
|
|
artifactsFor(pair("qa_job", "candidate", "candidate"), attempt, index * 10 + 1),
|
|
),
|
|
"2",
|
|
2,
|
|
],
|
|
])("%s", (_name, artifacts, consumerAttempt, expectedAttempt) => {
|
|
const result = runReleaseCheckArtifactResolve({
|
|
artifacts,
|
|
consumerAttempt,
|
|
pairs: [pair("qa_job", "candidate", "candidate")],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
|
|
expect(result.result.status, result.result.stderr).toBe(0);
|
|
expect(result.selection).toHaveLength(1);
|
|
expect(result.selection[0]?.producer_attempt).toBe(expectedAttempt);
|
|
});
|
|
|
|
it("selects candidate and baseline attempts independently", () => {
|
|
const candidate = pair("qa_lab_parity_lane_release_checks", "candidate", "candidate");
|
|
const baseline = pair("qa_lab_parity_lane_release_checks", "baseline", "baseline");
|
|
const result = runReleaseCheckArtifactResolve({
|
|
artifacts: [...artifactsFor(candidate, 1, 1), ...artifactsFor(baseline, 2, 11)],
|
|
consumerAttempt: "2",
|
|
pairs: [candidate, baseline],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
|
|
expect(result.result.status, result.result.stderr).toBe(0);
|
|
expect(
|
|
Object.fromEntries(
|
|
result.selection.map((selection) => [selection.variant, selection.producer_attempt]),
|
|
),
|
|
).toEqual({ baseline: 2, candidate: 1 });
|
|
});
|
|
|
|
it("fails when the latest producer attempt has no complete pair", () => {
|
|
const candidate = pair("qa_job", "candidate", "candidate");
|
|
const result = runReleaseCheckArtifactResolve({
|
|
artifacts: [
|
|
...artifactsFor(candidate, 1, 1),
|
|
releaseCheckArtifact({
|
|
id: 11,
|
|
name: `${candidate.statusBase}-2`,
|
|
runId,
|
|
}),
|
|
],
|
|
consumerAttempt: "2",
|
|
pairs: [candidate],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
|
|
expect(result.result.status).toBe(1);
|
|
expect(result.result.stderr).toContain(
|
|
`requires exactly one ${candidate.payloadBase}-2 artifact; found 0`,
|
|
);
|
|
expect(result.result.stderr.trimEnd()).toMatch(
|
|
/\[resolve-release-check-artifacts\] FAILED \(exit 1\)$/u,
|
|
);
|
|
});
|
|
|
|
it("fails on duplicate artifacts at the latest producer attempt", () => {
|
|
const candidate = pair("qa_job", "candidate", "candidate");
|
|
const artifacts = artifactsFor(candidate, 2, 1);
|
|
artifacts.push(
|
|
releaseCheckArtifact({
|
|
id: 11,
|
|
name: `${candidate.statusBase}-2`,
|
|
runId,
|
|
}),
|
|
);
|
|
const result = runReleaseCheckArtifactResolve({
|
|
artifacts,
|
|
consumerAttempt: "2",
|
|
pairs: [candidate],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
|
|
expect(result.result.status).toBe(1);
|
|
expect(result.result.stderr).toContain(
|
|
`requires exactly one ${candidate.statusBase}-2 artifact; found 2`,
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
artifacts: (candidate: ReleaseCheckArtifactPair) => [
|
|
...artifactsFor(candidate, 1, 1),
|
|
releaseCheckArtifact({
|
|
id: 11,
|
|
name: `${candidate.statusBase}-broken`,
|
|
runId,
|
|
}),
|
|
],
|
|
expected: "has malformed producer attempt",
|
|
name: "malformed newer evidence",
|
|
},
|
|
{
|
|
artifacts: (candidate: ReleaseCheckArtifactPair) => [
|
|
...artifactsFor(candidate, 1, 1),
|
|
...artifactsFor(candidate, 2, 11, { expiredPayload: true }),
|
|
],
|
|
expected: "is expired or has invalid expiry metadata",
|
|
name: "expired newer evidence",
|
|
},
|
|
])("does not fall back past $name", ({ artifacts, expected }) => {
|
|
const candidate = pair("qa_job", "candidate", "candidate");
|
|
const result = runReleaseCheckArtifactResolve({
|
|
artifacts: artifacts(candidate),
|
|
consumerAttempt: "2",
|
|
pairs: [candidate],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
|
|
expect(result.result.status).toBe(1);
|
|
expect(result.result.stderr).toContain(expected);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
mutate: (text: string) => text.replace("status=success", "status=success\nstatus=failure"),
|
|
name: "duplicate status fields",
|
|
},
|
|
{
|
|
mutate: (text: string) => text.replace("run_attempt=2", "run_attempt=bogus"),
|
|
name: "malformed status metadata",
|
|
},
|
|
])("rejects $name", ({ mutate }) => {
|
|
const candidate = pair("qa_job", "candidate", "candidate");
|
|
const resolved = runReleaseCheckArtifactResolve({
|
|
artifacts: artifactsFor(candidate, 2, 1),
|
|
consumerAttempt: "2",
|
|
pairs: [candidate],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
expect(resolved.result.status, resolved.result.stderr).toBe(0);
|
|
|
|
const validated = runReleaseCheckArtifactValidation({
|
|
selection: resolved.selection,
|
|
statusText: (selection) => mutate(releaseCheckStatusText(selection)),
|
|
});
|
|
expect(validated.result.status).toBe(1);
|
|
});
|
|
|
|
it("sets runtime parity ready=false for validated non-success evidence", () => {
|
|
const runtimePair = pair("qa_lab_runtime_parity_release_checks", "", "runtime-parity");
|
|
const resolved = runReleaseCheckArtifactResolve({
|
|
artifacts: artifactsFor(runtimePair, 2, 1),
|
|
consumerAttempt: "2",
|
|
pairs: [runtimePair],
|
|
runId,
|
|
targetSha,
|
|
});
|
|
expect(resolved.result.status, resolved.result.stderr).toBe(0);
|
|
|
|
const workdir = tempDirs.make("runtime-parity-ready-");
|
|
const trustedScript = resolve(
|
|
workdir,
|
|
"trusted-release-check-artifacts/scripts/github/resolve-release-check-artifacts.sh",
|
|
);
|
|
mkdirSync(resolve(trustedScript, ".."), { recursive: true });
|
|
symlinkSync(resolve(REPO_ROOT, RELEASE_CHECK_ARTIFACT_RESOLVER), trustedScript);
|
|
const selectionFile = resolve(workdir, "selection.json");
|
|
writeFileSync(selectionFile, JSON.stringify(resolved.selection));
|
|
const statusDir = resolve(workdir, ".artifacts/release-check-status");
|
|
mkdirSync(statusDir, { recursive: true });
|
|
const selection = resolved.selection[0]!;
|
|
writeFileSync(
|
|
resolve(
|
|
statusDir,
|
|
`${selection.job}-${selection.run_id}-${selection.producer_attempt}.env`,
|
|
),
|
|
releaseCheckStatusText(selection, "failure"),
|
|
);
|
|
const outputFile = resolve(workdir, "github-output");
|
|
const runtimeCoverage = workflowJob(
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"runtime_tool_coverage_release_checks",
|
|
);
|
|
const script = workflowStep(
|
|
runtimeCoverage,
|
|
"Verify runtime parity producer status",
|
|
).run?.replace(
|
|
"${{ steps.resolve_runtime_parity_artifacts.outputs.selection_file }}",
|
|
selectionFile,
|
|
);
|
|
expect(script).toBeTruthy();
|
|
const result = spawnSync("bash", ["-c", script!], {
|
|
cwd: workdir,
|
|
encoding: "utf8",
|
|
env: {
|
|
GITHUB_OUTPUT: outputFile,
|
|
PATH: process.env.PATH,
|
|
},
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(readFileSync(outputFile, "utf8")).toContain("ready=false");
|
|
});
|
|
});
|
|
|
|
it("keeps release QA status artifacts blocking in the verifier", () => {
|
|
const advisoryJobNames = [
|
|
"qa_lab_parity_lane_release_checks",
|
|
"qa_lab_parity_report_release_checks",
|
|
"qa_lab_runtime_parity_release_checks",
|
|
"qa_live_discord_release_checks",
|
|
"qa_live_whatsapp_release_checks",
|
|
"qa_live_slack_release_checks",
|
|
];
|
|
|
|
for (const jobName of advisoryJobNames) {
|
|
const job = workflowJob(RELEASE_CHECKS_WORKFLOW, jobName);
|
|
expect(job["continue-on-error"], jobName).toBeUndefined();
|
|
|
|
const recordStep = workflowStep(job, "Record advisory status");
|
|
expect(recordStep.if, jobName).toBe("always()");
|
|
expect(recordStep.run, jobName).toContain("status_path=");
|
|
expect(recordStep.run, jobName).toContain(".artifacts/release-check-status");
|
|
expect(recordStep.run, jobName).toContain("GITHUB_RUN_ID");
|
|
expect(recordStep.run, jobName).toContain("GITHUB_RUN_ATTEMPT");
|
|
expect(recordStep.run, jobName).toContain("target_sha=");
|
|
expect(recordStep.run, jobName).toContain("variant=");
|
|
expect(recordStep.env?.RELEASE_CHECK_TARGET_SHA, jobName).toBe(
|
|
"${{ needs.resolve_target.outputs.revision }}",
|
|
);
|
|
expect(recordStep.env?.RELEASE_CHECK_STEP_OUTCOMES, jobName).toContain("upload_");
|
|
|
|
const uploadStep = workflowStep(job, "Upload advisory status");
|
|
expect(uploadStep.if, jobName).toBe("always()");
|
|
expect(uploadStep.uses, jobName).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(uploadStep.with?.name, jobName).toContain("release-check-status-");
|
|
expect(uploadStep.with?.name, jobName).toContain(
|
|
"${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(uploadStep.with?.path, jobName).toMatch(
|
|
/^\.artifacts\/release-check-status\/.+\$\{\{ github\.run_id \}\}-\$\{\{ github\.run_attempt \}\}\.env$/u,
|
|
);
|
|
expect(uploadStep.with?.["if-no-files-found"], jobName).toBe("error");
|
|
}
|
|
|
|
for (const [jobName, stepName] of [
|
|
["qa_lab_parity_lane_release_checks", "Upload parity lane artifacts"],
|
|
["qa_lab_parity_report_release_checks", "Upload parity artifacts"],
|
|
["qa_lab_runtime_pair_lane_release_checks", "Upload runtime-pair lane artifacts"],
|
|
["qa_lab_runtime_parity_release_checks", "Upload runtime parity artifacts"],
|
|
["qa_live_discord_release_checks", "Upload Discord QA artifacts"],
|
|
["qa_live_whatsapp_release_checks", "Upload WhatsApp QA artifacts"],
|
|
["qa_live_slack_release_checks", "Upload Slack QA artifacts"],
|
|
] as const) {
|
|
const upload = workflowStep(workflowJob(RELEASE_CHECKS_WORKFLOW, jobName), stepName);
|
|
expect(upload.with?.name, `${jobName}/${stepName}`).toContain(
|
|
"${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
}
|
|
|
|
for (const jobName of [
|
|
"qa_lab_parity_report_release_checks",
|
|
"qa_lab_runtime_parity_release_checks",
|
|
"runtime_tool_coverage_release_checks",
|
|
"summary",
|
|
]) {
|
|
const checkout = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, jobName),
|
|
"Checkout trusted release artifact resolver",
|
|
);
|
|
expect(checkout.with).toMatchObject({
|
|
path: "trusted-release-check-artifacts",
|
|
ref: "${{ github.sha }}",
|
|
"sparse-checkout": RELEASE_CHECK_ARTIFACT_RESOLVER,
|
|
"sparse-checkout-cone-mode": false,
|
|
});
|
|
}
|
|
|
|
const telegramCaller = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_telegram_release_checks");
|
|
const telegramDispatch = workflowStep(telegramCaller, "Dispatch and await trusted Telegram QA");
|
|
expect(telegramDispatch.run).toContain('workflow="openclaw-release-telegram-qa.yml"');
|
|
expect(telegramDispatch.run).toContain('--repo "$GITHUB_REPOSITORY"');
|
|
expect(telegramDispatch.env).toMatchObject({
|
|
PARENT_WORKFLOW_REF: "${{ github.ref_name }}",
|
|
PARENT_WORKFLOW_SHA: "${{ github.sha }}",
|
|
TARGET_CONTEXT_REF: "${{ inputs.target_context_ref }}",
|
|
});
|
|
expect(telegramDispatch.run).toContain('--ref "$PARENT_WORKFLOW_REF"');
|
|
expect(telegramDispatch.run).toContain(
|
|
'-f expected_trusted_workflow_sha="$PARENT_WORKFLOW_SHA"',
|
|
);
|
|
expect(telegramDispatch.run).toContain('-f target_context_ref="$TARGET_CONTEXT_REF"');
|
|
expect(telegramDispatch.run).toContain('[[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]');
|
|
expect(telegramDispatch.run).not.toContain("commits/main");
|
|
expect(telegramDispatch.run).not.toContain("dispatch_attempt");
|
|
expect(telegramCaller["continue-on-error"]).toBeUndefined();
|
|
expect(telegramCaller.outputs?.identity_verified).toBe(
|
|
"${{ steps.dispatch.outputs.identity_verified }}",
|
|
);
|
|
expect(telegramDispatch.run?.indexOf('echo "identity_verified=true"')).toBeGreaterThan(
|
|
telegramDispatch.run?.indexOf('[[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]') ?? -1,
|
|
);
|
|
expect(telegramCaller["timeout-minutes"]).toBe(210);
|
|
|
|
const telegramStatus = workflowJob(RELEASE_TELEGRAM_QA_WORKFLOW, "advisory_status");
|
|
expect(telegramStatus["continue-on-error"]).toBeUndefined();
|
|
const telegramRecord = workflowStep(telegramStatus, "Record advisory status");
|
|
expect(telegramRecord.run?.trim()).toBe(
|
|
"set -euo pipefail\nnode scripts/release-telegram-qa.mjs advisory-status",
|
|
);
|
|
const telegramStatusUpload = workflowStep(telegramStatus, "Upload advisory status");
|
|
expect(telegramStatusUpload.if).toBe("always()");
|
|
expect(telegramStatusUpload.uses).toBe(UPLOAD_ARTIFACT_V7);
|
|
expect(telegramStatusUpload.with?.name).toBe(
|
|
"release-check-status-qa-live-telegram-${{ inputs.target_sha }}-${{ github.run_id }}-${{ github.run_attempt }}",
|
|
);
|
|
expect(telegramStatusUpload.with?.path).toContain(
|
|
"${{ steps.record_status.outputs.status_file }}",
|
|
);
|
|
expect(telegramStatusUpload.with?.["if-no-files-found"]).toBe("error");
|
|
const telegramRequire = workflowStep(
|
|
telegramStatus,
|
|
"Require successful Telegram release check",
|
|
);
|
|
expect(telegramRequire.if).toBe("always()");
|
|
expect(telegramRequire.run).toContain('[[ "$STATUS" == "success" ]]');
|
|
|
|
const summary = workflowJob(RELEASE_CHECKS_WORKFLOW, "summary");
|
|
expect(summary.needs).toContain("resolve_target");
|
|
expect(summary.permissions?.actions).toBe("read");
|
|
expect(summary.permissions?.contents).toBe("read");
|
|
const resolveStep = workflowStep(summary, "Resolve advisory evidence artifacts");
|
|
expect(resolveStep["continue-on-error"]).toBe(true);
|
|
expect(resolveStep.run).toContain(
|
|
"trusted-release-check-artifacts/scripts/github/resolve-release-check-artifacts.sh",
|
|
);
|
|
expect(resolveStep.run).toContain('--consumer-attempt "$GITHUB_RUN_ATTEMPT"');
|
|
expect(resolveStep.run).toContain("qa_lab_parity_lane_release_checks|candidate");
|
|
expect(resolveStep.run).toContain("qa_lab_parity_lane_release_checks|baseline");
|
|
const downloadStep = workflowStep(summary, "Download advisory status artifacts");
|
|
expect(downloadStep["continue-on-error"]).toBe(true);
|
|
expect(downloadStep.uses).toBe(DOWNLOAD_ARTIFACT_V8);
|
|
expect(downloadStep.with?.["artifact-ids"]).toBe(
|
|
"${{ steps.resolve_advisory_evidence.outputs.status_ids }}",
|
|
);
|
|
expect(downloadStep.with?.["merge-multiple"]).toBe(true);
|
|
expect(downloadStep.with?.pattern).toBeUndefined();
|
|
|
|
const verifyStep = workflowStep(summary, "Verify release check results");
|
|
expect(verifyStep.env).toMatchObject({
|
|
QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT: "${{ needs.qa_live_buzz_release_checks.result }}",
|
|
QA_LIVE_TELEGRAM_RELEASE_CHECKS_RESULT:
|
|
"${{ needs.qa_live_telegram_release_checks.outputs.conclusion || needs.qa_live_telegram_release_checks.result }}",
|
|
QA_LIVE_RELEASE_CHECKS_RESULT: "${{ needs.qa_live_release_checks.result }}",
|
|
RELEASE_CHECK_RUN_ATTEMPT: "${{ github.run_attempt }}",
|
|
RELEASE_CHECK_RUN_ID: "${{ github.run_id }}",
|
|
RELEASE_CHECK_TARGET_SHA: "${{ needs.resolve_target.outputs.revision }}",
|
|
RESOLVE_ADVISORY_EVIDENCE_OUTCOME: "${{ steps.resolve_advisory_evidence.outcome }}",
|
|
VALIDATE_ADVISORY_STATUSES_OUTCOME: "${{ steps.validate_advisory_statuses.outcome }}",
|
|
});
|
|
expectTextToIncludeAll(verifyStep.run, [
|
|
"release_check_result()",
|
|
"validated_status()",
|
|
"advisory-evidence-validated.json",
|
|
"missing or duplicate validated status",
|
|
"Advisory evidence resolution or validation failed",
|
|
'elif [[ "$fallback" != "success" && "$fallback" != "skipped" ]]; then',
|
|
'elif [[ "$fallback" == "success" ]]; then',
|
|
"advisory_status_override_allowed()",
|
|
'if advisory_status_override_allowed "$name"; then',
|
|
"::error::${name} ended with ${result}",
|
|
'"qa_live_release_checks=${QA_LIVE_RELEASE_CHECKS_RESULT}"',
|
|
'"qa_live_buzz_release_checks=${QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT}"',
|
|
]);
|
|
expect(verifyStep.run).not.toContain("qa_live_matrix_release_checks");
|
|
expect(verifyStep.run).not.toContain(
|
|
"QA release-check lanes are advisory and do not block release validation.",
|
|
);
|
|
expect(verifyStep.run).not.toContain("expected_status_artifact_count");
|
|
expect(verifyStep.run).not.toContain("actual_status_count");
|
|
|
|
const runtimeCoverage = workflowJob(
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"runtime_tool_coverage_release_checks",
|
|
);
|
|
expect(workflowStep(runtimeCoverage, "Resolve runtime parity artifacts").run).toContain(
|
|
"trusted-release-check-artifacts/scripts/github/resolve-release-check-artifacts.sh",
|
|
);
|
|
expect(
|
|
workflowStep(runtimeCoverage, "Download runtime parity status").with?.["artifact-ids"],
|
|
).toBe("${{ steps.resolve_runtime_parity_artifacts.outputs.status_ids }}");
|
|
expectTextToIncludeAll(
|
|
workflowStep(runtimeCoverage, "Verify runtime parity producer status").run,
|
|
["resolve-release-check-artifacts.sh validate", "ready=false", "ready=true"],
|
|
);
|
|
expect(
|
|
workflowStep(runtimeCoverage, "Download runtime parity artifacts").with?.["artifact-ids"],
|
|
).toBe("${{ steps.resolve_runtime_parity_artifacts.outputs.payload_ids }}");
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
emptyStderr: true,
|
|
expected: [],
|
|
name: "accepts a successful dispatched Telegram child",
|
|
params: { currentAttempt: "2", currentResult: "success" },
|
|
status: 0,
|
|
},
|
|
...(["cancelled", "failure", "skipped"] as const).map((currentResult) => ({
|
|
emptyStderr: false,
|
|
expected: [`::error::qa_live_telegram_release_checks ended with ${currentResult}`],
|
|
name: `rejects a ${currentResult} selected Telegram child`,
|
|
params: { currentAttempt: "2", currentResult, telegramSelected: true },
|
|
status: 1,
|
|
})),
|
|
{
|
|
emptyStderr: false,
|
|
expected: [],
|
|
name: "accepts a skipped unselected Telegram dispatch",
|
|
params: { currentAttempt: "2", currentResult: "skipped", telegramSelected: false },
|
|
status: 0,
|
|
},
|
|
{
|
|
emptyStderr: false,
|
|
expected: ["::error::Telegram dispatch identity was not verified"],
|
|
name: "rejects an unverified Telegram child identity",
|
|
params: {
|
|
currentAttempt: "2",
|
|
currentResult: "failure",
|
|
telegramIdentityVerified: false,
|
|
},
|
|
status: 1,
|
|
},
|
|
{
|
|
emptyStderr: false,
|
|
expected: [
|
|
"qa_live_telegram_release_checks ended with failure",
|
|
"::error::package_acceptance_release_checks ended with failure",
|
|
],
|
|
name: "keeps package failures blocking alongside a Telegram failure",
|
|
params: {
|
|
currentAttempt: "2",
|
|
currentResult: "failure",
|
|
resultOverrides: { PACKAGE_ACCEPTANCE_RELEASE_CHECKS_RESULT: "failure" },
|
|
},
|
|
status: 1,
|
|
},
|
|
{
|
|
emptyStderr: false,
|
|
expected: ["::error::resolve_target ended with failure"],
|
|
name: "keeps target resolution blocking before release children",
|
|
params: {
|
|
currentAttempt: "2",
|
|
currentResult: "skipped",
|
|
resolveResult: "failure",
|
|
telegramSelected: false,
|
|
},
|
|
status: 1,
|
|
},
|
|
{
|
|
emptyStderr: false,
|
|
expected: ["qa_live_telegram_release_checks ended with cancelled"],
|
|
name: "rejects a cancelled Telegram child for a release branch",
|
|
params: {
|
|
currentAttempt: "2",
|
|
currentResult: "cancelled",
|
|
workflowRef: "refs/heads/release/2026.7.10",
|
|
},
|
|
status: 1,
|
|
},
|
|
] as const)("$name", ({ emptyStderr, expected, params, status }) => {
|
|
const result = runReleaseChecksSummary(params);
|
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
|
|
expect(result.status).toBe(status);
|
|
if (emptyStderr) {
|
|
expect(result.stderr).toBe("");
|
|
}
|
|
for (const snippet of expected ?? []) {
|
|
expect(output).toContain(snippet);
|
|
}
|
|
});
|
|
|
|
it.each(["cancelled", "failure"] as const)(
|
|
"does not mask a later %s advisory status with an older successful job result",
|
|
(status) => {
|
|
const result = runReleaseChecksSummary({
|
|
currentAttempt: "2",
|
|
currentResult: "skipped",
|
|
discordResult: "success",
|
|
telegramSelected: false,
|
|
validatedStatuses: [
|
|
{
|
|
job: "qa_live_discord_release_checks",
|
|
status,
|
|
variant: "",
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(`${result.stdout}\n${result.stderr}`).toContain(
|
|
`::error::qa_live_discord_release_checks ended with ${status}`,
|
|
);
|
|
},
|
|
);
|
|
|
|
it("validates only jobs owned by the selected release-check phase", () => {
|
|
const independent = runReleaseChecksSummary({
|
|
currentAttempt: "1",
|
|
currentResult: "skipped",
|
|
phase: "independent",
|
|
resultOverrides: {
|
|
CROSS_OS_RELEASE_CHECKS_RESULT: "failure",
|
|
DOCKER_E2E_RELEASE_CHECKS_RESULT: "failure",
|
|
PACKAGE_ACCEPTANCE_RELEASE_CHECKS_RESULT: "failure",
|
|
PREPARE_RELEASE_PACKAGE_RESULT: "failure",
|
|
},
|
|
telegramSelected: false,
|
|
});
|
|
const candidate = runReleaseChecksSummary({
|
|
currentAttempt: "1",
|
|
currentResult: "failure",
|
|
phase: "candidate",
|
|
resultOverrides: {
|
|
INSTALL_SMOKE_RELEASE_CHECKS_RESULT: "failure",
|
|
LIVE_REPO_E2E_RELEASE_CHECKS_RESULT: "failure",
|
|
MATURITY_SCORECARD_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LAB_PARITY_LANE_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LAB_PARITY_REPORT_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LAB_RUNTIME_PARITY_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LIVE_BUZZ_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LIVE_DISCORD_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LIVE_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LIVE_SLACK_RELEASE_CHECKS_RESULT: "failure",
|
|
QA_LIVE_WHATSAPP_RELEASE_CHECKS_RESULT: "failure",
|
|
RUNTIME_TOOL_COVERAGE_RELEASE_CHECKS_RESULT: "failure",
|
|
},
|
|
telegramSelected: false,
|
|
});
|
|
const failedCandidate = runReleaseChecksSummary({
|
|
currentAttempt: "1",
|
|
currentResult: "skipped",
|
|
phase: "candidate",
|
|
resultOverrides: {
|
|
DOCKER_E2E_RELEASE_CHECKS_RESULT: "failure",
|
|
},
|
|
telegramSelected: false,
|
|
});
|
|
|
|
expect(independent.status, independent.stderr).toBe(0);
|
|
expect(candidate.status, candidate.stderr).toBe(0);
|
|
expect(failedCandidate.status).toBe(1);
|
|
expect(`${failedCandidate.stdout}\n${failedCandidate.stderr}`).toContain(
|
|
"::error::docker_e2e_release_checks ended with failure",
|
|
);
|
|
});
|
|
|
|
it("summarizes start delay separately from execution time in full validation", () => {
|
|
const workflow = readFileSync(FULL_RELEASE_VALIDATION_WORKFLOW, "utf8");
|
|
const parsedWorkflow = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW);
|
|
const summaryJob = parsedWorkflow.jobs?.summary;
|
|
const manifestStep = workflowStep(summaryJob ?? {}, "Write release validation manifest");
|
|
|
|
expect(workflow).toContain("Write release validation manifest");
|
|
expect(workflow).toContain("PERFORMANCE_RUN_ID: ${{ needs.performance.outputs.run_id }}");
|
|
expect(workflow).toContain("Upload release validation manifest");
|
|
expect(workflow).toContain("Download diagnostic drain attempts");
|
|
expect(workflow).toContain("full-release-validation-${{ github.run_id }}");
|
|
expect(workflow).toContain("full-release-diagnostic-manifest.json");
|
|
expect(workflow).not.toContain('gh run view "$run_id" --json createdAt,jobs');
|
|
expect(manifestStep.env?.RELEASE_EXECUTION_PLAN_PATH).toContain(
|
|
"full-release-execution-plan.json",
|
|
);
|
|
expect(manifestStep.env?.DIAGNOSTIC_DRAIN_PATH).toContain(
|
|
"full-release-diagnostic-manifest.json",
|
|
);
|
|
});
|
|
|
|
it("wires evidence attempts into the acceptance gate", () => {
|
|
const releaseResolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const releaseRun = workflowStep(releaseResolveJob, "Resolve full release validation run");
|
|
const releaseManifest = workflowStep(
|
|
releaseResolveJob,
|
|
"Download full release validation manifest",
|
|
);
|
|
const npmPublishJob = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm");
|
|
const npmRun = workflowStep(npmPublishJob, "Verify full release validation evidence");
|
|
const npmManifest = workflowStep(npmPublishJob, "Download full release validation manifest");
|
|
|
|
expect(releaseRun).toMatchObject({
|
|
id: "full_run",
|
|
env: {
|
|
FULL_RELEASE_VALIDATION_RUN_ID: "${{ inputs.full_release_validation_run_id }}",
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "${{ inputs.full_release_validation_run_attempt }}",
|
|
},
|
|
});
|
|
expect(releaseRun.run).toContain(
|
|
'run_endpoint+="/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}"',
|
|
);
|
|
expect(releaseResolveJob.outputs?.full_release_validation_run_attempt).toBe(
|
|
"${{ steps.full_run.outputs.attempt }}",
|
|
);
|
|
expect(releaseManifest.with).toMatchObject({
|
|
name: "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ steps.full_run.outputs.attempt }}",
|
|
"run-id": "${{ inputs.full_release_validation_run_id }}",
|
|
});
|
|
|
|
expect(npmRun.env).toMatchObject({
|
|
FULL_RELEASE_VALIDATION_RUN_ID: "${{ inputs.full_release_validation_run_id }}",
|
|
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: "${{ inputs.full_release_validation_run_attempt }}",
|
|
});
|
|
expect(npmRun.run).toContain(
|
|
"actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}",
|
|
);
|
|
expect(npmManifest.with).toMatchObject({
|
|
name: "full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ inputs.full_release_validation_run_attempt }}",
|
|
"run-id": "${{ inputs.full_release_validation_run_id }}",
|
|
});
|
|
});
|
|
|
|
it("keeps release publish artifacts and release-note ordering wired", () => {
|
|
const resolveJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "resolve_release_target");
|
|
const publishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const resolveFullRun = workflowStep(resolveJob, "Resolve full release validation run");
|
|
const resolveDownload = workflowStep(resolveJob, "Download full release validation manifest");
|
|
const trustedTooling = workflowStep(resolveJob, "Checkout trusted release validation tooling");
|
|
const validateManifest = workflowStep(resolveJob, "Validate full release validation manifest");
|
|
const publishDownload = workflowStep(publishJob, "Download full release validation manifest");
|
|
const publishOrchestration = releasePublishOrchestration(publishJob);
|
|
const npmPublishJob = workflowJob(OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm");
|
|
const npmCheckout = workflowStep(npmPublishJob, "Checkout");
|
|
const npmFullRun = workflowStep(npmPublishJob, "Verify full release validation evidence");
|
|
const npmDownload = workflowStep(npmPublishJob, "Download full release validation manifest");
|
|
const npmTarget = workflowStep(npmPublishJob, "Verify full release validation target");
|
|
|
|
expect(resolveFullRun.id).toBe("full_run");
|
|
expect(resolveFullRun.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe(
|
|
"${{ inputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(resolveJob.outputs?.full_release_validation_run_attempt).toBe(
|
|
"${{ steps.full_run.outputs.attempt }}",
|
|
);
|
|
expect(resolveDownload.with?.name).toBe(
|
|
"full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ steps.full_run.outputs.attempt }}",
|
|
);
|
|
expect(trustedTooling.with).toMatchObject({
|
|
ref: "${{ github.workflow_sha }}",
|
|
path: ".release-validation-tooling",
|
|
"persist-credentials": false,
|
|
"sparse-checkout": "scripts",
|
|
});
|
|
expect(validateManifest.env).toMatchObject({
|
|
EXPECTED_WORKFLOW_BRANCH: "${{ github.ref_name }}",
|
|
PUBLICATION_CONSUMER:
|
|
"${{ inputs.publish_docker_only && !inputs.publish_openclaw_npm && 'docker-only' || 'publisher' }}",
|
|
PUBLISH_DOCKER_ONLY: "${{ inputs.publish_docker_only }}",
|
|
PUBLISH_OPENCLAW_NPM: "${{ inputs.publish_openclaw_npm }}",
|
|
RELEASE_NPM_DIST_TAG: "${{ inputs.npm_dist_tag }}",
|
|
PLUGIN_PUBLISH_SCOPE: "${{ inputs.plugin_publish_scope }}",
|
|
RELEASE_PLUGINS: "${{ inputs.plugins }}",
|
|
PREPARED_PLUGINS: "${{ inputs.prepared_plugins }}",
|
|
WINDOWS_NODE_TAG: "${{ inputs.windows_node_tag }}",
|
|
WINDOWS_NODE_INSTALLER_DIGESTS: "${{ inputs.windows_node_installer_digests }}",
|
|
RUN_JSON_FILE: "${{ runner.temp }}/full-release-validation-run.json",
|
|
TRUSTED_WORKFLOW_FULL_REF: "${{ github.ref }}",
|
|
TRUSTED_WORKFLOW_REF: "${{ github.ref_name }}",
|
|
VALIDATOR_FILE:
|
|
"${{ github.workspace }}/.release-validation-tooling/scripts/validate-full-release-validation-evidence.mjs",
|
|
STRICT_VALIDATOR_FILE:
|
|
"${{ github.workspace }}/.release-validation-tooling/scripts/release-ci-summary.mjs",
|
|
});
|
|
expect(validateManifest.run).toContain('MANIFEST_FILE="$manifest"');
|
|
expect(validateManifest.run).toContain('node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"');
|
|
expect(validateManifest.run).not.toContain('node "$STRICT_VALIDATOR_FILE"');
|
|
expect(npmFullRun.env).toMatchObject({
|
|
PUBLICATION_CONSUMER: "core-npm",
|
|
RELEASE_NPM_DIST_TAG: "${{ inputs.npm_dist_tag }}",
|
|
});
|
|
expect(npmFullRun.run).not.toContain('node "$STRICT_VALIDATOR_FILE"');
|
|
expect(publishDownload.with?.name).toBe(
|
|
"full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ needs.resolve_release_target.outputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(publishOrchestration.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe(
|
|
"${{ needs.resolve_release_target.outputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(publishOrchestration.run).toContain('"${target_sha}" != "${TARGET_SHA}"');
|
|
expect(npmFullRun.env?.FULL_RELEASE_VALIDATION_RUN_ATTEMPT).toBe(
|
|
"${{ inputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(npmDownload.with?.name).toBe(
|
|
"full-release-validation-${{ inputs.full_release_validation_run_id }}-${{ inputs.full_release_validation_run_attempt }}",
|
|
);
|
|
expect(npmTarget.env?.FULL_RELEASE_VALIDATION_RUN_ID).toBeUndefined();
|
|
expect(npmTarget.run).not.toContain(
|
|
"node scripts/openclaw-npm-extended-stable-release.mjs verify-manifest",
|
|
);
|
|
expect(npmCheckout.with?.["fetch-depth"]).toBe(
|
|
"${{ inputs.release_evidence_mode == 'authorized-beta-focused-v1' && 0 || (inputs.preflight_run_id != '' && 1 || 0) }}",
|
|
);
|
|
|
|
const publishSteps = publishJob.steps ?? [];
|
|
const setupIndex = publishSteps.findIndex((step) => step.name === "Setup Node environment");
|
|
const notesIndex = publishSteps.findIndex(
|
|
(step) => step.name === "Prepare GitHub release notes",
|
|
);
|
|
const dispatchIndex = publishSteps.findIndex(
|
|
(step) => step.name === "Dispatch publish workflows",
|
|
);
|
|
expect(setupIndex).toBeGreaterThan(-1);
|
|
expect(notesIndex).toBeGreaterThan(setupIndex);
|
|
expect(publishSteps.some((step) => step.name === "Write Android release approval")).toBe(false);
|
|
expect(workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish_android").needs).toContain("publish");
|
|
expect(dispatchIndex).toBeGreaterThan(notesIndex);
|
|
expect(publishSteps[notesIndex]?.if).toBe("${{ inputs.publish_openclaw_npm }}");
|
|
|
|
const publishRun = publishOrchestration.run ?? "";
|
|
const createReleaseIndex = publishRun.lastIndexOf("create_or_update_github_release");
|
|
const verifyReleaseIndex = publishRun.lastIndexOf("verify_published_release");
|
|
const appendProofIndex = publishRun.lastIndexOf("append_release_proof_to_github_release");
|
|
const finalizeJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "finalize_github_release");
|
|
const finalizeRelease = workflowStep(finalizeJob, "Publish the verified draft release");
|
|
expect(createReleaseIndex).toBeGreaterThanOrEqual(0);
|
|
expect(verifyReleaseIndex).toBeGreaterThan(createReleaseIndex);
|
|
expect(appendProofIndex).toBeGreaterThan(verifyReleaseIndex);
|
|
expect(finalizeJob.needs).toEqual([
|
|
"publish",
|
|
"publish_docker",
|
|
"approve_github_release",
|
|
"finalize_github_release_before_docker",
|
|
]);
|
|
expect(finalizeJob.if).toContain("needs.publish_docker.result == 'success'");
|
|
expect(finalizeJob.if).toContain("inputs.prepared_plugins == ''");
|
|
expect(finalizeJob.if).toContain("needs.approve_github_release.result == 'success'");
|
|
expect(finalizeRelease.env).toMatchObject({
|
|
RELEASE_TAG: "${{ inputs.tag }}",
|
|
SOURCE_SHA: "${{ needs.publish.outputs.source_sha }}",
|
|
RELEASE_NPM_DIST_TAG: "${{ inputs.npm_dist_tag }}",
|
|
});
|
|
expect(finalizeRelease.run).toContain("node scripts/linux-app-channel.mjs finalize-core");
|
|
expect(finalizeRelease.run).toContain(
|
|
'--tag "$RELEASE_TAG" --source-sha "$SOURCE_SHA" --latest "$expected_latest"',
|
|
);
|
|
expect(finalizeRelease.run).toContain('--tooling-sha "$GITHUB_WORKFLOW_SHA"');
|
|
expect(finalizeRelease.run).toContain(
|
|
'--workflow-ref "$GITHUB_REF_NAME" --workflow-full-ref "$GITHUB_REF"',
|
|
);
|
|
expect(finalizeRelease.run).toContain(
|
|
'--release-publish-run-id "$GITHUB_RUN_ID" --release-publish-run-attempt "$GITHUB_RUN_ATTEMPT"',
|
|
);
|
|
expect(finalizeRelease.run).toContain(
|
|
'--release-publish-ref "$GITHUB_REF_NAME" --release-publish-full-ref "$GITHUB_REF"',
|
|
);
|
|
expect(finalizeRelease.run).not.toContain("gh release edit");
|
|
});
|
|
|
|
it("loads the strict release validator from the isolated trusted tooling bundle", () => {
|
|
const root = tempDirs.make("release-validation-tooling-");
|
|
mkdirSync(join(root, "lib", "cross-os-release-checks"), { recursive: true });
|
|
for (const source of [
|
|
"scripts/release-ci-summary.mjs",
|
|
"scripts/full-release-validation-policy.mjs",
|
|
"scripts/full-release-flake-classification.mjs",
|
|
...PUBLICATION_CONTRACT_FILES,
|
|
"scripts/lib/release-changelog.mjs",
|
|
"scripts/full-release-candidate-contract.mjs",
|
|
"scripts/lib/full-release-candidate-reuse.mjs",
|
|
"scripts/lib/full-release-child-request.mjs",
|
|
"scripts/lib/full-release-child-reuse.mjs",
|
|
"scripts/lib/full-release-evidence.mjs",
|
|
"scripts/lib/release-publish-inputs.mjs",
|
|
"scripts/plugin-sdk-api-release-evidence.mjs",
|
|
"scripts/lib/canonical-json.mjs",
|
|
"scripts/lib/cross-os-release-checks/suite-filter.mjs",
|
|
"scripts/lib/plain-gh.mjs",
|
|
"scripts/lib/release-context.mjs",
|
|
"scripts/lib/release-version.mjs",
|
|
"scripts/lib/record-shared.mjs",
|
|
"scripts/lib/upgrade-survivor-policy.mjs",
|
|
"scripts/lib/upgrade-survivor-scenarios.json",
|
|
]) {
|
|
copyFileSync(source, join(root, source.replace(/^scripts\//u, "")));
|
|
}
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
"--input-type=module",
|
|
"-e",
|
|
`await import(${JSON.stringify(pathToFileURL(join(root, "release-ci-summary.mjs")).href)})`,
|
|
],
|
|
{ encoding: "utf8", timeout: 20_000 },
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "a separately versioned correction on its own branch",
|
|
version: "2026.8.1-1",
|
|
tag: "v2026.8.1-1",
|
|
branch: "release/2026.8.1-1",
|
|
accepted: true,
|
|
},
|
|
{
|
|
name: "a same-source correction with only the base branch",
|
|
version: "2026.8.1",
|
|
tag: "v2026.8.1-1",
|
|
branch: "release/2026.8.1",
|
|
accepted: true,
|
|
},
|
|
{
|
|
name: "a beta on its frozen release branch",
|
|
version: "2026.8.1-beta.1",
|
|
tag: "v2026.8.1-beta.1",
|
|
branch: "release/2026.8.1",
|
|
accepted: true,
|
|
},
|
|
{
|
|
name: "a stable version on its frozen release branch",
|
|
version: "2026.8.1",
|
|
tag: "v2026.8.1",
|
|
branch: "release/2026.8.1",
|
|
accepted: true,
|
|
},
|
|
])("validates frozen npm release ancestry for $name", (scenario) => {
|
|
const metadata = workflowStep(
|
|
workflowJob(OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "check_dependencies_npm"),
|
|
"Validate release metadata",
|
|
);
|
|
const root = tempDirs.make("npm-preflight-release-ancestry-");
|
|
const origin = join(root, "origin");
|
|
const source = join(root, "source");
|
|
mkdirSync(origin);
|
|
const git = (cwd: string, ...args: string[]) =>
|
|
execFileSync(
|
|
"git",
|
|
[
|
|
"-c",
|
|
"user.name=OpenClaw Test",
|
|
"-c",
|
|
"user.email=openclaw-test@example.com",
|
|
"-c",
|
|
"commit.gpgsign=false",
|
|
"-c",
|
|
"core.hooksPath=/dev/null",
|
|
...args,
|
|
],
|
|
{ cwd, encoding: "utf8" },
|
|
).trim();
|
|
git(origin, "init", "-q", "--initial-branch=main");
|
|
writeFileSync(join(origin, "package.json"), JSON.stringify({ version: "2026.8.1" }));
|
|
git(origin, "add", "package.json");
|
|
git(origin, "commit", "-qm", "main before release");
|
|
git(origin, "switch", "-q", "-c", "release/2026.8.1");
|
|
git(origin, "commit", "--allow-empty", "-qm", "frozen release");
|
|
if (scenario.branch !== "release/2026.8.1") {
|
|
git(origin, "switch", "-q", "-c", scenario.branch);
|
|
}
|
|
if (scenario.version !== "2026.8.1") {
|
|
writeFileSync(join(origin, "package.json"), JSON.stringify({ version: scenario.version }));
|
|
git(origin, "commit", "-qam", "versioned candidate");
|
|
}
|
|
git(origin, "tag", scenario.tag);
|
|
if (scenario.version === "2026.8.1" && scenario.tag !== "v2026.8.1") {
|
|
git(origin, "tag", "v2026.8.1");
|
|
}
|
|
git(origin, "switch", "-q", "main");
|
|
git(origin, "commit", "--allow-empty", "-qm", "main advances independently");
|
|
git(
|
|
root,
|
|
"clone",
|
|
"-q",
|
|
"--depth=1",
|
|
"--branch",
|
|
scenario.tag,
|
|
pathToFileURL(origin).href,
|
|
source,
|
|
);
|
|
const tooling = join(source, ".release-harness", "scripts", "lib");
|
|
mkdirSync(tooling, { recursive: true });
|
|
for (const name of ["release-context.mjs", "release-version.mjs"]) {
|
|
copyFileSync(join(REPO_ROOT, "scripts", "lib", name), join(tooling, name));
|
|
}
|
|
const factsPath = join(root, "package-check-facts");
|
|
// Use real shallow Git history; the package-check stand-in enforces the
|
|
// same ancestry boundary without building or contacting a registry.
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
[
|
|
'timeout() { shift 3; "$@"; }',
|
|
"pnpm() {",
|
|
' [[ "$*" == release:openclaw:npm:check ]]',
|
|
' printf "%s\\n" "$RELEASE_TAG" "$RELEASE_MAIN_REF" > "$PACKAGE_CHECK_FACTS"',
|
|
' git merge-base --is-ancestor "$RELEASE_SHA" "$RELEASE_MAIN_REF"',
|
|
"}",
|
|
metadata.run,
|
|
].join("\n"),
|
|
],
|
|
{
|
|
cwd: source,
|
|
encoding: "utf8",
|
|
timeout: 10_000,
|
|
env: {
|
|
PATH: [dirname(process.execPath), process.env.PATH].join(":"),
|
|
PACKAGE_CHECK_FACTS: factsPath,
|
|
RELEASE_REF: scenario.tag,
|
|
RELEASE_TAG: "",
|
|
PREFLIGHT_ONLY: "true",
|
|
WORKFLOW_REF_NAME: "main",
|
|
OPENCLAW_NPM_PUBLISH_TAG: "beta",
|
|
OPENCLAW_NPM_RELEASE_SKIP_PACK_CHECK: "1",
|
|
},
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(scenario.accepted ? 0 : 1);
|
|
if (scenario.accepted) {
|
|
expect(readFileSync(factsPath, "utf8").trim().split("\n")).toEqual([
|
|
scenario.tag,
|
|
"refs/remotes/origin/" + scenario.branch,
|
|
]);
|
|
} else {
|
|
expect(result.stderr).toContain("Tagged commit is not reachable from main.");
|
|
expect(existsSync(factsPath)).toBe(false);
|
|
}
|
|
});
|
|
|
|
it("keeps optional Windows promotion downstream of published npm and GitHub releases", () => {
|
|
const workflow = readWorkflow(RELEASE_PUBLISH_WORKFLOW);
|
|
const publish = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const finalize = workflowJob(RELEASE_PUBLISH_WORKFLOW, "finalize_github_release");
|
|
const windows = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish_windows");
|
|
const dispatch = workflowStep(windows, "Dispatch detached Windows promotion");
|
|
const child = workflowJob(WINDOWS_NODE_RELEASE_WORKFLOW, "promote_signed_windows_installers");
|
|
|
|
for (const input of ["windows_node_tag", "windows_node_installer_digests"]) {
|
|
expect(workflow.on?.workflow_dispatch?.inputs?.[input]).toMatchObject({ required: false });
|
|
}
|
|
expect(publish.needs).toEqual(["resolve_release_target"]);
|
|
expect(finalize.needs).toEqual([
|
|
"publish",
|
|
"publish_docker",
|
|
"approve_github_release",
|
|
"finalize_github_release_before_docker",
|
|
]);
|
|
expect(windows.needs).toEqual(["resolve_release_target", "finalize_github_release"]);
|
|
expect(windows["continue-on-error"]).toBe(true);
|
|
expect(windows.if).toContain("needs.finalize_github_release.result == 'success'");
|
|
expect(windows.if).toContain(
|
|
"inputs.windows_node_tag != '' || inputs.windows_node_installer_digests != ''",
|
|
);
|
|
expect(dispatch.env).toMatchObject({
|
|
WINDOWS_NODE_TAG: "${{ inputs.windows_node_tag }}",
|
|
WINDOWS_NODE_INSTALLER_DIGESTS: "${{ inputs.windows_node_installer_digests }}",
|
|
PARENT_WORKFLOW_SHA: "${{ github.workflow_sha }}",
|
|
});
|
|
expect(workflowStep(windows, "Record failed Windows dispatch").if).toBe("${{ failure() }}");
|
|
expect(workflowStep(windows, "Upload Windows dispatch evidence").if).toBe("${{ always() }}");
|
|
expect(workflowStep(child, "Record Windows promotion outcome").if).toBe("${{ always() }}");
|
|
expect(workflowStep(child, "Upload Windows promotion evidence").if).toBe("${{ always() }}");
|
|
});
|
|
|
|
it.each([
|
|
{ scenario: "omitted", selected: false, hasDigests: false, dispatchFailure: false, exit: 0 },
|
|
{ scenario: "selected", selected: true, hasDigests: true, dispatchFailure: false, exit: 0 },
|
|
{
|
|
scenario: "dispatch failure",
|
|
selected: true,
|
|
hasDigests: true,
|
|
dispatchFailure: true,
|
|
exit: 1,
|
|
},
|
|
{
|
|
scenario: "missing digests",
|
|
selected: true,
|
|
hasDigests: false,
|
|
dispatchFailure: false,
|
|
exit: 1,
|
|
},
|
|
{
|
|
scenario: "extended-stable",
|
|
selected: true,
|
|
hasDigests: true,
|
|
dispatchFailure: false,
|
|
exit: 0,
|
|
},
|
|
])(
|
|
"dispatches optional Windows promotion without waiting: $scenario",
|
|
({ scenario, selected, hasDigests, dispatchFailure, exit }) => {
|
|
const shouldDispatch = selected && hasDigests && scenario !== "extended-stable";
|
|
const source = readFileSync("scripts/lib/release-publish-children.sh", "utf8");
|
|
const root = tempDirs.make("windows-detached-publish-");
|
|
const dispatchPath = join(root, "dispatch");
|
|
const summaryPath = join(root, "summary");
|
|
const workflowSha = "d".repeat(40);
|
|
const workflowRef = `release-publish/${workflowSha.slice(0, 12)}-123`;
|
|
const digests = JSON.stringify({
|
|
"OpenClawCompanion-Setup-x64.exe": `sha256:${"a".repeat(64)}`,
|
|
"OpenClawCompanion-Setup-arm64.exe": `sha256:${"b".repeat(64)}`,
|
|
});
|
|
writeFileSync(summaryPath, "");
|
|
const result = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -euo pipefail
|
|
dispatch_workflow_at_ref() {
|
|
printf '%s\\n' "$@" > "$DISPATCH_ARGS"
|
|
${dispatchFailure ? "return 1" : "echo 456"}
|
|
}
|
|
wait_for_run() { echo unexpected-windows-wait >&2; return 99; }
|
|
${shellFunctionSource(source, "is_stable_release")}
|
|
${shellFunctionSource(source, "dispatch_workflow")}
|
|
${shellFunctionSource(source, "promote_windows_release_assets")}
|
|
promote_windows_release_assets
|
|
`,
|
|
],
|
|
{
|
|
encoding: "utf8",
|
|
timeout: 10_000,
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
DISPATCH_ARGS: dispatchPath,
|
|
GITHUB_STEP_SUMMARY: summaryPath,
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
RELEASE_TAG: "v2026.9.1",
|
|
RELEASE_NPM_DIST_TAG: scenario === "extended-stable" ? "extended-stable" : "latest",
|
|
CHILD_WORKFLOW_REF: workflowRef,
|
|
PARENT_WORKFLOW_SHA: workflowSha,
|
|
WINDOWS_NODE_TAG: selected ? "v0.6.3" : "",
|
|
WINDOWS_NODE_INSTALLER_DIGESTS: hasDigests ? digests : "",
|
|
},
|
|
},
|
|
);
|
|
expect(result.status, result.stderr).toBe(exit);
|
|
expect(result.stderr).not.toContain("unexpected-windows-wait");
|
|
expect(existsSync(dispatchPath)).toBe(shouldDispatch);
|
|
if (shouldDispatch) {
|
|
expect(readFileSync(dispatchPath, "utf8").trim().split("\n")).toEqual([
|
|
workflowRef,
|
|
workflowSha,
|
|
"windows-node-release.yml",
|
|
"-f",
|
|
"tag=v2026.9.1",
|
|
"-f",
|
|
"windows_node_tag=v0.6.3",
|
|
"-f",
|
|
`expected_installer_digests=${digests}`,
|
|
]);
|
|
}
|
|
expect(readFileSync(summaryPath, "utf8").includes("actions/runs/456")).toBe(
|
|
shouldDispatch && !dispatchFailure,
|
|
);
|
|
},
|
|
);
|
|
|
|
it("validates Windows signatures and pinned digests in the promotion owner", () => {
|
|
const windowsWorkflow = readFileSync(WINDOWS_NODE_RELEASE_WORKFLOW, "utf8");
|
|
expect(windowsWorkflow).not.toContain("default: latest");
|
|
expect(windowsWorkflow).toContain("expected_installer_digests:");
|
|
expect(windowsWorkflow).toContain("expected_installer_digests must contain exactly");
|
|
expect(windowsWorkflow).toContain("must be an explicit openclaw-windows-node release tag");
|
|
expect(windowsWorkflow).toContain("$installerPatterns = @(");
|
|
expect(windowsWorkflow).toContain("Every matched installer is signature-checked");
|
|
expect(windowsWorkflow).toContain("Get-ChildItem -LiteralPath dist -File");
|
|
expect(windowsWorkflow).toContain(
|
|
"Downloaded Windows source asset does not match pinned digest",
|
|
);
|
|
expect(windowsWorkflow).toContain(
|
|
"--repo openclaw/openclaw-windows-node --json tagName,isDraft,isPrerelease,assets,url",
|
|
);
|
|
expect(windowsWorkflow).toContain(
|
|
"Windows source release must contain exactly one required asset",
|
|
);
|
|
expect(windowsWorkflow).toContain(
|
|
"Windows source release asset digest does not match the pinned digest",
|
|
);
|
|
expect(windowsWorkflow).toContain(
|
|
"CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US",
|
|
);
|
|
expect(windowsWorkflow).toContain("has unexpected signer subject");
|
|
expect(windowsWorkflow).toContain("OpenClawCompanion-SHA256SUMS.txt");
|
|
expect(windowsWorkflow).toContain("Verify promoted release asset contract");
|
|
expect(windowsWorkflow).toContain(
|
|
"Promoted OpenClawCompanion asset names do not exactly match the current contract",
|
|
);
|
|
expect(windowsWorkflow).toContain(
|
|
"$targetRelease = gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY --json assets",
|
|
);
|
|
expect(windowsWorkflow).toContain("Promoted Windows SHA-256 manifest does not match");
|
|
expect(windowsWorkflow).toContain("Promoted Windows release asset checksum mismatch");
|
|
});
|
|
|
|
it("keeps the signed Android APK contract independent of core publication", () => {
|
|
const releaseWorkflow = [
|
|
readFileSync("scripts/lib/release-publish-children.sh", "utf8"),
|
|
readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8"),
|
|
].join("\n");
|
|
const androidWorkflow = readFileSync(ANDROID_RELEASE_WORKFLOW, "utf8");
|
|
const androidDocs = readFileSync("docs/platforms/android.md", "utf8");
|
|
const approvalScript = readFileSync("scripts/validate-release-publish-approval.mjs", "utf8");
|
|
const androidJob = workflowJob(ANDROID_RELEASE_WORKFLOW, "publish_signed_android_apk");
|
|
const setupNode = workflowStep(androidJob, "Setup Node environment");
|
|
const setupNodeAction = parse(
|
|
readFileSync(".github/actions/setup-node-env/action.yml", "utf8"),
|
|
);
|
|
const androidSteps = androidJob.steps ?? [];
|
|
|
|
expect(setupNode.uses).toBe("./.github/actions/setup-node-env");
|
|
expect(setupNode.with?.["install-deps"] ?? setupNodeAction.inputs["install-deps"].default).toBe(
|
|
"true",
|
|
);
|
|
expect(androidSteps.indexOf(setupNode)).toBeLessThan(
|
|
androidSteps.indexOf(workflowStep(androidJob, "Create apps-signing read token")),
|
|
);
|
|
expect(androidWorkflow).toContain("environment: android-release");
|
|
expect(androidWorkflow).toContain(
|
|
"actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1",
|
|
);
|
|
expect(androidWorkflow).toContain("repositories: apps-signing");
|
|
expect(androidWorkflow).toContain("permission-contents: read");
|
|
expect(androidWorkflow).toContain("--mode materialize");
|
|
expect(androidWorkflow).not.toContain("APPS_SIGNING_DEPLOY_KEY");
|
|
expect(androidWorkflow).toContain("MATCH_PASSWORD");
|
|
expect(androidWorkflow).toContain("scripts/validate-release-publish-approval.mjs");
|
|
expect(releaseWorkflow).toContain("Write Android release approval");
|
|
expect(releaseWorkflow).toContain("Attest Android release approval");
|
|
expect(releaseWorkflow).toContain("Upload Android release approval");
|
|
expect(releaseWorkflow).toContain("android-release-approval-${{ github.run_id }}");
|
|
expect(releaseWorkflow).toContain("parentRunId: process.env.RELEASE_PUBLISH_RUN_ID");
|
|
expect(releaseWorkflow).toContain("releaseTag: process.env.RELEASE_TAG");
|
|
expect(releaseWorkflow).toContain("targetSha: process.env.TARGET_SHA");
|
|
expect(androidWorkflow).toContain("Download parent release approval");
|
|
expect(androidWorkflow).toContain(
|
|
"android-release-approval-${{ inputs.release_publish_run_id }}",
|
|
);
|
|
expect(androidWorkflow).toContain(
|
|
'--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/openclaw-release-publish.yml"',
|
|
);
|
|
expect(androidWorkflow).toContain('--source-ref "${EXPECTED_WORKFLOW_FULL_REF}"');
|
|
expect(androidWorkflow).toContain('--source-digest "${EXPECTED_WORKFLOW_SHA}"');
|
|
expect(approvalScript).toContain(
|
|
"Attested Android release approval does not match this run request.",
|
|
);
|
|
expect(androidWorkflow).toContain('--artifact", "third-party');
|
|
expect(androidWorkflow).toContain("OpenClaw-Android.apk");
|
|
expect(androidWorkflow).toContain("OpenClaw-Android-SHA256SUMS.txt");
|
|
expect(androidWorkflow).toContain("actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6");
|
|
expect(androidWorkflow).toContain("--signer-workflow");
|
|
expect(androidWorkflow).toContain('--source-ref "refs/tags/${RELEASE_TAG}"');
|
|
expect(androidWorkflow).toContain("--deny-self-hosted-runners");
|
|
expect(androidWorkflow).toContain("--verify-apk");
|
|
expect(androidWorkflow).toContain('expected_source_ref="refs/tags/${RELEASE_TAG}"');
|
|
expect(androidWorkflow).toContain("release_target_sha must be a full lowercase commit SHA");
|
|
expect(approvalScript).toContain("no longer resolves to approved target");
|
|
expect(androidWorkflow).toContain(
|
|
"must resolve to the same source commit as ${fallback_base_tag}",
|
|
);
|
|
expect(androidWorkflow).toContain("FALLBACK_ANDROID_BASE_TAG");
|
|
expect(androidWorkflow).toContain("FALLBACK_ANDROID_BASE_SHA");
|
|
expect(androidWorkflow).toContain('--source-digest "${FALLBACK_ANDROID_BASE_SHA}"');
|
|
expect(androidWorkflow).toContain("steps.release_source.outputs.fallback_base_tag == ''");
|
|
expect(androidWorkflow).toContain(
|
|
"OPENCLAW_BUILD_TIMESTAMP: ${{ steps.release_approval.outputs.build_timestamp }}",
|
|
);
|
|
expect(androidWorkflow).toContain("GIT_COMMIT: ${{ inputs.release_target_sha }}");
|
|
expect(approvalScript).toContain("tagName,isPrerelease,createdAt");
|
|
expect(androidWorkflow).toContain(
|
|
'build_timestamp="$(node scripts/validate-release-publish-approval.mjs)"',
|
|
);
|
|
expect(androidWorkflow).toContain(
|
|
"Reusing verified Android APK from ${FALLBACK_ANDROID_BASE_TAG}",
|
|
);
|
|
expect(androidWorkflow).toContain("Existing Android release asset ${asset_name} differs");
|
|
expect(androidWorkflow).not.toContain("--clobber");
|
|
|
|
expect(releaseWorkflow).toContain("promote_android_release_asset()");
|
|
expect(releaseWorkflow).toContain("is_android_release()");
|
|
expect(androidWorkflow).toContain("requires a final or correction OpenClaw release tag");
|
|
expect(androidWorkflow).toContain("previous_version_code");
|
|
expect(androidWorkflow).toContain("must exceed ${previous_tag} versionCode");
|
|
expect(androidWorkflow).toContain("standalone channel bootstrap");
|
|
expect(releaseWorkflow).toContain(
|
|
'dispatch_workflow_at_ref "${RELEASE_TAG}" "${TARGET_SHA}" android-release.yml',
|
|
);
|
|
expect(releaseWorkflow).toContain('-f release_target_sha="${TARGET_SHA}"');
|
|
expect(releaseWorkflow).toContain("verify_android_release_asset_contract");
|
|
expect(releaseWorkflow).toContain("Android release APK digest does not match");
|
|
expect(releaseWorkflow).toContain("Android APK asset contract: verified");
|
|
|
|
expect(releaseWorkflow).toContain("finalize_github_release:");
|
|
|
|
expect(androidDocs).toContain("github.com/openclaw/openclaw/releases");
|
|
expect(androidDocs).not.toContain("releases/latest/download/OpenClaw-Android.apk");
|
|
expect(androidDocs).toContain("gh attestation verify OpenClaw-Android.apk");
|
|
expect(androidDocs).toContain('--source-ref "refs/tags/${release_tag}"');
|
|
});
|
|
|
|
it("rejects malformed Windows checksum manifest lines before using entries", () => {
|
|
const verify =
|
|
workflowStep(
|
|
workflowJob(WINDOWS_NODE_RELEASE_WORKFLOW, "promote_signed_windows_installers"),
|
|
"Verify promoted release asset contract",
|
|
).run ?? "";
|
|
const validateIndex = verify.indexOf('throw "Invalid Windows SHA-256 manifest entry: $_"');
|
|
const parseIndex = verify.indexOf("[PSCustomObject]@{");
|
|
|
|
expect(validateIndex).toBeGreaterThan(-1);
|
|
expect(parseIndex).toBeGreaterThan(validateIndex);
|
|
});
|
|
|
|
it("rejects unsafe direct Windows recovery before uploading assets", () => {
|
|
const windowsWorkflow = readFileSync(WINDOWS_NODE_RELEASE_WORKFLOW, "utf8");
|
|
const classifyStableReleaseIndex = windowsWorkflow.indexOf("$stableRelease = -not (");
|
|
const rejectPrereleaseSourceIndex = windowsWorkflow.indexOf(
|
|
"if ($stableRelease -and $sourceRelease.isPrerelease)",
|
|
);
|
|
const rejectUnexpectedTargetAssetsIndex = windowsWorkflow.indexOf(
|
|
"Target OpenClaw release contains unexpected OpenClawCompanion assets before upload",
|
|
);
|
|
const uploadAssetsIndex = windowsWorkflow.indexOf("gh release upload $env:RELEASE_TAG");
|
|
|
|
expect(classifyStableReleaseIndex).toBeGreaterThan(-1);
|
|
expect(rejectPrereleaseSourceIndex).toBeGreaterThan(classifyStableReleaseIndex);
|
|
expect(windowsWorkflow).not.toContain("-not $targetRelease.isPrerelease");
|
|
expect(rejectUnexpectedTargetAssetsIndex).toBeGreaterThan(-1);
|
|
expect(uploadAssetsIndex).toBeGreaterThan(rejectUnexpectedTargetAssetsIndex);
|
|
});
|
|
|
|
it("publish requires the credentialed authorization path", () => {
|
|
for (const [workflowPath, authorizationJobName, gatedJobName, expectedBranch] of [
|
|
[
|
|
PLUGIN_NPM_RELEASE_WORKFLOW,
|
|
"validate_release_publish_approval",
|
|
"publish_plugins_npm",
|
|
"${{ inputs.release_publish_branch || github.ref_name }}",
|
|
],
|
|
[
|
|
PLUGIN_CLAWHUB_RELEASE_WORKFLOW,
|
|
"validate_release_publish_approval",
|
|
"pack_plugins_clawhub_artifacts",
|
|
"${{ inputs.release_publish_branch || github.ref_name }}",
|
|
],
|
|
[
|
|
OPENCLAW_NPM_RELEASE_WORKFLOW,
|
|
"validate_publish_request",
|
|
"publish_openclaw_npm",
|
|
"${{ inputs.release_publish_branch || github.ref_name }}",
|
|
],
|
|
[
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"validate_release_publish_approval",
|
|
"publish_bootstrap_plugins",
|
|
"${{ inputs.release_publish_branch }}",
|
|
],
|
|
] as const) {
|
|
const authorizationJob = workflowJob(workflowPath, authorizationJobName);
|
|
const authorization = workflowStep(authorizationJob, "Validate release publish approval run");
|
|
const gatedJob = workflowJob(workflowPath, gatedJobName);
|
|
const needs = Array.isArray(gatedJob.needs) ? gatedJob.needs : [gatedJob.needs];
|
|
expect(needs, workflowPath).toContain(authorizationJobName);
|
|
expect(authorization.env, workflowPath).toMatchObject({
|
|
EXPECTED_WORKFLOW_BRANCH: expectedBranch,
|
|
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
|
});
|
|
expectTextToIncludeAll(authorization.run, [
|
|
'${GITHUB_ACTOR}" != "github-actions[bot]"',
|
|
"validate-release-publish-approval.mjs",
|
|
]);
|
|
}
|
|
|
|
const npmPublish = workflowJob(PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm");
|
|
expect(npmPublish.if).toContain("always() && !cancelled()");
|
|
|
|
for (const workflowPath of [PLUGIN_NPM_RELEASE_WORKFLOW, OPENCLAW_NPM_RELEASE_WORKFLOW]) {
|
|
const authorization = workflowStep(
|
|
workflowJob(
|
|
workflowPath,
|
|
workflowPath === PLUGIN_NPM_RELEASE_WORKFLOW
|
|
? "validate_release_publish_approval"
|
|
: "validate_publish_request",
|
|
),
|
|
"Validate release publish approval run",
|
|
);
|
|
expectTextToIncludeAll(authorization.run, [
|
|
'export EXPECTED_WORKFLOW_FULL_REF="refs/tags/${EXPECTED_WORKFLOW_BRANCH}"',
|
|
'export EXPECTED_WORKFLOW_FULL_REF="refs/heads/${EXPECTED_WORKFLOW_BRANCH}"',
|
|
]);
|
|
expect(
|
|
readWorkflow(workflowPath).on?.workflow_dispatch?.inputs?.release_publish_branch,
|
|
).toBeDefined();
|
|
expect(
|
|
readWorkflow(workflowPath).on?.workflow_dispatch?.inputs?.release_publish_full_ref,
|
|
).toBeDefined();
|
|
}
|
|
|
|
for (const [workflowPath, publishJobName, environment] of [
|
|
[PLUGIN_NPM_RELEASE_WORKFLOW, "publish_plugins_npm", "npm-publish"],
|
|
[OPENCLAW_NPM_RELEASE_WORKFLOW, "publish_openclaw_npm", "npm-publish"],
|
|
[
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"publish_bootstrap_plugins",
|
|
"clawhub-plugin-bootstrap",
|
|
],
|
|
] as const) {
|
|
expect(workflowJob(workflowPath, publishJobName).environment, workflowPath).toBe(environment);
|
|
}
|
|
|
|
const clawHubApproval = workflowJob(
|
|
PLUGIN_CLAWHUB_RELEASE_WORKFLOW,
|
|
"approve_plugins_clawhub_release",
|
|
);
|
|
const clawHubAuthorization = workflowStep(
|
|
workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "validate_release_publish_approval"),
|
|
"Validate release publish approval run",
|
|
);
|
|
const clawHubPublish = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "publish_plugins_clawhub");
|
|
expect(clawHubAuthorization.run).toContain("repository: .repository.full_name");
|
|
expect(clawHubAuthorization.run).toContain(
|
|
"actions/runs/${RELEASE_PUBLISH_RUN_ID}/attempts/${EXPECTED_RUN_ATTEMPT}",
|
|
);
|
|
expect(clawHubAuthorization.run).toContain("path,");
|
|
expect(clawHubAuthorization.run).toContain("runAttempt: .run_attempt");
|
|
expect(clawHubAuthorization.run).not.toContain("gh run view");
|
|
expect(clawHubAuthorization.env).toMatchObject({
|
|
EXPECTED_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
|
|
EXPECTED_WORKFLOW_FULL_REF: "${{ inputs.release_publish_full_ref }}",
|
|
EXPECTED_WORKFLOW_SHA: "${{ inputs.release_publish_workflow_sha }}",
|
|
});
|
|
const clawHubInputs = readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch
|
|
?.inputs;
|
|
expect(clawHubInputs?.release_tag).toBeDefined();
|
|
expect(clawHubInputs?.release_publish_run_attempt).toBeDefined();
|
|
expect(clawHubInputs?.release_publish_full_ref).toBeDefined();
|
|
expect(clawHubInputs?.release_publish_workflow_sha).toBeDefined();
|
|
// The parent's attested approval receipt lets the child skip its own gate.
|
|
expect(clawHubApproval.environment).toBe(
|
|
"${{ needs.validate_release_publish_approval.outputs.parent_approval != 'receipt' && 'clawhub-plugin-release' || '' }}",
|
|
);
|
|
expect(clawHubPublish.needs).toContain("approve_plugins_clawhub_release");
|
|
|
|
const bootstrapWorkflow = ".github/workflows/plugin-clawhub-new.yml";
|
|
const authorizationJob = workflowJob(bootstrapWorkflow, "validate_release_publish_approval");
|
|
const approvalDownload = workflowStep(
|
|
authorizationJob,
|
|
"Download parent ClawHub bootstrap approval",
|
|
);
|
|
const authorization = workflowStep(authorizationJob, "Validate release publish approval run");
|
|
|
|
expect(authorizationJob.permissions).toMatchObject({
|
|
actions: "read",
|
|
attestations: "read",
|
|
contents: "read",
|
|
});
|
|
expect(approvalDownload.with).toMatchObject({
|
|
name: "clawhub-bootstrap-approval-${{ inputs.release_publish_run_id }}-${{ inputs.release_publish_run_attempt }}",
|
|
"run-id": "${{ inputs.release_publish_run_id }}",
|
|
});
|
|
expect(authorization.env).toMatchObject({
|
|
APPROVAL_PATH: "${{ runner.temp }}/clawhub-bootstrap-approval/approval.json",
|
|
CHILD_WORKFLOW_SHA: "${{ inputs.bootstrap_workflow_sha }}",
|
|
EXPECTED_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
|
|
EXPECTED_WORKFLOW_BRANCH: "${{ inputs.release_publish_branch }}",
|
|
RELEASE_PUBLISH_RUN_ID: "${{ inputs.release_publish_run_id }}",
|
|
RELEASE_TARGET_SHA: "${{ needs.resolve_bootstrap_plan.outputs.ref_revision }}",
|
|
});
|
|
expectTextToIncludeAll(authorization.run, [
|
|
'${GITHUB_ACTOR}" != "github-actions[bot]"',
|
|
"actions/runs/${RELEASE_PUBLISH_RUN_ID}/attempts/${EXPECTED_RUN_ATTEMPT}",
|
|
"repository: .repository.full_name",
|
|
'--source-ref "${EXPECTED_WORKFLOW_REF}"',
|
|
'--source-digest "${EXPECTED_WORKFLOW_SHA}"',
|
|
"validate-release-publish-approval.mjs",
|
|
]);
|
|
});
|
|
|
|
it("keeps release publication ownership and artifact boundaries wired", () => {
|
|
const packageJson = JSON.parse(readFileSync(PACKAGE_JSON, "utf8")) as {
|
|
scripts?: Record<string, string>;
|
|
};
|
|
const releasePublishJob = workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish");
|
|
const releaseSteps = releasePublishJob.steps ?? [];
|
|
const clawHubApproval = workflowJob(
|
|
PLUGIN_CLAWHUB_RELEASE_WORKFLOW,
|
|
"approve_plugins_clawhub_release",
|
|
);
|
|
const clawHubPublish = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "publish_plugins_clawhub");
|
|
const clawHubBootstrapValidation = workflowJob(
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"validate_bootstrap_artifact",
|
|
);
|
|
const clawHubBootstrapPublish = workflowJob(
|
|
".github/workflows/plugin-clawhub-new.yml",
|
|
"publish_bootstrap_plugins",
|
|
);
|
|
const postpublishEvidence = workflowStep(releasePublishJob, "Upload postpublish evidence");
|
|
|
|
expect(packageJson.scripts).toMatchObject({
|
|
"release:verify-beta": "node --import ./scripts/tsx.mjs scripts/release-verify-beta.ts",
|
|
"release:candidate":
|
|
"node --import ./scripts/tsx.mjs scripts/release-candidate-checklist.mts",
|
|
"release:beta": "node --import ./scripts/tsx.mjs scripts/release-candidate-checklist.mts",
|
|
"release:fast-pretag-check": "bash scripts/release-fast-pretag-check.sh",
|
|
});
|
|
expect(workflowStep(releasePublishJob, "Setup Node environment").with).toMatchObject({
|
|
"install-bun": "false",
|
|
"install-deps": "false",
|
|
});
|
|
expect(workflowStep(releasePublishJob, "Checkout trusted release tooling")).toBeDefined();
|
|
expect(
|
|
workflowStep(releasePublishJob, "Install trusted release tooling dependencies"),
|
|
).toBeDefined();
|
|
expect(workflowStep(releasePublishJob, "Resolve ClawHub release plan")).toBeDefined();
|
|
expect(releasePublishOrchestration(releasePublishJob)).toBeDefined();
|
|
|
|
const dispatchIndexForReceipt = releaseSteps.findIndex(
|
|
(step) => step.name === "Dispatch publish workflows",
|
|
);
|
|
const authorizeIndex = releaseSteps.findIndex(
|
|
(step) => step.name === "Authorize exact ClawHub package transactions",
|
|
);
|
|
const receiptIndex = releaseSteps.findIndex(
|
|
(step) => step.name === "Upload immutable ClawHub parent authorization",
|
|
);
|
|
const completionIndex = releaseSteps.findIndex(
|
|
(step) => step.name === "Complete publish workflows",
|
|
);
|
|
expect(authorizeIndex).toBeGreaterThan(dispatchIndexForReceipt);
|
|
expect(receiptIndex).toBeGreaterThan(authorizeIndex);
|
|
expect(completionIndex).toBeGreaterThan(receiptIndex);
|
|
expect(
|
|
workflowStep(releasePublishJob, "Upload immutable ClawHub parent authorization").with?.[
|
|
"if-no-files-found"
|
|
],
|
|
).toBe("error");
|
|
expect(
|
|
workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "seal_clawhub_transactions").needs,
|
|
).toContain("pack_plugins_clawhub_artifacts");
|
|
expect(clawHubApproval.needs).toContain("seal_clawhub_transactions");
|
|
expect(
|
|
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).jobs?.verify_published_clawhub_package,
|
|
).toBeUndefined();
|
|
// The parent's attested approval receipt lets the child skip its own gate.
|
|
expect(clawHubApproval.environment).toBe(
|
|
"${{ needs.validate_release_publish_approval.outputs.parent_approval != 'receipt' && 'clawhub-plugin-release' || '' }}",
|
|
);
|
|
expect(clawHubPublish.needs).toEqual([
|
|
"preview_plugins_clawhub",
|
|
"pack_plugins_clawhub_artifacts",
|
|
"seal_clawhub_transactions",
|
|
"approve_plugins_clawhub_release",
|
|
]);
|
|
expect(clawHubPublish.uses).toBe(
|
|
"openclaw/clawhub/.github/workflows/package-publish.yml@7e2aa3cec5d35c91bb6163aa6676541d795876c5",
|
|
);
|
|
expect(clawHubPublish.permissions).toMatchObject({
|
|
actions: "read",
|
|
contents: "read",
|
|
"id-token": "write",
|
|
});
|
|
expect(clawHubPublish.with?.trusted_tooling_identity_json).toBe(
|
|
"${{ needs.seal_clawhub_transactions.outputs.identity }}",
|
|
);
|
|
expect(clawHubPublish.with?.wait_for_publication).toBe(false);
|
|
const clawHubPreview = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub");
|
|
expect(
|
|
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
|
|
?.release_publish_run_attempt,
|
|
).toBeDefined();
|
|
expect(
|
|
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
|
|
?.release_publish_full_ref,
|
|
).toBeDefined();
|
|
expect(
|
|
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
|
|
?.release_publish_workflow_sha,
|
|
).toBeDefined();
|
|
expect(clawHubPreview.outputs?.trusted_tooling_identity_json).toBeUndefined();
|
|
const publishOrchestration = releasePublishOrchestration(releasePublishJob);
|
|
expect(publishOrchestration.env?.PARENT_WORKFLOW_FULL_REF).toBe("${{ github.ref }}");
|
|
expect(publishOrchestration.run).toContain(
|
|
'wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${PARENT_WORKFLOW_SHA}"',
|
|
);
|
|
expect(publishOrchestration.run).toContain("release_publish_full_ref");
|
|
expect(clawHubBootstrapValidation.environment).toBe("clawhub-plugin-bootstrap");
|
|
expect(clawHubBootstrapPublish.environment).toBe("clawhub-plugin-bootstrap");
|
|
|
|
const bootstrapSteps = clawHubBootstrapPublish.steps ?? [];
|
|
const bootstrapDownload = workflowStep(
|
|
clawHubBootstrapPublish,
|
|
"Download and verify immutable ClawHub bootstrap artifact",
|
|
);
|
|
const bootstrapRehash = workflowStep(
|
|
clawHubBootstrapPublish,
|
|
"Rehash immutable ClawHub bootstrap artifacts",
|
|
);
|
|
const bootstrapRegistry = workflowStep(
|
|
clawHubBootstrapPublish,
|
|
"Reconfirm configure-only registry bytes before credentials",
|
|
);
|
|
const bootstrapTag = workflowStep(
|
|
clawHubBootstrapPublish,
|
|
"Reconfirm release tag before credentials",
|
|
);
|
|
const bootstrapCredentials = workflowStep(
|
|
clawHubBootstrapPublish,
|
|
"Write ClawHub token config",
|
|
);
|
|
expect(bootstrapDownload.run).toContain("clawhub-bootstrap-artifact.mjs download");
|
|
expect(bootstrapDownload.run).toContain('--target-sha "${TARGET_SHA}"');
|
|
expect(bootstrapDownload.run).toContain('--workflow-sha "${WORKFLOW_SHA}"');
|
|
expect(bootstrapTag.run).toContain('rev-parse "${RELEASE_TAG}^{commit}"');
|
|
expect(bootstrapSteps.indexOf(bootstrapDownload)).toBeLessThan(
|
|
bootstrapSteps.indexOf(bootstrapRehash),
|
|
);
|
|
expect(bootstrapSteps.indexOf(bootstrapRehash)).toBeLessThan(
|
|
bootstrapSteps.indexOf(bootstrapRegistry),
|
|
);
|
|
expect(bootstrapSteps.indexOf(bootstrapRegistry)).toBeLessThan(
|
|
bootstrapSteps.indexOf(bootstrapTag),
|
|
);
|
|
expect(bootstrapSteps.indexOf(bootstrapTag)).toBeLessThan(
|
|
bootstrapSteps.indexOf(bootstrapCredentials),
|
|
);
|
|
|
|
expect(postpublishEvidence.if).toContain("always()");
|
|
expect(postpublishEvidence.if).toContain("inputs.publish_openclaw_npm");
|
|
expect(postpublishEvidence.with).toMatchObject({
|
|
"if-no-files-found": "error",
|
|
name: "openclaw-release-postpublish-evidence-${{ inputs.tag }}",
|
|
path: "${{ runner.temp }}/openclaw-release-postpublish-evidence/release-postpublish-evidence.json",
|
|
});
|
|
expect(postpublishEvidence.uses).toBe(UPLOAD_ARTIFACT_V7);
|
|
|
|
const notesIndex = releaseSteps.findIndex(
|
|
(step) => step.name === "Prepare GitHub release notes",
|
|
);
|
|
const dispatchIndex = releaseSteps.findIndex(
|
|
(step) => step.name === "Dispatch publish workflows",
|
|
);
|
|
const evidenceIndex = releaseSteps.findIndex(
|
|
(step) => step.name === "Upload postpublish evidence",
|
|
);
|
|
expect(notesIndex).toBeGreaterThan(-1);
|
|
expect(dispatchIndex).toBeGreaterThan(notesIndex);
|
|
expect(evidenceIndex).toBeGreaterThan(dispatchIndex);
|
|
|
|
const clawHubReleaseSource = readFileSync(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "utf8");
|
|
const clawHubBootstrapSource = readFileSync(".github/workflows/plugin-clawhub-new.yml", "utf8");
|
|
expect(clawHubReleaseSource).not.toContain("secrets.CLAWHUB_TOKEN");
|
|
expect(clawHubReleaseSource).not.toContain("clawhub_token:");
|
|
expect(clawHubBootstrapSource).toContain("secrets.CLAWHUB_TOKEN");
|
|
});
|
|
|
|
it("bounds the npm registry tarball download used for release resume", () => {
|
|
const publishRun =
|
|
releasePublishOrchestration(workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish")).run ?? "";
|
|
const resolvePublishState = shellFunctionSource(
|
|
publishRun,
|
|
"resolve_openclaw_npm_publish_state",
|
|
);
|
|
const registryDownload = resolvePublishState.match(
|
|
/curl -fsSL[\s\S]*?"\$\{published_tarball_url\}"/u,
|
|
)?.[0];
|
|
|
|
expect(registryDownload).toContain("--connect-timeout 10");
|
|
expect(registryDownload).toContain("--max-time 120");
|
|
expect(registryDownload).toContain("--retry 3");
|
|
expect(registryDownload).toContain("--retry-max-time 180");
|
|
expect(registryDownload).toContain('-o "${published_tarball_path}"');
|
|
});
|
|
|
|
it("fails closed when child environment identity or approval mutation fails", () => {
|
|
const publishRun =
|
|
releasePublishOrchestration(workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish")).run ?? "";
|
|
const verifyChild = shellFunctionSource(publishRun, "verify_child_run_sha");
|
|
const approvePending = shellFunctionSource(publishRun, "approve_pending_deployments");
|
|
const readGh = shellFunctionSource(publishRun, "gh_read");
|
|
const waitForRun = shellFunctionSource(publishRun, "wait_for_run");
|
|
const expectedSha = "a".repeat(40);
|
|
|
|
const mutationFailure = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -uo pipefail
|
|
GITHUB_REPOSITORY=openclaw/openclaw
|
|
gh() {
|
|
if [[ "$1" == "run" && "$2" == "view" ]]; then
|
|
printf '%s\\n' '{"headSha":"${expectedSha}","url":"https://example.invalid/run/123"}'
|
|
return 0
|
|
fi
|
|
if [[ "$1" == "api" && "$2" == "-X" && "$3" == "GET" ]]; then
|
|
printf '%s\\n' '[{"environment":{"id":7,"name":"clawhub-plugin-bootstrap"},"current_user_can_approve":true}]'
|
|
return 0
|
|
fi
|
|
if [[ "$1" == "api" && "$2" == "-X" && "$3" == "POST" ]]; then
|
|
return 42
|
|
fi
|
|
return 99
|
|
}
|
|
${readGh}
|
|
${verifyChild}
|
|
${approvePending}
|
|
status=0
|
|
approve_pending_deployments plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|
[[ "$status" -eq 2 ]]
|
|
`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(mutationFailure.status, mutationFailure.stderr).toBe(0);
|
|
|
|
const mismatchedApprovedSha = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -uo pipefail
|
|
GITHUB_REPOSITORY=openclaw/openclaw
|
|
GITHUB_STEP_SUMMARY=/dev/null
|
|
gh() {
|
|
if [[ "$1" == "run" && "$2" == "view" ]]; then
|
|
printf '%s\\n' '{"headSha":"${"b".repeat(40)}","url":"https://example.invalid/run/123"}'
|
|
return 0
|
|
fi
|
|
if [[ "$1" == "run" && "$2" == "cancel" ]]; then
|
|
return 0
|
|
fi
|
|
return 99
|
|
}
|
|
print_failed_run_summary() { :; }
|
|
${readGh}
|
|
${verifyChild}
|
|
${approvePending}
|
|
status=0
|
|
approve_pending_deployments plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|
[[ "$status" -eq 2 ]]
|
|
`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(mismatchedApprovedSha.status, mismatchedApprovedSha.stderr).toBe(0);
|
|
|
|
const mismatchedWaitSha = spawnSync(
|
|
"bash",
|
|
[
|
|
"-c",
|
|
`
|
|
set -uo pipefail
|
|
GITHUB_REPOSITORY=openclaw/openclaw
|
|
gh() {
|
|
if [[ "$1" == "run" && "$2" == "view" ]]; then
|
|
printf '%s\\n' '{"headSha":"${"b".repeat(40)}","url":"https://example.invalid/run/123"}'
|
|
return 0
|
|
fi
|
|
if [[ "$1" == "run" && "$2" == "cancel" ]]; then
|
|
return 0
|
|
fi
|
|
return 99
|
|
}
|
|
${readGh}
|
|
${verifyChild}
|
|
${waitForRun}
|
|
status=0
|
|
wait_for_run plugin-clawhub-new.yml 123 "${expectedSha}" || status=$?
|
|
[[ "$status" -eq 1 ]]
|
|
`,
|
|
],
|
|
{ encoding: "utf8" },
|
|
);
|
|
expect(mismatchedWaitSha.status, mismatchedWaitSha.stderr).toBe(0);
|
|
});
|
|
|
|
it("keeps release workflow setup aligned", () => {
|
|
const releaseChecks = readWorkflow(RELEASE_CHECKS_WORKFLOW);
|
|
const installSmoke = readWorkflow(INSTALL_SMOKE_REUSABLE_WORKFLOW);
|
|
const crossOs = readWorkflow(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW);
|
|
const liveE2e = readWorkflow(LIVE_E2E_WORKFLOW);
|
|
const qaLive = readWorkflow(QA_LIVE_TRANSPORTS_WORKFLOW);
|
|
const releaseWorkflowPaths = [
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
RELEASE_TELEGRAM_QA_WORKFLOW,
|
|
CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW,
|
|
LIVE_E2E_WORKFLOW,
|
|
NPM_TELEGRAM_WORKFLOW,
|
|
".github/workflows/openclaw-release-publish.yml",
|
|
".github/workflows/android-release.yml",
|
|
".github/workflows/openclaw-npm-release.yml",
|
|
".github/workflows/macos-release.yml",
|
|
".github/workflows/plugin-clawhub-release.yml",
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
PLUGIN_NPM_RELEASE_WORKFLOW,
|
|
];
|
|
|
|
for (const workflowPath of releaseWorkflowPaths) {
|
|
const workflow = readWorkflow(workflowPath);
|
|
expect(workflow.env?.NODE_VERSION, workflowPath).toBe("24.21.0");
|
|
expect(workflow.env?.PNPM_VERSION, workflowPath).toBeUndefined();
|
|
}
|
|
|
|
expect(releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"]).toBe(15);
|
|
expect(
|
|
workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"),
|
|
"Setup Node environment",
|
|
).with?.["install-deps"],
|
|
).toBe("true");
|
|
expect(installSmoke.jobs?.preflight?.["timeout-minutes"]).toBe(15);
|
|
expect(installSmoke.jobs?.["install-smoke-fast"]?.["timeout-minutes"]).toBe(120);
|
|
expect(installSmoke.jobs?.root_dockerfile_image?.["timeout-minutes"]).toBe(60);
|
|
expect(installSmoke.jobs?.root_dockerfile_image_ready?.["timeout-minutes"]).toBe(5);
|
|
expect(installSmoke.jobs?.qr_package_install_smoke?.["timeout-minutes"]).toBe(30);
|
|
expect(installSmoke.jobs?.root_dockerfile_smokes?.["timeout-minutes"]).toBe(90);
|
|
expect(installSmoke.jobs?.installer_smoke_update_image?.["timeout-minutes"]).toBe(45);
|
|
expect(installSmoke.jobs?.installer_smoke_nonroot_image?.["timeout-minutes"]).toBe(45);
|
|
expect(installSmoke.jobs?.installer_smoke_update?.["timeout-minutes"]).toBe(120);
|
|
expect(installSmoke.jobs?.installer_smoke_nonroot?.["timeout-minutes"]).toBe(60);
|
|
expect(installSmoke.jobs?.installer_smoke?.["timeout-minutes"]).toBe(5);
|
|
expect(installSmoke.jobs?.bun_global_install_smoke?.["timeout-minutes"]).toBe(60);
|
|
expect(installSmoke.jobs?.["docker-e2e-fast"]?.["timeout-minutes"]).toBe(12);
|
|
expect(crossOs.jobs?.prepare?.["timeout-minutes"]).toBe(90);
|
|
expect(
|
|
new Set(
|
|
evaluatedJobTimeouts(
|
|
CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW,
|
|
"cross_os_release_checks",
|
|
workflowJob(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, "cross_os_release_checks"),
|
|
),
|
|
),
|
|
).toEqual(new Set([60, 180]));
|
|
expect(qaLive.jobs?.authorize_actor?.["timeout-minutes"]).toBe(10);
|
|
expect(qaLive.jobs?.validate_selected_ref?.["timeout-minutes"]).toBe(30);
|
|
expect(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"]).toBe(30);
|
|
expect(liveE2e.jobs?.plan_release_workflow_matrices?.["timeout-minutes"]).toBe(10);
|
|
expect(liveE2e.jobs?.validate_release_live_cache?.["timeout-minutes"]).toBe(20);
|
|
expect(readFileSync(LIVE_E2E_WORKFLOW, "utf8")).toContain(
|
|
"timeout --foreground --kill-after=30s 8m pnpm test:live:cache",
|
|
);
|
|
});
|
|
|
|
it("keeps known bounded dominant child paths below the centralized drain owner", () => {
|
|
const fullRelease = readWorkflow(FULL_RELEASE_VALIDATION_WORKFLOW);
|
|
const fullReleaseCandidate = readWorkflow(FULL_RELEASE_CANDIDATE_WORKFLOW);
|
|
const pluginPrerelease = readWorkflow(PLUGIN_PRERELEASE_WORKFLOW);
|
|
const liveE2e = readWorkflow(LIVE_E2E_WORKFLOW);
|
|
const releaseChecks = readWorkflow(RELEASE_CHECKS_WORKFLOW);
|
|
const installSmoke = readWorkflow(INSTALL_SMOKE_REUSABLE_WORKFLOW);
|
|
const crossOs = readWorkflow(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW);
|
|
const packageAcceptance = readWorkflow(PACKAGE_ACCEPTANCE_WORKFLOW);
|
|
const qaLive = readWorkflow(QA_LIVE_TRANSPORTS_WORKFLOW);
|
|
const profiles = ["beta", "stable", "full"] as const;
|
|
const releaseDecisionTimeout = timeoutForProfile(
|
|
fullRelease.jobs?.release_decision?.["timeout-minutes"],
|
|
"beta",
|
|
);
|
|
const diagnosticDrainTimeout = timeoutForProfile(
|
|
fullRelease.jobs?.diagnostic_drain?.["timeout-minutes"],
|
|
"beta",
|
|
);
|
|
expect(releaseDecisionTimeout).toBe(720);
|
|
expect(diagnosticDrainTimeout).toBe(720);
|
|
for (const dispatchJob of [
|
|
"normal_ci",
|
|
"plugin_prerelease_independent",
|
|
"plugin_prerelease_candidate",
|
|
"release_checks_independent",
|
|
"release_checks_candidate",
|
|
"npm_telegram",
|
|
"performance",
|
|
]) {
|
|
expect(fullRelease.jobs?.[dispatchJob]?.["timeout-minutes"], dispatchJob).toBe(15);
|
|
}
|
|
|
|
const ciPreflight = workflowJob(CI_WORKFLOW, "preflight");
|
|
const ciIos = workflowJob(CI_WORKFLOW, "ios-build");
|
|
const ciGate = workflowJob(CI_WORKFLOW, "ci-gate");
|
|
expect(jobNeeds(ciIos)).toEqual(["preflight"]);
|
|
expect(jobNeeds(ciGate)).toEqual(expect.arrayContaining(["preflight", "ios-build"]));
|
|
const ciPath = [
|
|
timeoutForProfile(ciPreflight["timeout-minutes"], "beta"),
|
|
timeoutForProfile(ciIos["timeout-minutes"], "beta"),
|
|
timeoutForProfile(ciGate["timeout-minutes"], "beta"),
|
|
];
|
|
expect(ciPath).toEqual([20, 150, 5]);
|
|
const ciChildTimeout = ciPath.reduce((total, timeout) => total + timeout, 0);
|
|
expect(diagnosticDrainTimeout - ciChildTimeout).toBeGreaterThanOrEqual(60);
|
|
|
|
expect(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"]).toBe(30);
|
|
const pluginChildTimeouts = Object.fromEntries(
|
|
profiles.map((profile) => [
|
|
profile,
|
|
pluginPrereleaseTimeoutFloor(pluginPrerelease, liveE2e, profile),
|
|
]),
|
|
) as Record<(typeof profiles)[number], number>;
|
|
expect(pluginChildTimeouts).toEqual({ beta: 170, stable: 170, full: 200 });
|
|
for (const profile of profiles) {
|
|
expect(
|
|
diagnosticDrainTimeout - pluginChildTimeouts[profile],
|
|
`plugin-prerelease:${profile}`,
|
|
).toBeGreaterThanOrEqual(60);
|
|
}
|
|
|
|
const releasePackageJob = workflowJob(
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"package_acceptance_release_checks",
|
|
);
|
|
expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"))).toEqual([
|
|
"resolve_target",
|
|
]);
|
|
expect(jobNeeds(releasePackageJob)).toEqual(["resolve_target", "prepare_release_package"]);
|
|
expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "package_integrity"))).toEqual([
|
|
"resolve_package",
|
|
]);
|
|
expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "docker_acceptance"))).toEqual([
|
|
"resolve_package",
|
|
"package_integrity",
|
|
]);
|
|
expect(jobNeeds(workflowJob(LIVE_E2E_WORKFLOW, "prepare_docker_e2e_image"))).toEqual([
|
|
"validate_selected_ref",
|
|
]);
|
|
expect(jobNeeds(workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_lanes"))).toEqual(
|
|
expect.arrayContaining(["validate_selected_ref", "prepare_docker_e2e_image"]),
|
|
);
|
|
expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary"))).toContain(
|
|
"docker_acceptance",
|
|
);
|
|
expect(jobNeeds(workflowJob(PACKAGE_ACCEPTANCE_WORKFLOW, "summary"))).toContain(
|
|
"npm_12_install_sh",
|
|
);
|
|
expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"))).toContain(
|
|
"package_acceptance_release_checks",
|
|
);
|
|
const releasePackagePaths = Object.fromEntries(
|
|
profiles.map((profile) => [
|
|
profile,
|
|
[
|
|
timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], profile),
|
|
timeoutForProfile(
|
|
releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"],
|
|
profile,
|
|
),
|
|
timeoutForProfile(packageAcceptance.jobs?.resolve_package?.["timeout-minutes"], profile),
|
|
timeoutForProfile(
|
|
packageAcceptance.jobs?.package_integrity?.["timeout-minutes"],
|
|
profile,
|
|
),
|
|
timeoutForProfile(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"], profile),
|
|
timeoutForProfile(liveE2e.jobs?.prepare_docker_e2e_image?.["timeout-minutes"], profile),
|
|
Math.max(
|
|
...evaluatedJobTimeouts(
|
|
LIVE_E2E_WORKFLOW,
|
|
"validate_docker_lanes",
|
|
workflowJob(LIVE_E2E_WORKFLOW, "validate_docker_lanes"),
|
|
),
|
|
),
|
|
timeoutForProfile(packageAcceptance.jobs?.summary?.["timeout-minutes"], profile),
|
|
timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], profile),
|
|
],
|
|
]),
|
|
) as Record<(typeof profiles)[number], number[]>;
|
|
expect(releasePackagePaths).toEqual({
|
|
beta: [30, 15, 60, 10, 30, 60, 90, 5, 5],
|
|
stable: [30, 15, 60, 10, 30, 60, 90, 5, 5],
|
|
full: [30, 15, 60, 10, 30, 90, 90, 5, 5],
|
|
});
|
|
const releaseChecksParent = workflowJob(
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"release_checks_candidate",
|
|
);
|
|
expect(releaseChecksParent["timeout-minutes"]).toBe(15);
|
|
const releasePackageTimeouts = {
|
|
beta: releasePackagePaths.beta.reduce((total, timeout) => total + timeout, 0),
|
|
stable: releasePackagePaths.stable.reduce((total, timeout) => total + timeout, 0),
|
|
full: releasePackagePaths.full.reduce((total, timeout) => total + timeout, 0),
|
|
};
|
|
for (const [profile, childTimeout] of Object.entries(releasePackageTimeouts)) {
|
|
expect(
|
|
diagnosticDrainTimeout - childTimeout,
|
|
`release-package:${profile}`,
|
|
).toBeGreaterThanOrEqual(60);
|
|
}
|
|
|
|
const releaseSummary = workflowJob(RELEASE_CHECKS_WORKFLOW, "summary");
|
|
const releaseCrossOs = workflowJob(RELEASE_CHECKS_WORKFLOW, "cross_os_release_checks");
|
|
expect(jobNeeds(releaseCrossOs)).toEqual(["resolve_target", "prepare_release_package"]);
|
|
expect(jobNeeds(workflowJob(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, "prepare"))).toEqual([]);
|
|
expect(
|
|
jobNeeds(workflowJob(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, "cross_os_release_checks")),
|
|
).toEqual(["prepare"]);
|
|
expect(jobNeeds(releaseSummary)).toContain("cross_os_release_checks");
|
|
const releaseCrossOsPath = [
|
|
timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(releaseChecks.jobs?.prepare_release_package?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(crossOs.jobs?.prepare?.["timeout-minutes"], "stable"),
|
|
Math.max(
|
|
...evaluatedJobTimeouts(
|
|
CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW,
|
|
"cross_os_release_checks",
|
|
workflowJob(CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW, "cross_os_release_checks"),
|
|
),
|
|
),
|
|
timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"),
|
|
];
|
|
expect(releaseCrossOsPath).toEqual([30, 15, 90, 180, 5]);
|
|
|
|
const releaseInstall = workflowJob(RELEASE_CHECKS_WORKFLOW, "install_smoke_release_checks");
|
|
expect(jobNeeds(releaseInstall)).toEqual(["resolve_target"]);
|
|
expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "root_dockerfile_image"))).toEqual(
|
|
["preflight"],
|
|
);
|
|
expect(
|
|
jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "root_dockerfile_image_ready")),
|
|
).toEqual(["preflight", "root_dockerfile_image"]);
|
|
expect(
|
|
jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_candidate_payload")),
|
|
).toEqual(["preflight"]);
|
|
expect(
|
|
jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_update_image")),
|
|
).toEqual(["preflight"]);
|
|
expect(
|
|
jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_nonroot_image")),
|
|
).toEqual(["preflight"]);
|
|
expect(
|
|
jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_update")),
|
|
).toEqual(["preflight", "installer_smoke_candidate_payload", "installer_smoke_update_image"]);
|
|
expect(
|
|
jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke_nonroot")),
|
|
).toEqual(["preflight", "installer_smoke_candidate_payload", "installer_smoke_nonroot_image"]);
|
|
expect(jobNeeds(workflowJob(INSTALL_SMOKE_REUSABLE_WORKFLOW, "installer_smoke"))).toEqual([
|
|
"preflight",
|
|
"root_dockerfile_image",
|
|
"root_dockerfile_image_ready",
|
|
"installer_smoke_candidate_payload",
|
|
"installer_smoke_update_image",
|
|
"installer_smoke_update",
|
|
"installer_smoke_nonroot_image",
|
|
"installer_smoke_nonroot",
|
|
]);
|
|
expect(jobNeeds(releaseSummary)).toContain("install_smoke_release_checks");
|
|
const releaseInstallPath = [
|
|
timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(installSmoke.jobs?.preflight?.["timeout-minutes"], "stable"),
|
|
Math.max(
|
|
timeoutForProfile(
|
|
installSmoke.jobs?.installer_smoke_candidate_payload?.["timeout-minutes"],
|
|
"stable",
|
|
),
|
|
timeoutForProfile(
|
|
installSmoke.jobs?.installer_smoke_update_image?.["timeout-minutes"],
|
|
"stable",
|
|
),
|
|
),
|
|
timeoutForProfile(installSmoke.jobs?.installer_smoke_update?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(installSmoke.jobs?.installer_smoke?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"),
|
|
];
|
|
expect(releaseInstallPath).toEqual([30, 15, 75, 120, 5, 5]);
|
|
const releaseInstallNonrootPath = [
|
|
timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(installSmoke.jobs?.preflight?.["timeout-minutes"], "stable"),
|
|
Math.max(
|
|
timeoutForProfile(
|
|
installSmoke.jobs?.installer_smoke_candidate_payload?.["timeout-minutes"],
|
|
"stable",
|
|
),
|
|
timeoutForProfile(
|
|
installSmoke.jobs?.installer_smoke_nonroot_image?.["timeout-minutes"],
|
|
"stable",
|
|
),
|
|
),
|
|
timeoutForProfile(installSmoke.jobs?.installer_smoke_nonroot?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(installSmoke.jobs?.installer_smoke?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"),
|
|
];
|
|
expect(releaseInstallNonrootPath).toEqual([30, 15, 75, 60, 5, 5]);
|
|
|
|
const releaseQaLive = workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_release_checks");
|
|
expect(jobNeeds(releaseQaLive)).toEqual(["resolve_target"]);
|
|
expect(jobNeeds(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "validate_selected_ref"))).toEqual([
|
|
"authorize_actor",
|
|
]);
|
|
expect(jobNeeds(workflowJob(QA_LIVE_TRANSPORTS_WORKFLOW, "run_live_matrix"))).toEqual([
|
|
"authorize_actor",
|
|
"validate_selected_ref",
|
|
]);
|
|
expect(jobNeeds(releaseSummary)).toContain("qa_live_release_checks");
|
|
const releaseQaLivePath = [
|
|
timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(qaLive.jobs?.authorize_actor?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(qaLive.jobs?.validate_selected_ref?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(qaLive.jobs?.run_live_matrix?.["timeout-minutes"], "stable"),
|
|
timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "stable"),
|
|
];
|
|
expect(releaseQaLivePath).toEqual([30, 10, 30, 90, 5]);
|
|
|
|
for (const [pathName, path] of [
|
|
["cross-os", releaseCrossOsPath],
|
|
["install", releaseInstallPath],
|
|
["qa-live", releaseQaLivePath],
|
|
] as const) {
|
|
const childTimeout = path.reduce((total, timeout) => total + timeout, 0);
|
|
expect(
|
|
diagnosticDrainTimeout - childTimeout,
|
|
`release-checks:${pathName}`,
|
|
).toBeGreaterThanOrEqual(60);
|
|
}
|
|
|
|
expect(
|
|
jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "qa_live_telegram_release_checks")),
|
|
).toEqual(["resolve_target"]);
|
|
expect(jobNeeds(workflowJob(RELEASE_CHECKS_WORKFLOW, "summary"))).toContain(
|
|
"qa_live_telegram_release_checks",
|
|
);
|
|
const releaseTelegramPath = [
|
|
timeoutForProfile(releaseChecks.jobs?.resolve_target?.["timeout-minutes"], "beta"),
|
|
timeoutForProfile(
|
|
releaseChecks.jobs?.qa_live_telegram_release_checks?.["timeout-minutes"],
|
|
"beta",
|
|
),
|
|
timeoutForProfile(releaseChecks.jobs?.summary?.["timeout-minutes"], "beta"),
|
|
];
|
|
expect(releaseTelegramPath).toEqual([30, 210, 5]);
|
|
const releaseTelegramTimeout = releaseTelegramPath.reduce(
|
|
(total, timeout) => total + timeout,
|
|
0,
|
|
);
|
|
expect(diagnosticDrainTimeout - releaseTelegramTimeout).toBeGreaterThanOrEqual(60);
|
|
|
|
const npmTelegramChildTimeout = timeoutForProfile(
|
|
workflowJob(NPM_TELEGRAM_WORKFLOW, "run_package_telegram_e2e")["timeout-minutes"],
|
|
"beta",
|
|
);
|
|
expect(npmTelegramChildTimeout).toBe(60);
|
|
expect(diagnosticDrainTimeout - npmTelegramChildTimeout).toBeGreaterThanOrEqual(60);
|
|
|
|
const performanceResolve = workflowJob(PERFORMANCE_WORKFLOW, "resolve_target");
|
|
const performanceKova = workflowJob(PERFORMANCE_WORKFLOW, "kova");
|
|
const performanceSource = workflowJob(PERFORMANCE_WORKFLOW, "source_performance");
|
|
const performancePublish = workflowJob(PERFORMANCE_WORKFLOW, "publish");
|
|
const performanceArtifactGuard = workflowJob(PERFORMANCE_WORKFLOW, "artifact_only_guard");
|
|
expect(jobNeeds(performanceKova)).toEqual(["resolve_target"]);
|
|
expect(jobNeeds(performanceSource)).toEqual(["resolve_target"]);
|
|
expect(jobNeeds(performancePublish)).toEqual(["resolve_target", "kova", "source_performance"]);
|
|
expect(jobNeeds(performanceArtifactGuard)).toEqual(["resolve_target", "kova", "publish"]);
|
|
expect(performancePublish.if).toContain("inputs.publish_reports == true");
|
|
expect(performanceArtifactGuard.if).toContain("inputs.publish_reports != true");
|
|
expect(timeoutForProfile(performanceSource["timeout-minutes"], "beta")).toBeLessThanOrEqual(
|
|
timeoutForProfile(performanceKova["timeout-minutes"], "beta"),
|
|
);
|
|
const performanceArtifactPath = [
|
|
timeoutForProfile(performanceResolve["timeout-minutes"], "beta"),
|
|
timeoutForProfile(performanceKova["timeout-minutes"], "beta"),
|
|
timeoutForProfile(performanceArtifactGuard["timeout-minutes"], "beta"),
|
|
];
|
|
const performancePublishPath = [
|
|
timeoutForProfile(performanceResolve["timeout-minutes"], "beta"),
|
|
timeoutForProfile(performanceKova["timeout-minutes"], "beta"),
|
|
timeoutForProfile(performancePublish["timeout-minutes"], "beta"),
|
|
];
|
|
expect(performanceArtifactPath).toEqual([10, 240, 5]);
|
|
expect(performancePublishPath).toEqual([10, 240, 30]);
|
|
const performanceParent = workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "performance");
|
|
expect(performanceParent["timeout-minutes"]).toBe(15);
|
|
expect(workflowStep(performanceParent, "Dispatch OpenClaw Performance").run).toContain(
|
|
"-f publish_reports=false",
|
|
);
|
|
for (const [pathName, path] of [
|
|
["artifact-only", performanceArtifactPath],
|
|
["publish", performancePublishPath],
|
|
] as const) {
|
|
const childTimeout = path.reduce((total, timeout) => total + timeout, 0);
|
|
expect(childTimeout, `performance:${pathName}`).toBeLessThanOrEqual(diagnosticDrainTimeout);
|
|
expect(
|
|
diagnosticDrainTimeout - childTimeout,
|
|
`performance:${pathName}`,
|
|
).toBeGreaterThanOrEqual(60);
|
|
}
|
|
|
|
const candidateAcquisition = workflowJob(
|
|
FULL_RELEASE_VALIDATION_WORKFLOW,
|
|
"candidate_acquisition",
|
|
);
|
|
const candidatePrepare = workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "prepare");
|
|
const candidateBinding = workflowJob(FULL_RELEASE_CANDIDATE_WORKFLOW, "resolve_candidate");
|
|
expect(jobNeeds(workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "evidence_reuse"))).toEqual([
|
|
"resolve_target",
|
|
"plugin_compatibility_readiness",
|
|
]);
|
|
expect(jobNeeds(candidateAcquisition)).toEqual([
|
|
"resolve_target",
|
|
"evidence_reuse",
|
|
"prepare_npm_package",
|
|
]);
|
|
expect(jobNeeds(candidatePrepare)).toEqual(["discover"]);
|
|
expect(candidatePrepare.with?.prepare_only).toBe(true);
|
|
expect(jobNeeds(candidateBinding)).toEqual(["discover", "prepare"]);
|
|
expect(jobNeeds(releaseChecksParent)).toEqual([
|
|
"resolve_target",
|
|
"plugin_compatibility_readiness",
|
|
"evidence_reuse",
|
|
"candidate_acquisition",
|
|
]);
|
|
expect(jobNeeds(workflowJob(FULL_RELEASE_VALIDATION_WORKFLOW, "summary"))).toContain(
|
|
"release_checks_candidate",
|
|
);
|
|
const fullParentPath = [
|
|
timeoutForProfile(fullRelease.jobs?.resolve_target?.["timeout-minutes"], "full"),
|
|
timeoutForProfile(
|
|
fullRelease.jobs?.plugin_compatibility_readiness?.["timeout-minutes"],
|
|
"full",
|
|
),
|
|
timeoutForProfile(fullRelease.jobs?.evidence_reuse?.["timeout-minutes"], "full"),
|
|
timeoutForProfile(fullReleaseCandidate.jobs?.discover?.["timeout-minutes"], "full"),
|
|
timeoutForProfile(liveE2e.jobs?.validate_selected_ref?.["timeout-minutes"], "full"),
|
|
timeoutForProfile(liveE2e.jobs?.prepare_docker_e2e_image?.["timeout-minutes"], "full"),
|
|
timeoutForProfile(
|
|
liveE2e.jobs?.bind_full_release_candidate_evidence?.["timeout-minutes"],
|
|
"full",
|
|
),
|
|
timeoutForProfile(candidateBinding["timeout-minutes"], "full"),
|
|
timeoutForProfile(releaseChecksParent["timeout-minutes"], "full"),
|
|
];
|
|
expect(fullParentPath).toEqual([10, 10, 10, 10, 30, 90, 15, 5, 15]);
|
|
const fullParentTimeoutFloor = fullParentPath.reduce((total, timeout) => total + timeout, 0);
|
|
expect(fullParentTimeoutFloor).toBe(195);
|
|
expect(FULL_RELEASE_WAIT_TIMEOUT_MINUTES).toBe(diagnosticDrainTimeout);
|
|
});
|
|
|
|
it("bounds every direct job in nested release workflows", () => {
|
|
const boundedWorkflowPaths = [
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
INSTALL_SMOKE_REUSABLE_WORKFLOW,
|
|
CROSS_OS_RELEASE_CHECKS_REUSABLE_WORKFLOW,
|
|
LIVE_E2E_WORKFLOW,
|
|
PACKAGE_ACCEPTANCE_WORKFLOW,
|
|
QA_LIVE_TRANSPORTS_WORKFLOW,
|
|
RELEASE_TELEGRAM_QA_WORKFLOW,
|
|
NPM_TELEGRAM_WORKFLOW,
|
|
];
|
|
|
|
for (const path of boundedWorkflowPaths) {
|
|
const jobs = readWorkflow(path).jobs ?? {};
|
|
expect(Object.keys(jobs).length, path).toBeGreaterThan(0);
|
|
for (const [jobName, job] of Object.entries(jobs)) {
|
|
if (job.uses) {
|
|
// GitHub does not allow timeout-minutes on reusable-workflow caller jobs.
|
|
expect(job["timeout-minutes"], `${path}:${jobName}`).toBeUndefined();
|
|
continue;
|
|
}
|
|
|
|
const evaluatedTimeouts = evaluatedJobTimeouts(path, jobName, job);
|
|
expect(evaluatedTimeouts.length, `${path}:${jobName}`).toBeGreaterThan(0);
|
|
for (const timeout of evaluatedTimeouts) {
|
|
expect(Number.isFinite(timeout), `${path}:${jobName}`).toBe(true);
|
|
expect(timeout, `${path}:${jobName}`).toBeGreaterThan(0);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("documents checked extended-stable dispatch instead of a raw-SHA workflow ref", () => {
|
|
const releaseCi = readFileSync(".agents/skills/release-openclaw-ci/SKILL.md", "utf8");
|
|
// The CI page is an index over docs/ci/**. Read the whole tree so this
|
|
// assertion follows the content instead of a single file path. The walk is
|
|
// recursive because docs/ci pages are themselves split into subdirectories
|
|
// (docs/ci/scope-and-routing/*); a flat readdir silently drops those and
|
|
// turns a content move into a failure.
|
|
const ciDocs = [
|
|
readFileSync("docs/ci.md", "utf8"),
|
|
...readdirSync("docs/ci", { encoding: "utf8", recursive: true })
|
|
.filter((name) => name.endsWith(".md"))
|
|
.toSorted()
|
|
.map((name) => readFileSync(`docs/ci/${name}`, "utf8")),
|
|
].join("\n");
|
|
// Full release validation is an index over docs/reference/full-release-validation/*.
|
|
// Read the whole set so this assertion follows the content instead of a single file path.
|
|
const fullReleaseDocs = [
|
|
readFileSync("docs/reference/full-release-validation.md", "utf8"),
|
|
...readdirSync("docs/reference/full-release-validation")
|
|
.filter((name) => name.endsWith(".md"))
|
|
.toSorted()
|
|
.map((name) => readFileSync(`docs/reference/full-release-validation/${name}`, "utf8")),
|
|
].join("\n");
|
|
const liveUpdater = readFileSync(".agents/skills/openclaw-live-updater/SKILL.md", "utf8");
|
|
|
|
const canonicalExtendedStableDispatch = [
|
|
'VALIDATION_SHA="<exact-candidate-sha>"',
|
|
'TOOLING_SHA="<recorded-full-main-ancestor-sha>"',
|
|
'CONTEXT_REF="extended-stable/YYYY.M.33"',
|
|
"pnpm ci:full-release",
|
|
'--sha "$VALIDATION_SHA"',
|
|
'--target-ref "$CONTEXT_REF"',
|
|
'--workflow-sha "$TOOLING_SHA"',
|
|
"-f release_profile=stable",
|
|
"-f run_release_soak=true",
|
|
"-f fail_fast=false",
|
|
"-f rerun_group=all",
|
|
"-f reuse_evidence=false",
|
|
"-f dispatch_release_evidence=false",
|
|
];
|
|
expectTextToIncludeAll(liveUpdater, ['--sha "$MAIN_SHA"', '--workflow-sha "$MAIN_SHA"']);
|
|
for (const text of [releaseCi, fullReleaseDocs]) {
|
|
expectTextToIncludeAll(text, canonicalExtendedStableDispatch);
|
|
expect(text).not.toContain('--ref "$VALIDATION_SHA"');
|
|
expect(text).not.toContain('-f ref="$CONTEXT_REF"');
|
|
}
|
|
expectTextToIncludeAll(releaseCi, [
|
|
"`--ref` accepts a branch or tag name, not a raw commit",
|
|
'{"fullRef":"refs/heads/main","ref":"main","sha":"<tooling-sha>"}',
|
|
"Outside this extended-stable procedure, a direct canonical-branch dispatch",
|
|
"Current extended-stable validation requires distinct",
|
|
"Direct canonical-branch and mutable-`main` dispatches are not valid",
|
|
"--ref main",
|
|
'-f tag="$VALIDATION_SHA"',
|
|
"-f preflight_only=true",
|
|
"-f npm_dist_tag=extended-stable",
|
|
'-f release_candidate_branch="$CONTEXT_REF"',
|
|
]);
|
|
expectTextToIncludeAll(releaseCi, [
|
|
"standalone run is a supplemental validation-only preflight",
|
|
"Do not pass",
|
|
"publication `preflight_run_id`",
|
|
"Publication continues to use",
|
|
]);
|
|
expectTextToIncludeAll(ciDocs, [
|
|
'VALIDATION_SHA="<full-commit-sha>"',
|
|
'-f ref="$VALIDATION_SHA"',
|
|
'-f expected_sha="$VALIDATION_SHA"',
|
|
'TOOLING_SHA="<recorded-full-main-ancestor-sha>"',
|
|
'VALIDATION_SHA="<full-release-candidate-sha>"',
|
|
"--target-ref release/YYYY.M.PATCH",
|
|
'--workflow-sha "$TOOLING_SHA"',
|
|
]);
|
|
});
|
|
|
|
it("executes shared release candidate identity validation with its JSON input", () => {
|
|
const selectedSha = "a".repeat(40);
|
|
const candidate = {
|
|
packagePublished: false,
|
|
packageArtifactName: "docker-e2e-package-456-1",
|
|
packageArtifactId: "123",
|
|
packageArtifactDigest: "b".repeat(64),
|
|
packageArtifactRunId: "456",
|
|
packageArtifactRunAttempt: "1",
|
|
packageFileName: "openclaw-current.tgz",
|
|
packageSourceSha: selectedSha,
|
|
packageSha256: "c".repeat(64),
|
|
packageVersion: "2026.7.2",
|
|
imageArtifactName: "docker-e2e-shared-images-123-1",
|
|
imageArtifactId: "789",
|
|
imageArtifactDigest: "d".repeat(64),
|
|
imageArtifactRunId: "456",
|
|
imageArtifactRunAttempt: "1",
|
|
imageArchiveSha256: "e".repeat(64),
|
|
};
|
|
const validation = workflowStep(
|
|
workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package"),
|
|
"Validate shared release candidate identity",
|
|
).run;
|
|
expect(validation).toBeDefined();
|
|
const binDir = resolve(tempDirs.make("release-candidate-validation-"), "bin");
|
|
mkdirSync(binDir, { recursive: true });
|
|
const ghPath = resolve(binDir, "gh");
|
|
writeFileSync(
|
|
ghPath,
|
|
`#!/bin/sh
|
|
if [ "$#" -ne 4 ] || [ "$1" != "api" ] || [ "$2" != "--method" ] || [ "$3" != "GET" ]; then
|
|
exit 1
|
|
fi
|
|
case "$4" in
|
|
*/actions/artifacts/123)
|
|
printf '%s\n' '{"id":123,"name":"docker-e2e-package-456-1","expired":false,"digest":"sha256:${"b".repeat(64)}","workflow_run":{"id":456}}'
|
|
;;
|
|
*/actions/artifacts/790)
|
|
printf '%s\n' '{"id":790,"name":"docker-e2e-prepublish-plugin-registry-456-1","expired":false,"digest":"sha256:${"f".repeat(64)}","workflow_run":{"id":456}}'
|
|
;;
|
|
*/actions/runs/456/attempts/1)
|
|
printf '%s\n' '{"id":456,"run_attempt":1}'
|
|
;;
|
|
*) exit 1 ;;
|
|
esac
|
|
`,
|
|
);
|
|
chmodSync(ghPath, 0o755);
|
|
const validationEnv = {
|
|
...process.env,
|
|
GH_TOKEN: "test-token",
|
|
GITHUB_REPOSITORY: "openclaw/openclaw",
|
|
PATH: `${binDir}:${process.env.PATH ?? ""}`,
|
|
SELECTED_SHA: selectedSha,
|
|
};
|
|
|
|
const valid = spawnSync("bash", ["-c", validation ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...validationEnv,
|
|
CANDIDATE_ARTIFACT_JSON: JSON.stringify(candidate),
|
|
},
|
|
});
|
|
expect(valid.status, valid.stderr).toBe(0);
|
|
|
|
const { packagePublished: _packagePublished, ...missingProvenance } = candidate;
|
|
for (const invalid of [missingProvenance, { ...candidate, packagePublished: "false" }]) {
|
|
const rejected = spawnSync("bash", ["-c", validation ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...validationEnv,
|
|
CANDIDATE_ARTIFACT_JSON: JSON.stringify(invalid),
|
|
},
|
|
});
|
|
expect(rejected.status).not.toBe(0);
|
|
}
|
|
|
|
const registryCandidate = {
|
|
...candidate,
|
|
prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-456-1",
|
|
prepublishPluginRegistryArtifactId: "790",
|
|
prepublishPluginRegistryArtifactDigest: "f".repeat(64),
|
|
prepublishPluginRegistryArtifactRunId: "456",
|
|
prepublishPluginRegistryArtifactRunAttempt: "1",
|
|
prepublishPluginRegistryManifestSha256: "1".repeat(64),
|
|
};
|
|
const validRegistry = spawnSync("bash", ["-c", validation ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...validationEnv,
|
|
CANDIDATE_ARTIFACT_JSON: JSON.stringify(registryCandidate),
|
|
},
|
|
});
|
|
expect(validRegistry.status, validRegistry.stderr).toBe(0);
|
|
|
|
const partialRegistry = spawnSync("bash", ["-c", validation ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...validationEnv,
|
|
CANDIDATE_ARTIFACT_JSON: JSON.stringify({
|
|
...candidate,
|
|
prepublishPluginRegistryArtifactId: "790",
|
|
}),
|
|
},
|
|
});
|
|
expect(partialRegistry.status).not.toBe(0);
|
|
|
|
const mismatchedRegistryName = spawnSync("bash", ["-c", validation ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...validationEnv,
|
|
CANDIDATE_ARTIFACT_JSON: JSON.stringify({
|
|
...registryCandidate,
|
|
prepublishPluginRegistryArtifactName: "docker-e2e-prepublish-plugin-registry-999-1",
|
|
}),
|
|
},
|
|
});
|
|
expect(mismatchedRegistryName.status).not.toBe(0);
|
|
|
|
const mismatched = spawnSync("bash", ["-c", validation ?? ""], {
|
|
encoding: "utf8",
|
|
env: {
|
|
...validationEnv,
|
|
CANDIDATE_ARTIFACT_JSON: JSON.stringify(candidate),
|
|
SELECTED_SHA: "f".repeat(40),
|
|
},
|
|
});
|
|
expect(mismatched.status).not.toBe(0);
|
|
});
|
|
|
|
it("normalizes fresh and reused release package candidate identity", () => {
|
|
const output = workflowJob(RELEASE_CHECKS_WORKFLOW, "prepare_release_package").outputs
|
|
?.candidate_artifact_json;
|
|
expect(output).toContain("needs.resolve_target.outputs.candidate_artifact_json || format");
|
|
for (const field of [
|
|
"packagePublished",
|
|
"packageArtifactDigest",
|
|
"packageArtifactId",
|
|
"packageArtifactName",
|
|
"packageArtifactRunAttempt",
|
|
"packageArtifactRunId",
|
|
"packageFileName",
|
|
"packageSha256",
|
|
"packageSourceSha",
|
|
"packageVersion",
|
|
]) {
|
|
expect(output).toContain(`"${field}"`);
|
|
}
|
|
});
|
|
|
|
it("keeps release history checks blobless", () => {
|
|
const fullHistoryCheckouts: Array<[string, string, string]> = [
|
|
[LIVE_E2E_WORKFLOW, "validate_selected_ref", "Checkout workflow repository"],
|
|
[
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"resolve_target",
|
|
"Checkout selected ref for reachability fallback",
|
|
],
|
|
[PACKAGE_ACCEPTANCE_WORKFLOW, "resolve_package", "Checkout package workflow ref"],
|
|
[PLUGIN_NPM_RELEASE_WORKFLOW, "preview_plugins_npm", "Checkout"],
|
|
[PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub", "Checkout"],
|
|
[
|
|
".github/workflows/openclaw-cross-os-release-checks-reusable.yml",
|
|
"prepare",
|
|
"Checkout public source ref",
|
|
],
|
|
];
|
|
|
|
for (const [workflowPath, jobName, stepName] of fullHistoryCheckouts) {
|
|
expect(
|
|
workflowStep(workflowJob(workflowPath, jobName), stepName).with,
|
|
workflowPath,
|
|
).toMatchObject({
|
|
"fetch-depth": 0,
|
|
filter: "blob:none",
|
|
});
|
|
}
|
|
|
|
const scopedHistoryCheckouts: Array<[string, string, string]> = [
|
|
[OPENCLAW_NPM_RELEASE_WORKFLOW, "validate_publish_request", "Checkout"],
|
|
[RELEASE_PUBLISH_WORKFLOW, "resolve_release_target", "Checkout release tag"],
|
|
[OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "check_openclaw_npm", "Checkout"],
|
|
[OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "prepare_openclaw_npm", "Checkout"],
|
|
[OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "verify_openclaw_npm", "Checkout"],
|
|
[OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "check_sdk_npm", "Checkout trusted Plugin SDK API tooling"],
|
|
];
|
|
for (const [workflowPath, jobName, stepName] of scopedHistoryCheckouts) {
|
|
expect(
|
|
workflowStep(workflowJob(workflowPath, jobName), stepName).with,
|
|
`${workflowPath}:${jobName}`,
|
|
).toMatchObject({
|
|
"fetch-depth": 1,
|
|
filter: "blob:none",
|
|
});
|
|
}
|
|
|
|
const metadataOnlyCheckouts: Array<[string, string, string]> = [
|
|
[LIVE_E2E_WORKFLOW, "validate_selected_ref", "Checkout workflow repository"],
|
|
[
|
|
RELEASE_CHECKS_WORKFLOW,
|
|
"resolve_target",
|
|
"Checkout selected ref for reachability fallback",
|
|
],
|
|
];
|
|
for (const [workflowPath, jobName, stepName] of metadataOnlyCheckouts) {
|
|
expect(workflowStep(workflowJob(workflowPath, jobName), stepName).with).toMatchObject({
|
|
"sparse-checkout": "package.json",
|
|
"sparse-checkout-cone-mode": false,
|
|
});
|
|
}
|
|
|
|
const clawHubPackJob = workflowJob(
|
|
PLUGIN_CLAWHUB_RELEASE_WORKFLOW,
|
|
"pack_plugins_clawhub_artifacts",
|
|
);
|
|
const clawHubPackTargetGuard = workflowStep(clawHubPackJob, "Validate target revision");
|
|
expect(clawHubPackTargetGuard.env?.TARGET_SHA).toBe(
|
|
"${{ needs.preview_plugins_clawhub.outputs.ref_revision }}",
|
|
);
|
|
expect(clawHubPackTargetGuard.run).toContain('[[ ! "${TARGET_SHA}" =~ ^[a-f0-9]{40}$ ]]');
|
|
expect(workflowStep(clawHubPackJob, "Checkout").with).toMatchObject({
|
|
ref: "${{ needs.preview_plugins_clawhub.outputs.ref_revision }}",
|
|
"fetch-depth": 1,
|
|
"persist-credentials": false,
|
|
});
|
|
expect(clawHubPackJob.steps?.map((step) => step.name)).not.toContain(
|
|
"Checkout target revision",
|
|
);
|
|
});
|
|
|
|
it("provisions the trusted parser before packing a frozen npm candidate", () => {
|
|
const job = workflowJob(OPENCLAW_NPM_PREFLIGHT_WORKFLOW, "prepare_openclaw_npm");
|
|
const steps = job.steps ?? [];
|
|
const materialize = workflowStep(job, "Materialize trusted package preparation runtime");
|
|
const provision = workflowStep(job, "Provision trusted package preparation runtime");
|
|
const pack = workflowStep(job, "Pack and seal publishable npm package set");
|
|
|
|
expect(materialize.run).toContain("git -C .release-harness sparse-checkout add");
|
|
expect(materialize.run).toContain(".github/actions/setup-release-harness");
|
|
expect(materialize.run).toContain(".github/actions/setup-pnpm-store-cache");
|
|
expect(materialize.run).toContain("packages patches");
|
|
expect(provision.uses).toBe("./.release-harness/.github/actions/setup-release-harness");
|
|
expect(provision.with?.["node-version"]).toBe("${{ env.NODE_VERSION }}");
|
|
expect(steps.indexOf(materialize)).toBeLessThan(steps.indexOf(provision));
|
|
expect(steps.indexOf(provision)).toBeLessThan(steps.indexOf(pack));
|
|
});
|
|
|
|
it("validates the macOS release handoff before the GitHub release page exists", () => {
|
|
const macosRelease = readWorkflow(".github/workflows/macos-release.yml");
|
|
const validateJob = workflowJob(
|
|
".github/workflows/macos-release.yml",
|
|
"validate_macos_release_request",
|
|
);
|
|
const stepNames = validateJob.steps?.map((step) => step.name) ?? [];
|
|
const buildControlUi = validateJob.steps?.find((step) => step.name === "Build Control UI");
|
|
|
|
expect(stepNames).not.toContain("Ensure matching GitHub release exists");
|
|
expect(macosRelease.jobs?.validate_macos_release_request).toBeDefined();
|
|
expect(buildControlUi?.env?.OPENCLAW_CONTROL_UI_RELEASE_BUILD).toBe("1");
|
|
});
|
|
|
|
it("classifies fast pretag Control UI output as a release artifact", () => {
|
|
const script = readFileSync("scripts/release-fast-pretag-check.sh", "utf8");
|
|
|
|
expect(script).toContain("OPENCLAW_CONTROL_UI_RELEASE_BUILD=1 pnpm ui:build");
|
|
});
|
|
|
|
it("keeps every tracked repository skill visible to Git-aware syncs", () => {
|
|
const skillFiles = execFileSync("git", ["ls-files", ".agents/skills/*/SKILL.md"], {
|
|
encoding: "utf8",
|
|
})
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
|
|
expect(skillFiles.length).toBeGreaterThan(0);
|
|
const ignored = spawnSync("git", ["check-ignore", "--no-index", "--stdin"], {
|
|
encoding: "utf8",
|
|
input: `${skillFiles.join("\n")}\n`,
|
|
});
|
|
expect(ignored.status).toBe(1);
|
|
expect(ignored.stdout).toBe("");
|
|
expect(ignored.stderr).toBe("");
|
|
});
|
|
|
|
it("does not track generated node_modules entries", () => {
|
|
const tracked = execFileSync("git", ["ls-files", "-z", "--", ":(glob)**/node_modules/**"], {
|
|
encoding: "utf8",
|
|
});
|
|
|
|
expect(tracked).toBe("");
|
|
});
|
|
|
|
it("keeps tracked sync metadata and QA Mantis sources visible to remote full syncs", () => {
|
|
for (const path of [
|
|
".github/release/clawhub-cli/package-lock.json",
|
|
".gitignore",
|
|
"apps/android/.gitignore",
|
|
"docs/reference/templates/IDENTITY.md",
|
|
"docs/reference/templates/USER.md",
|
|
"extensions/qa-lab/src/mantis/cli.ts",
|
|
]) {
|
|
const result = spawnSync("git", ["check-ignore", "--no-index", path], {
|
|
encoding: "utf8",
|
|
});
|
|
|
|
expect(result.status).toBe(1);
|
|
expect(result.stdout).toBe("");
|
|
expect(result.stderr).toBe("");
|
|
}
|
|
});
|
|
});
|