fix(release): unblock plugin bootstrap recovery (#108243)

This commit is contained in:
Peter Steinberger
2026-07-15 03:34:08 -07:00
committed by GitHub
parent 99c43b1647
commit 54f70c5d8c
6 changed files with 51 additions and 17 deletions
+4
View File
@@ -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
+5 -9
View File
@@ -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 }}
+16 -1
View File
@@ -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;
@@ -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");
});
});
@@ -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");
@@ -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"');