fix(release): keep protected tooling foundation npm-only

This commit is contained in:
Vincent Koc
2026-08-20 23:47:04 -07:00
parent 6ea973c51f
commit 1c2fb9c745
5 changed files with 32 additions and 253 deletions
+2 -10
View File
@@ -962,7 +962,6 @@ jobs:
CHILD_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
PARENT_WORKFLOW_BRANCH: ${{ github.ref_name }}
PARENT_WORKFLOW_FULL_REF: ${{ github.ref }}
RELEASE_TAG: ${{ inputs.tag }}
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
@@ -2284,16 +2283,9 @@ jobs:
if [[ "$(jq -r '.normal.shouldDispatch' "${clawhub_plan_path}")" == "true" ]]; then
clawhub_dispatch_args=()
append_clawhub_dispatch_args normal
if [[ "${PARENT_WORKFLOW_FULL_REF}" == "refs/heads/main" ||
"${PARENT_WORKFLOW_FULL_REF}" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; then
clawhub_dispatch_args+=(
-f release_publish_full_ref="${PARENT_WORKFLOW_FULL_REF}"
-f release_publish_workflow_sha="${PARENT_WORKFLOW_SHA}"
)
fi
plugin_clawhub_run_id="$(dispatch_workflow_at_ref \
"$(jq -r '.normal.ref' "${clawhub_plan_path}")" \
"${PARENT_WORKFLOW_SHA}" \
"${TARGET_SHA}" \
"$(jq -r '.normal.workflow' "${clawhub_plan_path}")" \
"${clawhub_dispatch_args[@]}")"
else
@@ -2374,7 +2366,7 @@ jobs:
if [[ -n "${plugin_clawhub_run_id}" ]]; then
clawhub_result="$RUNNER_TEMP/clawhub-result.txt"
wait_run_pid=""
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${PARENT_WORKFLOW_SHA}" "${clawhub_result}"
wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${TARGET_SHA}" "${clawhub_result}"
clawhub_pid="${wait_run_pid}"
fi
if [[ -n "${plugin_clawhub_bootstrap_run_id}" ]]; then
+6 -117
View File
@@ -24,24 +24,10 @@ on:
description: Approved OpenClaw Release Publish workflow run id
required: false
type: string
release_publish_run_attempt:
description: Exact approved OpenClaw Release Publish workflow run attempt
required: false
type: string
release_publish_branch:
description: Branch name of the approving OpenClaw Release Publish workflow run
required: false
type: string
release_publish_full_ref:
description: Exact full ref of the approving OpenClaw Release Publish workflow tooling
required: false
default: ""
type: string
release_publish_workflow_sha:
description: Exact workflow SHA of the approving OpenClaw Release Publish tooling
required: false
default: ""
type: string
dry_run:
description: Validate the full ClawHub artifact handoff without publishing.
required: false
@@ -75,7 +61,6 @@ jobs:
matrix: ${{ steps.plan.outputs.matrix }}
bootstrap_matrix: ${{ steps.plan.outputs.bootstrap_matrix }}
missing_trusted_publisher_matrix: ${{ steps.plan.outputs.missing_trusted_publisher_matrix }}
trusted_tooling_identity_json: ${{ steps.tooling_identity.outputs.json }}
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -107,89 +92,9 @@ jobs:
fi
echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Capture trusted tooling identity
id: tooling_identity
env:
CALLER_FULL_REF: ${{ github.ref }}
CALLER_REF: ${{ github.ref_name }}
CALLER_RUN_ATTEMPT: ${{ github.run_attempt }}
CALLER_RUN_ID: ${{ github.run_id }}
CALLER_SHA: ${{ github.sha }}
PARENT_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
PARENT_RUN_ID: ${{ inputs.release_publish_run_id }}
PARENT_STATE_POLICY: ${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active-or-success' || 'recovery-active-or-success-or-failure') || '' }}
TOOLING_FULL_REF: ${{ inputs.release_publish_full_ref }}
TOOLING_REF: ${{ inputs.release_publish_branch }}
TOOLING_SHA: ${{ inputs.release_publish_workflow_sha }}
run: |
set -euo pipefail
if [[ -z "${TOOLING_FULL_REF}${TOOLING_SHA}" ]]; then
echo "json=" >> "$GITHUB_OUTPUT"
exit 0
fi
[[ -n "$TOOLING_FULL_REF" && -n "$TOOLING_REF" && -n "$TOOLING_SHA" ]] || {
echo "Trusted release tooling ref, full ref, and SHA must be provided together." >&2
exit 1
}
[[ -n "$PARENT_RUN_ID" && -n "$PARENT_RUN_ATTEMPT" && -n "$PARENT_STATE_POLICY" ]] || {
echo "Trusted release parent run id, attempt, and state policy must be provided together." >&2
exit 1
}
identity="$(
jq -cn \
--arg repository "$GITHUB_REPOSITORY" \
--arg workflow ".github/workflows/plugin-clawhub-release.yml" \
--arg runId "$CALLER_RUN_ID" \
--arg runAttempt "$CALLER_RUN_ATTEMPT" \
--arg ref "$CALLER_REF" \
--arg fullRef "$CALLER_FULL_REF" \
--arg sha "$CALLER_SHA" \
--arg toolingRef "$TOOLING_REF" \
--arg toolingFullRef "$TOOLING_FULL_REF" \
--arg toolingSha "$TOOLING_SHA" \
--arg parentRepository "$GITHUB_REPOSITORY" \
--arg parentWorkflow ".github/workflows/openclaw-release-publish.yml" \
--arg parentRunId "$PARENT_RUN_ID" \
--arg parentRunAttempt "$PARENT_RUN_ATTEMPT" \
--arg parentRef "$TOOLING_REF" \
--arg parentFullRef "$TOOLING_FULL_REF" \
--arg parentSha "$TOOLING_SHA" \
--arg parentStatePolicy "$PARENT_STATE_POLICY" \
'{
version: 2,
repository: $repository,
workflow: $workflow,
runId: $runId,
runAttempt: $runAttempt,
ref: $ref,
fullRef: $fullRef,
sha: $sha,
toolingRef: $toolingRef,
toolingFullRef: $toolingFullRef,
toolingSha: $toolingSha,
parentRepository: $parentRepository,
parentWorkflow: $parentWorkflow,
parentRunId: $parentRunId,
parentRunAttempt: $parentRunAttempt,
parentRef: $parentRef,
parentFullRef: $parentFullRef,
parentSha: $parentSha,
parentStatePolicy: $parentStatePolicy
}'
)"
echo "json=${identity}" >> "$GITHUB_OUTPUT"
- name: Validate OIDC source matches workflow ref
env:
RELEASE_PUBLISH_FULL_REF: ${{ inputs.release_publish_full_ref }}
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
RELEASE_PUBLISH_TOOLING_REF: ${{ inputs.release_publish_branch }}
RELEASE_PUBLISH_TOOLING_SHA: ${{ inputs.release_publish_workflow_sha }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ steps.ref.outputs.sha }}
WORKFLOW_FULL_REF: ${{ github.ref }}
WORKFLOW_REF: ${{ github.ref_name }}
WORKFLOW_SHA: ${{ github.sha }}
DRY_RUN: ${{ inputs.dry_run && 'true' || 'false' }}
run: |
@@ -199,22 +104,10 @@ jobs:
echo "Dry-run publish target differs from workflow ref; allowing validation-only dispatch."
exit 0
fi
[[ "$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$TARGET_SHA" ]] || {
echo "Split-ref ClawHub publication requires ref to be the exact candidate SHA." >&2
exit 1
}
[[ "$RELEASE_PUBLISH_TOOLING_REF" == "$WORKFLOW_REF" &&
"$RELEASE_PUBLISH_FULL_REF" == "$WORKFLOW_FULL_REF" &&
"$RELEASE_PUBLISH_TOOLING_SHA" == "$WORKFLOW_SHA" ]] || {
echo "Split-ref ClawHub publication tooling identity does not match the executing workflow." >&2
exit 1
}
[[ "$RELEASE_PUBLISH_RUN_ID" =~ ^[1-9][0-9]*$ &&
"$RELEASE_PUBLISH_RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]] || {
echo "Split-ref ClawHub publication requires the exact parent run id and attempt." >&2
exit 1
}
echo "Using trusted workflow tooling with separately bound candidate ${TARGET_SHA}."
echo "Plugin ClawHub OIDC publishes must run from the same ref that is being published." >&2
echo "The ref input is only supported for dry_run=true." >&2
echo "For real publishes, dispatch this workflow with --ref pointing at the target release tag/ref and omit the ref input." >&2
exit 1
fi
- name: Validate ref is on a trusted publish branch
@@ -392,9 +285,6 @@ jobs:
ALLOW_COMPLETED_SUCCESSFUL_PARENT: "true"
GH_TOKEN: ${{ github.token }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
EXPECTED_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
EXPECTED_WORKFLOW_FULL_REF: ${{ inputs.release_publish_full_ref || github.ref }}
EXPECTED_WORKFLOW_SHA: ${{ inputs.release_publish_workflow_sha || github.sha }}
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_publish_branch || github.ref_name }}
run: |
set -euo pipefail
@@ -411,7 +301,7 @@ jobs:
direct_recovery=true
echo "Direct Plugin ClawHub Release recovery with release_publish_run_id; relying on this workflow's clawhub-plugin-release environment approval."
fi
RUN_JSON="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RELEASE_PUBLISH_RUN_ID}" --jq '{workflowName: .name, headBranch: .head_branch, headSha: .head_sha, event, status, conclusion, url: .html_url, runAttempt: .run_attempt, repository: .repository.full_name, path}')"
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
pack_plugins_clawhub_artifacts:
@@ -498,7 +388,7 @@ jobs:
needs:
[preview_plugins_clawhub, pack_plugins_clawhub_artifacts, approve_plugins_clawhub_release]
if: 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')
uses: openclaw/clawhub/.github/workflows/package-publish.yml@a42cd2f73d6afb769b271d463fc111669cb7a499
uses: openclaw/clawhub/.github/workflows/package-publish.yml@d8096dfc039e86ab942ddf9ef117d04849fd84c1
permissions:
actions: read
contents: read
@@ -521,7 +411,6 @@ jobs:
source_path: ${{ matrix.plugin.packageDir }}
inspector_artifact_name: ${{ matrix.plugin.artifactName }}-inspector
publish_json_artifact_name: ${{ matrix.plugin.artifactName }}-publish-json
trusted_tooling_identity_json: ${{ needs.preview_plugins_clawhub.outputs.trusted_tooling_identity_json }}
verify_published_clawhub_package:
needs: [preview_plugins_clawhub, publish_plugins_clawhub]
+3 -5
View File
@@ -353,17 +353,15 @@ export async function buildOpenClawReleaseClawHubPlan(
return {
bootstrapWorkflowSha,
clawHubWorkflowRef: bootstrapWorkflowRef,
clawHubWorkflowRef: releaseTag,
releasePublishBranch,
normal: createDispatchTarget({
workflow: "plugin-clawhub-release.yml",
ref: bootstrapWorkflowRef,
ref: releaseTag,
packages: normalPackages,
releasePublishRunId,
releasePublishBranch,
releasePublishRunAttempt,
includePublishScope: true,
targetRef: releaseSha,
}),
bootstrap: createDispatchTarget({
workflow: "plugin-clawhub-new.yml",
@@ -386,7 +384,7 @@ export async function buildOpenClawReleaseClawHubPlan(
missingTrustedPlugins: joinPackageNames(missingTrustedPlugins),
},
verifier: {
clawHubWorkflowRef: bootstrapWorkflowRef,
clawHubWorkflowRef: releaseTag,
},
};
}
+3 -5
View File
@@ -1315,19 +1315,17 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
},
);
expect(plan.clawHubWorkflowRef).toBe(`release-publish/${"d".repeat(12)}-12345`);
expect(plan.clawHubWorkflowRef).toBe("v2026.4.1-beta.1");
expect(plan.bootstrapWorkflowSha).toBe("d".repeat(40));
expect(plan.releasePublishBranch).toBe("main");
expect(plan.normal).toEqual({
workflow: "plugin-clawhub-release.yml",
ref: `release-publish/${"d".repeat(12)}-12345`,
ref: "v2026.4.1-beta.1",
shouldDispatch: true,
packages: ["@openclaw/demo-plugin"],
inputs: {
publish_scope: "selected",
ref: "a".repeat(40),
plugins: "@openclaw/demo-plugin",
release_publish_run_attempt: "2",
release_publish_run_id: "12345",
release_publish_branch: "main",
},
@@ -1357,7 +1355,7 @@ describe("buildOpenClawReleaseClawHubPlan", () => {
missingTrustedPlugins: "@openclaw/demo-three",
});
expect(plan.verifier).toEqual({
clawHubWorkflowRef: `release-publish/${"d".repeat(12)}-12345`,
clawHubWorkflowRef: "v2026.4.1-beta.1",
});
});
+18 -116
View File
@@ -1963,35 +1963,6 @@ describe("package acceptance workflow", () => {
]);
});
it("binds normal ClawHub dispatch, approval, and waiting to tooling SHA", () => {
const publishRun =
workflowStep(workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"), "Dispatch publish workflows")
.run ?? "";
const waitForRun = shellFunctionSource(publishRun, "wait_for_run");
const appendClawHubArgs = shellFunctionSource(publishRun, "append_clawhub_dispatch_args");
const normalDispatch = publishRun.match(
/plugin_clawhub_run_id="\$\(dispatch_workflow_at_ref[\s\S]*?\)"\n/u,
)?.[0];
expect(normalDispatch).toContain('"${PARENT_WORKFLOW_SHA}"');
expect(normalDispatch).not.toContain('"${TARGET_SHA}"');
expect(publishRun).toContain(
'wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${PARENT_WORKFLOW_SHA}"',
);
expect(publishRun).not.toContain(
'wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${TARGET_SHA}"',
);
expect(waitForRun).toContain(
'approve_pending_deployments "${workflow}" "${run_id}" "${expected_sha}"',
);
expect(appendClawHubArgs).toContain(
"'.[$target].inputs | to_entries[] | [.key, .value] | @tsv'",
);
expect(readFileSync("scripts/lib/openclaw-release-clawhub-plan.ts", "utf8")).toMatch(
/normal: createDispatchTarget\(\{[\s\S]*?ref: bootstrapWorkflowRef,[\s\S]*?targetRef: releaseSha,/u,
);
});
it("compares dependency evidence zip contents independently of archive timestamps", () => {
const orchestration = workflowStep(
workflowJob(RELEASE_PUBLISH_WORKFLOW, "publish"),
@@ -7170,7 +7141,6 @@ describe("package artifact reuse", () => {
".github/workflows/plugin-clawhub-new.yml",
"publish_bootstrap_plugins",
);
const publishOrchestration = workflowStep(releasePublishJob, "Dispatch publish workflows");
const postpublishEvidence = workflowStep(releasePublishJob, "Upload postpublish evidence");
expect(packageJson.scripts).toMatchObject({
@@ -7197,103 +7167,35 @@ describe("package artifact reuse", () => {
"approve_plugins_clawhub_release",
]);
expect(clawHubPublish.uses).toBe(
"openclaw/clawhub/.github/workflows/package-publish.yml@a42cd2f73d6afb769b271d463fc111669cb7a499",
"openclaw/clawhub/.github/workflows/package-publish.yml@d8096dfc039e86ab942ddf9ef117d04849fd84c1",
);
expect(clawHubPublish.permissions).toMatchObject({
actions: "read",
contents: "read",
"id-token": "write",
});
expect(clawHubPublish.with?.trusted_tooling_identity_json).toBe(
"${{ needs.preview_plugins_clawhub.outputs.trusted_tooling_identity_json }}",
);
expect(clawHubPublish.with?.trusted_tooling_identity_json).toBeUndefined();
const clawHubPreview = workflowJob(PLUGIN_CLAWHUB_RELEASE_WORKFLOW, "preview_plugins_clawhub");
expect(clawHubPreview.outputs?.trusted_tooling_identity_json).toBe(
"${{ steps.tooling_identity.outputs.json }}",
);
const toolingIdentity = workflowStep(clawHubPreview, "Capture trusted tooling identity");
expect(toolingIdentity.env).toMatchObject({
CALLER_FULL_REF: "${{ github.ref }}",
CALLER_REF: "${{ github.ref_name }}",
CALLER_RUN_ATTEMPT: "${{ github.run_attempt }}",
CALLER_RUN_ID: "${{ github.run_id }}",
CALLER_SHA: "${{ github.sha }}",
PARENT_RUN_ATTEMPT: "${{ inputs.release_publish_run_attempt }}",
PARENT_RUN_ID: "${{ inputs.release_publish_run_id }}",
PARENT_STATE_POLICY:
"${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active-or-success' || 'recovery-active-or-success-or-failure') || '' }}",
TOOLING_FULL_REF: "${{ inputs.release_publish_full_ref }}",
TOOLING_REF: "${{ inputs.release_publish_branch }}",
TOOLING_SHA: "${{ inputs.release_publish_workflow_sha }}",
});
for (const field of [
"version: 2",
"repository: $repository",
"workflow: $workflow",
"runId: $runId",
"runAttempt: $runAttempt",
"ref: $ref",
"fullRef: $fullRef",
"sha: $sha",
"toolingRef: $toolingRef",
"toolingFullRef: $toolingFullRef",
"toolingSha: $toolingSha",
"parentRepository: $parentRepository",
"parentWorkflow: $parentWorkflow",
"parentRunId: $parentRunId",
"parentRunAttempt: $parentRunAttempt",
"parentRef: $parentRef",
"parentFullRef: $parentFullRef",
"parentSha: $parentSha",
"parentStatePolicy: $parentStatePolicy",
]) {
expect(toolingIdentity.run).toContain(field);
}
expect(toolingIdentity.run).toContain(
"Trusted release parent run id, attempt, and state policy must be provided together.",
);
expect(publishOrchestration.env?.PARENT_WORKFLOW_FULL_REF).toBe("${{ github.ref }}");
expect(publishOrchestration.run).toContain(
'-f release_publish_full_ref="${PARENT_WORKFLOW_FULL_REF}"',
);
expect(publishOrchestration.run).toContain(
'-f release_publish_workflow_sha="${PARENT_WORKFLOW_SHA}"',
);
expect(publishOrchestration.run).toContain(
'-f release_publish_run_attempt="${GITHUB_RUN_ATTEMPT}"',
);
expect(
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
?.release_publish_run_attempt,
).toMatchObject({ required: false, type: "string" });
const oidcSourceValidation = workflowStep(
clawHubPreview,
"Validate OIDC source matches workflow ref",
).toBeUndefined();
expect(
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
?.release_publish_full_ref,
).toBeUndefined();
expect(
readWorkflow(PLUGIN_CLAWHUB_RELEASE_WORKFLOW).on?.workflow_dispatch?.inputs
?.release_publish_workflow_sha,
).toBeUndefined();
expect(clawHubPreview.outputs?.trusted_tooling_identity_json).toBeUndefined();
const publishOrchestration = workflowStep(releasePublishJob, "Dispatch publish workflows");
expect(publishOrchestration.env?.PARENT_WORKFLOW_FULL_REF).toBeUndefined();
expect(publishOrchestration.run).toContain(
'wait_for_run_background plugin-clawhub-release.yml "${plugin_clawhub_run_id}" "${TARGET_SHA}"',
);
expectTextToIncludeAll(oidcSourceValidation.run, [
"Split-ref ClawHub publication requires ref to be the exact candidate SHA.",
"Split-ref ClawHub publication tooling identity does not match the executing workflow.",
"Split-ref ClawHub publication requires the exact parent run id and attempt.",
]);
const clawHubPlanSource = readFileSync("scripts/lib/openclaw-release-clawhub-plan.ts", "utf8");
expect(clawHubPlanSource).toContain("clawHubWorkflowRef: bootstrapWorkflowRef");
expect(clawHubPlanSource).toMatch(
/normal: createDispatchTarget\(\{[\s\S]*?ref: bootstrapWorkflowRef,[\s\S]*?targetRef: releaseSha,/u,
);
const frozenCandidateWorkflow = parse(
execFileSync("git", ["show", "2d25f59b4a5:.github/workflows/plugin-clawhub-release.yml"], {
encoding: "utf8",
}),
) as Workflow;
expect(
frozenCandidateWorkflow.on?.workflow_dispatch?.inputs?.release_publish_full_ref,
).toBeUndefined();
expect(
frozenCandidateWorkflow.on?.workflow_dispatch?.inputs?.release_publish_workflow_sha,
).toBeUndefined();
expect(
frozenCandidateWorkflow.on?.workflow_dispatch?.inputs?.release_publish_run_attempt,
).toBeUndefined();
expect(publishOrchestration.run).not.toContain("release_publish_full_ref");
expect(publishOrchestration.run).not.toContain("release_publish_workflow_sha");
expect(clawHubBootstrapValidation.environment).toBe("clawhub-plugin-bootstrap");
expect(clawHubBootstrapPublish.environment).toBe("clawhub-plugin-bootstrap");