Files
cc-ci/docs/perf/deploys.md
autonomic-bot edf34e3e53 claim(2b): deploy budget confirmed minimal+enforced (1+N_cold_deps); B1-B4 claimed
Phase 2b confirm-and-document outcome: per-recipe test-sequence deploy budget is
already minimal — `deploys == 1 (base, shared by all 5 tiers) + N_cold_deps` — and
tighter than plan B1's nominal `1+1(upgrade)+N` because the upgrade is an in-place
chaos redeploy of the prev-version base, not a separate deploy. Enforced as a hard
failure by DG4.1 (expected = 1 + deps_deployed_count, run_recipe_ci.py:1005-1010).
No redundant deploy found; none removed (none existed).

- docs/perf/deploys.md: the budget record (B4), names the out-of-budget WC5 reseed
- STATUS-2b.md: B1-B4 claim with WHAT/HOW/EXPECTED/WHERE for cold verify
- JOURNAL-2b.md / BACKLOG-2b.md / DECISIONS.md: reasoning + settled note
- consume machine-docs/BUILDER-INBOX.md (Adversary heads-up processed)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:35:46 +00:00

91 lines
5.2 KiB
Markdown

# 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-<recipe>` 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.)