From 608cc742a58e67e4f257260485eae6831e7e16b5 Mon Sep 17 00:00:00 2001 From: codex Date: Wed, 8 Jul 2026 13:11:43 +0200 Subject: [PATCH] tools: archive coulomb Gitea org for read-only mirror policy Script uses Gitea API to set archived=true on all org repos after Forgejo cutover (RAIL-HO-WP-0005). Ran 2026-07-08: 79/79 archived on coulombcore. --- tools/gitea-set-readonly.sh | 112 ++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100755 tools/gitea-set-readonly.sh diff --git a/tools/gitea-set-readonly.sh b/tools/gitea-set-readonly.sh new file mode 100755 index 0000000..c945b28 --- /dev/null +++ b/tools/gitea-set-readonly.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Archive all coulomb org repos on coulombcore Gitea (read-only mirror policy). +# Per RAIL-HO-WP-0005 / forgejo-tier3 playbook org-wide operator action. +set -euo pipefail + +CONF="${GITEA_CONF:-$HOME/.railiance_gitea.conf}" +API="${GITEA_API:-https://gitea.coulomb.social}" +ORG="${GITEA_ORG:-coulomb}" +DRY_RUN="${DRY_RUN:-0}" +LIMIT="${PAGE_LIMIT:-50}" + +usage() { + cat <<'USAGE' +Usage: gitea-set-readonly.sh [repo ...] + + With no args: archive every repo in org coulomb (idempotent). + With slugs: archive only those coulomb/ repos. + +Environment: + GITEA_CONF Token file (default: ~/.railiance_gitea.conf) + GITEA_API HTTPS API base (default: https://gitea.coulomb.social) + DRY_RUN=1 Print actions only +USAGE +} + +if [[ -f "$CONF" ]]; then + # shellcheck disable=SC1090 + source "$CONF" +fi +: "${GITEA_TOKEN:?Set GITEA_TOKEN or provide $CONF}" + +auth=(-H "Authorization: token ${GITEA_TOKEN}") + +fetch_all_repos() { + local page=1 + local out=/tmp/gitea-readonly-repos.jsonl + : >"$out" + while true; do + local http body + body=$(mktemp) + http=$(curl -sS --max-time 30 -o "$body" -w '%{http_code}' \ + "${auth[@]}" "${API}/api/v1/orgs/${ORG}/repos?limit=${LIMIT}&page=${page}") + if [[ "$http" != "200" ]]; then + echo "FAIL list page=${page} http=${http}" >&2 + cat "$body" >&2 + exit 1 + fi + local count + count=$(python3 -c "import json; print(len(json.load(open('$body'))))") + python3 -c "import json; [print(r['name']) for r in json.load(open('$body'))]" >>"$out" + rm -f "$body" + [[ "$count" -lt "$LIMIT" ]] && break + page=$((page + 1)) + done + sort -u "$out" +} + +archive_repo() { + local name="$1" + local http archived + http=$(curl -sS --max-time 30 -o /tmp/gitea-get.json -w '%{http_code}' \ + "${auth[@]}" "${API}/api/v1/repos/${ORG}/${name}") + if [[ "$http" == "404" ]]; then + echo "SKIP ${name} (not found)" + return 0 + fi + if [[ "$http" != "200" ]]; then + echo "FAIL ${name} GET http=${http}" >&2 + return 1 + fi + archived=$(python3 -c "import json; print(json.load(open('/tmp/gitea-get.json')).get('archived', False))") + if [[ "$archived" == "True" ]]; then + echo "OK ${name} (already archived)" + return 0 + fi + if [[ "$DRY_RUN" == "1" ]]; then + echo "DRY ${name} -> archived" + return 0 + fi + http=$(curl -sS --max-time 30 -o /tmp/gitea-patch.json -w '%{http_code}' \ + -X PATCH "${auth[@]}" -H "Content-Type: application/json" \ + -d '{"archived":true}' "${API}/api/v1/repos/${ORG}/${name}") + if [[ "$http" == "200" ]]; then + echo "ARCH ${name}" + return 0 + fi + echo "FAIL ${name} PATCH http=${http}" >&2 + cat /tmp/gitea-patch.json >&2 + return 1 +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +if [[ $# -gt 0 ]]; then + mapfile -t TARGETS < <(printf '%s\n' "$@") +else + mapfile -t TARGETS < <(fetch_all_repos) +fi + +echo "=== Gitea read-only: archive ${#TARGETS[@]} repo(s) in ${ORG} ===" +echo "API: ${API}" + +failed=0 +for name in "${TARGETS[@]}"; do + archive_repo "$name" || failed=$((failed + 1)) +done + +echo "=== Done (failed=${failed}) ===" +[[ "$failed" -eq 0 ]] \ No newline at end of file