diff --git a/.github/workflows/plugin-clawhub-new.yml b/.github/workflows/plugin-clawhub-new.yml index 02f066ffcd2c..2b5cc7601b66 100644 --- a/.github/workflows/plugin-clawhub-new.yml +++ b/.github/workflows/plugin-clawhub-new.yml @@ -667,6 +667,8 @@ jobs: ARTIFACT_SIZE: ${{ needs.pack_bootstrap_plugins.outputs.artifact_size }} GH_TOKEN: ${{ github.token }} TARGET_SHA: ${{ needs.resolve_bootstrap_plan.outputs.ref_revision }} + WORKFLOW_HEAD_BRANCH: ${{ github.ref_name }} + WORKFLOW_REF: ${{ github.ref }} WORKFLOW_SHA: ${{ github.sha }} CLAWHUB_TOOLCHAIN_INTEGRITY: ${{ steps.clawhub_cli.outputs.integrity }} CLAWHUB_TOOLCHAIN_SHA256: ${{ steps.clawhub_cli.outputs.lock_sha256 }} @@ -688,6 +690,8 @@ jobs: --run-attempt "${ARTIFACT_RUN_ATTEMPT}" \ --run-id "${ARTIFACT_RUN_ID}" \ --target-sha "${TARGET_SHA}" \ + --workflow-head-branch "${WORKFLOW_HEAD_BRANCH}" \ + --workflow-ref "${WORKFLOW_REF}" \ --workflow-sha "${WORKFLOW_SHA}" - name: Rehash immutable ClawHub bootstrap artifacts diff --git a/.github/workflows/plugin-npm-release.yml b/.github/workflows/plugin-npm-release.yml index 0a7e4c338097..73f7a16475cc 100644 --- a/.github/workflows/plugin-npm-release.yml +++ b/.github/workflows/plugin-npm-release.yml @@ -1240,7 +1240,7 @@ jobs: PACKAGE_VERSION: ${{ matrix.plugin.version }} run: node scripts/verify-plugin-npm-published-runtime.mjs "${PACKAGE_NAME}@${PACKAGE_VERSION}" - - name: Publish approved Meta bootstrap tarball + - name: Publish approved bootstrap tarball if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -1251,19 +1251,15 @@ jobs: TARBALL_PATH: ${{ steps.publication_evidence.outputs.tarball_path }} run: | set -euo pipefail - [[ "$PACKAGE_NAME" == "@openclaw/meta-provider" && "$PACKAGE_DIR" == "extensions/meta" ]] || { - echo "npm token bootstrap is restricted to the approved Meta provider package." >&2 - exit 1 - } [[ "$PACKAGE_VERSION" == *"-beta."* && "$PUBLISH_TAG" == "beta" ]] || { - echo "Meta npm token bootstrap requires an approved beta package and beta tag." >&2 + echo "npm token bootstrap requires an approved beta package and beta tag." >&2 exit 1 } [[ -n "${NPM_TOKEN// }" ]] || { - echo "Meta npm token bootstrap requires the protected npm release token." >&2 + echo "npm token bootstrap requires the protected npm release token." >&2 exit 1 } - publish_home="$(mktemp -d "${RUNNER_TEMP}/meta-npm-bootstrap.XXXXXX")" + publish_home="$(mktemp -d "${RUNNER_TEMP}/plugin-npm-bootstrap.XXXXXX")" cleanup() { rm -rf "$publish_home" } @@ -1287,7 +1283,7 @@ jobs: --provenance \ --tag "$PUBLISH_TAG" - - name: Verify Meta bootstrap published runtime + - name: Verify bootstrap published runtime if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' env: PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }} diff --git a/scripts/lib/clawhub-bootstrap-artifact.mjs b/scripts/lib/clawhub-bootstrap-artifact.mjs index d2b61aec0d95..9b551d0bba75 100644 --- a/scripts/lib/clawhub-bootstrap-artifact.mjs +++ b/scripts/lib/clawhub-bootstrap-artifact.mjs @@ -23,6 +23,8 @@ const REPOSITORY_PATTERN = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u; const PACKAGE_NAME_PATTERN = /^@openclaw\/[a-z0-9][a-z0-9._-]*$/u; const PACKAGE_DIR_PATTERN = /^extensions\/[a-z0-9][a-z0-9._-]*$/u; const TAG_PATTERN = /^[a-z0-9][a-z0-9._-]*$/u; +const PROTECTED_WORKFLOW_TAG_PATTERN = + /^refs\/tags\/(release-publish\/([a-f0-9]{12})-[1-9][0-9]*)$/u; const VERSION_PATTERN = /^[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(?:-(?:alpha|beta)\.[1-9][0-9]*|-[1-9][0-9]*)?$/u; const TOOLCHAIN_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)$/u; @@ -431,6 +433,17 @@ export async function downloadClawHubBootstrapArtifact(options) { const producerJobName = requireString(options.producerJobName, "producerJobName"); const targetSha = requirePattern(options.targetSha, COMMIT_PATTERN, "targetSha"); const workflowSha = requirePattern(options.workflowSha, COMMIT_PATTERN, "workflowSha"); + const workflowHeadBranch = requireString(options.workflowHeadBranch, "workflowHeadBranch"); + const workflowRef = requireString(options.workflowRef, "workflowRef"); + const protectedWorkflowTag = PROTECTED_WORKFLOW_TAG_PATTERN.exec(workflowRef); + const trustedMain = workflowRef === "refs/heads/main" && workflowHeadBranch === "main"; + const trustedProtectedTag = + protectedWorkflowTag !== null && + workflowHeadBranch === protectedWorkflowTag[1] && + protectedWorkflowTag[2] === workflowSha.slice(0, 12); + if (!trustedMain && !trustedProtectedTag) { + fail("workflowRef must be main or the SHA-pinned release-publish tag."); + } const artifactDigest = requirePattern(options.artifactDigest, SHA256_PATTERN, "artifactDigest"); const artifactName = requireString(options.artifactName, "artifactName"); const repository = requirePattern(options.repository, REPOSITORY_PATTERN, "repository"); @@ -488,7 +501,7 @@ export async function downloadClawHubBootstrapArtifact(options) { runAttempt, runId, workflowEvent: "workflow_dispatch", - workflowHeadBranch: "main", + workflowHeadBranch, workflowPath: ".github/workflows/plugin-clawhub-new.yml", workflowSha, }, @@ -745,6 +758,8 @@ async function main() { targetSha: args.target_sha, token: process.env.GH_TOKEN, workflowSha: args.workflow_sha, + workflowHeadBranch: args.workflow_head_branch, + workflowRef: args.workflow_ref, }); process.stdout.write(`${JSON.stringify(result)}\n`); return; diff --git a/test/scripts/clawhub-bootstrap-artifact.test.ts b/test/scripts/clawhub-bootstrap-artifact.test.ts index 4cf1cacf9416..8457ebf00f23 100644 --- a/test/scripts/clawhub-bootstrap-artifact.test.ts +++ b/test/scripts/clawhub-bootstrap-artifact.test.ts @@ -285,6 +285,8 @@ describe("ClawHub bootstrap artifact manifest", () => { it("rejects preexisting and symlinked download output roots before fetching", async () => { const paths = fixture(); const downloadOptions = { + workflowHeadBranch: "main", + workflowRef: "refs/heads/main", artifactDigest: "d".repeat(64), artifactId: "456", artifactName: `clawhub-bootstrap-${targetSha.slice(0, 12)}-123-2`, @@ -313,6 +315,14 @@ describe("ClawHub bootstrap artifact manifest", () => { outputRoot: existingRoot, }), ).rejects.toThrow("output directory must not already exist"); + await expect( + downloadClawHubBootstrapArtifact({ + ...downloadOptions, + outputRoot: existingRoot, + workflowHeadBranch: `release-publish/${workflowSha.slice(0, 12)}-123`, + workflowRef: `refs/tags/release-publish/${workflowSha.slice(0, 12)}-123`, + }), + ).rejects.toThrow("output directory must not already exist"); const symlinkTarget = join(paths.artifactRoot, "symlink-target"); const symlinkRoot = join(paths.artifactRoot, "symlink-output"); @@ -324,6 +334,15 @@ describe("ClawHub bootstrap artifact manifest", () => { outputRoot: symlinkRoot, }), ).rejects.toThrow("output directory must not already exist"); + + await expect( + downloadClawHubBootstrapArtifact({ + ...downloadOptions, + outputRoot: join(paths.artifactRoot, "unused-output"), + workflowHeadBranch: `release-publish/${workflowSha.slice(0, 12)}-123`, + workflowRef: `refs/heads/release-publish/${workflowSha.slice(0, 12)}-123`, + }), + ).rejects.toThrow("workflowRef must be main or the SHA-pinned release-publish tag"); }); }); diff --git a/test/scripts/package-acceptance-workflow.test.ts b/test/scripts/package-acceptance-workflow.test.ts index 10176359bf78..c389cdaacae4 100644 --- a/test/scripts/package-acceptance-workflow.test.ts +++ b/test/scripts/package-acceptance-workflow.test.ts @@ -3963,6 +3963,10 @@ describe("package artifact reuse", () => { expect(clawHubNewWorkflow).toContain( "Download and verify immutable ClawHub bootstrap artifact", ); + expect(clawHubNewWorkflow).toContain("WORKFLOW_HEAD_BRANCH: ${{ github.ref_name }}"); + expect(clawHubNewWorkflow).toContain("WORKFLOW_REF: ${{ github.ref }}"); + expect(clawHubNewWorkflow).toContain('--workflow-head-branch "${WORKFLOW_HEAD_BRANCH}"'); + expect(clawHubNewWorkflow).toContain('--workflow-ref "${WORKFLOW_REF}"'); expect(clawHubNewWorkflow).toContain("Rehash immutable ClawHub bootstrap artifacts"); expect(clawHubNewWorkflow).toContain("Download parent ClawHub bootstrap approval"); expect(clawHubNewWorkflow).toContain("RELEASE_APPROVAL_KIND: clawhub-bootstrap"); diff --git a/test/scripts/plugin-npm-extended-stable-workflow.test.ts b/test/scripts/plugin-npm-extended-stable-workflow.test.ts index 5db665dea85a..5d8dd91b8738 100644 --- a/test/scripts/plugin-npm-extended-stable-workflow.test.ts +++ b/test/scripts/plugin-npm-extended-stable-workflow.test.ts @@ -329,15 +329,11 @@ describe("plugin npm extended-stable workflow", () => { }); expect(publish.env?.NODE_AUTH_TOKEN).toBeUndefined(); expect(publish.env?.NPM_TOKEN).toBeUndefined(); - const bootstrap = step( - parsed.jobs?.publish_plugins_npm, - "Publish approved Meta bootstrap tarball", - ); + const bootstrap = step(parsed.jobs?.publish_plugins_npm, "Publish approved bootstrap tarball"); expect(bootstrap.if).toContain("npm-token-bootstrap"); expect(bootstrap.env?.NPM_TOKEN).toBe("${{ secrets.NPM_TOKEN }}"); - expect(bootstrap.run).toContain( - '[[ "$PACKAGE_NAME" == "@openclaw/meta-provider" && "$PACKAGE_DIR" == "extensions/meta" ]]', - ); + expect(bootstrap.env?.PACKAGE_NAME).toContain("publication_evidence.outputs.package_name"); + expect(bootstrap.run).not.toContain("@openclaw/meta-provider"); expect(bootstrap.run).toContain("NPM_CONFIG_USERCONFIG"); expect(bootstrap.run).toContain("unset NODE_AUTH_TOKEN NPM_TOKEN NODE_OPTIONS"); expect(bootstrap.run).toContain('npm publish "$TARBALL_PATH"');