198 lines
7.5 KiB
Bash
Executable File
198 lines
7.5 KiB
Bash
Executable File
#!/bin/sh
|
|
set -eu
|
|
|
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
. "$SCRIPT_DIR/release-lib.sh"
|
|
RECOVERY_MANIFEST="$SCRIPT_DIR/delivered-stable-recoveries.json"
|
|
|
|
TAG="${1:-}"
|
|
EXPECTED_COMMIT="${2:-}"
|
|
REPOSITORY="${DWS_RELEASE_OFFICIAL_REPOSITORY:-DingTalk-Real-AI/dingtalk-workspace-cli}"
|
|
|
|
[ -n "$TAG" ] && [ -n "$EXPECTED_COMMIT" ] || {
|
|
printf 'usage: verify-delivered-stable.sh <stable-tag> <commit>\n' >&2
|
|
exit 2
|
|
}
|
|
release_is_stable_version "$TAG" || {
|
|
printf 'invalid stable delivery baseline: %s\n' "$TAG" >&2
|
|
exit 2
|
|
}
|
|
command -v curl >/dev/null 2>&1 || { printf 'curl is required to verify stable delivery\n' >&2; exit 1; }
|
|
command -v python3 >/dev/null 2>&1 || { printf 'python3 is required to verify stable delivery\n' >&2; exit 1; }
|
|
|
|
API_TOKEN="${DWS_RELEASE_GITHUB_TOKEN:-}"
|
|
if [ -z "$API_TOKEN" ] && [ "${GITHUB_ACTIONS:-false}" != "true" ] && command -v gh >/dev/null 2>&1; then
|
|
API_TOKEN="$(gh auth token 2>/dev/null || true)"
|
|
fi
|
|
if [ -z "$API_TOKEN" ]; then
|
|
API_TOKEN="${GITHUB_TOKEN:-}"
|
|
fi
|
|
github_get() {
|
|
if [ -n "$API_TOKEN" ]; then
|
|
curl -fsSL \
|
|
-H 'Accept: application/vnd.github+json' \
|
|
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
|
-H "Authorization: Bearer $API_TOKEN" \
|
|
"https://api.github.com/$1" 2>/dev/null && return 0
|
|
fi
|
|
curl -fsSL \
|
|
-H 'Accept: application/vnd.github+json' \
|
|
-H 'X-GitHub-Api-Version: 2026-03-10' \
|
|
"https://api.github.com/$1"
|
|
}
|
|
|
|
if git rev-parse --verify --quiet "refs/tags/$TAG^{commit}" >/dev/null; then
|
|
local_commit="$(git rev-parse "refs/tags/$TAG^{commit}")"
|
|
[ "$local_commit" = "$EXPECTED_COMMIT" ] || {
|
|
printf 'stable tag %s resolves to %s, expected %s\n' "$TAG" "$local_commit" "$EXPECTED_COMMIT" >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
stable_state="$(
|
|
github_get "repos/$REPOSITORY/releases/tags/$TAG" \
|
|
| python3 -c 'import json,sys
|
|
r=json.load(sys.stdin)
|
|
print("%s\t%s\t%s" % (r.get("tag_name", ""), str(r.get("draft", "")).lower(), str(r.get("prerelease", "")).lower()))'
|
|
)" || {
|
|
printf 'could not query previous stable release %s in %s\n' "$TAG" "$REPOSITORY" >&2
|
|
exit 1
|
|
}
|
|
[ "$stable_state" = "$(printf '%s\tfalse\tfalse' "$TAG")" ] || {
|
|
printf '%s is not a public stable GitHub Release in %s (got: %s)\n' \
|
|
"$TAG" "$REPOSITORY" "$stable_state" >&2
|
|
exit 1
|
|
}
|
|
|
|
stable_runs="$(
|
|
github_get "repos/$REPOSITORY/actions/workflows/release.yml/runs?branch=$TAG&event=push&status=completed&per_page=100" \
|
|
| python3 -c 'import json,sys
|
|
for run in json.load(sys.stdin).get("workflow_runs", []):
|
|
print("%s\t%s\t%s" % (run.get("head_sha", ""), run.get("head_branch", ""), run.get("conclusion", "")))'
|
|
)" || {
|
|
printf 'could not query Release workflow delivery for %s in %s\n' "$TAG" "$REPOSITORY" >&2
|
|
exit 1
|
|
}
|
|
printf '%s\n' "$stable_runs" | awk -F '\t' -v sha="$EXPECTED_COMMIT" -v tag="$TAG" '
|
|
$1 == sha && $2 == tag && $3 == "success" { found = 1 }
|
|
END { exit(found ? 0 : 1) }
|
|
' && delivered_by_push=1 || delivered_by_push=0
|
|
|
|
if [ "$delivered_by_push" -ne 1 ]; then
|
|
if DWS_RELEASE_OFFICIAL_REPOSITORY="$REPOSITORY" \
|
|
DWS_RELEASE_GITHUB_TOKEN="$API_TOKEN" \
|
|
"$SCRIPT_DIR/verify-release-workflow-delivery.sh" "$TAG" "$EXPECTED_COMMIT" 2>/dev/null; then
|
|
printf 'Delivered stable baseline verified through trusted default-branch delivery: %s -> %s\n' \
|
|
"$TAG" "$EXPECTED_COMMIT"
|
|
exit 0
|
|
fi
|
|
|
|
recovery_proof="$(
|
|
python3 - "$RECOVERY_MANIFEST" "$TAG" "$EXPECTED_COMMIT" <<'PY'
|
|
import json
|
|
import sys
|
|
|
|
path, tag, commit = sys.argv[1:]
|
|
with open(path, encoding="utf-8") as handle:
|
|
payload = json.load(handle)
|
|
if set(payload) != {"recoveries"} or not isinstance(payload["recoveries"], list):
|
|
raise SystemExit("invalid delivered stable recovery manifest")
|
|
|
|
required = {
|
|
"tag", "commit", "run_id", "workflow_sha", "head_branch",
|
|
"run_attempt", "reviewed", "review_reason",
|
|
}
|
|
matches = []
|
|
identities = set()
|
|
for entry in payload["recoveries"]:
|
|
if not isinstance(entry, dict) or set(entry) != required:
|
|
raise SystemExit("invalid delivered stable recovery entry")
|
|
identity = (entry["tag"], entry["commit"])
|
|
if identity in identities:
|
|
raise SystemExit("duplicate delivered stable recovery entry")
|
|
identities.add(identity)
|
|
if (
|
|
not isinstance(entry["tag"], str)
|
|
or not isinstance(entry["commit"], str)
|
|
or not isinstance(entry["run_id"], int)
|
|
or entry["run_id"] <= 0
|
|
or not isinstance(entry["workflow_sha"], str)
|
|
or not isinstance(entry["head_branch"], str)
|
|
or not isinstance(entry["run_attempt"], int)
|
|
or entry["run_attempt"] <= 0
|
|
or entry["reviewed"] is not True
|
|
or not isinstance(entry["review_reason"], str)
|
|
or not entry["review_reason"].strip()
|
|
):
|
|
raise SystemExit("invalid delivered stable recovery evidence")
|
|
if identity == (tag, commit):
|
|
matches.append(entry)
|
|
|
|
if len(matches) > 1:
|
|
raise SystemExit("ambiguous delivered stable recovery evidence")
|
|
if matches:
|
|
entry = matches[0]
|
|
print(entry["run_id"])
|
|
print(entry["workflow_sha"])
|
|
print(entry["head_branch"])
|
|
print(entry["run_attempt"])
|
|
PY
|
|
)" || exit 1
|
|
[ -n "$recovery_proof" ] || {
|
|
printf 'Release workflow did not complete successfully for stable baseline %s at %s\n' \
|
|
"$TAG" "$EXPECTED_COMMIT" >&2
|
|
exit 1
|
|
}
|
|
|
|
recovery_run_id="$(printf '%s\n' "$recovery_proof" | sed -n '1p')"
|
|
recovery_workflow_sha="$(printf '%s\n' "$recovery_proof" | sed -n '2p')"
|
|
recovery_head_branch="$(printf '%s\n' "$recovery_proof" | sed -n '3p')"
|
|
recovery_run_attempt="$(printf '%s\n' "$recovery_proof" | sed -n '4p')"
|
|
recovery_run_state="$(
|
|
github_get "repos/$REPOSITORY/actions/runs/$recovery_run_id" \
|
|
| python3 -c 'import json,sys
|
|
r=json.load(sys.stdin)
|
|
print("%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s" % (
|
|
r.get("event", ""), r.get("status", ""), r.get("conclusion", ""),
|
|
r.get("head_sha", ""), r.get("head_branch", ""), r.get("run_attempt", ""),
|
|
r.get("path", ""), r.get("repository", {}).get("full_name", "")))'
|
|
)" || {
|
|
printf 'could not query reviewed recovery run %s for %s\n' "$recovery_run_id" "$TAG" >&2
|
|
exit 1
|
|
}
|
|
expected_recovery_state="$(printf 'workflow_dispatch\tcompleted\tsuccess\t%s\t%s\t%s\t.github/workflows/release.yml\t%s' \
|
|
"$recovery_workflow_sha" "$recovery_head_branch" "$recovery_run_attempt" "$REPOSITORY")"
|
|
[ "$recovery_run_state" = "$expected_recovery_state" ] || {
|
|
printf 'reviewed recovery run %s does not match the pinned delivery proof for %s\n' \
|
|
"$recovery_run_id" "$TAG" >&2
|
|
exit 1
|
|
}
|
|
|
|
recovery_jobs="$(
|
|
github_get "repos/$REPOSITORY/actions/runs/$recovery_run_id/jobs?per_page=100" \
|
|
| python3 -c 'import json,sys
|
|
for job in json.load(sys.stdin).get("jobs", []):
|
|
print("%s\t%s\t%s\t%s" % (
|
|
job.get("name", ""), job.get("status", ""),
|
|
job.get("conclusion", ""), job.get("head_sha", "")))'
|
|
)" || {
|
|
printf 'could not query reviewed recovery jobs for %s\n' "$TAG" >&2
|
|
exit 1
|
|
}
|
|
for recovery_job in release verify-darwin-signatures publish-release; do
|
|
printf '%s\n' "$recovery_jobs" | awk -F '\t' -v name="$recovery_job" -v sha="$recovery_workflow_sha" '
|
|
$1 == name && $2 == "completed" && $3 == "success" && $4 == sha { found++ }
|
|
END { exit(found == 1 ? 0 : 1) }
|
|
' || {
|
|
printf 'reviewed recovery run %s is missing successful job %s for %s\n' \
|
|
"$recovery_run_id" "$recovery_job" "$TAG" >&2
|
|
exit 1
|
|
}
|
|
done
|
|
printf 'Delivered stable baseline verified through reviewed recovery run %s: %s -> %s\n' \
|
|
"$recovery_run_id" "$TAG" "$EXPECTED_COMMIT"
|
|
exit 0
|
|
fi
|
|
|
|
printf 'Delivered stable baseline verified: %s -> %s\n' "$TAG" "$EXPECTED_COMMIT"
|