mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 03:15:46 -06:00
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:
@@ -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" \
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user