fix(recipe-upgrade): default to extending an existing open upgrade PR, not a parallel one

When an open upgrade PR already exists for a recipe (branch upgrade-*), push
the new work onto ITS branch and update+re-test that PR — one evolving
upgrade PR per recipe instead of spawning a second parallel PR. Only open a
fresh upgrade-<version> PR when none exists. Unrelated open PRs (e.g. backup
fixes) are still never touched; merged-upstream PRs still close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-06-02 01:54:58 +00:00
parent 35f83a4b74
commit 5f814307ad
2 changed files with 63 additions and 46 deletions

View File

@ -76,9 +76,11 @@ ssh cc-ci "GITEA_USERNAME='$GITEA_USERNAME' GITEA_PASSWORD='$GITEA_PASSWORD' GIT
< /srv/cc-ci/.claude/skills/recipe-upgrade/open-recipe-pr.sh
```
This (a) re-syncs mirror `main` to true upstream main, (b) **closes any open PR already merged
upstream** (merging it would be a no-op), then pushes the upgrade branch and opens the new PR
**alongside** any other still-open PRs — it does **NOT** close superseded/unrelated open PRs; the
operator decides which to merge/close. The new PR's diff is still exactly this upgrade against the
upstream** (merging it would be a no-op), then handles the upgrade PR: **if an open upgrade PR already
exists for this recipe (branch `upgrade-*`), the new work is pushed onto THAT PR's branch and the PR is
updated + re-tested** — one evolving upgrade PR per recipe, not a second parallel one. Only if none
exists is a fresh `upgrade-<version>` PR opened. Unrelated open PRs (e.g. a backup fix) are **NOT**
closed or touched — the operator decides on those. The PR's diff is exactly this upgrade against the
real current upstream main. Capture the `PR_URL`. Optionally export `RECIPE_PR_BODY` first (image-tag
table + the planned operator-action notes). Re-running with the same target version updates the
existing same-branch PR rather than duplicating it.
@ -152,7 +154,8 @@ Always state explicitly that **nothing was merged** — the PR(s) await operator
`/upgrade-all` cron always runs the **default** — it never auto-updates tests.
- **Mirror reflects reality.** Each run force-syncs the `recipe-maintainers/<recipe>` mirror `main` to
true upstream main and closes open PRs already merged upstream. It does NOT close superseded/unrelated
open PRs — a new upgrade PR is opened alongside them and the operator decides which to merge/close.
open PRs. **An existing open upgrade PR is extended (new work pushed onto its branch + re-tested)
rather than spawning a second upgrade PR** — one evolving upgrade PR per recipe.
- **Prefer a recipe-only PR.** Only open a cc-ci test PR (under `--with-tests`) when the upgrade is
correct but a test is genuinely stale — never to paper over a real upgrade regression.
- **Never weaken a test**; **bounded** changes (the upgrade + minimal test update, not rewrites).

View File

@ -19,10 +19,11 @@
# never reflected it.
#
# Default mode (open a PR — preconditions: HEAD has the upgrade commit(s)):
# - Pushes the upgrade branch and opens the new PR ALONGSIDE any other still-open
# PRs for this recipe. It does NOT close superseded/unrelated open PRs — stacking
# a fresh PR on top of the open ones is intentional; the operator decides which to
# merge/close. (Only PRs already merged into upstream main are closed — see above.)
# - If an open UPGRADE PR already exists for this recipe (branch 'upgrade-*'), the new work is
# pushed onto THAT PR's branch and the PR is updated + re-tested — ONE evolving upgrade PR per
# recipe, NOT a second parallel PR. Otherwise a fresh 'upgrade-<version>' branch + PR is opened.
# - Unrelated open PRs (e.g. a backup fix) are NEVER closed or touched — left for the operator.
# - Only PRs already merged into upstream main are closed (no-op merges) — see above.
#
# --reconcile-only: just sync main + close merged-upstream PRs; no push, no new
# PR (used when a recipe is up to date but its mirror may be stale).
@ -79,18 +80,11 @@ echo "→ Force-syncing gitea/main to upstream main (${NEW_MAIN_SHA:0:8})..."
git push --force gitea "${NEW_MAIN_SHA}:refs/heads/main"
git fetch --quiet gitea '+refs/heads/*:refs/remotes/gitea/*' || true
# --- Determine the branch we're about to (re)open, if in open mode ---
BRANCH=""
# --- Preconditions for open mode ---
if [ "${MODE}" != "--reconcile-only" ]; then
DIVERGED=$(git log --oneline origin/main..HEAD 2>/dev/null || true)
[ -n "${DIVERGED}" ] || { echo "ERROR: HEAD has no commits beyond origin/main. Nothing to PR."; exit 1; }
LATEST_MSG=$(git log -1 --pretty=%s HEAD)
if echo "${LATEST_MSG}" | grep -qiE "upgrade to [0-9]"; then
VERSION=$(echo "${LATEST_MSG}" | grep -oiE "upgrade to [0-9][^[:space:]]+" | awk '{print $NF}')
BRANCH="upgrade-${VERSION}"
else
BRANCH="upgrade-$(git rev-parse --short HEAD)"
fi
fi
# --- Reconcile open PRs against the freshly-synced upstream main ---
@ -104,28 +98,27 @@ close_pr() { # <index> <reason>
echo " ✗ closed PR #${idx}${reason}"
}
merged_upstream() { # <head_ref> -> 0 if the branch's tree already equals upstream main (no-op merge)
local ref="$1" mt
git rev-parse --verify --quiet "refs/remotes/gitea/${ref}" >/dev/null || return 1
mt=$(git merge-tree --write-tree "${NEW_MAIN_SHA}" "refs/remotes/gitea/${ref}" 2>/dev/null) || return 1
[ -n "${mt}" ] && [ "${mt}" = "${MAIN_TREE}" ]
}
echo "→ Reconciling open PRs on ${NAMESPACE}/${RECIPE}..."
OPEN_PRS=$(curl -s "${AUTH[@]}" "${API}/repos/${NAMESPACE}/${RECIPE}/pulls?state=open&base=main&limit=50")
# Emit "index<TAB>head_ref" per open PR.
# Close merged-upstream PRs (no-op merges); leave everything else open. Also pick an existing open
# UPGRADE PR (branch 'upgrade-*' — this tool's convention) to EXTEND, if one exists.
EXTEND_PR=""; EXTEND_BRANCH=""
while IFS=$'\t' read -r IDX HEAD_REF; do
[ -n "${IDX:-}" ] || continue
# Same branch we're re-opening? leave it — the force-push below updates it.
if [ -n "${BRANCH}" ] && [ "${HEAD_REF}" = "${BRANCH}" ]; then
echo " • PR #${IDX} is on ${BRANCH} (the branch being updated) — keeping"
if merged_upstream "${HEAD_REF}"; then
close_pr "${IDX}" "its changes are already in upstream main (merged upstream); mirror main re-synced"
continue
fi
# Is this PR already a no-op against upstream main (merged upstream)?
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
# Do NOT auto-close superseded/unrelated open PRs. Open the new PR alongside them and
# let the operator decide which to merge/close. Only merged-upstream PRs are closed above.
echo " • PR #${IDX} (${HEAD_REF}) still open vs upstream main — leaving open (operator decides)"
echo " • PR #${IDX} (${HEAD_REF}) still open vs upstream main — leaving open"
if [ "${MODE}" != "--reconcile-only" ] && [ -z "${EXTEND_PR}" ] && printf '%s' "${HEAD_REF}" | grep -q '^upgrade-'; then
EXTEND_PR="${IDX}"; EXTEND_BRANCH="${HEAD_REF}"
fi
done < <(echo "${OPEN_PRS}" | jq -r '.[] | [(.number | tostring), .head.ref] | @tsv' 2>/dev/null || true)
@ -134,7 +127,19 @@ if [ "${MODE}" = "--reconcile-only" ]; then
exit 0
fi
# --- Push the upgrade branch and open the new PR in place of the closed ones ---
# --- DEFAULT: if an open upgrade PR exists, push the new work onto ITS branch and re-test it, so
# there's ONE evolving upgrade PR per recipe rather than parallel ones. Only if none exists do we
# open a fresh upgrade-<version> PR. Unrelated open PRs are never touched. ---
if [ -n "${EXTEND_PR}" ]; then
BRANCH="${EXTEND_BRANCH}"
echo "→ Extending existing open upgrade PR #${EXTEND_PR} (branch ${BRANCH}) with the new work — it will be re-tested; NOT opening a separate PR."
elif echo "${LATEST_MSG}" | grep -qiE "upgrade to [0-9]"; then
VERSION=$(echo "${LATEST_MSG}" | grep -oiE "upgrade to [0-9][^[:space:]]+" | awk '{print $NF}')
BRANCH="upgrade-${VERSION}"
else
BRANCH="upgrade-$(git rev-parse --short HEAD)"
fi
echo "→ Pushing branch '${BRANCH}'..."
git push --force gitea "HEAD:refs/heads/${BRANCH}"
@ -144,19 +149,28 @@ PR_BODY="${PR_BODY}
Tested green on the cc-ci recipe CI server (full suite, cold, against this PR head). NOT merged — for operator review.
cc @trav @notplants"
PAYLOAD=$(jq -n \
--arg title "${LATEST_MSG}" \
--arg body "${PR_BODY}" \
--arg head "${BRANCH}" \
'{title:$title,body:$body,head:$head,base:"main",reviewers:["trav","notplants"]}')
RESP=$(mktemp)
PS=$(curl -s -o "${RESP}" -w "%{http_code}" "${AUTH[@]}" -H "Content-Type: application/json" -X POST "${API}/repos/${NAMESPACE}/${RECIPE}/pulls" -d "${PAYLOAD}")
if [ "${PS}" = "201" ]; then
echo "PR_URL=$(jq -r '.html_url' "${RESP}")"
elif [ "${PS}" = "409" ] || grep -q "pull request already exists" "${RESP}" 2>/dev/null; then
echo "PR_URL=https://${GITEA_URL}/${NAMESPACE}/${RECIPE}/pulls (branch ${BRANCH} already has a PR — updated by force-push)"
if [ -n "${EXTEND_PR}" ]; then
# The existing PR's branch now points at the new work, so it re-tests on the next !testme.
# Refresh its title + body to match the new work.
curl -s -o /dev/null "${AUTH[@]}" -H "Content-Type: application/json" -X PATCH \
"${API}/repos/${NAMESPACE}/${RECIPE}/pulls/${EXTEND_PR}" \
-d "$(jq -n --arg t "${LATEST_MSG}" --arg b "${PR_BODY}" '{title:$t,body:$b}')" || true
echo "PR_URL=https://${GITEA_URL}/${NAMESPACE}/${RECIPE}/pulls/${EXTEND_PR} (extended existing PR — re-test it)"
else
echo "ERROR: PR creation failed (HTTP ${PS}):"; cat "${RESP}"; rm -f "${RESP}"; exit 1
PAYLOAD=$(jq -n \
--arg title "${LATEST_MSG}" \
--arg body "${PR_BODY}" \
--arg head "${BRANCH}" \
'{title:$title,body:$body,head:$head,base:"main",reviewers:["trav","notplants"]}')
RESP=$(mktemp)
PS=$(curl -s -o "${RESP}" -w "%{http_code}" "${AUTH[@]}" -H "Content-Type: application/json" -X POST "${API}/repos/${NAMESPACE}/${RECIPE}/pulls" -d "${PAYLOAD}")
if [ "${PS}" = "201" ]; then
echo "PR_URL=$(jq -r '.html_url' "${RESP}")"
elif [ "${PS}" = "409" ] || grep -q "pull request already exists" "${RESP}" 2>/dev/null; then
echo "PR_URL=https://${GITEA_URL}/${NAMESPACE}/${RECIPE}/pulls (branch ${BRANCH} already has a PR — updated by force-push)"
else
echo "ERROR: PR creation failed (HTTP ${PS}):"; cat "${RESP}"; rm -f "${RESP}"; exit 1
fi
rm -f "${RESP}"
fi
rm -f "${RESP}"