#!/usr/bin/env bash # recipe-mirror-sync :: faithfully reconcile a recipe MIRROR to its coopcloud UPSTREAM. # (phase canon §2.C — adapted from recipe-upgrade's `open-recipe-pr.sh --reconcile-only`.) # --------------------------------------------------------------------------------------- # Runs ON cc-ci (root) against the recipe checkout at ~/.abra/recipes/. Invoked by the # weekly canonical sweep BEFORE the per-recipe CI so the sweep measures true upstream tags/latest. # # FAITHFUL MIRROR SYNC ONLY — never pushes our own changes: # - upstream `main` + tags (git.coopcloud.tech/coop-cloud/) are force-synced onto the # mirror (git.autonomic.zone/recipe-maintainers/): mirror main := upstream main, and all # upstream tags pushed. This guarantees every open PR is measured against the real upstream. # - any open mirror PR whose changes are ALREADY IN upstream main (merging it would be a no-op — # i.e. it was merged upstream) is closed; UNRELATED PRs are left untouched. # - NEVER merges anything; NEVER pushes a branch other than the faithful main/tags mirror. # # Why this is adapted rather than the verbatim open-recipe-pr.sh: that script assumes the clone's # `origin` IS coopcloud upstream, but abra recipe clones on cc-ci have inconsistent remotes (origin # is sometimes the mirror, sometimes coopcloud, sometimes there is no `upstream`). This version # pins an explicit `upstream` remote from the recipe name, and also syncs TAGS (canon needs the # release-tag trigger to see upstream tags). Auth: the bot gitea token (push via oauth2 URL, API via # Authorization header) so it is self-contained, not dependent on host .git-credentials state. set -o errexit -o nounset -o pipefail RECIPE="${1:?usage: recipe-mirror-sync.sh }" export PATH="/run/current-system/sw/bin:${PATH}" GITEA_HOST="${GITEA_URL:-git.autonomic.zone}" NAMESPACE="${GITEA_NAMESPACE:-recipe-maintainers}" UPSTREAM_URL="https://git.coopcloud.tech/coop-cloud/${RECIPE}.git" TOKEN_FILE="${CCCI_GITEA_TOKEN_FILE:-/run/secrets/bridge_gitea_token}" RECIPE_DIR="${HOME}/.abra/recipes/${RECIPE}" [ -r "${TOKEN_FILE}" ] || { echo "ERROR: gitea token not readable at ${TOKEN_FILE}" exit 1 } TOKEN="$(tr -d '[:space:]' <"${TOKEN_FILE}")" API="https://${GITEA_HOST}/api/v1" MIRROR_PUSH="https://oauth2:${TOKEN}@${GITEA_HOST}/${NAMESPACE}/${RECIPE}.git" auth=(-H "Authorization: token ${TOKEN}") # Ensure the recipe is cloned (abra recipe fetch clones from the catalogue → upstream). [ -d "${RECIPE_DIR}/.git" ] || abra recipe fetch "${RECIPE}" -n >/dev/null 2>&1 || true [ -d "${RECIPE_DIR}/.git" ] || { echo "ERROR: ${RECIPE_DIR} is not a git repo" exit 1 } cd "${RECIPE_DIR}" # Pin an explicit upstream remote (coopcloud) regardless of how `origin` is configured. if git remote | grep -qx upstream; then git remote set-url upstream "${UPSTREAM_URL}" else git remote add upstream "${UPSTREAM_URL}" fi echo "→ Fetching upstream main + tags (${UPSTREAM_URL})..." git fetch --quiet --tags upstream main NEW_MAIN_SHA="$(git rev-parse refs/remotes/upstream/main)" MAIN_TREE="$(git rev-parse "${NEW_MAIN_SHA}^{tree}")" # Ensure the mirror repo exists (reconcile-only: if absent, nothing to do). STATUS="$(curl -s -o /dev/null -w "%{http_code}" "${auth[@]}" "${API}/repos/${NAMESPACE}/${RECIPE}")" if [ "${STATUS}" = "404" ]; then echo "→ Mirror ${NAMESPACE}/${RECIPE} does not exist; nothing to reconcile." exit 0 elif [ "${STATUS}" != "200" ]; then echo "ERROR: unexpected HTTP ${STATUS} checking mirror repo" exit 1 fi if git remote | grep -qx gitea; then git remote set-url gitea "${MIRROR_PUSH}" else git remote add gitea "${MIRROR_PUSH}" fi echo "→ Force-syncing mirror main := upstream main (${NEW_MAIN_SHA:0:8}) + tags..." git push --force gitea "${NEW_MAIN_SHA}:refs/heads/main" git push --force --tags gitea git fetch --quiet gitea '+refs/heads/*:refs/remotes/gitea/*' || true # Close open mirror PRs whose changes are already in upstream main (merged upstream); leave others. echo "→ Reconciling open PRs on ${NAMESPACE}/${RECIPE}..." close_pr() { # curl -s -o /dev/null "${auth[@]}" -H "Content-Type: application/json" -X POST \ "${API}/repos/${NAMESPACE}/${RECIPE}/issues/${1}/comments" \ -d "$(jq -n --arg b "Auto-closed by cc-ci canonical sweep: ${2}" '{body:$b}')" || true curl -s -o /dev/null "${auth[@]}" -H "Content-Type: application/json" -X PATCH \ "${API}/repos/${NAMESPACE}/${RECIPE}/pulls/${1}" -d '{"state":"closed"}' || true echo " ✗ closed PR #${1} — ${2}" } OPEN_PRS="$(curl -s "${auth[@]}" "${API}/repos/${NAMESPACE}/${RECIPE}/pulls?state=open&base=main&limit=50")" while IFS=$'\t' read -r IDX HEAD_REF; do [ -n "${IDX:-}" ] || continue merged=0 if git rev-parse --verify --quiet "refs/remotes/gitea/${HEAD_REF}" >/dev/null; then mt="$(git merge-tree --write-tree "${NEW_MAIN_SHA}" "refs/remotes/gitea/${HEAD_REF}" 2>/dev/null)" || mt="" [ -n "${mt}" ] && [ "${mt}" = "${MAIN_TREE}" ] && merged=1 fi if [ "${merged}" = "1" ]; then close_pr "${IDX}" "its changes are already in upstream main (merged upstream); mirror main re-synced" else echo " • PR #${IDX} (${HEAD_REF}) still open vs upstream main — left as-is" fi done < <(echo "${OPEN_PRS}" | jq -r '.[] | [(.number | tostring), .head.ref] | @tsv' 2>/dev/null || true) echo "✓ mirror-sync done for ${NAMESPACE}/${RECIPE} (main+tags synced to upstream; merged-upstream PRs closed)."