fix(release): keep protected tooling trusted after main moves (#126881)

* fix(release): keep protected tooling trusted after main moves

* fix(release): cover protected tooling recovery paths

* fix(release): honor live tooling contracts

* fix(release): revalidate tooling at npm publish

* fix(release): bind npm publishers to live tooling

* fix(release): preserve trusted dispatch identity

* fix(release): revalidate parent authorization

* fix(release): bind ClawHub to release parent

* docs(release): define frozen tooling identity

* test(release): align ClawHub protected dispatch ref

* fix(release): trust protected plugin npm preflight tooling

* docs(release): scope protected writer guarantees

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

* test(release): cover trusted npm preflight tooling
This commit is contained in:
Vincent Koc
2026-08-21 00:24:31 -07:00
committed by GitHub
parent b470422371
commit fa86caf94f
28 changed files with 3119 additions and 248 deletions
+37 -1
View File
@@ -13,6 +13,11 @@ on:
required: false
default: ""
type: string
trusted_workflow_json:
description: Trusted release tooling identity JSON
required: false
default: ""
type: string
target_context_ref:
description: Optional canonical release branch or tag context for an exact-SHA target
required: false
@@ -160,7 +165,7 @@ env:
# Read retries and one-shot dispatch recovery share this classifier; dispatch POSTs never retry.
GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN: "HTTP 5[0-9][0-9]|Server Error|invalid character .* looking for beginning of value|error connecting to|context deadline exceeded|connection reset by peer|connection refused|TLS handshake timeout|i/o timeout|network is unreachable|(^|[^A-Za-z0-9_])EOF([^A-Za-z0-9_]|$)|ETIMEDOUT|ECONNRESET|EAI_AGAIN"
NODE_VERSION: "24.16.0"
RELEASE_ISOLATION_TOOLING_CONTRACT: "1"
RELEASE_ISOLATION_TOOLING_CONTRACT: "2"
jobs:
resolve_target:
@@ -169,6 +174,7 @@ jobs:
timeout-minutes: 10
outputs:
sha: ${{ steps.resolve.outputs.sha }}
trusted_workflow_json: ${{ steps.tooling_identity.outputs.json }}
live_suite_filter: ${{ steps.filters.outputs.live_suite_filter }}
cross_os_suite_filter: ${{ steps.filters.outputs.cross_os_suite_filter }}
steps:
@@ -181,6 +187,28 @@ jobs:
persist-credentials: false
submodules: false
- name: Resolve trusted workflow identity
id: tooling_identity
env:
GH_TOKEN: ${{ github.token }}
REQUESTED_IDENTITY_JSON: ${{ inputs.trusted_workflow_json }}
WORKFLOW_CONTRACT: ${{ env.RELEASE_ISOLATION_TOOLING_CONTRACT }}
WORKFLOW_FULL_REF: ${{ github.ref }}
WORKFLOW_REF: ${{ github.ref_name }}
WORKFLOW_SHA: ${{ github.sha }}
run: |
set -euo pipefail
identity="$(
node workflow/scripts/release-tooling-identity.mjs resolve \
--repository "$GITHUB_REPOSITORY" \
--workflow-contract "$WORKFLOW_CONTRACT" \
--workflow-ref "$WORKFLOW_REF" \
--workflow-full-ref "$WORKFLOW_FULL_REF" \
--workflow-sha "$WORKFLOW_SHA" \
--requested-identity-json "$REQUESTED_IDENTITY_JSON"
)"
echo "json=${identity}" >> "$GITHUB_OUTPUT"
- name: Resolve target SHA
id: resolve
env:
@@ -457,6 +485,7 @@ jobs:
SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }}
ALLOW_UNRELEASED_CHANGELOG: ${{ inputs.allow_unreleased_changelog || (inputs.target_context_ref == '' && (inputs.ref == 'main' || inputs.ref == 'refs/heads/main')) }}
PLUGIN_PRERELEASE_NODE_EXCLUDE_PATTERNS_JSON: ${{ inputs.plugin_prerelease_node_exclude_patterns_json }}
TRUSTED_WORKFLOW_JSON: ${{ needs.resolve_target.outputs.trusted_workflow_json }}
run: |
set -euo pipefail
# Lane-selection inputs must match the prior run's manifest exactly;
@@ -492,10 +521,17 @@ jobs:
allowUnreleasedChangelog: $allowUnreleasedChangelog,
pluginPrereleaseNodeExcludePatternsJson: $pluginPrereleaseNodeExcludePatternsJson
}')"
trusted_workflow_json="${TRUSTED_WORKFLOW_JSON}"
trusted_workflow_ref="$(jq -er '.ref | select(type == "string" and length > 0)' <<< "$trusted_workflow_json")"
trusted_workflow_full_ref="$(jq -er '.fullRef | select(type == "string" and length > 0)' <<< "$trusted_workflow_json")"
trusted_workflow_sha="$(jq -er '.sha | select(type == "string" and test("^[0-9a-f]{40}$"))' <<< "$trusted_workflow_json")"
bash workflow/scripts/github/find-reusable-release-validation.sh \
--target-sha "$TARGET_SHA" \
--workflow-sha "$GITHUB_SHA" \
--workflow-ref "$WORKFLOW_REF" \
--trusted-workflow-ref "$trusted_workflow_ref" \
--trusted-workflow-full-ref "$trusted_workflow_full_ref" \
--trusted-workflow-sha "$trusted_workflow_sha" \
--release-profile "$RELEASE_PROFILE" \
--run-release-soak "$RUN_RELEASE_SOAK" \
--inputs-json "$inputs_json" \
+97 -8
View File
@@ -33,6 +33,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
plugin_npm_run_id:
description: Successful Plugin NPM Release run id for the exact extended-stable branch and release SHA
required: false
@@ -800,6 +804,7 @@ jobs:
- name: Require trusted workflow ref for publish
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.tag }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
RELEASE_CANDIDATE_BRANCH: ${{ inputs.release_candidate_branch }}
@@ -839,9 +844,13 @@ jobs:
echo "SHA-pinned release-publish tag does not match the OpenClaw npm workflow SHA." >&2
exit 1
}
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main || {
echo "SHA-pinned OpenClaw npm workflow revision is not reachable from current main." >&2
workflow_tag="${WORKFLOW_REF#refs/tags/}"
remote_workflow_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${workflow_tag}" \
--jq '.object | select(.type == "commit") | .sha | select(test("^[a-f0-9]{40}$"))'
)"
[[ "${remote_workflow_sha}" == "${WORKFLOW_SHA}" ]] || {
echo "SHA-pinned release-publish tag does not resolve to the OpenClaw npm workflow SHA." >&2
exit 1
}
sha_pinned_release_publish=true
@@ -860,6 +869,7 @@ jobs:
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: ${{ inputs.full_release_validation_run_attempt }}
PLUGIN_NPM_RUN_ID: ${{ inputs.plugin_npm_run_id }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
run: |
set -euo pipefail
if [[ -z "${PREFLIGHT_RUN_ID}" ]]; then
@@ -889,11 +899,18 @@ jobs:
echo "Workflow-dispatched real publish requires release_publish_run_id from the approved OpenClaw Release Publish workflow." >&2
exit 1
fi
if [[ -n "${RELEASE_PUBLISH_RUN_ID// }" && ! "${RELEASE_PUBLISH_RUN_ATTEMPT}" =~ ^[1-9][0-9]*$ ]]; then
echo "release_publish_run_id requires the exact positive release_publish_run_attempt." >&2
exit 1
fi
- name: Validate release publish approval run
env:
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: ${{ github.ref }}
EXPECTED_WORKFLOW_SHA: ${{ github.workflow_sha }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
@@ -910,7 +927,7 @@ jobs:
direct_recovery=true
echo "Direct OpenClaw npm recovery with release_publish_run_id; relying on this workflow's npm-release environment approval."
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
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}')"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
publish_openclaw_npm:
@@ -1024,8 +1041,34 @@ jobs:
if [[ "$RELEASE_NPM_DIST_TAG" == "extended-stable" && "$preflight_head_branch" == "$EXPECTED_EXTENDED_STABLE_BRANCH" ]]; then
extended_stable_preflight=true
fi
if [[ "$preflight_head_branch" != "main" && "refs/heads/${preflight_head_branch}" != "$WORKFLOW_REF" && "$extended_stable_preflight" != "true" ]]; then
echo "OpenClaw npm preflight run must come from main or the active protected release branch." >&2
active_branch_preflight=false
if [[ "$preflight_head_branch" != release-publish/* && "refs/heads/${preflight_head_branch}" == "$WORKFLOW_REF" ]]; then
active_branch_preflight=true
fi
protected_release_publish_preflight=false
if [[ "$WORKFLOW_REF" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]; then
workflow_sha_prefix="${BASH_REMATCH[1]}"
[[ "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "${WORKFLOW_SHA:0:12}" == "$workflow_sha_prefix" ]] || {
echo "Protected release-publish tag does not match the workflow SHA at the npm publish boundary." >&2
exit 1
}
workflow_tag="${WORKFLOW_REF#refs/tags/}"
# The npm-release environment can wait after request validation.
# Re-read the tag here so a later move cannot authorize publication.
remote_workflow_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${workflow_tag}" \
--jq '.object | select(.type == "commit") | .sha | select(test("^[a-f0-9]{40}$"))'
)"
[[ "$remote_workflow_sha" == "$WORKFLOW_SHA" ]] || {
echo "Protected release-publish tag moved after npm-release approval." >&2
exit 1
}
if [[ "$preflight_head_branch" == "$workflow_tag" && "$preflight_head_sha" == "$WORKFLOW_SHA" ]]; then
protected_release_publish_preflight=true
fi
fi
if [[ "$preflight_head_branch" != "main" && "$active_branch_preflight" != "true" && "$extended_stable_preflight" != "true" && "$protected_release_publish_preflight" != "true" ]]; then
echo "OpenClaw npm preflight run must come from main, the active protected release branch, or the exact protected release-publish tag." >&2
exit 1
fi
if ! git -C trusted-workflow cat-file -e "${preflight_head_sha}^{commit}" 2>/dev/null; then
@@ -1062,14 +1105,40 @@ jobs:
FULL_RELEASE_VALIDATION_RUN_ATTEMPT: ${{ inputs.full_release_validation_run_attempt }}
EXPECTED_WORKFLOW_BRANCH: ${{ inputs.release_candidate_branch || github.ref_name }}
STRICT_VALIDATOR_FILE: ${{ github.workspace }}/trusted-workflow/scripts/release-ci-summary.mjs
TRUSTED_WORKFLOW_FULL_REF: ${{ github.ref }}
TRUSTED_WORKFLOW_REF: ${{ github.ref_name }}
TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: |
set -euo pipefail
EXPECTED_SHA="$(git rev-parse HEAD)"
MANIFEST_FILE="full-release-validation/full-release-validation-manifest.json"
export EXPECTED_SHA MANIFEST_FILE
timeout --signal=TERM --kill-after=10s 120s git fetch --filter=blob:none --no-tags origin +refs/heads/main:refs/remotes/origin/main
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}" | \
trusted_workflow_commit_ref="refs/remotes/origin/main"
if [[ "${TRUSTED_WORKFLOW_FULL_REF}" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; then
trusted_workflow_commit_ref="refs/tags/${TRUSTED_WORKFLOW_REF}"
timeout --signal=TERM --kill-after=10s 120s git fetch --filter=blob:none --no-tags origin \
"+${TRUSTED_WORKFLOW_FULL_REF}:${trusted_workflow_commit_ref}"
if [[ "$(git rev-parse "${trusted_workflow_commit_ref}^{commit}")" != "${WORKFLOW_SHA}" ]]; then
echo "Trusted release-publish tag moved after workflow dispatch." >&2
exit 1
fi
else
timeout --signal=TERM --kill-after=10s 120s git fetch --filter=blob:none --no-tags origin \
+refs/heads/main:refs/remotes/origin/main
fi
TRUSTED_MAIN_REF="${trusted_workflow_commit_ref}" \
gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${FULL_RELEASE_VALIDATION_RUN_ID}/attempts/${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}" | \
TRUSTED_MAIN_REF="${trusted_workflow_commit_ref}" \
node trusted-workflow/scripts/validate-full-release-validation-evidence.mjs
node "$STRICT_VALIDATOR_FILE" \
--validate-run "$FULL_RELEASE_VALIDATION_RUN_ID" \
--trusted-workflow-ref "$TRUSTED_WORKFLOW_REF" \
--trusted-workflow-full-ref "$TRUSTED_WORKFLOW_FULL_REF" \
--trusted-workflow-sha "$TRUSTED_WORKFLOW_SHA" \
--json \
--verifier-source-sha "$WORKFLOW_SHA" \
--verifier-source-file "$STRICT_VALIDATOR_FILE" >/dev/null
- name: Verify plugin npm release run metadata
if: ${{ inputs.npm_dist_tag == 'extended-stable' }}
@@ -1350,11 +1419,29 @@ jobs:
id: publish
env:
BYPASS_EXTENDED_STABLE_GUARD: ${{ inputs.bypass_extended_stable_guard }}
GH_TOKEN: ${{ github.token }}
OPENCLAW_PREPACK_PREPARED: "1"
OPENCLAW_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag }}
PUBLISH_TARBALL_PATH: ${{ steps.preflight_provenance.outputs.tarball_path }}
RELEASE_PUBLISH_PARENT_STATE_POLICY: ${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
WORKFLOW_FULL_REF: ${{ github.ref }}
WORKFLOW_REF: ${{ github.ref_name }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: |
set -euo pipefail
verify_release_tooling_identity() {
node trusted-workflow/scripts/release-tooling-identity.mjs verify \
--repository "$GITHUB_REPOSITORY" \
--workflow-ref "$WORKFLOW_REF" \
--workflow-full-ref "$WORKFLOW_FULL_REF" \
--workflow-sha "$WORKFLOW_SHA" \
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \
--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT" \
--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY" \
--allow-prevalidated-ref
}
publish_target="${PUBLISH_TARBALL_PATH}"
if [[ -n "${publish_target}" ]]; then
publish_target="./${publish_target}"
@@ -1372,11 +1459,13 @@ jobs:
echo "${package_name}@${package_version} is already published; reusing it."
return 0
fi
verify_release_tooling_identity
bash scripts/openclaw-npm-publish.sh --publish "./${tarball_path}"
}
while IFS=$'\t' read -r package_name tarball_name; do
publish_if_missing "$package_name" "preflight-tarball/$tarball_name"
done < <(jq -r '(.corePackageTarballs // [])[] | [.packageName, .tarballName] | @tsv' preflight-tarball/preflight-manifest.json)
verify_release_tooling_identity
bash scripts/openclaw-npm-publish.sh --publish "${publish_target}"
- name: Verify extended-stable registry readback
+52 -13
View File
@@ -219,12 +219,13 @@ jobs:
echo "SHA-pinned release publish tag does not match workflow SHA ${WORKFLOW_SHA}." >&2
exit 1
fi
merge_base_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/compare/${WORKFLOW_SHA}...main" \
--jq '.merge_base_commit.sha | select(test("^[a-f0-9]{40}$"))'
workflow_tag="${WORKFLOW_REF#refs/tags/}"
remote_workflow_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${workflow_tag}" \
--jq '.object | select(.type == "commit") | .sha | select(test("^[a-f0-9]{40}$"))'
)"
if [[ "${merge_base_sha}" != "${WORKFLOW_SHA}" ]]; then
echo "SHA-pinned release publish tag revision is not reachable from current main." >&2
if [[ "${remote_workflow_sha}" != "${WORKFLOW_SHA}" ]]; then
echo "SHA-pinned release publish tag does not resolve to workflow SHA ${WORKFLOW_SHA}." >&2
exit 1
fi
sha_pinned_release_publish=true
@@ -328,6 +329,7 @@ jobs:
PREFLIGHT_RUN_ID: ${{ inputs.preflight_run_id }}
RELEASE_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }}
RELEASE_TAG: ${{ inputs.tag }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: |
set -euo pipefail
@@ -395,8 +397,19 @@ jobs:
if [[ "$RELEASE_NPM_DIST_TAG" == "extended-stable" && "$preflight_head_branch" == "$expected_extended_stable_branch" ]]; then
extended_stable_preflight=true
fi
if [[ "$preflight_head_branch" != "main" && "refs/heads/${preflight_head_branch}" != "$GITHUB_REF" && "$extended_stable_preflight" != "true" ]]; then
echo "OpenClaw npm preflight run must come from main or the active protected release branch." >&2
active_branch_preflight=false
if [[ "$preflight_head_branch" != release-publish/* && "refs/heads/${preflight_head_branch}" == "$GITHUB_REF" ]]; then
active_branch_preflight=true
fi
protected_release_publish_preflight=false
if [[ "$GITHUB_REF" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; then
workflow_tag="${GITHUB_REF#refs/tags/}"
if [[ "$preflight_head_branch" == "$workflow_tag" && "$preflight_head_sha" == "$WORKFLOW_SHA" ]]; then
protected_release_publish_preflight=true
fi
fi
if [[ "$preflight_head_branch" != "main" && "$active_branch_preflight" != "true" && "$extended_stable_preflight" != "true" && "$protected_release_publish_preflight" != "true" ]]; then
echo "OpenClaw npm preflight run must come from main, the active protected release branch, or the exact protected release-publish tag." >&2
exit 1
fi
if [[ "$preflight_conclusion" != "success" || "$preflight_event" != "workflow_dispatch" || "$preflight_path" != ".github/workflows/openclaw-npm-release.yml" ]]; then
@@ -576,8 +589,10 @@ jobs:
EXPECTED_SHA: ${{ steps.ref.outputs.sha }}
EXPECTED_RELEASE_PROFILE: ${{ inputs.release_profile }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
TRUSTED_WORKFLOW_FULL_REF: ${{ github.ref }}
TRUSTED_WORKFLOW_REF: ${{ github.ref_name }}
TRUSTED_WORKFLOW_SHA: ${{ github.workflow_sha }}
RUN_JSON_FILE: ${{ runner.temp }}/full-release-validation-run.json
TRUSTED_MAIN_REF: refs/remotes/origin/main
VALIDATOR_FILE: ${{ runner.temp }}/release-validation-tooling/validate-full-release-validation-evidence.mjs
STRICT_VALIDATOR_FILE: ${{ runner.temp }}/release-validation-tooling/release-ci-summary.mjs
run: |
@@ -588,9 +603,30 @@ jobs:
ls -la "${RUNNER_TEMP}/full-release-validation-manifest" >&2 || true
exit 1
fi
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main
MANIFEST_FILE="$manifest" node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"
trusted_workflow_commit_ref="refs/remotes/origin/main"
if [[ "${TRUSTED_WORKFLOW_FULL_REF}" =~ ^refs/tags/release-publish/[a-f0-9]{12}-[1-9][0-9]*$ ]]; then
trusted_workflow_commit_ref="refs/tags/${TRUSTED_WORKFLOW_REF}"
git fetch --no-tags origin \
"+${TRUSTED_WORKFLOW_FULL_REF}:${trusted_workflow_commit_ref}"
if [[ "$(git rev-parse "${trusted_workflow_commit_ref}^{commit}")" != "${GITHUB_SHA}" ]]; then
echo "Trusted release-publish tag moved after workflow dispatch." >&2
exit 1
fi
else
git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main
fi
TRUSTED_MAIN_REF="${trusted_workflow_commit_ref}" \
MANIFEST_FILE="$manifest" \
node "$VALIDATOR_FILE" < "$RUN_JSON_FILE"
node "$STRICT_VALIDATOR_FILE" \
--validate-run "$FULL_RELEASE_VALIDATION_RUN_ID" \
--trusted-workflow-ref "$TRUSTED_WORKFLOW_REF" \
--trusted-workflow-full-ref "$TRUSTED_WORKFLOW_FULL_REF" \
--trusted-workflow-sha "$TRUSTED_WORKFLOW_SHA" \
--json \
--verifier-source-sha "$GITHUB_SHA" \
--verifier-source-file "$STRICT_VALIDATOR_FILE" >/dev/null
workflow_name="$(jq -r '.workflowName // ""' "$manifest")"
target_sha="$(jq -r '.targetSha // ""' "$manifest")"
@@ -1491,7 +1527,9 @@ jobs:
fi
resume_state="$(node --import tsx "${GITHUB_WORKSPACE}/.release-harness/scripts/openclaw-npm-resume-run.mts" \
--repo "${GITHUB_REPOSITORY}" \
--run-id "${OPENCLAW_NPM_RESUME_RUN_ID}")"
--run-id "${OPENCLAW_NPM_RESUME_RUN_ID}" \
--trusted-workflow-ref "${PARENT_WORKFLOW_BRANCH}" \
--trusted-workflow-full-ref "${GITHUB_REF}")"
resume_url="$(printf '%s' "${resume_state}" | jq -er '.url')"
openclaw_npm_expected_workflow_ref="$(printf '%s' "${resume_state}" | jq -er '.workflowRef')"
openclaw_npm_expected_workflow_sha="$(printf '%s' "${resume_state}" | jq -er '.workflowSha')"
@@ -2235,7 +2273,7 @@ jobs:
bootstrap_workflow_sha="$(verify_bootstrap_workflow_sha)"
fi
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}")
npm_args=(-f publish_scope="${PLUGIN_PUBLISH_SCOPE}" -f ref="${TARGET_SHA}" -f release_publish_run_id="${GITHUB_RUN_ID}" -f release_publish_run_attempt="${GITHUB_RUN_ATTEMPT}")
if [[ -n "${PLUGINS}" ]]; then
npm_args+=(-f plugins="${PLUGINS}")
fi
@@ -2311,6 +2349,7 @@ jobs:
-f full_release_validation_run_id="${FULL_RELEASE_VALIDATION_RUN_ID}" \
-f full_release_validation_run_attempt="${FULL_RELEASE_VALIDATION_RUN_ATTEMPT}" \
-f release_publish_run_id="${GITHUB_RUN_ID}" \
-f release_publish_run_attempt="${GITHUB_RUN_ATTEMPT}" \
-f plugin_sdk_api_acknowledgement="${PLUGIN_SDK_API_ACKNOWLEDGEMENT}" \
-f npm_dist_tag="${RELEASE_NPM_DIST_TAG}")"
echo "- OpenClaw npm run ID: \`${openclaw_npm_run_id}\`" >> "$GITHUB_STEP_SUMMARY"
+81 -29
View File
@@ -22,6 +22,8 @@ on:
- "scripts/lib/actions-artifact-archive.mjs"
- "scripts/plugin-npm-publish.sh"
- "scripts/plugin-publication-artifact.mjs"
- "scripts/release-tooling-identity.d.mts"
- "scripts/release-tooling-identity.mjs"
- "scripts/plugin-npm-release-check.ts"
- "scripts/plugin-npm-release-plan.ts"
- "scripts/verify-plugin-npm-published-runtime.mts"
@@ -47,6 +49,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
preflight_only:
description: Prepare and verify immutable plugin npm artifacts without publishing
required: true
@@ -96,10 +102,38 @@ jobs:
fetch-depth: 0
filter: blob:none
- name: Checkout trusted preflight tooling
if: github.event_name == 'workflow_dispatch' && inputs.preflight_only
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
ref: ${{ github.workflow_sha }}
path: .release-tooling
fetch-depth: 1
sparse-checkout: |
scripts/lib/record-shared.mjs
scripts/release-tooling-identity.mjs
sparse-checkout-cone-mode: false
- name: Resolve checked-out ref
id: ref
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Verify trusted preflight tooling identity
if: github.event_name == 'workflow_dispatch' && inputs.preflight_only
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW_FULL_REF: ${{ github.ref }}
WORKFLOW_REF: ${{ github.ref_name }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: |
set -euo pipefail
node .release-tooling/scripts/release-tooling-identity.mjs verify \
--repository "$GITHUB_REPOSITORY" \
--workflow-ref "$WORKFLOW_REF" \
--workflow-full-ref "$WORKFLOW_FULL_REF" \
--workflow-sha "$WORKFLOW_SHA"
- name: Validate ref is on a trusted publish branch
env:
NPM_DIST_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.npm_dist_tag || 'default' }}
@@ -107,6 +141,7 @@ jobs:
TRUSTED_PUBLISHER_PREFLIGHT: ${{ github.event_name == 'workflow_dispatch' && inputs.trusted_publisher_preflight || false }}
PUBLISH_SCOPE: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_scope || '' }}
RELEASE_PLUGINS: ${{ github.event_name == 'workflow_dispatch' && inputs.plugins || '' }}
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ github.event_name == 'workflow_dispatch' && inputs.release_publish_run_attempt || '' }}
RELEASE_PUBLISH_RUN_ID: ${{ github.event_name == 'workflow_dispatch' && inputs.release_publish_run_id || '' }}
SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
WORKFLOW_REF: ${{ github.ref }}
@@ -118,19 +153,19 @@ jobs:
exit 1
fi
if [[ "${PREFLIGHT_ONLY}" == "true" ]]; then
if [[ "${WORKFLOW_REF}" != "refs/heads/main" ]] || [[ ! "${WORKFLOW_SHA}" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "Plugin npm preflight must run from a trusted main workflow revision." >&2
exit 1
fi
if [[ ! "${SOURCE_REF}" =~ ^[0-9a-fA-F]{40}$ ]] || [[ "$(git rev-parse HEAD)" != "$(git rev-parse "${SOURCE_REF}^{commit}")" ]]; then
echo "Plugin npm preflight requires ref to be the exact 40-character source SHA." >&2
exit 1
fi
if [[ -n "${RELEASE_PUBLISH_RUN_ID// }" ]]; then
echo "Plugin npm preflight must not include release_publish_run_id." >&2
if [[ -n "${RELEASE_PUBLISH_RUN_ID// }" || -n "${RELEASE_PUBLISH_RUN_ATTEMPT// }" ]]; then
echo "Plugin npm preflight must not include a release publish parent run tuple." >&2
exit 1
fi
fi
if [[ -n "${RELEASE_PUBLISH_RUN_ID// }" && ! "${RELEASE_PUBLISH_RUN_ATTEMPT}" =~ ^[1-9][0-9]*$ ]]; then
echo "release_publish_run_id requires the exact positive release_publish_run_attempt." >&2
exit 1
fi
if [[ "${NPM_DIST_TAG}" == "extended-stable" ]]; then
if [[ "${PUBLISH_SCOPE}" != "all-publishable" || -n "${RELEASE_PLUGINS// }" ]]; then
echo "Extended-stable plugin publication requires publish_scope=all-publishable without an explicit plugin list." >&2
@@ -158,10 +193,6 @@ jobs:
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin \
+refs/heads/main:refs/remotes/origin/main \
'+refs/heads/release/*:refs/remotes/origin/release/*'
if [[ "${PREFLIGHT_ONLY}" == "true" ]] && ! git merge-base --is-ancestor "${WORKFLOW_SHA}" origin/main; then
echo "Plugin npm preflight workflow revision is not reachable from main." >&2
exit 1
fi
if git merge-base --is-ancestor HEAD origin/main; then
exit 0
fi
@@ -307,6 +338,9 @@ jobs:
env:
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: ${{ github.ref }}
EXPECTED_WORKFLOW_SHA: ${{ github.workflow_sha }}
EXPECTED_WORKFLOW_BRANCH: ${{ github.ref_name }}
run: |
set -euo pipefail
@@ -323,7 +357,7 @@ jobs:
direct_recovery=true
echo "Direct Plugin NPM Release recovery with release_publish_run_id; relying on this workflow's npm-release environment approval."
fi
RUN_JSON="$(gh run view "$RELEASE_PUBLISH_RUN_ID" --repo "$GITHUB_REPOSITORY" --json workflowName,headBranch,event,status,conclusion,url)"
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}')"
printf '%s' "$RUN_JSON" | DIRECT_RELEASE_RECOVERY="${direct_recovery}" node scripts/validate-release-publish-approval.mjs
preview_plugin_pack:
@@ -1103,6 +1137,9 @@ jobs:
PACKAGE_NAME: ${{ matrix.plugin.packageName }}
PACKAGE_VERSION: ${{ matrix.plugin.version }}
PUBLISH_TAG: ${{ matrix.plugin.publishTag }}
RELEASE_PUBLISH_PARENT_STATE_POLICY: ${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
TARGET_SHA: ${{ needs.preview_plugins_npm.outputs.ref_revision }}
WORKFLOW_HEAD_BRANCH: ${{ github.ref_name }}
WORKFLOW_REF: ${{ github.ref }}
@@ -1161,24 +1198,14 @@ jobs:
exit 1
}
if [[ "$publish_route" == "npm-token-bootstrap" ]]; then
sha_pinned_release_publish=false
if [[ "$WORKFLOW_REF" =~ ^refs/tags/release-publish/([a-f0-9]{12})-[1-9][0-9]*$ ]]; then
workflow_sha_prefix="${BASH_REMATCH[1]}"
[[ "$WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ && "${WORKFLOW_SHA:0:12}" == "$workflow_sha_prefix" ]] || {
echo "npm token bootstrap release-publish tag does not match the workflow SHA." >&2
exit 1
}
sha_pinned_release_publish=true
fi
[[ "$WORKFLOW_REF" == "refs/heads/main" || "$sha_pinned_release_publish" == "true" ]] || {
echo "npm token bootstrap requires trusted main tooling or a protected SHA-pinned release-publish tag." >&2
exit 1
}
timeout --signal=TERM --kill-after=10s 120s git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main
git merge-base --is-ancestor "$WORKFLOW_SHA" origin/main || {
echo "npm token bootstrap workflow revision is not reachable from current main." >&2
exit 1
}
node scripts/release-tooling-identity.mjs verify \
--repository "$GITHUB_REPOSITORY" \
--workflow-ref "$WORKFLOW_HEAD_BRANCH" \
--workflow-full-ref "$WORKFLOW_REF" \
--workflow-sha "$WORKFLOW_SHA" \
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \
--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT" \
--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"
fi
artifact_id="$(jq -er '.id' "$artifact_metadata")"
artifact_digest="$(jq -er '.digest' "$artifact_metadata")"
@@ -1307,8 +1334,18 @@ jobs:
- name: Publish with trusted publisher
if: steps.publication_evidence.outputs.publish_route == 'npm-oidc' && steps.npm_package_version.outputs.already_published != 'true'
env:
GH_TOKEN: ${{ github.token }}
OPENCLAW_NPM_PUBLISH_AUTH_MODE: trusted-publisher
OPENCLAW_PLUGIN_NPM_PUBLISH_TAG: ${{ inputs.npm_dist_tag == 'extended-stable' && inputs.npm_dist_tag || '' }}
OPENCLAW_RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
OPENCLAW_RELEASE_PUBLISH_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
OPENCLAW_RELEASE_PUBLISH_PARENT_STATE_POLICY: ${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}
OPENCLAW_RELEASE_TOOLING_ALLOW_PREVALIDATED_REF: "true"
OPENCLAW_RELEASE_TOOLING_FULL_REF: ${{ github.ref }}
OPENCLAW_RELEASE_TOOLING_IDENTITY_REQUIRED: "true"
OPENCLAW_RELEASE_TOOLING_REF: ${{ github.ref_name }}
OPENCLAW_RELEASE_TOOLING_REPOSITORY: ${{ github.repository }}
OPENCLAW_RELEASE_TOOLING_SHA: ${{ github.workflow_sha }}
run: bash scripts/plugin-npm-publish.sh --repo-root .publication-target --publish "${{ matrix.plugin.packageDir }}"
- name: Verify OIDC published runtime
@@ -1368,12 +1405,19 @@ jobs:
- name: Publish approved bootstrap tarball
if: steps.publication_evidence.outputs.publish_route == 'npm-token-bootstrap' && steps.bootstrap_npm_package_version.outputs.already_published != 'true'
env:
GH_TOKEN: ${{ github.token }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
PACKAGE_DIR: ${{ matrix.plugin.packageDir }}
PACKAGE_NAME: ${{ steps.publication_evidence.outputs.package_name }}
PACKAGE_VERSION: ${{ steps.publication_evidence.outputs.package_version }}
PUBLISH_TAG: ${{ steps.publication_evidence.outputs.publish_tag }}
RELEASE_PUBLISH_PARENT_STATE_POLICY: ${{ inputs.release_publish_run_id != '' && (github.actor == 'github-actions[bot]' && 'active' || 'manual-recovery') || '' }}
RELEASE_PUBLISH_RUN_ID: ${{ inputs.release_publish_run_id }}
RELEASE_PUBLISH_RUN_ATTEMPT: ${{ inputs.release_publish_run_attempt }}
TARBALL_PATH: ${{ steps.publication_evidence.outputs.tarball_path }}
WORKFLOW_FULL_REF: ${{ github.ref }}
WORKFLOW_REF: ${{ github.ref_name }}
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: |
set -euo pipefail
[[ "$PACKAGE_VERSION" == *"-beta."* && "$PUBLISH_TAG" == "beta" ]] || {
@@ -1399,6 +1443,14 @@ jobs:
unset NODE_AUTH_TOKEN NPM_TOKEN NODE_OPTIONS
# A timeout can race a committed publish. On rerun, the preceding check
# accepts only this tarball's exact integrity and shasum before skipping.
node scripts/release-tooling-identity.mjs verify \
--repository "$GITHUB_REPOSITORY" \
--workflow-ref "$WORKFLOW_REF" \
--workflow-full-ref "$WORKFLOW_FULL_REF" \
--workflow-sha "$WORKFLOW_SHA" \
--release-publish-run-id "$RELEASE_PUBLISH_RUN_ID" \
--release-publish-run-attempt "$RELEASE_PUBLISH_RUN_ATTEMPT" \
--release-publish-parent-state-policy "$RELEASE_PUBLISH_PARENT_STATE_POLICY"
HOME="$publish_home" \
NPM_CONFIG_GLOBALCONFIG=/dev/null \
NPM_CONFIG_IGNORE_SCRIPTS=true \