/recipe-upgrade --with-tests, /ci-test-review and /cc-ci-tests-update all author test changes, and all three had only 'never weaken a test' as guidance. That did not stop the plausible failure: the fixture INSERTed rows into the app's database, which was correct for v2 and silently wrong for v3, where a site must belong to a team. Events were acked 202 and discarded; the recipe sat RED for six weeks. The rule that would have prevented it — set state up through the app's own interface, not its database — now lives in tests/STYLE.md in the cc-ci repo, and each of the three paths is told to read it before editing a test.
371 lines
29 KiB
Markdown
371 lines
29 KiB
Markdown
---
|
||
name: recipe-upgrade
|
||
description: Upgrade ONE Co-op Cloud recipe end-to-end and verify it on the cc-ci CI server. Researches available upstream upgrades — fetching each image's release notes via a persisted per-recipe registry (cc-ci-plan/upstream/<recipe>.md) + the recipe README — plans them (breaking changes, migrations, config), implements the bump (image tags + recipe version label + config), opens a recipe PR, and verifies it by posting `!testme` on the PR (real CI; results visible in the PR; iterates up to 3×). DEFAULT: recipe PR only, using existing tests — if a test fails because it is genuinely stale, it leaves an explanatory COMMENT on the PR for the operator (does NOT touch tests). With `--with-tests`: also opens + verifies a PR to update the stale cc-ci test. NEVER merges. The per-recipe worker behind /upgrade-all. Invoke as /recipe-upgrade <recipe> [--with-tests].
|
||
---
|
||
|
||
# recipe-upgrade
|
||
|
||
Autonomous, end-to-end upgrade of **one** recipe, **gated by the cc-ci CI server**. This is the
|
||
cc-ci analogue of recipe-maintainer's `/recipe-upgrade-full`, but the test gate is **cc-ci** (the real
|
||
recipe CI), and it inherits `ci-test-review`'s discipline: classify recipe-vs-CI, create + **verify**
|
||
fix PRs, **never merge**, never weaken a test.
|
||
|
||
It drives cc-ci over `ssh cc-ci` (cc-ci has abra, the recipe checkouts at `~/.abra/recipes/`, the
|
||
harness, and the bot token to push mirror branches). The Gitea PR API call uses the orchestrator's
|
||
`/srv/cc-ci/.testenv` creds. Plans/reports go to `/srv/cc-ci/.cc-ci-logs/upgrades/`.
|
||
|
||
**Boundaries (same as ci-test-review):** the test execution + PR verification are deterministic (the
|
||
cc-ci harness decides pass/fail). **Create + verify, NEVER merge** (operator merges). **Never weaken a
|
||
test** to make a PR green.
|
||
|
||
## Arguments
|
||
- `<recipe>` — the recipe to upgrade (required).
|
||
- `--with-tests` (optional) — **opt in to updating cc-ci tests.** Without it (the DEFAULT, and what the
|
||
weekly `/upgrade-all` cron uses), this skill **only ever opens a recipe PR using the existing tests**;
|
||
if a test fails and is diagnosed as genuinely stale, it **leaves a comment** on the recipe PR
|
||
explaining why and stops — it does **not** modify any test. With `--with-tests`, a stale-test failure
|
||
additionally gets a **verified cc-ci test-update PR** (step 5b). A real upgrade regression is fixed +
|
||
re-tested in **both** modes.
|
||
|
||
## Procedure
|
||
|
||
> ### ⚠️ Run abra over a pseudo-TTY (or it FATAs `inappropriate ioctl for device`)
|
||
> `abra` needs a TTY. Over plain `ssh cc-ci 'abra …'` there is none, so many abra commands die with
|
||
> **`FATA inappropriate ioctl for device`** / `the input device is not a TTY`. This is how the working
|
||
> harness avoids it (`runner/harness/abra.py`): wrap every abra call in a pseudo-TTY via util-linux
|
||
> `script`, and pass `-n` (`--no-input`) so it never prompts. **Always invoke abra like this:**
|
||
> ```
|
||
> ssh cc-ci 'script -qec "abra <args> -n" /dev/null'
|
||
> ```
|
||
> (`git`/other commands do NOT need the wrapper — only `abra`.) Do this for every abra command below.
|
||
|
||
### 1. Plan the upgrade (research — follow recipe-upgrade-plan methodology)
|
||
|
||
> ### ⚠️ Two gotchas that silently skip recipes — handle BOTH before the version check
|
||
> 1. **go-git auth to the private mirror.** Recipes hosted on `git.autonomic.zone` (custom-html(-tiny),
|
||
> discourse, ghost, immich, lasuite-docs, plausible, uptime-kuma) fail with
|
||
> **`FATA unable to fetch tags … authentication required: Unauthorized`** because abra's embedded
|
||
> go-git reads `remote.origin.url` from `.git/config` **literally** and **ignores `git url.insteadOf`
|
||
> / credential helpers**. Fix: bake the bot creds into the origin URL first (idempotent):
|
||
> `git -C ~/.abra/recipes/<r> remote set-url origin "https://$GITEA_USERNAME:$GITEA_PASSWORD@git.autonomic.zone/recipe-maintainers/<r>.git"` (only when origin is on git.autonomic.zone).
|
||
> 2. **Dirty worktree** is usually just the untracked cc-ci overlay (`compose.ccci.yml`) — don't skip on
|
||
> that. `git stash -u` before the checks and `stash pop` after.
|
||
|
||
**Sync the mirror from TRUE upstream FIRST, then check for upgrades against that** — so the check sees
|
||
the real current recipe, not a stale mirror.
|
||
|
||
- **(a) Reconcile the mirror from coopcloud (always — do this BEFORE the check).** Run:
|
||
```
|
||
set -a; . /srv/cc-ci/.testenv; set +a
|
||
ssh cc-ci "GITEA_USERNAME='$GITEA_USERNAME' GITEA_PASSWORD='$GITEA_PASSWORD' GITEA_URL='$GITEA_URL' bash -s <recipe> --reconcile-only" \
|
||
< /srv/cc-ci/.claude/skills/recipe-upgrade/open-recipe-pr.sh
|
||
```
|
||
This fetches **true upstream from coopcloud** (`git.coopcloud.tech/coop-cloud/<recipe>`, default branch
|
||
main OR master — every recipe has a coopcloud correspondent and none are forked) and force-syncs the
|
||
`recipe-maintainers/<recipe>` mirror's `main` + tags to be **identical to it**. (The checkout's
|
||
`origin` is the mirror, so the reconcile fetches coopcloud *explicitly* via an `upstream` remote — it
|
||
does NOT sync the mirror to itself.) It also **closes any open mirror PR already in upstream main**.
|
||
|
||
- **(b) Refresh the local checkout + check for upgrades** (creds + stash per the box; each abra call
|
||
pty-wrapped) — the checkout pulls from the now-coopcloud-synced mirror:
|
||
```
|
||
ssh cc-ci 'export PATH=/run/current-system/sw/bin:$PATH; R=<recipe>; \
|
||
url=$(git -C ~/.abra/recipes/$R config --get remote.origin.url); \
|
||
case "$url" in https://git.autonomic.zone/*) \
|
||
git -C ~/.abra/recipes/$R remote set-url origin "https://'"$GITEA_USERNAME"':'"$GITEA_PASSWORD"'@${url#https://}";; esac; \
|
||
git -C ~/.abra/recipes/$R stash -u >/dev/null 2>&1 || true; \
|
||
script -qec "abra recipe fetch $R --force -n" /dev/null; \
|
||
git -C ~/.abra/recipes/$R fetch origin main; \
|
||
script -qec "abra recipe versions $R -m -n" /dev/null; \
|
||
script -qec "abra recipe upgrade $R -m -n" /dev/null; \
|
||
git -C ~/.abra/recipes/$R stash pop >/dev/null 2>&1 || true'
|
||
```
|
||
Skip a recipe as `SKIPPED — dirty-worktree` **only if it is still dirty with TRACKED changes after the
|
||
stash** (real local edits), not for the untracked overlay.
|
||
- **abra says no upgrades → trust abra for every image whose tag it CAN read; only cross-check upstream
|
||
for images whose tag format abra CAN'T parse.** `abra recipe upgrade` is the **first and trusted**
|
||
approach: for a normally-tagged image (a plain semver/calver tag abra can evaluate) where abra finds no
|
||
newer version, that service genuinely **is** up-to-date — do **not** second-guess it with an upstream
|
||
check. The blind spot is **only** images with a tag format abra cannot handle, where it silently
|
||
contributes **no** candidate:
|
||
- **tag+digest pins** (`image:tag@sha256:…`) — abra FATAs `Docker references with both a tag and digest
|
||
are currently not supported` (this is exactly how **immich** kept getting skipped);
|
||
- a **digest-only pin** (`image@sha256:…`, no tag) — nothing for abra to compare.
|
||
So **before** writing `SKIPPED — up-to-date`, do a **direct upstream check ONLY for those abra-unparseable
|
||
images** (not for images abra evaluated fine):
|
||
1. Read `cc-ci-plan/upstream/<recipe>.md` (per-image releases/changelog URLs; discover + write it per
|
||
the registry bullet below if missing).
|
||
2. For each such image, list the **upstream registry tags** and pick the newest tag the app version
|
||
supports (the §2 "list the registry tags, pick the newest one the app supports — don't blindly take
|
||
the max" method), and compare it to the **currently pinned** tag.
|
||
3. If any has a newer supported upstream tag than what's pinned → there **is** an upgrade: proceed to
|
||
plan/implement it (per §2, preserving any digest pin).
|
||
- **Conclude `SKIPPED — up-to-date` only when** abra found no bump for the images it could read **and**
|
||
the direct check finds nothing newer for the abra-unparseable images. If every image had a normal tag
|
||
abra could read, abra's verdict stands alone — no upstream check needed. A `tag and digest … not
|
||
supported` FATA is a **trigger for the upstream check on that image, never a skip on its own.**
|
||
- Check `git log HEAD..origin/main` and upstream PRs (`git.coopcloud.tech/coop-cloud/<recipe>/pulls`)
|
||
— if someone already started the bump, **re-plan from the tip of `origin/main`**, not from scratch.
|
||
- **Release-notes registry (discover once, reuse — parity with recipe-maintainer's `upstream.md`).**
|
||
Read `cc-ci-plan/upstream/<recipe>.md` — a per-image map of source repo + releases/changelog URL. If
|
||
it's missing, or an image in `compose.yml` isn't covered (new/renamed service), discover the upstream
|
||
project + releases page for each uncovered image (WebSearch the image → its GitHub/source repo →
|
||
releases/changelog), **write/update** `cc-ci-plan/upstream/<recipe>.md` and commit it
|
||
(`git -C /srv/cc-ci-orch add cc-ci-plan/upstream/<recipe>.md && git -C /srv/cc-ci-orch commit -m "upstream(<recipe>): release-notes sources" && git -C /srv/cc-ci-orch push`).
|
||
Format + how it's used: `cc-ci-plan/upstream/README.md`.
|
||
- **For each service with an upgrade, fetch the release notes from the registry URLs** (WebFetch)
|
||
between current → target versions, and call out **breaking changes / required migrations / new-or-
|
||
renamed config / dependency version requirements** in an "Operator Action Required" section.
|
||
- **Read the recipe's README** (`~/.abra/recipes/<recipe>/README.md`, if present) for upgrade-specific
|
||
instructions, migration steps, or breaking-change notes shipped with the recipe itself.
|
||
- Write the plan to `/srv/cc-ci/.cc-ci-logs/upgrades/<recipe>-upgrade-<YYYY-MM-DD>.md`: goal, image
|
||
tag table (service / current → new), recipe version bump (+ semver reasoning), required compose/
|
||
config changes, risks. (No human review gate — proceed straight to implement.)
|
||
|
||
### 2. Implement the upgrade (follow recipe-upgrade-apply methodology, on cc-ci)
|
||
On cc-ci's `~/.abra/recipes/<recipe>` (wrap every abra call per the pseudo-TTY box above —
|
||
`ssh cc-ci 'script -qec "abra … -n" /dev/null'`):
|
||
- `script -qec "abra recipe upgrade <recipe> -n" /dev/null` for the tags it can bump; hand-edit
|
||
`compose.yml` for any tags it won't, plus the config/env/volume/label changes the plan calls for.
|
||
- **tag+digest pins (`image:tag@sha256:…`) — abra can't bump these (it FATAs `tag and digest … not
|
||
supported`), so do it by hand, and KEEP the digest pin:** decide the new tag from the direct
|
||
upstream-tag check (see `/upgrade-all` box item 4 — list the registry tags, pick the newest one the
|
||
app version supports; do not blindly take the max). Then **re-resolve the digest for the new tag** and
|
||
re-append it so the pin style is preserved (a digest is a reproducibility/security pin — never silently
|
||
drop it): `docker buildx imagetools inspect <image>:<newtag> --format '{{.Manifest.Digest}}'` (or
|
||
`docker manifest inspect`), then write `image: <image>:<newtag>@<newdigest>`. If the app and its
|
||
digest-pinned sidecars must move together (e.g. immich-server ↔ its `postgres:…-vectorchord…` DB),
|
||
bump them as a matched, compatibility-checked set per the plan.
|
||
- **Do NOT bump the `coop-cloud.${STACK_NAME}.version` label in the PR — record the recommended release
|
||
bump instead.** The version bump + tag + publish are all done at the very end by a single real
|
||
`abra recipe release` (run by the operator after the upstream PR merges — see recipe-maintainer's
|
||
`/recipe-upstream`); the upgrade PR only carries the image-tag + config changes. So **leave the version
|
||
label untouched** and just **decide the semver bump** from the upgrade's nature per the plan
|
||
(breaking→`-x` major, new feature→`-y` minor, patch/security→`-z` patch). Put the recommended release
|
||
command — `abra recipe release <recipe> -<x|y|z>` (NO `--dry-run`) — in the PR body (§3) so the operator
|
||
can run it verbatim to publish. At release time `abra recipe release` computes the final `a.b.c+x.y.z`
|
||
from the current label + the flag + the app image tag and syncs the label then.
|
||
⚠️ **Never run `abra recipe release` in this skill** (with or without `--dry-run`) — a real release
|
||
PUBLISHES (pushes the new version to the recipe's git origin) and we NEVER push the recipe's
|
||
upstream/`main`; we open a PR from a branch on the mirror. Publishing is the operator's final step.
|
||
(`abra recipe upgrade <recipe> -n` for the image-tag bumps above is unchanged.)
|
||
- `script -qec "abra recipe lint <recipe> -C -n" /dev/null` — fix obvious lint errors; if unfixable,
|
||
record it and continue.
|
||
- Commit on a branch describing the image-tag/config changes, e.g.
|
||
`git commit -m "chore: upgrade <service> to <new-tag>"`. Do **not** mention a recipe version (none is
|
||
bumped here) and do **not** create/push a tag — the branch name falls back to `upgrade-<short-sha>` (see
|
||
`open-recipe-pr.sh`). Do **not** push to upstream; the version bump + tag + publish are the operator's
|
||
final `abra recipe release` step.
|
||
|
||
### 2a. Advisory scan (deterministic; ADDITIVE — run it, never skip it)
|
||
|
||
Run the deterministic scanner for the exact upgrade window and **paste its markdown block verbatim
|
||
into the per-recipe log**:
|
||
|
||
```
|
||
python3 /srv/cc-ci/cc-ci-plan/advisory-scan.py <recipe> --from <old-app-version> --to <new-app-version> \
|
||
[--image <name>=<old>:<new>]...
|
||
```
|
||
|
||
**Pass an `--image` for EVERY sidecar you upgraded** (redis, postgres, nginx …), not just the app —
|
||
each image is judged by its own versions, and an image you don't name is not counted at all. Repeat
|
||
the flag for each one and pass them **all in a single call** (the count is a union across images).
|
||
e.g. discourse moving app 3.5.3→2026.7.1 *and* redis 7.4→8.10:
|
||
|
||
```
|
||
... --from 3.5.3 --to 2026.7.1 --image redis=7.4:8.10
|
||
```
|
||
|
||
→ 140 CVEs (123 app + 17 redis), where the redis seventeen include a **critical** (CVE-2025-49844)
|
||
that is invisible if the sidecar is left out. `<name>` is substring-matched against source repo names,
|
||
so make it specific enough to hit exactly one.
|
||
|
||
**If the scan reports advisories it could NOT judge, re-run it with `--adjudicate`.** That is a second
|
||
pass: it collects each open case's full evidence (advisory prose, references, affected ranges, every
|
||
release naming the CVE) and asks YOU to decide FIXED / NOT-FIXED / STILL-UNKNOWN with a reason citing
|
||
that evidence. The deterministic number is a **floor** — add every FIXED to the count. Say
|
||
STILL-UNKNOWN rather than inferring from memory, and never record an undecided CVE as unaffected.
|
||
It also shows what pass 1 already decided; if a verdict looks wrong given its evidence, say so.
|
||
|
||
It queries, per recipe: the **GitHub Security Advisories API** for every source repo in
|
||
`cc-ci-plan/upstream/<recipe>.md` (CVE + GHSA + severity + vulnerable/patched ranges, so
|
||
"fixed by THIS upgrade" is computed, not guessed), every **vendor release/security URL** in that
|
||
registry (fetched + regex-scanned for CVE ids), and **OSV** where a package mapping exists.
|
||
|
||
**This does NOT replace your own release-note reading — it is an ADDITIONAL evidence source.** Do
|
||
exactly what you did before, then union the two: the CVE count you report is the union of the CVEs
|
||
you found in the notes and the CVEs the scan found. Never let the scan lower a count you established
|
||
by reading.
|
||
|
||
Why it exists: gitea 1.27.1 fixed CVE-2026-60004 and CVE-2026-59774 (both CVSS 9.8). Both are named
|
||
only in the vendor's blog security section — the GitHub *release notes* mention neither — so the
|
||
release-note read found one unrelated minor item and the weekly report printed a CVE count of "1",
|
||
then "none". Advisory databases lagged too (OSV 404'd on both; NVD's API had neither by CPE, id, or
|
||
keyword), which is why the GitHub advisory API and the vendor pages lead.
|
||
|
||
If the scanner reports **failed sources**, say so in the log — an incomplete scan must not read as
|
||
a clean one. If a vendor publishes security notes at a URL the registry lacks (gitea's
|
||
`blog.gitea.com`), **add it to `cc-ci-plan/upstream/<recipe>.md`** so the next scan sees it.
|
||
|
||
### 2b. Direct deploy + inspect on cc-ci — live feedback BEFORE CI (recipe-maintainer style)
|
||
Before opening the PR / running `!testme`, deploy the WIP recipe **directly** on the cc-ci server and
|
||
watch it converge — the way recipe-maintainer tests on `cctest`. This gives you **live logs +
|
||
container state** to actually SEE what the upgrade does (a failed migration, a crash-loop, a config
|
||
gap), instead of only a pass/fail CI card. It is **ADDITIONAL to — not a replacement for** — the
|
||
`!testme` verification + its 3-attempt budget (steps 4–5); it just front-loads the diagnosis so you
|
||
arrive at CI already knowing the upgrade converges, and waste fewer of those 3 runs on basics.
|
||
|
||
Deploy the recipe head you just edited with **`--chaos`** (deploys the local WIP without a clean
|
||
published commit), under a dedicated `dev-<recipe>` domain on the local swarm (`--server default`).
|
||
Apply the usual gotchas (pseudo-TTY wrap every abra call; creds baked into the mirror origin; stash
|
||
the untracked overlay). `abra app new` generates the `.env` + secrets:
|
||
```
|
||
ssh cc-ci 'export PATH=/run/current-system/sw/bin:$PATH; set -a; . /srv/cc-ci/.testenv; set +a; \
|
||
R=<recipe>; D=dev-$R.ci.commoninternet.net; \
|
||
script -qec "abra app new $R --server default --domain $D -n" /dev/null 2>&1 | tail -3; \
|
||
script -qec "abra app deploy $D --chaos -n" /dev/null 2>&1 | tail -20'
|
||
```
|
||
- **Inspect logs directly** — prefer `docker service logs` over ssh (more reliable than `abra app
|
||
logs` non-interactively, per recipe-maintainer `references/recipe-maintainer/learnings.md`):
|
||
`ssh cc-ci 'docker stack services ${D//./_}; docker service logs --tail 200 ${D//./_}_app'`
|
||
(swap `_app` for the failing service; pseudo-TTY wrap any `abra app logs`/`abra app cmd`).
|
||
- **Iterate:** edit the recipe → `abra app deploy $D --chaos --force` to cycle → re-read logs, until it
|
||
converges, serves, and any migration/config behaves. This is where you debug the real upgrade with
|
||
full visibility — fold what you learn back into the recipe edit (step 2).
|
||
- **Tear down — MANDATORY, on every exit path** (success, RED, or abort — shared swarm): the dev
|
||
deploy is yours and you MUST remove it before you finish this recipe / move to the next one. Treat
|
||
it as a `finally`:
|
||
```
|
||
ssh cc-ci 'export PATH=/run/current-system/sw/bin:$PATH; D=dev-<recipe>.ci.commoninternet.net; \
|
||
script -qec "abra app undeploy $D -n" /dev/null 2>&1 | tail -2; \
|
||
script -qec "abra app rm $D -n --no-input" /dev/null 2>&1 | tail -2'
|
||
```
|
||
Then **verify nothing leaked**: `ssh cc-ci 'docker stack ls --format "{{.Name}}" | grep -c "^dev-<recipe with . as _>"'`
|
||
should print `0` (and no `dev-<recipe>_*` volumes remain). If the recipe failed, tear the dev deploy
|
||
down anyway, THEN report the failure — never leave it running.
|
||
Backstop (defence-in-depth, NOT a substitute for the explicit teardown above): `/upgrade-all` runs
|
||
the `dev-*` reaper (`reap-dev-deploys.sh`) + the orphan-sweep at the START and END of every run, so a
|
||
crashed/abandoned loop's `dev-` deploy is cleared by the next run — but you must still clean up yourself.
|
||
- Caveats: shared swarm — keep to **ONE** `dev-<recipe>` instance at a time and tear it down before the
|
||
next recipe; the `dev-<recipe>` domain is distinct from the harness's per-run domains and from the
|
||
`warm-*` canonicals, so the sweep removes a leaked one without touching live services.
|
||
- 🚫 **This loop deploys + inspects ONLY — it must NEVER modify the cc-ci tests or the harness.** You
|
||
have host access here, but the cc-ci `tests/**` and `runner/**` are the independent gate, not yours to
|
||
touch: do not edit a test/overlay to make a `dev-` deploy or a `!testme` go green. Editing cc-ci tests
|
||
is gated **exclusively** by `--with-tests` (step 5b); in DEFAULT mode a stale test gets a PR **comment**,
|
||
never an edit. Fix the *recipe*, never the gate. (Interim rule while the upgrader still runs on the
|
||
cc-ci host — see `cc-ci-plan/IDEAS.md`, "Move the upgrader's direct deploy onto separate infra".)
|
||
|
||
### 3. Open the recipe PR (never merge)
|
||
```
|
||
set -a; . /srv/cc-ci/.testenv; set +a
|
||
ssh cc-ci "GITEA_USERNAME='$GITEA_USERNAME' GITEA_PASSWORD='$GITEA_PASSWORD' GITEA_URL='$GITEA_URL' bash -s <recipe>" \
|
||
< /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 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`. 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
|
||
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.
|
||
`**Upstream release notes:** <service> <old>→<new>: <url>`. These links go in the PR comment/body
|
||
itself (so a reviewer sees exactly what changed upstream) — do NOT relegate them to the plan/report
|
||
only. The body must **also** carry the recommended release command — `abra recipe release <recipe> -<x|y|z>`
|
||
(NO `--dry-run`) — on its own line, since the PR does not bump the version label: this is the operator's
|
||
final publish step (bumps the label + tags + pushes) after the upstream PR merges. Re-running with the
|
||
same target version updates the existing same-branch PR rather than duplicating it.
|
||
|
||
### 4. VERIFY by running `!testme` ON THE PR (results visible in the PR; iterate ≤3×)
|
||
Trigger the **real** CI on the recipe PR by posting `!testme` — the bridge runs the harness on cc-ci
|
||
and posts the run + its results back to the PR, so the operator sees them in the PR. Use the PR index
|
||
from step 3's `PR_URL` (`.../pulls/<N>`):
|
||
```
|
||
/srv/cc-ci/.claude/skills/recipe-upgrade/testme-on-pr.sh <recipe> <pr-index> # POST=1: posts !testme + polls
|
||
# if it prints VERDICT=PENDING, poll again WITHOUT re-triggering:
|
||
POST=0 /srv/cc-ci/.claude/skills/recipe-upgrade/testme-on-pr.sh <recipe> <pr-index>
|
||
```
|
||
- **GREEN** → the recipe PR is verified on cc-ci, results in the PR, ready for operator merge. Done.
|
||
- **RED** → go to step 5. You get **up to 3 `!testme` runs total** on this PR (initial + 2 retries):
|
||
use them to fix and re-test a real upgrade regression (step 5a). Each retry is a fresh `!testme`
|
||
after you push a fix, so every attempt's result is recorded in the PR.
|
||
|
||
### 5. If `!testme` is RED → diagnose from the run (recipe regression vs stale cc-ci TEST)
|
||
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
|
||
it on the recipe branch, push, and re-run `!testme` (within the 3-run budget). The recipe PR is only
|
||
"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`.
|
||
|
||
**5b. The upgrade is correct but a cc-ci TEST is genuinely stale/wrong** for the new version (asserts
|
||
old behavior, an overlay needs updating, a readiness gate changed). `!testme` can't go green without a
|
||
test change, and a test change is **gated by `--with-tests`**:
|
||
|
||
- **DEFAULT (no `--with-tests`) — comment, don't touch tests.** Post a **comment on the recipe PR**
|
||
that: states the upgrade itself looks correct; identifies the specific test that failed and **why it
|
||
appears stale** (what the new version changed vs what the test asserts); and tells the operator they
|
||
can **re-run `/recipe-upgrade <recipe> --with-tests`** to also open + verify a cc-ci test-update PR.
|
||
Do **NOT** modify any test. Report `SUCCESS-PENDING-TESTS` (recipe PR open; `!testme` red on a
|
||
stale test; operator to decide).
|
||
- **`--with-tests` — open + verify a cc-ci test PR.** Make it the `ci-test-review` way:
|
||
0. **READ `tests/STYLE.md` in the cc-ci repo FIRST.** It is the rulebook for changing a test, and
|
||
it is written against the failures this pipeline has actually produced. The two that matter most
|
||
here: **set state up through the app's own interface, never its database** (a plausible fixture
|
||
that INSERTed rows passed on v2 and silently broke on v3, holding the recipe RED for six weeks),
|
||
and **gate on version rather than writing a fixture that supports both** — old-version tests can
|
||
simply be deleted, since the older version is only exercised through the upgrade tier.
|
||
1. Branch `recipe-maintainers/cc-ci` in a **separate clone** (single-writer: never push `main`,
|
||
never touch the build loops' `/cc-ci` `/cc-ci-adv` clones); update the test/overlay.
|
||
2. **Verify the recipe upgrade WITH the updated test applied.** `!testme` on the recipe PR uses the
|
||
*deployed/main* cc-ci tests, so it can't see the unmerged test change — verify this combo by
|
||
running the harness with the cc-ci branch checked out on cc-ci:
|
||
`RECIPE=<recipe> REF=upgrade-<new-version> /srv/cc-ci/.claude/skills/ci-test-review/verify-pr.sh`
|
||
(after checking out the cc-ci test branch on cc-ci + rebuilding if needed), plus a small
|
||
regression sample. Green ⇔ the upgrade passes under the corrected test.
|
||
3. Open the **cc-ci test PR** via the Gitea API. Note in BOTH PRs that they are a dependent pair
|
||
(the recipe PR's `!testme` goes green only once the test PR is merged). Report `SUCCESS+TESTPR`.
|
||
|
||
**FLAKY** (passes on a re-`!testme`) → note it; don't author a fix for a flake.
|
||
|
||
Never weaken a test to turn a red upgrade green.
|
||
|
||
### 6. Report — single parseable RESULT line + summary
|
||
Write the report to `/srv/cc-ci/.cc-ci-logs/upgrades/<recipe>-upgrade-<YYYY-MM-DD>.md` and print, as
|
||
the **last line**, one of these exact prefixes (so `/upgrade-all` can collect it):
|
||
|
||
- `RESULT: SUCCESS — <recipe> <old> → <new>, !testme GREEN, recipe PR: <url>`
|
||
- `RESULT: SUCCESS-PENDING-TESTS — <recipe> <old> → <new>, recipe PR: <url>; !testme RED on a stale test (commented; re-run --with-tests to update tests)`
|
||
- `RESULT: SUCCESS+TESTPR — <recipe> <old> → <new>; recipe PR: <url>; cc-ci test PR: <url> (verified with the test change; pair)`
|
||
- `RESULT: FAILED — <recipe> at <step>: <one-line reason>` (e.g. upgrade not green after 3 !testme runs)
|
||
- `RESULT: SKIPPED — <recipe>: <up-to-date | dirty-worktree | reason>`
|
||
|
||
Always state explicitly that **nothing was merged** — the PR(s) await operator review.
|
||
|
||
## Guardrails
|
||
- **cc-ci is the gate, via `!testme` on the PR** — the harness decides pass/fail and the results live
|
||
in the PR (operator-visible); AI plans, implements, diagnoses. Up to 3 `!testme` runs per PR.
|
||
- **Create + verify, NEVER merge.** Recipe PR (and any cc-ci test PR) are operator-merged after review.
|
||
- **Tests are opt-in.** Default = recipe PR only, existing tests; a stale-test failure gets an
|
||
explanatory PR **comment**, never a test edit. Only `--with-tests` opens a cc-ci test PR. The weekly
|
||
`/upgrade-all` cron always runs the **default** — it never auto-updates tests.
|
||
- 🚫 **NEVER modify the cc-ci tests or harness (`tests/**`, `runner/**`) unless explicitly invoked with
|
||
`--with-tests`.** This is absolute and mode-gated — it holds in every step, including the step-2b
|
||
direct-deploy loop where you have cc-ci host access. The tests are the independent gate; never edit
|
||
them to turn a `dev-` deploy or `!testme` green. In DEFAULT mode, fix the recipe or leave a PR comment
|
||
— 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
|
||
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)
|
||
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).
|
||
- **Real abra path** throughout (`abra recipe upgrade` / `abra app deploy` via the harness).
|
||
- **Single-writer / coordination:** dedicated branches + separate clones; never push `main` or disturb
|
||
the build loops' clones; the shared Swarm is stateful — tear down what you deploy; run when you have
|
||
effectively-exclusive use of the host (or serialize). See `ci-test-review` for the shared rules.
|