Readiness score vs. upgrade decision
Mirops reports two things on two separate axes, and keeping them separate is central to the design:
- The readiness score (0–100) is a health gauge. Higher is healthier.
- The decision (
decision.level) is the upgrade gate. It is deterministic.
A healthy score never overrides a blocker, and a low score never invents one. A cluster can score 88 and still be CRITICAL (an add-on is incompatible); it can score 40 and only be WARNING (unstable, but nothing hard-blocks the drain).
This static view highlights the separation: readiness describes health, while deterministic blockers control the upgrade gate.
The verdict
decision.level is the authoritative field. decision.allow and decision.blockers come with it:
| Level | allow | Meaning |
|---|---|---|
SAFE | true | No blocker, and the score meets the profile's SAFE threshold. |
WARNING | true | Attention required (e.g. score below the bar, unstable pods) but no deterministic blocker. |
CRITICAL | false | At least one deterministic blocker — do not upgrade until it's fixed. |
ERROR | — | The analysis could not run (e.g. targetVersion isn't higher than the current version). |
decision.blockers lists every reason it's CRITICAL, so you fix them all — not just the first.
The readiness score
The base score is the weighted sum of four dimensions (out of 100):
| Dimension | Weight | Measures |
|---|---|---|
| Health | 25 | Proportion of pods ready. |
| Capacity | 30 | CPU/memory headroom (requests / allocatable), split 15/15. |
| Stability | 20 | Crash-loops, restart trend, and pod drop since the last run. |
| Compatibility | 25 | Add-on compatibility with the target version. |
When AI scoring is enabled, the final score is blended base × 0.7 + AI × 0.3. The AI only touches the gauge — it can never block or unblock an upgrade.
The blockers
A CRITICAL verdict comes from deterministic conditions the engine can prove — not from the score:
- A PodDisruptionBudget that would stall a node drain.
- CPU pressure > 90% or memory pressure > 90% of allocatable.
- Too many pods not ready (beyond the profile's ratio).
- An incompatible add-on for the target version.
- A
LostPersistentVolumeClaim.
Each of these blocks regardless of the score, because each would concretely break an upgrade in progress.
An example
A cluster you want to move from 1.29 → 1.31: a payments Deployment (3 replicas) across two nodes, behind ingress-nginx 1.11, with TLS from cert-manager 1.16, a PodDisruptionBudget that keeps 2 of the 3 pods available, and a payments-db StatefulSet on a Bound PVC. Upgrade analysis reads the mirror against 1.31:
| Signal in the mirror | What it tells the decision |
|---|---|
| Compatibility | ingress-nginx 1.11 supports Kubernetes only up to 1.30 → incompatible with 1.31. cert-manager 1.16 supports up to 1.32 → fine. |
| Relationships | payments sits behind ingress-nginx, so the incompatible add-on's risk propagates to it. |
| Disruption | the PDB (min 2/3) still lets one pod drain at a time → the node drain won't stall. |
| Storage | the PVC is Bound → no data-availability blocker. |
The incompatible add-on makes it CRITICAL, whatever the score: upgrade ingress-nginx to ≥1.12 first, then the move to 1.31 is safe. Run it again afterwards and the same mirror yields the same answer — deterministic.
Why two axes
If the score were the gate, a team couldn't trust it: "why did it block today but not yesterday, with the same cluster?" By keeping the gate deterministic and the score a gauge, the verdict is reproducible and CI-safe — the same mirror always yields the same decision.
Next
- See the strictness knobs in scoring profiles.
- See the full field reference in decisions.