fix(release): pin publish workflows to protected tags

This commit is contained in:
Peter Steinberger
2026-07-15 09:51:50 +01:00
parent 9c0adbf93a
commit bb1a7e5069
7 changed files with 139 additions and 45 deletions
+48 -24
View File
@@ -174,9 +174,9 @@ jobs:
tideclaw_alpha_publish=true
fi
sha_pinned_release_publish=false
if [[ "${WORKFLOW_REF}" =~ ^refs/heads/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]; then
if [[ "${WORKFLOW_REF}" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]; then
if [[ "${WORKFLOW_SHA:0:12}" != "${BASH_REMATCH[1]}" ]]; then
echo "SHA-pinned release publish branch does not match workflow SHA ${WORKFLOW_SHA}." >&2
echo "SHA-pinned release publish tag does not match workflow SHA ${WORKFLOW_SHA}." >&2
exit 1
fi
merge_base_sha="$(
@@ -184,13 +184,13 @@ jobs:
--jq '.merge_base_commit.sha | select(test("^[a-f0-9]{40}$"))'
)"
if [[ "${merge_base_sha}" != "${WORKFLOW_SHA}" ]]; then
echo "SHA-pinned release publish workflow revision is not reachable from current main." >&2
echo "SHA-pinned release publish tag revision is not reachable from current main." >&2
exit 1
fi
sha_pinned_release_publish=true
fi
if [[ "${WORKFLOW_REF}" != "refs/heads/main" && "${tideclaw_alpha_publish}" != "true" && "${sha_pinned_release_publish}" != "true" ]]; then
echo "OpenClaw Release Publish must use trusted main workflow tooling, a SHA-pinned release-publish branch, or a matching Tideclaw alpha branch." >&2
echo "OpenClaw Release Publish must use trusted main workflow tooling, a protected SHA-pinned release-publish tag, or a matching Tideclaw alpha branch." >&2
exit 1
fi
if [[ "${PUBLISH_OPENCLAW_NPM}" == "true" && "${PLUGIN_PUBLISH_SCOPE}" != "all-publishable" ]]; then
@@ -594,23 +594,31 @@ jobs:
id: clawhub_plan
env:
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
BOOTSTRAP_WORKFLOW_REF: ${{ startsWith(github.ref, 'refs/tags/release-publish/') && github.ref_name || 'main' }}
GH_TOKEN: ${{ github.token }}
PARENT_WORKFLOW_BRANCH: ${{ github.ref_name }}
RELEASE_TAG: ${{ inputs.tag }}
PLUGIN_PUBLISH_SCOPE: ${{ inputs.plugin_publish_scope }}
PLUGINS: ${{ inputs.plugins }}
run: |
set -euo pipefail
plan_path="${RUNNER_TEMP}/openclaw-release-clawhub-plan.json"
bootstrap_workflow_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" \
--jq '.object.sha | select(test("^[a-f0-9]{40}$"))'
)"
if [[ "${BOOTSTRAP_WORKFLOW_REF}" == "main" ]]; then
bootstrap_workflow_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" \
--jq '.object.sha | select(test("^[a-f0-9]{40}$"))'
)"
else
# Protected release-publish tags bind every child to this exact
# parent revision, so unrelated main movement cannot change tooling.
bootstrap_workflow_sha="${GITHUB_SHA}"
fi
plan_args=(
--bootstrap-workflow-ref "${BOOTSTRAP_WORKFLOW_REF}"
--bootstrap-workflow-sha "${bootstrap_workflow_sha}"
--release-tag "${RELEASE_TAG}"
--release-sha "${TARGET_SHA}"
--release-publish-branch "${CHILD_WORKFLOW_REF}"
--release-publish-branch "${PARENT_WORKFLOW_BRANCH}"
--release-publish-run-attempt "${GITHUB_RUN_ATTEMPT}"
--release-publish-run-id "${GITHUB_RUN_ID}"
--plugin-publish-scope "${PLUGIN_PUBLISH_SCOPE}"
@@ -634,14 +642,15 @@ jobs:
fi
jq -e \
--arg branch "${CHILD_WORKFLOW_REF}" \
--arg bootstrap_ref "${BOOTSTRAP_WORKFLOW_REF}" \
--arg branch "${PARENT_WORKFLOW_BRANCH}" \
--arg bootstrap_workflow_sha "${bootstrap_workflow_sha}" \
--arg attempt "${GITHUB_RUN_ATTEMPT}" \
--arg run_id "${GITHUB_RUN_ID}" \
--arg tag "${RELEASE_TAG}" \
--arg target "${TARGET_SHA}" \
'.bootstrapWorkflowSha == $bootstrap_workflow_sha and
.bootstrap.ref == "main" and
.bootstrap.ref == $bootstrap_ref and
.bootstrap.inputs.ref == $target and
.bootstrap.inputs.bootstrap_workflow_sha == $bootstrap_workflow_sha and
.bootstrap.inputs.release_tag == $tag and
@@ -667,7 +676,7 @@ jobs:
workflow: "OpenClaw Release Publish",
parentRunId: process.env.GITHUB_RUN_ID,
parentRunAttempt: Number(process.env.GITHUB_RUN_ATTEMPT),
workflowBranch: process.env.CHILD_WORKFLOW_REF,
workflowBranch: process.env.PARENT_WORKFLOW_BRANCH,
parentWorkflowSha: process.env.GITHUB_SHA,
bootstrapWorkflowSha: plan.bootstrapWorkflowSha,
releaseTag: process.env.RELEASE_TAG,
@@ -761,6 +770,7 @@ jobs:
TARGET_SHA: ${{ needs.resolve_release_target.outputs.sha }}
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
PARENT_WORKFLOW_BRANCH: ${{ github.ref_name }}
RELEASE_TAG: ${{ inputs.tag }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
@@ -883,16 +893,28 @@ jobs:
}
verify_bootstrap_workflow_sha() {
local approved_sha current_sha
local approved_ref approved_sha current_main_sha
approved_ref="$(jq -er '.bootstrap.ref | select(type == "string" and length > 0)' "${CLAWHUB_PLAN_PATH}")"
approved_sha="$(jq -er '.bootstrapWorkflowSha | select(test("^[a-f0-9]{40}$"))' "${CLAWHUB_PLAN_PATH}")"
current_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" \
--jq '.object.sha | select(test("^[a-f0-9]{40}$"))'
)"
[[ "${current_sha}" == "${approved_sha}" ]] || {
echo "Trusted main moved from approved ClawHub bootstrap workflow SHA ${approved_sha} to ${current_sha}; rerun release approval." >&2
exit 1
}
if [[ "${approved_ref}" == "main" ]]; then
current_main_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" \
--jq '.object.sha | select(test("^[a-f0-9]{40}$"))'
)"
[[ "${approved_sha}" == "${current_main_sha}" ]] || {
echo "Trusted main moved from approved ClawHub bootstrap workflow SHA ${approved_sha} to ${current_main_sha}; rerun release approval." >&2
exit 1
}
else
[[ "${approved_ref}" == "${CHILD_WORKFLOW_REF}" ]] || {
echo "Approved ClawHub bootstrap workflow ref ${approved_ref} does not match protected child workflow ref ${CHILD_WORKFLOW_REF}." >&2
exit 1
}
[[ "${approved_sha}" == "${PARENT_WORKFLOW_SHA}" ]] || {
echo "Approved ClawHub bootstrap workflow SHA ${approved_sha} does not match parent workflow SHA ${PARENT_WORKFLOW_SHA}." >&2
exit 1
}
fi
printf '%s\n' "${approved_sha}"
}
@@ -1657,7 +1679,7 @@ jobs:
android_release_run_id="$(dispatch_workflow_at_ref "${RELEASE_TAG}" "${TARGET_SHA}" android-release.yml \
-f tag="${RELEASE_TAG}" \
-f release_publish_run_id="${GITHUB_RUN_ID}" \
-f release_publish_branch="${CHILD_WORKFLOW_REF}" \
-f release_publish_branch="${PARENT_WORKFLOW_BRANCH}" \
-f release_target_sha="${TARGET_SHA}" \
-f direct_release_recovery=false)"
# Promotion runs in a background subshell; hand the run id to the
@@ -1952,12 +1974,14 @@ jobs:
fi
}
bootstrap_summary_ref="$(jq -er '.bootstrap.ref | select(type == "string" and length > 0)' "${CLAWHUB_PLAN_PATH}")"
bootstrap_summary_sha="$(jq -er '.bootstrapWorkflowSha | select(test("^[a-f0-9]{40}$"))' "${CLAWHUB_PLAN_PATH}")"
{
echo "### Publish sequence"
echo
echo "- Workflow ref: \`${CHILD_WORKFLOW_REF}\`"
echo "- Normal ClawHub workflow ref: release tag \`${RELEASE_TAG}\`"
echo "- ClawHub bootstrap workflow ref: trusted \`main\`, target \`${TARGET_SHA}\`"
echo "- ClawHub bootstrap workflow ref: \`${bootstrap_summary_ref}\` at \`${bootstrap_summary_sha}\`"
echo "- Release tag: \`${RELEASE_TAG}\`"
echo "- Release SHA: \`${TARGET_SHA}\`"
echo "- Release approval: this workflow job"
+29 -9
View File
@@ -75,7 +75,7 @@ jobs:
ref: ${{ github.sha }}
fetch-depth: 0
- name: Require trusted main workflow source
- name: Require trusted workflow source
env:
APPROVED_WORKFLOW_SHA: ${{ inputs.bootstrap_workflow_sha }}
PRETAG_VALIDATION: ${{ inputs.pretag_validation }}
@@ -83,26 +83,42 @@ jobs:
WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
[[ "${WORKFLOW_REF}" == "refs/heads/main" ]] || {
echo "Plugin ClawHub New must be dispatched from trusted main." >&2
exit 1
}
[[ "$(git rev-parse HEAD)" == "${WORKFLOW_SHA}" ]] || {
echo "Trusted workflow checkout does not match github.sha." >&2
exit 1
}
if [[ "${PRETAG_VALIDATION}" != "true" ]]; then
# Repository release-tag rules make this exact-SHA ref immutable.
# Parent attestation is checked again before environment approval.
[[ "${GITHUB_ACTOR}" == "github-actions[bot]" ]] || {
echo "Plugin ClawHub bootstrap release must be dispatched by the approved parent workflow." >&2
exit 1
}
[[ "${APPROVED_WORKFLOW_SHA}" =~ ^[a-f0-9]{40}$ ]] || {
echo "Plugin ClawHub bootstrap requires an exact approved workflow SHA." >&2
exit 1
}
if [[ "${WORKFLOW_REF}" == "refs/heads/main" ]]; then
:
elif [[ "${WORKFLOW_REF}" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ && "${APPROVED_WORKFLOW_SHA:0:12}" == "${BASH_REMATCH[1]}" ]]; then
:
else
echo "Plugin ClawHub bootstrap requires trusted main or the protected SHA-pinned release-publish tag." >&2
exit 1
fi
[[ "${WORKFLOW_SHA}" == "${APPROVED_WORKFLOW_SHA}" ]] || {
echo "Plugin ClawHub New workflow SHA does not match the parent-approved trusted-main SHA." >&2
exit 1
}
elif [[ -n "${APPROVED_WORKFLOW_SHA}" && "${WORKFLOW_SHA}" != "${APPROVED_WORKFLOW_SHA}" ]]; then
echo "Plugin ClawHub pre-tag validation workflow SHA does not match the requested trusted-main SHA." >&2
exit 1
else
[[ "${WORKFLOW_REF}" == "refs/heads/main" ]] || {
echo "Plugin ClawHub pre-tag validation must be dispatched from trusted main." >&2
exit 1
}
if [[ -n "${APPROVED_WORKFLOW_SHA}" && "${WORKFLOW_SHA}" != "${APPROVED_WORKFLOW_SHA}" ]]; then
echo "Plugin ClawHub pre-tag validation workflow SHA does not match the requested trusted-main SHA." >&2
exit 1
fi
fi
- name: Resolve checked-out ref
@@ -362,10 +378,14 @@ jobs:
EXPECTED_WORKFLOW_SHA="$(
jq -er '.headSha | select(test("^[a-f0-9]{40}$"))' <<< "${RUN_JSON}"
)"
EXPECTED_WORKFLOW_REF="refs/heads/${EXPECTED_WORKFLOW_BRANCH}"
if [[ "${EXPECTED_WORKFLOW_BRANCH}" =~ ^release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; then
EXPECTED_WORKFLOW_REF="refs/tags/${EXPECTED_WORKFLOW_BRANCH}"
fi
gh attestation verify "${APPROVAL_PATH}" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/openclaw-release-publish.yml" \
--source-ref "refs/heads/${EXPECTED_WORKFLOW_BRANCH}" \
--source-ref "${EXPECTED_WORKFLOW_REF}" \
--source-digest "${EXPECTED_WORKFLOW_SHA}" \
--deny-self-hosted-runners
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
+16 -1
View File
@@ -23,6 +23,7 @@ type ClawHubDispatchTarget = {
};
type OpenClawReleaseClawHubPlanArgs = {
bootstrapWorkflowRef: string;
bootstrapWorkflowSha: string;
releaseTag: string;
releaseSha: string;
@@ -98,6 +99,14 @@ function requireCommitSha(value: string | undefined, label: string): string {
return sha;
}
function requireBootstrapWorkflowRef(value: string | undefined): string {
const ref = requireArg(value, "--bootstrap-workflow-ref");
if (ref !== "main" && !/^release-publish\/[a-f0-9]{12}-[1-9][0-9]*$/u.test(ref)) {
throw new Error("--bootstrap-workflow-ref must be main or a SHA-pinned release-publish tag.");
}
return ref;
}
function requirePositiveInteger(value: string | undefined, label: string): string {
const result = requireArg(value, label);
if (!/^[1-9][0-9]*$/u.test(result)) {
@@ -227,6 +236,7 @@ export function parseOpenClawReleaseClawHubPlanArgs(
let releaseTag: string | undefined;
let releaseSha: string | undefined;
let bootstrapWorkflowRef: string | undefined;
let bootstrapWorkflowSha: string | undefined;
let releasePublishBranch: string | undefined;
let releasePublishRunAttempt: string | undefined;
@@ -247,6 +257,9 @@ export function parseOpenClawReleaseClawHubPlanArgs(
};
switch (arg) {
case "--bootstrap-workflow-ref":
bootstrapWorkflowRef = next();
break;
case "--bootstrap-workflow-sha":
bootstrapWorkflowSha = next();
break;
@@ -289,6 +302,7 @@ export function parseOpenClawReleaseClawHubPlanArgs(
}
return {
bootstrapWorkflowRef: requireBootstrapWorkflowRef(bootstrapWorkflowRef),
bootstrapWorkflowSha: requireCommitSha(bootstrapWorkflowSha, "--bootstrap-workflow-sha"),
releaseTag: requireArg(releaseTag, "--release-tag"),
releaseSha: requireCommitSha(releaseSha, "--release-sha"),
@@ -311,6 +325,7 @@ export async function buildOpenClawReleaseClawHubPlan(
registryBaseUrl?: string;
} = {},
): Promise<OpenClawReleaseClawHubPlan> {
const bootstrapWorkflowRef = requireBootstrapWorkflowRef(args.bootstrapWorkflowRef);
const bootstrapWorkflowSha = requireCommitSha(args.bootstrapWorkflowSha, "bootstrapWorkflowSha");
const releaseTag = requireArg(args.releaseTag, "releaseTag");
const releaseSha = requireCommitSha(args.releaseSha, "releaseSha");
@@ -350,7 +365,7 @@ export async function buildOpenClawReleaseClawHubPlan(
}),
bootstrap: createDispatchTarget({
workflow: "plugin-clawhub-new.yml",
ref: "main",
ref: bootstrapWorkflowRef,
packages: bootstrapPackages,
releasePublishRunId,
releasePublishBranch,
+10 -2
View File
@@ -1202,6 +1202,7 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
const plan = await buildOpenClawReleaseClawHubPlan(
{
bootstrapWorkflowRef: `release-publish/${"d".repeat(12)}-12345`,
bootstrapWorkflowSha: "d".repeat(40),
releaseTag: "v2026.4.1-beta.1",
releaseSha: "a".repeat(40),
@@ -1235,7 +1236,7 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
});
expect(plan.bootstrap).toEqual({
workflow: "plugin-clawhub-new.yml",
ref: "main",
ref: `release-publish/${"d".repeat(12)}-12345`,
shouldDispatch: true,
packages: ["@openclaw/demo-two", "@openclaw/demo-three"],
inputs: {
@@ -1289,6 +1290,7 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
const plan = await buildOpenClawReleaseClawHubPlan(
{
bootstrapWorkflowRef: `release-publish/${"d".repeat(12)}-12345`,
bootstrapWorkflowSha: "d".repeat(40),
releaseTag: "v2026.4.1-beta.1",
releaseSha: "b".repeat(40),
@@ -1308,7 +1310,7 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
expect(plan.normal.shouldDispatch).toBe(false);
expect(plan.bootstrap).toMatchObject({
workflow: "plugin-clawhub-new.yml",
ref: "main",
ref: `release-publish/${"d".repeat(12)}-12345`,
shouldDispatch: true,
packages: ["@openclaw/demo-plugin"],
inputs: {
@@ -1333,6 +1335,8 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
it("rejects incompatible all-publishable plugin selection args", () => {
expect(() =>
parseOpenClawReleaseClawHubPlanArgs([
"--bootstrap-workflow-ref",
`release-publish/${"d".repeat(12)}-12345`,
"--bootstrap-workflow-sha",
"d".repeat(40),
"--release-tag",
@@ -1355,6 +1359,8 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
it("requires an exact lowercase release SHA for bootstrap targeting", () => {
const baseArgs = [
"--bootstrap-workflow-ref",
`release-publish/${"d".repeat(12)}-12345`,
"--bootstrap-workflow-sha",
"d".repeat(40),
"--release-tag",
@@ -1376,6 +1382,8 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
it("requires an exact parent release run attempt for bootstrap approval binding", () => {
const args = [
"--bootstrap-workflow-ref",
`release-publish/${"d".repeat(12)}-12345`,
"--bootstrap-workflow-sha",
"d".repeat(40),
"--release-tag",
@@ -343,7 +343,7 @@ describe("package acceptance workflow", () => {
expect(runReleasePublishInputValidation({ PUBLISH_OPENCLAW_NPM: "false" }).status).toBe(0);
});
it("accepts only main-reachable SHA-pinned release publish branches", () => {
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`;
@@ -352,7 +352,7 @@ describe("package acceptance workflow", () => {
const pinnedEnv = {
GITHUB_REPOSITORY: "openclaw/openclaw",
PATH: `${binDir}:${process.env.PATH}`,
WORKFLOW_REF: `refs/heads/release-publish/${workflowSha.slice(0, 12)}-123`,
WORKFLOW_REF: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`,
WORKFLOW_SHA: workflowSha,
};
@@ -364,11 +364,11 @@ describe("package acceptance workflow", () => {
const mismatchedName = runReleasePublishInputValidation({
...pinnedEnv,
WORKFLOW_REF: `refs/heads/release-publish/${"b".repeat(12)}-123`,
WORKFLOW_REF: `refs/tags/release-publish/${"b".repeat(12)}-123`,
});
expect(mismatchedName.status).toBe(1);
expect(mismatchedName.stderr).toContain(
"SHA-pinned release publish branch does not match workflow SHA",
"SHA-pinned release publish tag does not match workflow SHA",
);
const unreachable = runReleasePublishInputValidation({
@@ -377,7 +377,7 @@ describe("package acceptance workflow", () => {
});
expect(unreachable.status).toBe(1);
expect(unreachable.stderr).toContain(
"SHA-pinned release publish workflow revision is not reachable from current main",
"SHA-pinned release publish tag revision is not reachable from current main",
);
});
@@ -407,6 +407,7 @@ describe("package acceptance workflow", () => {
}
expect(publishOrchestration.env?.PARENT_WORKFLOW_SHA).toBe("${{ github.sha }}");
expect(publishOrchestration.env?.CHILD_WORKFLOW_REF).toBe("${{ github.ref_name }}");
expectTextToIncludeAll(publishOrchestration.run, [
'gh api "repos/${GITHUB_REPOSITORY}/commits/${encoded_workflow_ref}"',
'if [[ "$resolved_workflow_sha" != "$expected_sha" ]]',
@@ -3646,9 +3647,15 @@ describe("package artifact reuse", () => {
expect(trustedClawHubPlan.run).toContain(
'--release-publish-run-attempt "${GITHUB_RUN_ATTEMPT}"',
);
expect(trustedClawHubPlan.run).toContain(
'--bootstrap-workflow-ref "${BOOTSTRAP_WORKFLOW_REF}"',
);
expect(trustedClawHubPlan.run).toContain(
'--bootstrap-workflow-sha "${bootstrap_workflow_sha}"',
);
expect(trustedClawHubPlan.run).toContain('if [[ "${BOOTSTRAP_WORKFLOW_REF}" == "main" ]]');
expect(trustedClawHubPlan.run).toContain('bootstrap_workflow_sha="${GITHUB_SHA}"');
expect(trustedClawHubPlan.run).toContain(".bootstrap.ref == $bootstrap_ref");
expect(trustedClawHubPlan.run).toContain(
'gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main"',
);
@@ -3682,6 +3689,9 @@ describe("package artifact reuse", () => {
expect(releaseWorkflow).toContain(".proofLines.bootstrap");
expect(releaseWorkflow).toContain("Bootstrap/repair candidates:");
expect(releaseWorkflow).toContain("Trusted-publisher repair plugins:");
expect(releaseWorkflow).toContain(
'echo "- ClawHub bootstrap workflow ref: \\`${bootstrap_summary_ref}\\` at \\`${bootstrap_summary_sha}\\`"',
);
expect(releaseWorkflow).toContain(
"Waiting for plugin-clawhub-new.yml bootstrap to finish before continuing release publish.",
);
@@ -3691,6 +3701,12 @@ describe("package artifact reuse", () => {
const verifyBootstrapWorkflowIndex = releaseWorkflow.indexOf(
'bootstrap_workflow_sha="$(verify_bootstrap_workflow_sha)"',
);
expect(releaseWorkflow).toContain('if [[ "${approved_ref}" == "main" ]]');
expect(releaseWorkflow).toContain('[[ "${approved_ref}" == "${CHILD_WORKFLOW_REF}" ]]');
expect(releaseWorkflow).toContain('[[ "${approved_sha}" == "${PARENT_WORKFLOW_SHA}" ]]');
expect(releaseWorkflow).toContain(
"Trusted main moved from approved ClawHub bootstrap workflow SHA",
);
const dispatchPluginNpmIndex = releaseWorkflow.indexOf(
'plugin_npm_run_id="$(dispatch_workflow plugin-npm-release.yml',
);
@@ -3762,7 +3778,7 @@ describe("package artifact reuse", () => {
expect(releaseInputGuard).toContain(
'[[ "${WORKFLOW_REF}" != "refs/heads/main" && "${tideclaw_alpha_publish}" != "true" && "${sha_pinned_release_publish}" != "true" ]]',
);
expect(releaseInputGuard).toContain("refs/heads/release-publish/");
expect(releaseInputGuard).toContain("refs/tags/release-publish/");
expect(releaseInputGuard).not.toContain("refs/heads/release/");
expect(releaseInputGuard).toContain(
'"${RELEASE_TAG}" == *"-alpha."* && "${RELEASE_NPM_DIST_TAG}" == "alpha"',
@@ -3894,7 +3910,7 @@ describe("package artifact reuse", () => {
expect(clawHubNewWorkflow).toContain("verify-clawhub-published-artifact.mjs");
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-branch "${PARENT_WORKFLOW_BRANCH}"');
expect(releaseWorkflow).toContain('--release-publish-run-attempt "${GITHUB_RUN_ATTEMPT}"');
expect(releaseWorkflow).toContain('--release-publish-run-id "${GITHUB_RUN_ID}"');
expect(releaseWorkflow).toContain('--release-sha "${TARGET_SHA}"');
@@ -71,8 +71,10 @@ describe("Plugin ClawHub New workflow", () => {
const resolve = job("resolve_bootstrap_plan");
const checkout = step(resolve, "Checkout");
expect(checkout.with?.ref).toBe("${{ github.sha }}");
const guard = step(resolve, "Require trusted main workflow source").run ?? "";
const guard = step(resolve, "Require trusted workflow source").run ?? "";
expect(guard).toContain('WORKFLOW_REF}" == "refs/heads/main"');
expect(guard).toContain('GITHUB_ACTOR}" == "github-actions[bot]"');
expect(guard).toContain("refs/tags/release-publish/");
expect(guard).toContain(
"Plugin ClawHub New workflow SHA does not match the parent-approved trusted-main SHA.",
);
@@ -142,6 +144,10 @@ describe("Plugin ClawHub New workflow", () => {
expect(validation.run).toContain(
"actions/runs/${RELEASE_PUBLISH_RUN_ID}/attempts/${EXPECTED_RUN_ATTEMPT}",
);
expect(validation.run).toContain(
'EXPECTED_WORKFLOW_REF="refs/tags/${EXPECTED_WORKFLOW_BRANCH}"',
);
expect(validation.run).toContain('--source-ref "${EXPECTED_WORKFLOW_REF}"');
expect(validation.run).toContain('--source-digest "${EXPECTED_WORKFLOW_SHA}"');
});
+6 -1
View File
@@ -62,6 +62,8 @@ describe("release wrapper scripts", () => {
it("loads the OpenClaw ClawHub plan CLI and validates required arguments before planning", () => {
const result = runTsxScript("scripts/openclaw-release-clawhub-plan.ts", [
"--bootstrap-workflow-ref",
"main",
"--bootstrap-workflow-sha",
"b".repeat(40),
"--release-tag",
@@ -83,9 +85,12 @@ describe("release wrapper scripts", () => {
const oldTarget = createOldReleaseTarget();
const repositoryRoot = process.cwd();
const releaseSha = "a".repeat(40);
const bootstrapWorkflowRef = `release-publish/${"b".repeat(12)}-123`;
const plan = runTsxScript(
join(repositoryRoot, "scripts/openclaw-release-clawhub-plan.ts"),
[
"--bootstrap-workflow-ref",
bootstrapWorkflowRef,
"--bootstrap-workflow-sha",
"b".repeat(40),
"--release-tag",
@@ -106,7 +111,7 @@ describe("release wrapper scripts", () => {
expect(plan.status, plan.stderr).toBe(0);
expect(JSON.parse(plan.stdout)).toMatchObject({
bootstrapWorkflowSha: "b".repeat(40),
bootstrap: { ref: "main", shouldDispatch: false },
bootstrap: { ref: bootstrapWorkflowRef, shouldDispatch: false },
normal: { ref: "v2026.7.1-beta.3", shouldDispatch: false },
});
expect(plan.stderr).not.toContain("old target planner invoked");