bread-ecosystem/scripts/cleanup-old-mirror-workflows.sh
Breadway ab4e882baa Add push-mirror provisioning + old mirror.yml cleanup scripts
Replaces the per-repo .forgejo/workflows/mirror.yml + MIRROR_TOKEN
pattern with Forgejo's native Push Mirror feature, provisioned
centrally instead of per-repo:

- setup-push-mirrors.sh: reads the repo list live from the Forgejo API
  (GET /users/Breadway/repos — confirmed Breadway is a user account,
  not an org, so the /orgs/ endpoint 404s and this falls back
  correctly) instead of a hardcoded repo list, checks each repo's
  existing push_mirrors for idempotency, and POSTs a new one
  (sync_on_commit + 8h interval) for any repo missing one. Private
  repos are skipped by default (found novacana-engine on the live
  account) since mirroring one to a public GitHub repo is a
  disclosure decision this script should never make silently — pass
  --include-private to override per-run. --dry-run prints every
  request (GH token redacted) without POSTing.

- cleanup-old-mirror-workflows.sh: deletes mirror.yml from each repo's
  default branch (live commit via the contents API, not a local
  change) and removes the MIRROR_TOKEN secret. Refuses to run at all
  — dry-run included — without an explicit
  --i-have-verified-push-mirrors-work flag, since it should only ever
  run after confirming the new push mirrors are actually syncing.

Neither script has been run for real. setup-push-mirrors.sh has only
been run with --dry-run against the live Forgejo API (read-only GETs);
cleanup-old-mirror-workflows.sh has not been run at all beyond
confirming its guardrail refuses to execute.
2026-07-17 14:11:30 +08:00

190 lines
7.5 KiB
Bash
Executable file

#!/usr/bin/env bash
# cleanup-old-mirror-workflows.sh — retire the per-repo GitHub mirroring
# pattern now that Forgejo native Push Mirrors (see setup-push-mirrors.sh)
# do the same job centrally.
#
# THIS SCRIPT IS DESTRUCTIVE AND TOUCHES LIVE, RUNNING INFRASTRUCTURE:
# 1. Deletes .forgejo/workflows/mirror.yml from the DEFAULT BRANCH of every
# repo returned by the Forgejo API that has one (via the contents API —
# this is a real commit to each repo's default branch, not a local/
# worktree change).
# 2. Deletes the MIRROR_TOKEN Actions secret from every repo that has one.
#
# Do not run this until you have confirmed, for real, that push mirrors
# created by setup-push-mirrors.sh are actually syncing to GitHub (check
# a repo's Settings > Push Mirrors in the Forgejo web UI, or GET
# /repos/{owner}/{repo}/push_mirrors and look at last_update / last_error,
# and confirm commits are actually landing on the GitHub side). Until then,
# removing mirror.yml would silently kill the only thing currently keeping
# GitHub in sync.
#
# As a guardrail, this script refuses to do anything unless invoked with
# --i-have-verified-push-mirrors-work. There is no way around that flag
# short of editing this script, which is the point.
#
# Requires: bash, curl, jq
#
# Reads the same token file as setup-push-mirrors.sh:
# FORGEJO_TOKEN_FILE default ~/.config/forgejo/token
#
# Env vars:
# FORGEJO_BASE https://git.breadway.dev
# FORGEJO_OWNER Breadway
#
# Flags:
# --i-have-verified-push-mirrors-work required, see above
# --dry-run print what would be deleted, make
# no changes (combine with the
# confirmation flag or this refuses
# to run at all — even dry-run mode
# is gated, so nobody can quietly
# drop the guardrail out of the
# invocation by force of habit)
# --only repo1,repo2 comma-separated allowlist
#
# Usage (once verified):
# scripts/cleanup-old-mirror-workflows.sh --i-have-verified-push-mirrors-work --dry-run
# scripts/cleanup-old-mirror-workflows.sh --i-have-verified-push-mirrors-work
set -euo pipefail
FORGEJO_BASE="${FORGEJO_BASE:-https://git.breadway.dev}"
FORGEJO_OWNER="${FORGEJO_OWNER:-Breadway}"
FORGEJO_TOKEN_FILE="${FORGEJO_TOKEN_FILE:-${HOME}/.config/forgejo/token}"
CONFIRMED=0
DRY_RUN=0
ONLY_REPOS=""
while [[ $# -gt 0 ]]; do
case "$1" in
--i-have-verified-push-mirrors-work) CONFIRMED=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
--only) ONLY_REPOS="$2"; shift 2 ;;
-h|--help)
sed -n '2,42p' "$0"
exit 0
;;
*)
echo "error: unknown argument: $1" >&2
exit 2
;;
esac
done
if [[ "${CONFIRMED}" != 1 ]]; then
cat >&2 <<'EOF'
error: refusing to run.
This script deletes mirror.yml from the default branch of every mirrored
repo and removes the MIRROR_TOKEN secret. That's a real, immediate change
to production CI on every one of those repos, and it also permanently
disables the *old* mirroring path.
Before running this:
1. Run setup-push-mirrors.sh for real (not --dry-run).
2. Confirm, for at least one repo, that the push mirror actually synced
(Forgejo web UI: repo Settings > Push Mirrors > check "Last Update"
and that there's no "Last Error"; and check the GitHub side directly).
3. Only then re-run this script with:
--i-have-verified-push-mirrors-work
Add --dry-run (in addition to the flag above) to preview without changing
anything.
EOF
exit 1
fi
for bin in curl jq; do
command -v "${bin}" >/dev/null 2>&1 || { echo "error: ${bin} is required" >&2; exit 2; }
done
[[ -f "${FORGEJO_TOKEN_FILE}" ]] || { echo "error: Forgejo token file not found at ${FORGEJO_TOKEN_FILE}" >&2; exit 2; }
FORGEJO_TOKEN="$(<"${FORGEJO_TOKEN_FILE}")"
api() {
# api METHOD PATH [JSON_BODY] -> prints response body, exits nonzero on HTTP error
local method="$1" path="$2" body="${3:-}"
if [[ -n "${body}" ]]; then
curl -fsS -X "${method}" \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
-d "${body}" \
"${FORGEJO_BASE}/api/v1${path}"
else
curl -fsS -X "${method}" \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_BASE}/api/v1${path}"
fi
}
owner_kind="org"
if ! curl -fsS -o /dev/null -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_BASE}/api/v1/orgs/${FORGEJO_OWNER}" 2>/dev/null; then
owner_kind="user"
fi
if [[ "${owner_kind}" == "org" ]]; then
repos_json="$(api GET "/orgs/${FORGEJO_OWNER}/repos?limit=50")"
else
repos_json="$(api GET "/users/${FORGEJO_OWNER}/repos?limit=50")"
fi
mapfile -t repo_names < <(echo "${repos_json}" | jq -r '.[].name')
if [[ "${DRY_RUN}" == 1 ]]; then
echo "# --dry-run: no deletions will be made"
fi
echo
for name in "${repo_names[@]}"; do
if [[ -n "${ONLY_REPOS}" ]]; then
IFS=',' read -ra allow <<< "${ONLY_REPOS}"
match=0
for a in "${allow[@]}"; do [[ "${a}" == "${name}" ]] && match=1; done
[[ "${match}" == 1 ]] || continue
fi
default_branch="$(echo "${repos_json}" | jq -r --arg n "${name}" '.[] | select(.name==$n) | .default_branch')"
# Contents API: GET returns the file's sha, which the DELETE call needs.
file_info="$(curl -fsS -o /tmp/cleanup_probe.json -w '%{http_code}' \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_BASE}/api/v1/repos/${FORGEJO_OWNER}/${name}/contents/.forgejo/workflows/mirror.yml?ref=${default_branch}" || true)"
if [[ "${file_info}" == "200" ]]; then
sha="$(jq -r '.sha' /tmp/cleanup_probe.json)"
if [[ "${DRY_RUN}" == 1 ]]; then
echo "WOULD-DELETE ${name}: .forgejo/workflows/mirror.yml (sha ${sha}) from ${default_branch}"
else
echo "DELETE ${name}: .forgejo/workflows/mirror.yml from ${default_branch}"
del_body="$(jq -n --arg msg "ci: remove mirror.yml, superseded by native push mirror" \
--arg sha "${sha}" --arg branch "${default_branch}" \
'{message: $msg, sha: $sha, branch: $branch}')"
api DELETE "/repos/${FORGEJO_OWNER}/${name}/contents/.forgejo/workflows/mirror.yml" "${del_body}" >/dev/null
fi
else
echo "SKIP ${name}: no .forgejo/workflows/mirror.yml on ${default_branch}"
fi
secret_check="$(curl -fsS -o /dev/null -w '%{http_code}' \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_BASE}/api/v1/repos/${FORGEJO_OWNER}/${name}/actions/secrets" || true)"
has_mirror_token="$(curl -fsS -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_BASE}/api/v1/repos/${FORGEJO_OWNER}/${name}/actions/secrets" \
| jq -r '[.[] | select(.name=="MIRROR_TOKEN")] | length')"
if [[ "${has_mirror_token}" -gt 0 ]]; then
if [[ "${DRY_RUN}" == 1 ]]; then
echo "WOULD-DELETE ${name}: MIRROR_TOKEN secret"
else
echo "DELETE ${name}: MIRROR_TOKEN secret"
curl -fsS -X DELETE -H "Authorization: token ${FORGEJO_TOKEN}" \
"${FORGEJO_BASE}/api/v1/repos/${FORGEJO_OWNER}/${name}/actions/secrets/MIRROR_TOKEN" >/dev/null
fi
else
echo "SKIP ${name}: no MIRROR_TOKEN secret"
fi
done
rm -f /tmp/cleanup_probe.json