Compare commits

..
3 changed files with 40 additions and 39 deletions
+10 -6
View File
@@ -264,11 +264,13 @@ ssh cc-ci "GITEA_USERNAME='$GITEA_USERNAME' GITEA_PASSWORD='$GITEA_PASSWORD' GIT
``` ```
This (a) re-syncs mirror `main` to true upstream main, (b) **closes any open PR already merged 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 handles the upgrade PR: **if an open upgrade PR already 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 exists for this recipe (branch `upgrade-*`), its branch is replaced with one commit directly on the
updated + re-tested** — one evolving upgrade PR per recipe, not a second parallel one. Only if none current upstream tip using `--force-with-lease`, then the PR is updated + re-tested** — one evolving
upgrade PR per recipe with one commit, not a second parallel PR. Only if none
exists is a fresh `upgrade-<version>` PR opened. Unrelated open PRs (e.g. a backup fix) are **NOT** 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 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`. Export `RECIPE_PR_BODY` first (image-tag table + the real current upstream main. This squash happens **before `!testme`**, so CI tests the one-commit PR
head. Capture the `PR_URL`. Export `RECIPE_PR_BODY` first (image-tag table + the
planned operator-action notes) — and **the PR body MUST link the upstream release notes**: for each planned operator-action notes) — and **the PR body MUST link the upstream release notes**: for each
upgraded image/service, pull its release-notes URL from the registry `cc-ci-plan/upstream/<recipe>.md` upgraded image/service, pull its release-notes URL from the registry `cc-ci-plan/upstream/<recipe>.md`
(between the current → new version) and include one explicit line per service in the body, e.g. (between the current → new version) and include one explicit line per service in the body, e.g.
@@ -297,7 +299,9 @@ POST=0 /srv/cc-ci/.claude/skills/recipe-upgrade/testme-on-pr.sh <recipe> <pr-ind
Read the failing run (linked from the PR / the harness log on cc-ci) and classify: Read the failing run (linked from the PR / the harness log on cc-ci) and classify:
**5a. The UPGRADE itself is broken** (real regression: bad tag, missing migration, config gap) — fix **5a. The UPGRADE itself is broken** (real regression: bad tag, missing migration, config gap) — fix
it on the recipe branch, push, and re-run `!testme` (within the 3-run budget). The recipe PR is only it on the local recipe branch, then re-run `open-recipe-pr.sh` from step 3 to squash the corrected
tree into one commit on the PR branch **before** re-running `!testme` (within the 3-run budget).
Do not push a fix commit directly to the PR branch. The recipe PR is only
"working" once `!testme` is GREEN. If still red after 3 runs, leave the PR open (its red `!testme` "working" once `!testme` is GREEN. If still red after 3 runs, leave the PR open (its red `!testme`
results stand as the evidence) and report `FAILED — upgrade not green after 3 !testme runs`. results stand as the evidence) and report `FAILED — upgrade not green after 3 !testme runs`.
@@ -359,8 +363,8 @@ Always state explicitly that **nothing was merged** — the PR(s) await operator
— never touch the gate. (Interim safeguard while the upgrader runs on the cc-ci host; see `IDEAS.md`.) — never touch the gate. (Interim safeguard while the upgrader runs on the cc-ci host; see `IDEAS.md`.)
- **Mirror reflects reality.** Each run force-syncs the `recipe-maintainers/<recipe>` mirror `main` to - **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 true upstream main and closes open PRs already merged upstream. It does NOT close superseded/unrelated
open PRs. **An existing open upgrade PR is extended (new work pushed onto its branch + re-tested) open PRs. **An existing open upgrade PR is rewritten with one commit on the current upstream tip
rather than spawning a second upgrade PR** — one evolving upgrade PR per recipe. using `--force-with-lease`, then re-tested** — 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 - **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. 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). - **Never weaken a test**; **bounded** changes (the upgrade + minimal test update, not rewrites).
+28 -32
View File
@@ -20,11 +20,9 @@
# never reflected it. # never reflected it.
# #
# Default mode (open a PR — preconditions: HEAD has the upgrade commit(s)): # Default mode (open a PR — preconditions: HEAD has the upgrade commit(s)):
# - If an open UPGRADE PR already exists for this recipe (branch 'upgrade-*'), the new work is added # - Publish the final recipe tree as ONE commit directly on current upstream main before !testme.
# as a commit ON TOP of THAT PR's branch (a fast-forward — NO force-push) and the PR is updated + # If an open upgrade PR exists, replace its branch with this single commit using
# re-tested — ONE evolving upgrade PR per recipe, NOT a second parallel PR. Otherwise a fresh # --force-with-lease, then re-test the same PR. Otherwise open a fresh upgrade PR.
# 'upgrade-<version>' branch + PR is opened. (The only force-push here is the mirror-main sync
# above, which intentionally mirrors upstream main; PR branches are never force-pushed.)
# - Unrelated open PRs (e.g. a backup fix) are NEVER closed or touched — left for the operator. # - 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. # - Only PRs already merged into upstream main are closed (no-op merges) — see above.
# #
@@ -165,9 +163,8 @@ if [ "${MODE}" = "--reconcile-only" ]; then
exit 0 exit 0
fi fi
# --- DEFAULT: if an open upgrade PR exists, push the new work onto ITS branch and re-test it, so # --- DEFAULT: publish one commit with the final tree on current upstream main. This collapses
# there's ONE evolving upgrade PR per recipe rather than parallel ones. Only if none exists do we # old local commits and merge commits while preserving the exact content to be tested. ---
# open a fresh upgrade-<version> PR. Unrelated open PRs are never touched. ---
if [ -n "${EXTEND_PR}" ]; then if [ -n "${EXTEND_PR}" ]; then
BRANCH="${EXTEND_BRANCH}" 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." echo "→ Extending existing open upgrade PR #${EXTEND_PR} (branch ${BRANCH}) with the new work — it will be re-tested; NOT opening a separate PR."
@@ -178,39 +175,38 @@ else
BRANCH="upgrade-$(git rev-parse --short HEAD)" BRANCH="upgrade-$(git rev-parse --short HEAD)"
fi fi
# Land the work on the PR branch WITHOUT force-pushing. If the branch already exists (extend case, or # Do not create an empty PR when the proposed tree equals current upstream main.
# a stale same-named branch), add a commit ON TOP of its tip — a fast-forward, so the PR's history is UPSTREAM_TREE=$(git rev-parse "${NEW_MAIN_SHA}^{tree}")
# preserved and it re-tests. Otherwise push a fresh branch. WORK_TREE=$(git rev-parse 'HEAD^{tree}')
[ "${WORK_TREE}" != "${UPSTREAM_TREE}" ] || {
echo "ERROR: proposed recipe tree is identical to upstream main; nothing to PR."
exit 1
}
# The PR branch is a single commit on the current upstream tip. Reusing an existing PR requires a
# guarded rewrite; the lease rejects concurrent updates instead of overwriting another writer.
SQUASHED=$(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 "${WORK_TREE}" -p "${NEW_MAIN_SHA}" -m "${LATEST_MSG}")
if git rev-parse --verify --quiet "refs/remotes/gitea/${BRANCH}" >/dev/null; then if git rev-parse --verify --quiet "refs/remotes/gitea/${BRANCH}" >/dev/null; then
EXIST_TIP=$(git rev-parse "refs/remotes/gitea/${BRANCH}") EXIST_TIP=$(git rev-parse "refs/remotes/gitea/${BRANCH}")
if [ "$(git rev-parse 'HEAD^{tree}')" = "$(git rev-parse "${EXIST_TIP}^{tree}")" ]; then if [ "$(git rev-parse "${EXIST_TIP}^{tree}")" = "${WORK_TREE}" ] &&
echo "→ '${BRANCH}' already has this exact tree — nothing new to push (will still re-test)." [ "$(git rev-parse "${EXIST_TIP}^" 2>/dev/null || true)" = "${NEW_MAIN_SHA}" ]; then
echo "→ '${BRANCH}' already has this one-commit diff — nothing new to push (will still re-test)."
else else
# Parent the new commit on the branch tip AND (when the branch predates it) on upstream main, so echo "→ Replacing '${BRANCH}' with one commit on upstream ${NEW_MAIN_SHA:0:8} (force-with-lease)..."
# the recorded HISTORY matches the tree we are pushing. Without the second parent the merge-base git push --force-with-lease="refs/heads/${BRANCH}:${EXIST_TIP}" gitea \
# stays stale: git would later treat upstream's post-branch changes as "removed by this PR" and a "${SQUASHED}:refs/heads/${BRANCH}"
# 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}" "${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 fi
else else
echo "→ Pushing new branch '${BRANCH}'..." echo "→ Pushing one-commit branch '${BRANCH}' on upstream ${NEW_MAIN_SHA:0:8}..."
git push gitea "HEAD:refs/heads/${BRANCH}" git push gitea "${SQUASHED}:refs/heads/${BRANCH}"
fi fi
PR_BODY="${RECIPE_PR_BODY:-$(printf 'Recipe upgrade.\n\nCommits on top of upstream main:\n\n%s\n' "$(git log origin/main..HEAD --pretty='- %h %s')")}" PR_BODY="${RECIPE_PR_BODY:-$(printf 'Recipe upgrade as one commit on upstream %s.\n\n%s\n' "${NEW_MAIN_SHA:0:8}" "$(git diff --stat "${NEW_MAIN_SHA}" "${SQUASHED}")")}"
PR_BODY="${PR_BODY} 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. CI verification is run against this one-commit PR head. NOT merged — for operator review.
cc @trav @notplants" cc @trav @notplants"
+2 -1
View File
@@ -197,7 +197,8 @@ description `"Upgrade <recipe> on cc-ci"`) with a prompt like:
> Run the `/recipe-upgrade <recipe>` skill end-to-end — **DEFAULT mode, do NOT pass `--with-tests`** > Run the `/recipe-upgrade <recipe>` skill end-to-end — **DEFAULT mode, do NOT pass `--with-tests`**
> (`/srv/cc-ci/.claude/skills/recipe-upgrade/SKILL.md`): plan, implement, **directly deploy + inspect > (`/srv/cc-ci/.claude/skills/recipe-upgrade/SKILL.md`): plan, implement, **directly deploy + inspect
> the WIP on cc-ci for live feedback (step 2b, `--chaos`, tear it down after)**, open a recipe PR, and > the WIP on cc-ci for live feedback (step 2b, `--chaos`, tear it down after)**, open a recipe PR as
> **one commit based on current upstream main**, and
> verify it by posting `!testme` on the PR (results visible in the PR; iterate ≤3×). Use the **existing > verify it by posting `!testme` on the PR (results visible in the PR; iterate ≤3×). Use the **existing
> tests** — if a test fails because it is genuinely stale, **leave an explanatory comment on the PR** > tests** — if a test fails because it is genuinely stale, **leave an explanatory comment on the PR**
> for the operator and do NOT modify any test. Drive cc-ci over `ssh cc-ci`. Do NOT prompt. Do NOT push > for the operator and do NOT modify any test. Drive cc-ci over `ssh cc-ci`. Do NOT prompt. Do NOT push