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