ci: split plugin ClawHub publishing paths

* feat: partition clawhub plugin release candidates

* fix: read clawhub trusted publisher config endpoint

* feat: split clawhub plugin bootstrap workflow

* ci: split plugin clawhub publish paths

* ci: pin clawhub package publish workflow

* ci: keep clawhub bootstrap token out of builds

* ci: fix clawhub release dry-run gating

* ci: align clawhub oidc publish refs

* ci: make clawhub bootstrap recovery idempotent

* ci: route clawhub repair candidates through bootstrap

* ci: preserve tideclaw alpha clawhub guards

* ci: simplify clawhub release ref handling

* ci: extract clawhub release routing plan

* ci: extract clawhub release runtime state

* test: guard clawhub release helper executability

* ci: pin ClawHub CLI for plugin publishing

* ci: allow historical ClawHub dry-run validation

* ci: fix ClawHub bootstrap token handoff
This commit is contained in:
Patrick Erichsen
2026-06-12 20:16:06 -07:00
committed by GitHub
parent ded3a93058
commit 6cf06e8e7e
12 changed files with 2197 additions and 211 deletions
+160 -13
View File
@@ -1,5 +1,5 @@
// Package Acceptance Workflow tests cover package acceptance workflow script behavior.
import { readdirSync, readFileSync } from "node:fs";
import { readdirSync, readFileSync, statSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { parse } from "yaml";
@@ -56,6 +56,10 @@ function readWorkflow(path: string): Workflow {
return parse(readFileSync(path, "utf8")) as Workflow;
}
function isExecutable(path: string): boolean {
return (statSync(path).mode & 0o111) !== 0;
}
function workflowPaths(): string[] {
return readdirSync(".github/workflows")
.filter((name) => name.endsWith(".yml"))
@@ -1378,7 +1382,6 @@ describe("package artifact reuse", () => {
it("keeps release QA and repo E2E lanes off scarce 32-core runners", () => {
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
const qaWorkflow = readFileSync(QA_LIVE_TRANSPORTS_WORKFLOW, "utf8");
const liveE2eWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
for (const jobName of [
@@ -1547,9 +1550,14 @@ describe("package artifact reuse", () => {
};
const releaseWorkflow = readFileSync(RELEASE_PUBLISH_WORKFLOW, "utf8");
const clawHubWorkflow = readFileSync(".github/workflows/plugin-clawhub-release.yml", "utf8");
const clawHubNewWorkflow = readFileSync(".github/workflows/plugin-clawhub-new.yml", "utf8");
const pluginNpmWorkflow = readFileSync(".github/workflows/plugin-npm-release.yml", "utf8");
const openclawNpmWorkflow = readFileSync(".github/workflows/openclaw-npm-release.yml", "utf8");
const approvalScript = readFileSync("scripts/validate-release-publish-approval.mjs", "utf8");
const clawHubReleasePlanScript = readFileSync(
"scripts/lib/openclaw-release-clawhub-plan.ts",
"utf8",
);
const clawHubResolveRefIndex = clawHubWorkflow.indexOf("- name: Resolve checked-out ref");
const clawHubValidateRefIndex = clawHubWorkflow.indexOf(
"- name: Validate ref is on a trusted publish branch",
@@ -1571,30 +1579,80 @@ describe("package artifact reuse", () => {
expect(packageJson.scripts?.["release:fast-pretag-check"]).toBe(
"bash scripts/release-fast-pretag-check.sh",
);
expect(clawHubWorkflow).toContain('CLAWHUB_REF: "c9bb13023598dcc547fdf4a93b9d42512b8c8854"');
expect(clawHubWorkflow).toContain('CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"');
expect(clawHubWorkflow).not.toContain("CLAWHUB_REPOSITORY:");
expect(clawHubWorkflow).not.toContain("CLAWHUB_REF:");
expect(clawHubWorkflow).toContain("pack_plugins_clawhub_artifacts:");
expect(clawHubWorkflow).toContain("Verify package-local runtime build");
expect(clawHubWorkflow).toContain("Install pinned ClawHub CLI wrapper");
expect(clawHubWorkflow).toContain("Pack ClawHub package artifact");
expect(clawHubWorkflow).toContain("Upload ClawHub package artifact");
expect(clawHubWorkflow).toContain("Validate OIDC source matches workflow ref");
expect(clawHubWorkflow).toContain(
"Dry-run target ref to validate; real OIDC publishes must dispatch the workflow with --ref set to the target release tag/ref",
);
expect(clawHubWorkflow).toContain(
"Plugin ClawHub OIDC publishes must run from the same ref that is being published.",
);
expect(clawHubWorkflow).toContain("The ref input is only supported for dry_run=true.");
expect(clawHubWorkflow).toContain(
"Dry-run publish target differs from workflow ref; allowing validation-only dispatch.",
);
expect(clawHubWorkflow).toContain(
"github.event_name == 'workflow_dispatch' && inputs.dry_run != true && inputs.publish_scope == 'selected' && steps.plan.outputs.skipped_published_count != '0'",
);
expect(clawHubWorkflow).toContain(
"uses: openclaw/clawhub/.github/workflows/package-publish.yml@9d49df109d4ad3dc8a6ecf05d26b39f46d294721",
);
expect(clawHubWorkflow).toContain("dry_run:");
expect(clawHubWorkflow).toContain("default: false");
expect(clawHubWorkflow).toContain("approve_plugin_clawhub_release:");
expect(clawHubWorkflow).not.toContain("approve_plugin_clawhub_release:");
expect(clawHubWorkflow).toContain("approve_plugins_clawhub_release:");
expect(clawHubWorkflow).toContain("environment: clawhub-plugin-release");
expect(clawHubWorkflow).toContain("inputs.dry_run != true");
expect(clawHubWorkflow).toContain("Approve ClawHub package publish");
expect(clawHubWorkflow).toContain("release_publish_branch:");
expect(clawHubWorkflow).toContain(
"always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugin_clawhub_release.result == 'success')",
"TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
);
expect(clawHubWorkflow).toContain(
"uses: openclaw/clawhub/.github/workflows/package-publish.yml@c9bb13023598dcc547fdf4a93b9d42512b8c8854",
"EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
);
expect(clawHubWorkflow).toContain(
"always() && github.event_name == 'workflow_dispatch' && needs.preview_plugins_clawhub.outputs.has_candidates == 'true' && needs.pack_plugins_clawhub_artifacts.result == 'success' && (inputs.dry_run == true || needs.approve_plugins_clawhub_release.result == 'success')",
);
expect(clawHubWorkflow).toContain("dry_run: ${{ inputs.dry_run }}");
expect(clawHubWorkflow).toContain("package_artifact_name: ${{ matrix.plugin.artifactName }}");
expect(clawHubWorkflow).toContain("clawhub_token: ${{ secrets.CLAWHUB_TOKEN }}");
expect(clawHubWorkflow).toContain("source_repo: ${{ github.repository }}");
expect(clawHubWorkflow).toContain(
"source_commit: ${{ needs.preview_plugins_clawhub.outputs.ref_revision }}",
);
expect(clawHubWorkflow).toContain("source_ref: ${{ github.ref }}");
expect(clawHubWorkflow).toContain("source_path: ${{ matrix.plugin.packageDir }}");
expect(clawHubWorkflow).toContain(
"inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector",
);
expect(clawHubWorkflow).toContain(
"publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json",
);
expect(clawHubWorkflow).toContain("tags: ${{ matrix.plugin.publishTag }}");
expect(clawHubWorkflow).toContain("dry_run: ${{ inputs.dry_run }}");
expect(clawHubWorkflow).not.toContain("secrets.CLAWHUB_TOKEN");
expect(clawHubWorkflow).not.toContain("clawhub_token:");
expect(clawHubWorkflow).toContain("bootstrapCandidates");
expect(clawHubWorkflow).toContain("missingTrustedPublisher");
expect(clawHubWorkflow).toContain("bootstrap_candidate_count");
expect(clawHubWorkflow).toContain("missing_trusted_publisher_count");
expect(clawHubWorkflow).toContain("Bootstrap candidates requiring token bootstrap:");
expect(clawHubWorkflow).toContain("Missing trusted publisher candidates:");
expect(clawHubWorkflow).toContain("verify_published_clawhub_package:");
expect(clawHubWorkflow).toContain("inputs.dry_run != true");
expect(clawHubWorkflow).toContain("Verify published ClawHub package");
expect(clawHubWorkflow).not.toContain("bash scripts/plugin-clawhub-publish.sh --publish");
expect(clawHubWorkflow).not.toContain("Write ClawHub token config");
expect(clawHubWorkflow).toContain("bun install failed while preparing ClawHub CLI; retrying");
expect(clawHubWorkflow).not.toContain("Checkout ClawHub CLI source");
expect(clawHubWorkflow).not.toContain("packages/clawhub/src/cli.ts");
expect(clawHubWorkflow).not.toContain(
"bun install failed while preparing ClawHub CLI; retrying",
);
expect(clawHubWorkflow).toContain("max-parallel: 32");
expect(clawHubResolveRefIndex).toBeGreaterThanOrEqual(0);
expect(clawHubValidateRefIndex).toBeGreaterThan(clawHubResolveRefIndex);
@@ -1602,6 +1660,29 @@ describe("package artifact reuse", () => {
expect(clawHubMetadataIndex).toBeGreaterThan(clawHubSetupIndex);
expect(releaseWorkflow).toContain("Plugin npm run ID");
expect(releaseWorkflow).toContain("Plugin ClawHub run ID");
expect(releaseWorkflow).toContain("plugin-clawhub-new.yml");
expect(releaseWorkflow).toContain("Plugin ClawHub bootstrap run ID");
expect(releaseWorkflow).toContain("scripts/openclaw-release-clawhub-plan.ts");
expect(releaseWorkflow).toContain("scripts/openclaw-release-clawhub-runtime-state.ts");
expect(isExecutable("scripts/openclaw-release-clawhub-plan.ts")).toBe(true);
expect(isExecutable("scripts/openclaw-release-clawhub-runtime-state.ts")).toBe(true);
expect(releaseWorkflow).toContain("openclaw-release-clawhub-plan.json");
expect(releaseWorkflow).toContain("openclaw-release-clawhub-runtime-state");
expect(releaseWorkflow).toContain("bootstrap_plugins");
expect(releaseWorkflow).toContain("missing_trusted_plugins");
expect(releaseWorkflow).toContain(".summary.bootstrapPlugins");
expect(releaseWorkflow).toContain(".summary.missingTrustedPlugins");
expect(releaseWorkflow).toContain("append_clawhub_dispatch_args");
expect(releaseWorkflow).toContain("write_clawhub_runtime_state");
expect(releaseWorkflow).toContain(".[$target].inputs | to_entries[]");
expect(releaseWorkflow).toContain(".verifierArgs[]");
expect(releaseWorkflow).toContain(".proofLines.normal");
expect(releaseWorkflow).toContain(".proofLines.bootstrap");
expect(releaseWorkflow).toContain("Bootstrap/repair candidates:");
expect(releaseWorkflow).toContain("Trusted-publisher repair plugins:");
expect(releaseWorkflow).toContain(
"Waiting for plugin-clawhub-new.yml bootstrap to finish before continuing release publish.",
);
expect(releaseWorkflow).toContain("OpenClaw npm run ID");
expect(releaseWorkflow).toContain("npm_telegram_run_id");
expect(releaseWorkflow).toContain('release_publish_run_id="${GITHUB_RUN_ID}"');
@@ -1612,7 +1693,7 @@ describe("package artifact reuse", () => {
);
expect(releaseWorkflow).toContain("registry tarball");
expect(releaseWorkflow).toContain("release SHA");
expect(releaseWorkflow).toContain("not awaited by this proof");
expect(clawHubReleasePlanScript).toContain("not awaited by this proof");
expect(releaseWorkflow).toContain("wait_for_job_success");
expect(releaseWorkflow).toContain("Validate release publish approval");
expect(releaseWorkflow).toContain('conclusion" == "skipped"');
@@ -1621,13 +1702,17 @@ describe("package artifact reuse", () => {
expect(releaseWorkflow).toContain("release:verify-beta");
expect(releaseWorkflow).toContain('--workflow-ref "${CHILD_WORKFLOW_REF}"');
expect(releaseWorkflow).toContain("--skip-github-release");
expect(clawHubReleasePlanScript).toContain("--plugin-clawhub-bootstrap-run");
expect(releaseWorkflow).toContain('verify_args+=(--plugins "${PLUGINS}")');
expect(releaseWorkflow).toContain("openclaw-release-postpublish-evidence");
expect(releaseWorkflow).toContain("Failed child job summary");
expect(releaseWorkflow).toContain("Workflow completion waits for ClawHub");
expect(releaseWorkflow).toContain("Workflow completion does not wait for ClawHub");
expect(releaseWorkflow).toContain('[[ "${WAIT_FOR_CLAWHUB}" == "true" ]]');
expect(releaseWorkflow).toContain("--skip-clawhub");
expect(releaseWorkflow).toContain(
'[[ -n "${plugin_clawhub_bootstrap_run_id}" && "${WAIT_FOR_CLAWHUB}" == "true" ]]',
);
expect(clawHubReleasePlanScript).toContain("--skip-clawhub");
expect(pluginNpmWorkflow).toContain("Validate release publish approval run");
expect(clawHubWorkflow).toContain("Validate release publish approval run");
expect(openclawNpmWorkflow).toContain("Validate release publish approval run");
@@ -1651,9 +1736,71 @@ describe("package artifact reuse", () => {
expect(approvalScript).toContain("must still be in_progress");
expect(approvalScript).toContain("completed with success/failure");
expect(pluginNpmWorkflow).toContain("environment: npm-release");
expect(clawHubWorkflow).toContain("environment: clawhub-plugin-release");
expect(clawHubWorkflow.match(/environment: clawhub-plugin-release/g)?.length).toBe(1);
expect(clawHubNewWorkflow).toContain("name: Plugin ClawHub New");
expect(clawHubNewWorkflow).toContain('CLAWHUB_CLI_PACKAGE: "clawhub@0.21.0"');
expect(clawHubNewWorkflow).not.toContain("CLAWHUB_REPOSITORY:");
expect(clawHubNewWorkflow).not.toContain("CLAWHUB_REF:");
expect(clawHubNewWorkflow).toContain("environment: clawhub-plugin-bootstrap");
expect(clawHubNewWorkflow).toContain("secrets.CLAWHUB_TOKEN");
expect(clawHubNewWorkflow).not.toContain(
"uses: openclaw/clawhub/.github/workflows/package-publish.yml",
);
expect(clawHubNewWorkflow).not.toContain("clawhub_token:");
expect(clawHubNewWorkflow).toContain("Validate pinned ClawHub trusted publisher CLI support");
expect(clawHubNewWorkflow).toContain('npm exec --yes --package "${CLAWHUB_CLI_PACKAGE}"');
expect(clawHubNewWorkflow).toContain(
"CLAW-277 03 - Split OpenClaw plugin ClawHub publishing into OIDC release and token bootstrap workflows",
);
expect(clawHubNewWorkflow).toContain("Usage: clawhub package trusted-publisher set");
expect(clawHubNewWorkflow).toContain("Write ClawHub token config");
expect(clawHubNewWorkflow).toContain("CLAWHUB_CONFIG_PATH=${config_path}");
expect(clawHubNewWorkflow).toContain(
"CLAWHUB_REGISTRY is required for token-gated ClawHub bootstrap.",
);
expect(clawHubNewWorkflow).toContain(
"CLAWHUB_TOKEN is required for token-gated ClawHub bootstrap.",
);
expect(clawHubNewWorkflow).toContain("JSON.stringify({ registry, token }, null, 2)");
expect(clawHubNewWorkflow).toContain("Publish ClawHub bootstrap package");
expect(clawHubNewWorkflow).toContain("bash scripts/plugin-clawhub-publish.sh --publish");
expect(clawHubNewWorkflow).toContain("bootstrapMode");
expect(clawHubNewWorkflow).toContain("BOOTSTRAP_MODE: ${{ matrix.plugin.bootstrapMode }}");
expect(clawHubNewWorkflow).toContain("requiresManualOverride");
expect(clawHubNewWorkflow).toContain(
'OPENCLAW_CLAWHUB_MANUAL_OVERRIDE_REASON="GitHub Actions trusted publisher repair before OIDC migration"',
);
expect(clawHubNewWorkflow).toContain("configure-only");
expect(clawHubNewWorkflow).toContain(
"version is already present on ClawHub; configuring trusted publisher only",
);
expect(clawHubNewWorkflow).toContain(
"EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
);
expect(clawHubNewWorkflow).toContain(
"TRUSTED_PUBLISH_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}",
);
expect(clawHubNewWorkflow).toContain('OPENCLAW_PLUGIN_NPM_RUNTIME_BUILD: "0"');
expect(clawHubNewWorkflow).toContain("trusted-publisher set");
expect(clawHubNewWorkflow).toContain("--workflow-filename plugin-clawhub-release.yml");
expect(clawHubNewWorkflow).not.toContain("--environment clawhub-plugin-release");
expect(clawHubNewWorkflow).toContain("trustedPublisher?.environment != null");
expect(clawHubNewWorkflow).toContain("without an environment pin");
expect(clawHubNewWorkflow).not.toContain("Checkout ClawHub CLI source");
expect(clawHubNewWorkflow).not.toContain("packages/clawhub/src/cli.ts");
expect(clawHubNewWorkflow).toContain("verify_bootstrap_clawhub_package:");
expect(clawHubNewWorkflow).toContain("Verify bootstrap ClawHub package and trusted publisher");
expect(clawHubNewWorkflow).toContain("/trusted-publisher");
expect(clawHubNewWorkflow).toContain('trustedPublisher?.repository !== "openclaw/openclaw"');
expect(openclawNpmWorkflow).toContain("environment: npm-release");
expect(releaseWorkflow).toContain("default: from-validation");
expect(releaseWorkflow).toContain('--release-publish-branch "${CHILD_WORKFLOW_REF}"');
expect(releaseWorkflow).toContain('--release-publish-run-id "${GITHUB_RUN_ID}"');
expect(releaseWorkflow).toContain("jq -r '.normal.ref' \"${clawhub_plan_path}\"");
expect(releaseWorkflow).toContain("jq -r '.normal.workflow' \"${clawhub_plan_path}\"");
expect(releaseWorkflow).toContain("jq -r '.bootstrap.ref' \"${clawhub_plan_path}\"");
expect(releaseWorkflow).toContain("jq -r '.bootstrap.workflow' \"${clawhub_plan_path}\"");
expect(releaseWorkflow).toContain('--clawhub-workflow-ref "${clawhub_workflow_ref}"');
expect(releaseWorkflow).toContain(
'if [[ "$EXPECTED_RELEASE_PROFILE" != "from-validation" && "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then',
);
@@ -15,6 +15,7 @@ describe("parseReleaseVerifyBetaArgs", () => {
repo: "openclaw/openclaw",
registry: "https://clawhub.ai",
workflowRef: undefined,
clawHubWorkflowRef: undefined,
pluginSelection: [],
evidenceOut: undefined,
skipPostpublish: false,
@@ -32,6 +33,8 @@ describe("parseReleaseVerifyBetaArgs", () => {
"2026.5.10-beta.3",
"--workflow-ref",
"release/2026.5.10",
"--clawhub-workflow-ref",
"v2026.5.10-beta.3",
"--plugins",
"@openclaw/plugin-a,@openclaw/plugin-b",
"--full-release-validation-run",
@@ -42,6 +45,8 @@ describe("parseReleaseVerifyBetaArgs", () => {
"22",
"--plugin-clawhub-run",
"33",
"--plugin-clawhub-bootstrap-run",
"34",
"--npm-telegram-run",
"44",
"--evidence-out",
@@ -58,6 +63,7 @@ describe("parseReleaseVerifyBetaArgs", () => {
repo: "openclaw/openclaw",
registry: "https://clawhub.ai",
workflowRef: "release/2026.5.10",
clawHubWorkflowRef: "v2026.5.10-beta.3",
pluginSelection: ["@openclaw/plugin-a", "@openclaw/plugin-b"],
evidenceOut: ".artifacts/release-evidence.json",
skipPostpublish: true,
@@ -69,6 +75,7 @@ describe("parseReleaseVerifyBetaArgs", () => {
openclawNpm: "11",
pluginNpm: "22",
pluginClawHub: "33",
pluginClawHubBootstrap: "34",
npmTelegram: "44",
},
});