Files
openclaw/scripts/release-telegram-provenance.sh
T

262 lines
10 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
gh_with_retry() {
local stdout stderr_file stderr_output output status attempt
for attempt in 1 2 3 4 5; do
stderr_file="$(mktemp)"
set +e
stdout="$(gh "$@" 2>"$stderr_file")"
status=$?
set -e
if [[ "$status" -eq 0 ]]; then
if [[ -s "$stderr_file" ]]; then
cat "$stderr_file" >&2
fi
rm -f "$stderr_file"
printf '%s\n' "$stdout"
return 0
fi
stderr_output="$(cat "$stderr_file")"
rm -f "$stderr_file"
output="$stdout"
if [[ -n "$stderr_output" ]]; then
output+="${output:+$'\n'}${stderr_output}"
fi
if [[ "$output" =~ $GH_TRANSIENT_SERVER_OR_NETWORK_PATTERN ]]; then
echo "::warning::Transient GitHub response from gh $* on attempt ${attempt}; retrying." >&2
sleep $((attempt * 3))
continue
fi
printf '%s\n' "$output" >&2
return "$status"
done
printf '%s\n' "$output" >&2
return "$status"
}
candidate_root="${CANDIDATE_ROOT:?}"
candidate_git_dir="${CANDIDATE_GIT_DIR:-}"
remote_git_dir="${candidate_git_dir:-.}"
candidate_sha="$TARGET_SHA"
if [[ -n "$candidate_git_dir" ]]; then
[[ "$(git -C "$candidate_git_dir" rev-parse HEAD)" == "$candidate_sha" ]]
fi
normalized_context_ref="${TARGET_CONTEXT_REF:-}"
normalized_context_ref="${normalized_context_ref#refs/heads/}"
normalized_context_ref="${normalized_context_ref#refs/tags/}"
context_release_branch=""
context_release_tag=""
frozen_release_branch_pattern=""
if [[ "$normalized_context_ref" =~ ^release/([0-9]{4}\.[0-9]+\.[0-9]+)$ ]]; then
release_version="${BASH_REMATCH[1]}"
release_version_pattern="${release_version//./\.}"
candidate_version="$(jq -er '.version' "${candidate_root}/package.json")"
if [[ "$candidate_version" == "$release_version" ]]; then
context_release_branch="$normalized_context_ref"
elif [[ "$candidate_version" =~ ^${release_version_pattern}-beta\.[0-9]+$ ]]; then
context_release_branch="$normalized_context_ref"
candidate_version_pattern="${candidate_version//./\.}"
frozen_release_branch_pattern="^release/${candidate_version_pattern}-code-frozen(-r[1-9][0-9]*)?$"
else
echo "Telegram candidate version ${candidate_version} does not belong to release ${release_version}." >&2
exit 1
fi
elif [[ "$normalized_context_ref" =~ ^extended-stable/([0-9]{4}\.[0-9]+\.33)$ ]]; then
context_version="${BASH_REMATCH[1]}"
candidate_version="$(jq -er '.version' "${candidate_root}/package.json")"
if [[ "$candidate_version" != "$context_version" ]]; then
echo "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}." >&2
exit 1
fi
context_release_branch="$normalized_context_ref"
elif [[ "$normalized_context_ref" =~ ^v([0-9]{4}\.[0-9]+\.[0-9]+(-(alpha|beta)\.[0-9]+)?)$ ]]; then
context_version="${BASH_REMATCH[1]}"
candidate_version="$(jq -er '.version' "${candidate_root}/package.json")"
if [[ "$candidate_version" != "$context_version" ]]; then
echo "Telegram candidate version ${candidate_version} does not match context ${normalized_context_ref}." >&2
exit 1
fi
context_release_tag="$normalized_context_ref"
fi
repository_owner="${GITHUB_REPOSITORY%%/*}"
repository_name="${GITHUB_REPOSITORY#*/}"
candidate_metadata_json="$(
# GraphQL expands these variables server-side, not in the shell.
# shellcheck disable=SC2016
gh_with_retry api graphql \
-f query='query($owner:String!,$name:String!,$oid:GitObjectID!){repository(owner:$owner,name:$name){object(oid:$oid){... on Commit{oid signature{isValid state signer{login}} associatedPullRequests(first:100){nodes{state headRefOid headRepository{nameWithOwner} baseRefName baseRepository{nameWithOwner} mergeCommit{oid} mergedBy{login}}}}}}}' \
-f owner="$repository_owner" \
-f name="$repository_name" \
-f oid="$candidate_sha"
)"
pr_head_count="$(
jq -er \
--arg repo "$GITHUB_REPOSITORY" \
--arg sha "$candidate_sha" \
'[.data.repository.object.associatedPullRequests.nodes[] |
select(.state == "OPEN" and .headRepository.nameWithOwner == $repo and
.headRefOid == $sha)] | length' \
<<<"$candidate_metadata_json"
)"
if [[ "$pr_head_count" != "0" ]]; then
echo "Telegram candidate ${candidate_sha} is an open same-repository PR head." >&2
exit 1
fi
compare_status="$(
gh_with_retry api \
"repos/${GITHUB_REPOSITORY}/compare/${candidate_sha}...main" \
--jq '.status'
)"
trusted_reason=""
trusted_release_branch=""
if [[ -n "$context_release_branch" ]]; then
branch_sha="$(
git -C "$remote_git_dir" ls-remote --exit-code --refs origin \
"refs/heads/${context_release_branch}" |
awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }' ||
true
)"
if [[ "$branch_sha" == "$candidate_sha" ]]; then
trusted_reason="release-branch-head"
trusted_release_branch="$context_release_branch"
fi
elif [[ -n "$context_release_tag" ]]; then
tag_refs="$(
git -C "$remote_git_dir" ls-remote --exit-code origin \
"refs/tags/${context_release_tag}" "refs/tags/${context_release_tag}^{}"
)"
awk -v sha="$candidate_sha" '$1 == sha { found = 1 } END { exit(found ? 0 : 1) }' \
<<<"$tag_refs"
trusted_reason="release-tag"
elif [[ "$compare_status" == "ahead" || "$compare_status" == "identical" ]]; then
trusted_reason="main-ancestor"
else
normalized_ref="${TARGET_REF#refs/heads/}"
if [[ "$normalized_ref" =~ ^(release/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*|extended-stable/[0-9]{4}\.[1-9][0-9]*\.33)$ ]]; then
branch_sha="$(
git -C "$remote_git_dir" ls-remote --exit-code --refs origin \
"refs/heads/${normalized_ref}" |
awk 'NR == 1 { print $1 } END { if (NR != 1) exit 1 }'
)"
[[ "$branch_sha" == "$candidate_sha" ]]
trusted_reason="release-branch-head"
trusted_release_branch="$normalized_ref"
elif [[ "$TARGET_REF" =~ ^refs/tags/v ]] || [[ "$TARGET_REF" =~ ^v ]]; then
normalized_tag="${TARGET_REF#refs/tags/}"
tag_refs="$(
git -C "$remote_git_dir" ls-remote --exit-code origin \
"refs/tags/${normalized_tag}" "refs/tags/${normalized_tag}^{}"
)"
awk -v sha="$candidate_sha" '$1 == sha { found = 1 } END { exit(found ? 0 : 1) }' \
<<<"$tag_refs"
trusted_reason="release-tag"
elif [[ "$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$candidate_sha" ]]; then
matching_release_branches="$(
gh_with_retry api --paginate \
"repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \
--jq '.[].name' |
awk '$0 ~ /^release\/[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*$/ ||
$0 ~ /^extended-stable\/[0-9]{4}\.[1-9][0-9]*\.33$/ { print }'
)"
if [[ "$(wc -l <<<"$matching_release_branches" | tr -d ' ')" == "1" &&
-n "$matching_release_branches" ]]; then
trusted_reason="release-branch-head"
trusted_release_branch="$matching_release_branches"
else
matching_release_tags="$(
git -C "$remote_git_dir" ls-remote origin 'refs/tags/v*' |
awk -v sha="$candidate_sha" '$1 == sha { sub(/\^\{\}$/, "", $2); print $2 }' |
sort -u
)"
if [[ -n "$matching_release_tags" ]]; then
trusted_reason="release-tag"
fi
fi
fi
fi
if [[ -z "$trusted_reason" && -n "$frozen_release_branch_pattern" &&
"$TARGET_REF" =~ ^[a-f0-9]{40}$ && "$TARGET_REF" == "$candidate_sha" ]]; then
matching_frozen_release_branches="$(
gh_with_retry api --paginate \
"repos/${GITHUB_REPOSITORY}/commits/${candidate_sha}/branches-where-head" \
--jq '.[].name' |
awk -v frozen="$frozen_release_branch_pattern" '$0 ~ frozen { print }'
)"
if [[ "$(wc -l <<<"$matching_frozen_release_branches" | tr -d ' ')" == "1" &&
-n "$matching_frozen_release_branches" ]]; then
trusted_reason="frozen-release-branch-head"
trusted_release_branch="$matching_frozen_release_branches"
fi
fi
if [[ -z "$trusted_reason" ]]; then
echo "Telegram candidate ${candidate_sha} is not trusted release provenance." >&2
exit 1
fi
if [[ "$trusted_reason" != "main-ancestor" ]]; then
signature_status="$(
jq -er \
--arg sha "$candidate_sha" \
'.data.repository.object |
select(.oid == $sha) |
if .signature == null then "missing"
elif .signature.isValid == true and .signature.state == "VALID" and
(.signature.signer.login // "") != "" then "valid"
else "invalid"
end' \
<<<"$candidate_metadata_json"
)"
if [[ "$signature_status" == "invalid" ]]; then
echo "Release candidate ${candidate_sha} has an invalid commit signature." >&2
exit 1
fi
signer="$(jq -r '.data.repository.object.signature.signer.login // ""' <<<"$candidate_metadata_json")"
if [[ "$trusted_reason" == "frozen-release-branch-head" &&
( "$signature_status" != "valid" || "$signer" == "web-flow" ) ]]; then
echo "Frozen release candidate ${candidate_sha} requires a valid maintainer signature." >&2
exit 1
fi
permission_actor="$signer"
if [[ "$signature_status" == "missing" || "$signer" == "web-flow" ]]; then
if [[ "$trusted_reason" != "release-branch-head" || -z "$trusted_release_branch" ]]; then
echo "Unsigned or GitHub web-flow candidates require an exact release branch head." >&2
exit 1
fi
matching_merge_prs="$(
jq -c \
--arg repo "$GITHUB_REPOSITORY" \
--arg sha "$candidate_sha" \
'[.data.repository.object.associatedPullRequests.nodes[] |
select(.state == "MERGED" and .baseRepository.nameWithOwner == $repo and
.mergeCommit.oid == $sha)]' \
<<<"$candidate_metadata_json"
)"
if [[ "$(jq 'length' <<<"$matching_merge_prs")" != "1" ]]; then
echo "Unsigned or GitHub web-flow candidate ${candidate_sha} requires one exact merged same-repository PR." >&2
exit 1
fi
permission_actor="$(
jq -er '.[0].mergedBy.login | select(type == "string" and length > 0)' \
<<<"$matching_merge_prs"
)"
fi
permission_json="$(
gh_with_retry api \
"repos/${GITHUB_REPOSITORY}/collaborators/${permission_actor}/permission"
)"
permission="$(jq -r '.permission // ""' <<<"$permission_json")"
role_name="$(jq -r '.role_name // ""' <<<"$permission_json")"
if [[ "$permission" != "admin" && "$role_name" != "maintain" ]]; then
echo "Release candidate actor ${permission_actor} lacks maintain/admin access." >&2
exit 1
fi
fi
echo "Telegram candidate trust reason: ${trusted_reason}"