#!/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"