diff --git a/.claude/skills/recipe-upgrade/open-recipe-pr.sh b/.claude/skills/recipe-upgrade/open-recipe-pr.sh index ab86dfa..e214573 100755 --- a/.claude/skills/recipe-upgrade/open-recipe-pr.sh +++ b/.claude/skills/recipe-upgrade/open-recipe-pr.sh @@ -102,6 +102,27 @@ 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) + + # --- Keep the LOCAL work current with the freshly-synced upstream main (anti-drift) --- + # The push path below grafts `HEAD^{tree}` WHOLESALE onto the PR branch. If this checkout is not + # based on the upstream main we just synced, every upstream change made since the branch was cut + # is silently ABSENT from the pushed tree — the PR (and the CI that verifies it) then describes a + # tree that will never deploy. Observed on gitea PR #5 (2026-08-10): its base predated upstream's + # "BREAKING CHANGE: remove forgejo", so `!testme` verified a forgejo-bearing tree while main had + # dropped it. Merge upstream in FIRST, and fail loudly rather than paper over a conflict. + if ! git merge-base --is-ancestor "${NEW_MAIN_SHA}" HEAD; then + echo "→ Local work predates upstream main (${NEW_MAIN_SHA:0:8}) — merging upstream in first..." + if ! GIT_AUTHOR_NAME="${GITEA_USERNAME}" GIT_AUTHOR_EMAIL="${GITEA_USERNAME}@git.autonomic.zone" \ + GIT_COMMITTER_NAME="${GITEA_USERNAME}" GIT_COMMITTER_EMAIL="${GITEA_USERNAME}@git.autonomic.zone" \ + git merge --no-edit "${NEW_MAIN_SHA}" >/dev/null 2>&1; then + git merge --abort 2>/dev/null || true + echo "ERROR: cannot auto-merge upstream main (${NEW_MAIN_SHA:0:8}) into the local ${RECIPE} work." + echo " Upstream changed files this upgrade also touches. Resolve by hand in" + echo " ${RECIPE_DIR}, then re-run. Refusing to push a tree that omits upstream changes." + exit 1 + fi + echo " ✓ upstream merged into the local work" + fi fi # --- Reconcile open PRs against the freshly-synced upstream main --- @@ -165,9 +186,19 @@ if git rev-parse --verify --quiet "refs/remotes/gitea/${BRANCH}" >/dev/null; the if [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse "${EXIST_TIP}^{tree}")" ]; then echo "→ '${BRANCH}' already has this exact tree — nothing new to push (will still re-test)." else + # Parent the new commit on the branch tip AND (when the branch predates it) on upstream main, so + # the recorded HISTORY matches the tree we are pushing. Without the second parent the merge-base + # stays stale: git would later treat upstream's post-branch changes as "removed by this PR" and a + # merge could revert them (the gitea #5 / forgejo-removal drift, 2026-08-10). No force-push: this + # is still a fast-forward from the branch tip. + EXTRA_PARENT=() + if ! git merge-base --is-ancestor "${NEW_MAIN_SHA}" "${EXIST_TIP}"; then + EXTRA_PARENT=(-p "${NEW_MAIN_SHA}") + echo " (also parenting on upstream main ${NEW_MAIN_SHA:0:8} — branch predated it)" + fi ONTOP=$(GIT_AUTHOR_NAME="${GITEA_USERNAME}" GIT_AUTHOR_EMAIL="${GITEA_USERNAME}@git.autonomic.zone" \ GIT_COMMITTER_NAME="${GITEA_USERNAME}" GIT_COMMITTER_EMAIL="${GITEA_USERNAME}@git.autonomic.zone" \ - git commit-tree "$(git rev-parse 'HEAD^{tree}')" -p "${EXIST_TIP}" -m "${LATEST_MSG}") + git commit-tree "$(git rev-parse 'HEAD^{tree}')" -p "${EXIST_TIP}" "${EXTRA_PARENT[@]}" -m "${LATEST_MSG}") echo "→ Adding the new work on top of '${BRANCH}' (fast-forward, no force-push)..." git push gitea "${ONTOP}:refs/heads/${BRANCH}" fi