四个生产工作流改用同一条发布修订:最近一次代码门禁成功的提交可以发布,后面只多文档不再挡住。生产环境拒绝测试验证码。63 个还没上生产的迁移写了分级、演练脚本和首发步骤。没有点任何生产按钮。 自测:发布修订 7 通过,演练安全 3 通过,tsc 0 错,lint 0 error。本机空库演练在服务角色读人物表处按设计停止(退出码 4,其他人物已确认行数为 0)。 Co-Authored-By: Grok (agent loop)
276 lines
8.1 KiB
Bash
Executable File
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"
|