diff --git a/.github/workflows/openclaw-release-publish.yml b/.github/workflows/openclaw-release-publish.yml index e3327fe6bbb5..886ef371cd80 100644 --- a/.github/workflows/openclaw-release-publish.yml +++ b/.github/workflows/openclaw-release-publish.yml @@ -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" diff --git a/.github/workflows/plugin-clawhub-new.yml b/.github/workflows/plugin-clawhub-new.yml index fcc106b2882f..02f066ffcd2c 100644 --- a/.github/workflows/plugin-clawhub-new.yml +++ b/.github/workflows/plugin-clawhub-new.yml @@ -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 diff --git a/scripts/lib/openclaw-release-clawhub-plan.ts b/scripts/lib/openclaw-release-clawhub-plan.ts index 60f9ef70720d..a4620b8cc8ca 100644 --- a/scripts/lib/openclaw-release-clawhub-plan.ts +++ b/scripts/lib/openclaw-release-clawhub-plan.ts @@ -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 { + 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, diff --git a/test/plugin-clawhub-release.test.ts b/test/plugin-clawhub-release.test.ts index 4fadc13b5ff8..4f60a0eee1cd 100644 --- a/test/plugin-clawhub-release.test.ts +++ b/test/plugin-clawhub-release.test.ts @@ -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", diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 12b4ba3f39c2..6f20abc3c6c9 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -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}"'); diff --git a/test/scripts/plugin-clawhub-new-workflow.test.ts b/test/scripts/plugin-clawhub-new-workflow.test.ts index a41bfb41a253..a157bfba50e8 100644 --- a/test/scripts/plugin-clawhub-new-workflow.test.ts +++ b/test/scripts/plugin-clawhub-new-workflow.test.ts @@ -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}"'); }); diff --git a/test/scripts/release-wrapper-scripts.test.ts b/test/scripts/release-wrapper-scripts.test.ts index cc2facb931b5..8bf95b191093 100644 --- a/test/scripts/release-wrapper-scripts.test.ts +++ b/test/scripts/release-wrapper-scripts.test.ts @@ -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");