feat(canon): M1.2 release-tag trigger + faithful mirror-sync in the weekly sweep (§2.C/§2.D)
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
- warm_reconcile.sweep_decision(latest_tag, canon_version): pure new-release-tag trigger keyed on version_key (NOT commit) — new tag>canon → run; ==/older → skip no-new-version (even with untagged main commits); no tag → skip never-released. Unit-tested. - scripts/recipe-mirror-sync.sh: faithful mirror sync (adapted from open-recipe-pr.sh --reconcile-only) — explicit coopcloud `upstream` remote (robust to inconsistent clone remotes), syncs main+TAGS, closes merged-upstream PRs, leaves unrelated PRs, bot-token auth. - nightly_sweep rewritten: per enrolled recipe → mirror_sync → fetch → sweep_decision → run_on_tag (checkout the release tag + CCCI_SKIP_FETCH=1 so head IS the tag → tagged-promote gate passes, REF empty → promote allowed). Skips logged; run-twice → skip-all determinism. - smoke-tested recipe-mirror-sync.sh live on custom-html: faithful no-op main/tags push, closed merged-upstream PR #2, left pending PR #5. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
108
scripts/recipe-mirror-sync.sh
Executable file
108
scripts/recipe-mirror-sync.sh
Executable file
@ -0,0 +1,108 @@
|
||||
#!/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/<recipe>. 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/<recipe>) are force-synced onto the
|
||||
# mirror (git.autonomic.zone/recipe-maintainers/<recipe>): 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 <recipe>}"
|
||||
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() { # <index> <reason>
|
||||
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)."
|
||||
Reference in New Issue
Block a user