fix(release): keep frozen validation independent of main (#126622)

* fix(release): freeze validation tooling identity

* fix(release): enforce frozen validation contract

* fix(release): validate candidate identity in parent

* fix(ci): close release isolation gate findings
This commit is contained in:
Vincent Koc
2026-08-20 04:32:38 -07:00
committed by GitHub
parent a59abcf4a8
commit c28c279afa
17 changed files with 851 additions and 156 deletions
+83 -4
View File
@@ -162,6 +162,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"
jobs:
resolve_target:
@@ -192,10 +193,25 @@ jobs:
--expected-sha "$EXPECTED_SHA" \
--github-output "$GITHUB_OUTPUT"
- name: Checkout target package manifest
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ steps.resolve.outputs.sha }}
path: target
sparse-checkout: package.json
sparse-checkout-cone-mode: false
fetch-depth: 1
persist-credentials: false
submodules: false
- name: Validate release inputs
env:
GH_TOKEN: ${{ github.token }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
SKIP_PACKAGE_TELEGRAM_E2E: ${{ inputs.skip_package_telegram_e2e }}
TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }}
TARGET_REF: ${{ inputs.ref }}
TARGET_SHA: ${{ steps.resolve.outputs.sha }}
run: |
set -euo pipefail
if [[ "$SKIP_PACKAGE_TELEGRAM_E2E" == "true" && "$RELEASE_PROFILE" != "beta" ]]; then
@@ -203,6 +219,67 @@ jobs:
exit 1
fi
context_ref="${TARGET_CONTEXT_REF:-$TARGET_REF}"
context_ref="${context_ref#refs/heads/}"
context_ref="${context_ref#refs/tags/}"
target_version="$(jq -er '.version | select(type == "string")' target/package.json)"
release_version_pattern=""
expected_version=""
identity_kind=""
if [[ "$context_ref" =~ ^release/([0-9]{4}\.([1-9]|1[0-2])\.[1-9][0-9]*)$ ]]; then
expected_version="${BASH_REMATCH[1]}"
release_version_pattern="${expected_version//./\\.}"
identity_kind="release branch"
if [[ "$target_version" != "$expected_version" &&
! "$target_version" =~ ^${release_version_pattern}-beta\.[1-9][0-9]*$ ]]; then
echo "Target package version ${target_version} does not belong to release branch ${context_ref}; expected ${expected_version} or a beta prerelease of it." >&2
exit 1
fi
elif [[ "$context_ref" =~ ^extended-stable/([0-9]{4}\.([1-9]|1[0-2])\.33)$ ]]; then
expected_version="${BASH_REMATCH[1]}"
identity_kind="extended-stable branch"
elif [[ "$context_ref" =~ ^v([0-9]{4}\.([1-9]|1[0-2])\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then
expected_version="${BASH_REMATCH[1]}"
identity_kind="release tag"
elif [[ -n "$TARGET_CONTEXT_REF" ]]; then
echo "target_context_ref must be a canonical OpenClaw release branch or tag." >&2
exit 1
fi
if [[ -n "$expected_version" &&
"$identity_kind" != "release branch" &&
"$target_version" != "$expected_version" ]]; then
echo "Target package version ${target_version} does not match ${identity_kind} ${context_ref}; expected ${expected_version}." >&2
exit 1
fi
if [[ -n "$TARGET_CONTEXT_REF" ]]; then
if [[ ! "$TARGET_REF" =~ ^[a-f0-9]{40}$ || "$TARGET_REF" != "$TARGET_SHA" ]]; then
echo "target_context_ref requires ref to be the resolved full Validation SHA." >&2
exit 1
fi
repository_url="https://github.com/${GITHUB_REPOSITORY}.git"
if [[ "$identity_kind" == "release tag" ]]; then
remote_sha="$(git ls-remote --tags "$repository_url" "refs/tags/${context_ref}^{}" | awk 'NR == 1 { print $1 }')"
if [[ -z "$remote_sha" ]]; then
remote_sha="$(git ls-remote --tags "$repository_url" "refs/tags/${context_ref}" | awk 'NR == 1 { print $1 }')"
fi
if [[ "$remote_sha" != "$TARGET_SHA" ]]; then
echo "Target SHA ${TARGET_SHA} does not match release tag ${context_ref} at ${remote_sha:-missing}." >&2
exit 1
fi
else
remote_sha="$(git ls-remote --heads "$repository_url" "refs/heads/${context_ref}" | awk 'NR == 1 { print $1 }')"
if [[ -z "$remote_sha" ]]; then
echo "Release context branch ${context_ref} does not resolve." >&2
exit 1
fi
comparison_status="$(gh api "repos/${GITHUB_REPOSITORY}/compare/${TARGET_SHA}...${remote_sha}" --jq .status)"
if [[ "$comparison_status" != "ahead" && "$comparison_status" != "identical" ]]; then
echo "Target SHA ${TARGET_SHA} is not reachable from release context branch ${context_ref} at ${remote_sha}." >&2
exit 1
fi
fi
fi
- name: Summarize target
env:
TARGET_REF: ${{ inputs.ref }}
@@ -230,6 +307,7 @@ jobs:
echo "- Target ref: \`${TARGET_REF}\`"
echo "- Validation SHA: \`${TARGET_SHA}\`"
echo "- Tooling SHA: \`${TOOLING_SHA}\`"
echo "- Frozen tuple: \`${TARGET_SHA} / ${TOOLING_SHA} / ${RERUN_GROUP}\`"
echo "- Release soak lanes: \`${RUN_RELEASE_SOAK}\`"
echo "- Fail fast: \`${FAIL_FAST}\`"
echo "- Package Acceptance Telegram E2E deferred: \`${SKIP_PACKAGE_TELEGRAM_E2E}\`"
@@ -633,7 +711,7 @@ jobs:
local workflow="$1"
local dispatch_run_name="$2"
shift 2
local dispatch_output dispatch_status dispatch_run_ids matches_json match_count run_id status conclusion url poll_count run_json jobs_json child_head_sha encoded_workflow_ref current_workflow_sha expected_workflow_id
local dispatch_output dispatch_status dispatch_run_ids matches_json match_count run_id status conclusion url poll_count run_json jobs_json child_head_sha encoded_workflow_ref current_workflow_sha expected_workflow_id started_epoch elapsed_seconds elapsed_minutes
encoded_workflow_ref="$(jq -rn --arg value "$CHILD_WORKFLOW_REF" '$value | @uri')"
current_workflow_sha="$(
@@ -722,6 +800,7 @@ jobs:
fi
echo "Dispatched ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
echo "run_id=${run_id}" >> "$GITHUB_OUTPUT"
started_epoch="$(date +%s)"
fail_fast_failed_jobs() {
if [[ "$FAIL_FAST" != "true" ]]; then
@@ -792,9 +871,9 @@ jobs:
poll_count=$((poll_count + 1))
if (( poll_count % 5 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
elapsed_seconds=$(( $(date +%s) - started_epoch ))
elapsed_minutes=$(( elapsed_seconds / 60 ))
echo "Still waiting on ${workflow} after ${elapsed_minutes}m: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: (.url // .html_url)}' || true
fi
sleep 60
+30 -39
View File
@@ -127,7 +127,7 @@ on:
type: string
concurrency:
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ inputs.rerun_group }}
group: openclaw-release-checks-${{ inputs.expected_sha || inputs.ref }}-${{ github.sha }}-${{ inputs.rerun_group }}
cancel-in-progress: ${{ startsWith(github.ref, 'refs/heads/tideclaw/alpha/') }}
env:
@@ -2081,6 +2081,8 @@ jobs:
- name: Dispatch and await trusted Telegram QA
env:
GH_TOKEN: ${{ github.token }}
PARENT_WORKFLOW_REF: ${{ github.ref_name }}
PARENT_WORKFLOW_SHA: ${{ github.sha }}
TARGET_CONTEXT_REF: ${{ inputs.target_context_ref }}
TARGET_SHA: ${{ needs.resolve_target.outputs.revision }}
shell: bash
@@ -2094,7 +2096,7 @@ jobs:
RUN_NAME="$run_name" gh api -X GET \
"repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \
-F event=workflow_dispatch \
-F branch=main \
-F branch="$PARENT_WORKFLOW_REF" \
-F per_page=100 \
--jq '[.workflow_runs[] | select(.display_title == env.RUN_NAME) | {id, head_sha}]'
}
@@ -2127,51 +2129,40 @@ jobs:
trap 'exit 130' INT
trap 'exit 143' TERM
for dispatch_attempt in 1 2 3 4 5; do
expected_trusted_workflow_sha="$(
gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha
)"
dispatch_id="release-checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-${dispatch_attempt}-$(openssl rand -hex 16)"
run_name="OpenClaw Release Telegram QA ${dispatch_id}"
run_id=""
child_head_sha=""
dispatch_id="release-checks-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-$(openssl rand -hex 16)"
run_name="OpenClaw Release Telegram QA ${dispatch_id}"
child_head_sha=""
gh workflow run "$workflow" \
--repo "$GITHUB_REPOSITORY" \
--ref main \
-f dispatch_id="$dispatch_id" \
-f expected_trusted_workflow_sha="$expected_trusted_workflow_sha" \
-f target_context_ref="$TARGET_CONTEXT_REF" \
-f target_ref="$TARGET_SHA" \
-f target_sha="$TARGET_SHA"
gh workflow run "$workflow" \
--repo "$GITHUB_REPOSITORY" \
--ref "$PARENT_WORKFLOW_REF" \
-f dispatch_id="$dispatch_id" \
-f expected_trusted_workflow_sha="$PARENT_WORKFLOW_SHA" \
-f target_context_ref="$TARGET_CONTEXT_REF" \
-f target_ref="$TARGET_SHA" \
-f target_sha="$TARGET_SHA"
for _ in $(seq 1 60); do
matches_json="$(find_child_runs)"
match_count="$(jq 'length' <<<"$matches_json")"
if ((match_count > 1)); then
echo "Multiple Telegram QA runs matched ${run_name}; refusing to guess." >&2
exit 1
fi
if ((match_count == 1)); then
run_id="$(jq -r '.[0].id' <<<"$matches_json")"
child_head_sha="$(jq -r '.[0].head_sha' <<<"$matches_json")"
break
fi
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "Could not find exact dispatched Telegram QA run ${run_name}." >&2
for _ in $(seq 1 60); do
matches_json="$(find_child_runs)"
match_count="$(jq 'length' <<<"$matches_json")"
if ((match_count > 1)); then
echo "Multiple Telegram QA runs matched ${run_name}; refusing to guess." >&2
exit 1
fi
if [[ "$child_head_sha" == "$expected_trusted_workflow_sha" ]]; then
if ((match_count == 1)); then
run_id="$(jq -r '.[0].id' <<<"$matches_json")"
child_head_sha="$(jq -r '.[0].head_sha' <<<"$matches_json")"
break
fi
echo "Trusted main moved from ${expected_trusted_workflow_sha} to ${child_head_sha} during dispatch attempt ${dispatch_attempt}; retrying." >&2
cancel_child
run_id=""
sleep 5
done
if [[ -z "$run_id" ]]; then
echo "Trusted main kept moving during Telegram QA dispatch; refusing an unpinned child." >&2
echo "Could not find exact dispatched Telegram QA run ${run_name}." >&2
exit 1
fi
if [[ "$child_head_sha" != "$PARENT_WORKFLOW_SHA" ]]; then
echo "Telegram QA used tooling SHA ${child_head_sha}, expected ${PARENT_WORKFLOW_SHA}." >&2
cancel_child
exit 1
fi
@@ -3,12 +3,10 @@ name: OpenClaw Release Telegram QA
run-name: ${{ github.event_name == 'workflow_dispatch' && format('OpenClaw Release Telegram QA {0}', inputs.dispatch_id) || 'OpenClaw Release Telegram QA' }}
on:
# Transitional compatibility for supported release refs whose parent still calls @main.
# Current main dispatches this workflow so qa-live-shared secrets stay in this run.
workflow_call:
inputs:
expected_trusted_workflow_sha:
description: Resolved main SHA authorized for this trusted workflow
description: Exact trusted tooling SHA authorized for this workflow
required: true
type: string
target_ref:
@@ -38,7 +36,7 @@ on:
required: true
type: string
expected_trusted_workflow_sha:
description: Resolved main SHA authorized for this trusted workflow
description: Exact trusted tooling SHA authorized for this workflow
required: true
type: string
target_ref:
@@ -76,7 +74,7 @@ jobs:
workflow_repository: ${{ steps.identity.outputs.workflow_repository }}
workflow_sha: ${{ steps.identity.outputs.workflow_sha }}
steps:
- name: Verify dispatched-main identity
- name: Verify dispatched workflow identity
id: identity
env:
CALLER_WORKFLOW_REF: ${{ github.workflow_ref }}
@@ -93,7 +91,8 @@ jobs:
set -euo pipefail
expected_repository="openclaw/openclaw"
expected_ref="${expected_repository}/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main"
workflow_path=".github/workflows/openclaw-release-telegram-qa.yml"
caller_path=".github/workflows/openclaw-release-checks.yml"
if [[ "$GITHUB_REPOSITORY" != "$expected_repository" ||
! "$EXPECTED_TRUSTED_WORKFLOW_SHA" =~ ^[a-f0-9]{40}$ ||
! "$TARGET_SHA" =~ ^[a-f0-9]{40}$ ||
@@ -113,13 +112,38 @@ jobs:
echo "Telegram QA release context requires an exact-SHA target ref." >&2
exit 1
fi
if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
expected_ref="${expected_repository}/${workflow_path}@${GITHUB_REF}"
elif [[ "$GITHUB_REF" =~ ^refs/heads/release-ci/([a-f0-9]{12})-([1-9][0-9]*)$ ]]; then
release_ci_sha_prefix="${BASH_REMATCH[1]}"
if [[ "$release_ci_sha_prefix" != "${EXPECTED_TRUSTED_WORKFLOW_SHA:0:12}" ]]; then
echo "Telegram QA release-ci ref does not match the authorized tooling SHA." >&2
exit 1
fi
expected_ref="${expected_repository}/${workflow_path}@${GITHUB_REF}"
elif [[ "$GITHUB_REF" =~ ^refs/heads/release/[0-9]{4}\.([1-9]|1[0-2])\.[1-9][0-9]*$ ||
"$GITHUB_REF" =~ ^refs/heads/extended-stable/[0-9]{4}\.([1-9]|1[0-2])\.33$ ]]; then
expected_ref="${expected_repository}/${workflow_path}@${GITHUB_REF}"
else
echo "Telegram QA tooling ref must be exact main, canonical release or extended-stable, or canonical SHA-bound release-ci." >&2
exit 1
fi
if [[ "$GITHUB_SHA" != "$EXPECTED_TRUSTED_WORKFLOW_SHA" ||
"$WORKFLOW_SHA" != "$EXPECTED_TRUSTED_WORKFLOW_SHA" ]]; then
echo "Telegram QA tooling SHA does not match the authorized workflow SHA." >&2
exit 1
fi
INVOCATION_MODE=reusable
if [[ "$WORKFLOW_REF" == "$expected_ref" ]]; then
INVOCATION_MODE=dispatch
[[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" &&
"$GITHUB_REF" == "refs/heads/main" &&
"$GITHUB_SHA" == "$EXPECTED_TRUSTED_WORKFLOW_SHA" &&
"$WORKFLOW_SHA" == "$EXPECTED_TRUSTED_WORKFLOW_SHA" ]]
[[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]
else
expected_caller_ref="${expected_repository}/${caller_path}@${GITHUB_REF}"
if [[ "$WORKFLOW_REF" != "$expected_caller_ref" ||
"$CALLER_WORKFLOW_SHA" != "$EXPECTED_TRUSTED_WORKFLOW_SHA" ]]; then
echo "Telegram QA reusable caller does not match the authorized tooling tuple." >&2
exit 1
fi
fi
export INVOCATION_MODE
@@ -815,6 +839,7 @@ jobs:
env:
ARCHIVE_NAME: ${{ needs.build_candidate.outputs.archive_name }}
ARCHIVE_SHA256: ${{ needs.build_candidate.outputs.archive_sha256 }}
CALLED_WORKFLOW_REF: ${{ needs.trusted_identity.outputs.workflow_ref }}
CALLED_WORKFLOW_SHA: ${{ needs.trusted_identity.outputs.workflow_sha }}
CANDIDATE_TREE: ${{ needs.build_candidate.outputs.candidate_tree }}
GH_TOKEN: ${{ github.token }}
@@ -830,7 +855,7 @@ jobs:
gh attestation verify "$archive_path" \
--repo "$GITHUB_REPOSITORY" \
--cert-identity "https://github.com/openclaw/openclaw/.github/workflows/openclaw-release-telegram-qa.yml@refs/heads/main" \
--cert-identity "https://github.com/${CALLED_WORKFLOW_REF}" \
--signer-digest "$CALLED_WORKFLOW_SHA" \
--source-ref "$GITHUB_REF" \
--source-digest "$GITHUB_SHA" \
+1 -1
View File
@@ -40,7 +40,7 @@ permissions:
contents: read
concurrency:
group: plugin-prerelease-${{ inputs.target_ref }}
group: plugin-prerelease-${{ inputs.target_ref }}-${{ github.sha }}
cancel-in-progress: ${{ inputs.target_ref == 'main' }}
env: