Files
Jyotisha/deploy/resolve-release-revision.sh
T
jesse-uxandGrok (agent loop) 00e6efdbfa 发布可以跟上只多了文档的测试环境头
四个生产工作流改用同一条发布修订:最近一次代码门禁成功的提交可以发布,后面只多文档不再挡住。生产环境拒绝测试验证码。63 个还没上生产的迁移写了分级、演练脚本和首发步骤。没有点任何生产按钮。

自测:发布修订 7 通过,演练安全 3 通过,tsc 0 错,lint 0 error。本机空库演练在服务角色读人物表处按设计停止(退出码 4,其他人物已确认行数为 0)。

Co-Authored-By: Grok (agent loop)
2026-10-14 10:24:40 +08:00

276 lines
8.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Resolve the production release SHA shared by the four production workflows.
#
# Validate mode (default): REQUESTED_SHA must be a successful staging push of
# backend-quality-gate, main and staging must be the same head H, and
# is-docs-only-range.sh REQUESTED_SHA H must exit 0.
# Discover mode (RESOLVE_MODE=discover): R is the newest such push whose range
# to H is docs-only.
# CHECK_RELEASE_GATE=true also requires a successful release-quality-gate
# dispatch whose head is R or H.
#
# stdout is three KEY=VALUE lines: release_sha, gate_run_id, head_sha.
# Exit 0 accepted, 1 refused, 2 when the Gitea API cannot be read.
# Tests set JYOTISHA_HTTP_FIXTURE_DIR and JYOTISHA_DOCS_ONLY_MODE=git.
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
python="${JYOTISHA_PYTHON:-python3}"
mode="${RESOLVE_MODE:-validate}"
api_url="${GITEA_API_URL:-https://git.copse.top/api/v1}"
repository="${GITEA_REPOSITORY:-root/Jyotisha}"
docs_mode="${JYOTISHA_DOCS_ONLY_MODE:-api}"
if [[ "$mode" != validate && "$mode" != discover ]]; then
echo "RESOLVE_MODE must be validate or discover" >&2
exit 2
fi
http_get() {
local url="$1" dest="$2"
if [ -n "${JYOTISHA_HTTP_FIXTURE_DIR:-}" ]; then
if ! URL="$url" DEST="$dest" FIXTURE_DIR="$JYOTISHA_HTTP_FIXTURE_DIR" "$python" - <<'PY'
import json, os, sys
from pathlib import Path
url = os.environ["URL"]
root = Path(os.environ["FIXTURE_DIR"])
manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
for item in manifest:
if item["match"] in url:
status = int(item.get("status", 200))
if status != 200:
sys.stderr.write("fixture refused the Gitea request\n")
sys.exit(1)
Path(os.environ["DEST"]).write_bytes((root / item["file"]).read_bytes())
sys.exit(0)
sys.stderr.write("no fixture matched the Gitea request\n")
sys.exit(1)
PY
then
return 1
fi
return 0
fi
local -a auth=()
if [ -n "${GITEA_TOKEN:-}" ]; then
auth=(--header "Authorization: token ${GITEA_TOKEN}")
fi
if ! curl --fail --silent --show-error --connect-timeout 15 --max-time 60 --retry 3 --retry-all-errors \
"${auth[@]}" "$url" --output "$dest"; then
return 1
fi
}
fetch_json() {
local url="$1" dest
dest="$(mktemp)"
if ! http_get "$url" "$dest"; then
rm -f -- "$dest"
echo "Gitea API request failed" >&2
exit 2
fi
printf '%s' "$dest"
}
read_ref_sha() {
local branch="$1" dest sha
dest="$(fetch_json "$api_url/repos/$repository/git/refs/heads/$branch")"
if ! sha="$("$python" - "$branch" "$dest" <<'PY'
import json, re, sys
branch, path = sys.argv[1], sys.argv[2]
try:
payload = json.loads(open(path, encoding="utf-8").read())
except (OSError, json.JSONDecodeError):
sys.exit(2)
ref = "refs/heads/" + branch
if not isinstance(payload, list) or len(payload) != 1 or payload[0].get("ref") != ref:
sys.exit(2)
sha = ((payload[0].get("object") or {}).get("sha") or "")
if not re.fullmatch(r"[0-9a-f]{40}", sha):
sys.exit(2)
print(sha)
PY
)"; then
rm -f -- "$dest"
echo "Gitea ref for $branch is unreadable" >&2
exit 2
fi
rm -f -- "$dest"
printf '%s' "$sha"
}
gate_id_for() {
local sha="$1" dest gate_id
dest="$(fetch_json "$api_url/repos/$repository/actions/runs?head_sha=$sha&branch=staging&event=push&status=success&limit=100")"
set +e
gate_id="$("$python" - "$sha" "$dest" <<'PY'
import json, sys
sha, path = sys.argv[1], sys.argv[2]
try:
payload = json.loads(open(path, encoding="utf-8").read())
except (OSError, json.JSONDecodeError):
sys.exit(2)
runs = payload.get("workflow_runs") or []
matched = []
for run in runs:
workflow = str(run.get("path") or "").split("@", 1)[0]
if (
workflow.endswith("backend-quality-gate.yml")
and run.get("head_sha") == sha
and run.get("head_branch") == "staging"
and run.get("event") == "push"
and run.get("conclusion") == "success"
):
matched.append(run)
if not matched:
sys.exit(1)
matched.sort(key=lambda run: int(run.get("id") or 0), reverse=True)
print(int(matched[0]["id"]))
PY
)"
local status=$?
set -e
rm -f -- "$dest"
if [ "$status" -eq 2 ]; then
echo "Gitea gate run list is unreadable" >&2
exit 2
fi
if [ "$status" -ne 0 ]; then
echo "no successful exact-SHA production quality gate run found" >&2
exit 1
fi
printf '%s' "$gate_id"
}
require_docs_only() {
local base="$1" head="$2" status
set +e
bash "$script_dir/is-docs-only-range.sh" "--$docs_mode" "$base" "$head" >&2
status=$?
set -e
if [ "$status" -ne 0 ]; then
echo "release SHA is not a docs-only ancestor of the staging head" >&2
exit 1
fi
}
require_release_gate() {
local release="$1" head="$2" sha dest status found=false
for sha in "$release" "$head"; do
dest="$(fetch_json "$api_url/repos/$repository/actions/runs?head_sha=$sha&event=workflow_dispatch&status=success&limit=100")"
set +e
"$python" - "$sha" "$dest" <<'PY'
import json, sys
sha, path = sys.argv[1], sys.argv[2]
try:
payload = json.loads(open(path, encoding="utf-8").read())
except (OSError, json.JSONDecodeError):
sys.exit(2)
runs = payload.get("workflow_runs") or []
for run in runs:
workflow = str(run.get("path") or "").split("@", 1)[0]
if (
workflow.endswith("release-quality-gate.yml")
and run.get("head_sha") == sha
and run.get("event") == "workflow_dispatch"
and run.get("conclusion") == "success"
):
sys.exit(0)
sys.exit(1)
PY
status=$?
set -e
rm -f -- "$dest"
if [ "$status" -eq 0 ]; then
found=true
break
fi
if [ "$status" -eq 2 ]; then
echo "Gitea release gate list is unreadable" >&2
exit 2
fi
done
if [ "$found" != true ]; then
echo "no successful manual release quality gate found for the release SHA or current main" >&2
exit 1
fi
}
staging_head="$(read_ref_sha staging)"
main_head="$(read_ref_sha main)"
if [ "$main_head" != "$staging_head" ]; then
echo "main and staging must identify the same reviewed release" >&2
exit 1
fi
head_sha="$staging_head"
if [ "$mode" = validate ]; then
[[ "${REQUESTED_SHA:-}" =~ ^[0-9a-f]{40}$ ]] || {
echo "deploy_sha must be a lowercase full commit SHA" >&2
exit 1
}
release_sha="$REQUESTED_SHA"
gate_run_id="$(gate_id_for "$release_sha")"
require_docs_only "$release_sha" "$head_sha"
else
list="$(fetch_json "$api_url/repos/$repository/actions/runs?branch=staging&event=push&status=success&limit=100")"
set +e
candidates="$("$python" - "$list" <<'PY'
import json, sys
try:
payload = json.loads(open(sys.argv[1], encoding="utf-8").read())
except (OSError, json.JSONDecodeError):
sys.exit(2)
best = {}
for run in payload.get("workflow_runs") or []:
workflow = str(run.get("path") or "").split("@", 1)[0]
sha = run.get("head_sha") or ""
if (
workflow.endswith("backend-quality-gate.yml")
and run.get("head_branch") == "staging"
and run.get("event") == "push"
and run.get("conclusion") == "success"
and len(sha) == 40
):
run_id = int(run.get("id") or 0)
if sha not in best or run_id > best[sha]:
best[sha] = run_id
for sha, run_id in sorted(best.items(), key=lambda item: item[1], reverse=True):
print(f"{sha} {run_id}")
PY
)"
status=$?
set -e
rm -f -- "$list"
if [ "$status" -eq 2 ]; then
echo "Gitea gate run list is unreadable" >&2
exit 2
fi
release_sha=""
gate_run_id=""
while read -r candidate_sha candidate_id; do
[ -n "$candidate_sha" ] || continue
set +e
bash "$script_dir/is-docs-only-range.sh" "--$docs_mode" "$candidate_sha" "$head_sha" >&2
status=$?
set -e
if [ "$status" -eq 0 ]; then
release_sha="$candidate_sha"
gate_run_id="$candidate_id"
break
fi
done <<<"$candidates"
if [ -z "$release_sha" ]; then
echo "no docs-only staging quality gate can be released" >&2
exit 1
fi
fi
if [ "${CHECK_RELEASE_GATE:-}" = true ]; then
require_release_gate "$release_sha" "$head_sha"
fi
printf 'release_sha=%s\n' "$release_sha"
printf 'gate_run_id=%s\n' "$gate_run_id"
printf 'head_sha=%s\n' "$head_sha"