# Per-recipe deploy budget (Phase 2b) **Question:** does a recipe's full CI test sequence redeploy more than necessary? **Answer:** No. The budget is already minimal — and in fact tighter than the nominal `1 base + 1 upgrade + N_deps` — because the upgrade tier shares the base deployment. ## The budget For one cold `!testme`/`run_recipe_ci.py` run of a recipe: ``` deploys == 1 (base) + N_cold_deps ``` - **1 base deploy**, shared by **install → upgrade → backup → restore → custom/functional**. All five tiers run against this single deployment. (`run_recipe_ci.py:819`, `lifecycle.deploy_app` → `_record_deploy`.) - **+ 1 per COLD declared dependency** (e.g. an SSO provider deployed in-run), each deployed **once** and reused (`deps.py:81-120`, one `deploy_app` per dep). A **live-warm** dep (e.g. a resident keycloak that only gets a per-run realm, not a fresh deploy) contributes **0**. - The **upgrade tier adds NO deploy.** When the upgrade tier runs, the *base* deploy is done at the **previous published version** (`run_recipe_ci.py:746-754`: `base = prev or target`), and the upgrade is an **in-place `abra app deploy --chaos`** redeploy of the PR-head code onto that same running app (`generic.perform_upgrade` → `lifecycle.chaos_redeploy`). `chaos_redeploy` does **not** call `deploy_app`, so it is **not counted** — and it is the *real* upgrade the PR's changes are exercised by (HC1), verified by `assert_upgraded` on the chaos-version label. - **backup and restore add NO deploy.** They operate on the same running app (`perform_backup`/`perform_restore` → `backup_app`/`restore_app`); neither calls `deploy_app`. ### Reconciliation with the plan's nominal budget Plan B1 states the nominal minimum as `1 (base) + 1 (upgrade tier) + N_deps`, assuming the upgrade tier needs its own prior-version deploy. The cc-ci design is **stricter**: the base deploy *is* the prior-version deploy (when upgrade runs), and the upgrade is performed **in place**. So the prior-version deploy and the base deploy are the **same** deploy — there is no separate upgrade deploy. Net actual budget: `1 + N_cold_deps`. This is the deploy-sharing the operator expected. ## Enforcement (not just claimed) The harness counts every `deploy_app()` (the only caller of `_record_deploy`, `lifecycle.py:107-211`) into a per-run countfile and **hard-fails** on a mismatch: - `expected_deploy_count = 1 + deps_deployed_count` — `run_recipe_ci.py:984` (`deps_deployed_count` excludes warm deps, `:982-983`). - RUN SUMMARY prints `deploy-count = N (expect M)` — `run_recipe_ci.py:986`. - `if deploy_count != expected_deploy_count: … overall = 1` (DG4.1 violation, non-zero exit) — `run_recipe_ci.py:1005-1010`. So every green run is a *proof* that the recipe stayed within budget: a redundant redeploy would push `deploy_count` above `expected` and turn the run red. No recipe can silently exceed the budget. ### Verify from a cold clone ``` RECIPE=ghost STAGES=install,upgrade,backup,restore,custom cc-ci-run runner/run_recipe_ci.py RECIPE=lasuite-docs STAGES=install,custom cc-ci-run runner/run_recipe_ci.py ``` Expected RUN SUMMARY lines: - no-dep recipe (ghost): `deploy-count = 1 (expect 1)`, all tiers `pass`. - cold-dep recipe (lasuite-docs + cold keycloak): `deploy-count = 2 (expect 2)` — `deps deployed: ['keycloak']` — all tiers `pass`, `DEPS teardown` clean. - warm-dep recipe (lasuite-meet, live-warm keycloak): `deploy-count = 1 (expect 1)`, `deps deployed: ['keycloak']`. Observed across all Phase 2 recipe runs: every recipe ran at `deploy-count = 1` (no/warm deps) or `deploy-count = 2 (expect 2)` (one cold dep). No run exceeded `1 + N_cold_deps`. ## No test weakened to share the deploy Sharing one deployment does **not** skip or soften any check: - install, upgrade, backup, restore, custom each still run their **real generic + overlay assertions** against the shared app (`run_lifecycle_tier`, `ALL_STAGES`). - the upgrade is a **real** prev→PR-head crossover (`assert_upgraded` on the chaos-version label), not a no-op. - backup→restore is **real data-integrity** (P4: seed → backup → mutate → restore → assert the seeded data survived), not health-only. - per-run isolation/teardown is unchanged (`DEPS teardown`, app undeploy, volume/secret cleanup). Only the **deploy count** is constrained; coverage is untouched. ## Out of scope of the budget (intentionally) - **WC5 canonical promote** (`promote_canonical`, `run_recipe_ci.py:682-707`) deploys a separate `warm-` app to (re)seed the warm-cache canonical. It runs **only** on a green cold run on LATEST, **after** the deploy-count assertion, and explicitly **pops** `CCCI_DEPLOY_COUNT_FILE` (`:697`) so it does not perturb the per-run test budget. It is warm-cache maintenance, not a test deploy. - **`--quick` fast lane** (`run_quick`) reuses an existing data-warm canonical and is a separate optimization path; the cold full run above is the budget of record. ## Conclusion The per-recipe deploy budget is **already minimal** and **enforced**: `1 + N_cold_deps`, with the upgrade tier sharing the base deploy in place. No redundant deploy was found; none was removed because none existed. (Phase 2b, 2026-05-31.)