Raised mid-session on 2026-09-08 after live feedback against checkout-api: today's Releases tab renders one card per environment. That's great for "is prod healthy right now" and bad for "did v2.3.9 actually reach prod, and when." Worked through below using checkout-api's real cicd.yaml pipeline — dev → test → staging → pre‑prod → prod — and its real image-tag convention, <version>-<7-char-sha>.
One card per real (cluster, namespace) environment, ordered by promotionOrder. Each card is self-contained: current image, health, a supply-chain strip, a Promote button when there's somewhere to go.
An image is built once (one row) and tracked as it moves. Good for "when did this build reach prod" — a real question, e.g. after an incident. Bad for "what's live right now," which now means scanning every row for whichever one currently touches each environment.
Rows are releases (newest first), columns are environments in promotionOrder. Read a column down for current fleet state; read a row across for one build's full journey. This is the same shape Octopus Deploy and Spinnaker converged on for exactly this ambiguity — it isn't a compromise between the two views above, it's both of them at once. Click the pending cell below.
| Dev | Test | Staging | Pre‑prod | Prod | |
|---|---|---|---|---|---|
| v2.4.0-a91f3c2 | ✓ |
· |
· |
· |
· |
| v2.3.9-77bd110 | ✓ |
✓ |
✓ |
↻ |
· |
| v2.3.8-114ffa2 | ✓ |
✓ |
✓ |
✓ |
✓ |
| v2.3.7-c02d9e1 | · |
· |
· |
· |
· |
Same three questions the team keeps actually asking, checked against each shape.
| 1 · Env cards | 2 · Release rows | 3 · Matrix | |
|---|---|---|---|
| What's live in Prod right now? | Direct — read the Prod card | Scan every row for the one reaching Prod | Direct — read the Prod column |
| Did v2.3.9 ever reach Prod, and when? | Not answerable without opening deploy history per env | Direct — read that row | Direct — read that row |
| Promote click target | Button on the env card (image is implicit) | Button on the row (target env is implicit) | The exact empty cell — image and target env both explicit |
| New backend calls required | — (shipped) | None — same deployHistory, grouped by image | None — same deployHistory + gitopsPrs, regrouped |
| Cost to build from today's tab | — | High — the whole tab's structure changes | Additive — cards move from top-level to cell-expand |
Every card component that exists today — ReleaseCard, PipelineFlow, the cosign/SLSA badges, PromoteDialog — keeps working unmodified as the thing a matrix cell expands into. The only new work is reshaping data that's already being fetched:
releases = groupBy(imageTag,
environments.flatMap(env => env.deployHistory.map(h => ({...h, env: env.env}))))
// newest-introduced-in-dev first; a row is "current" if its image
// matches environments[0].image (today's dev card, unchanged)
One real tightening this forces, worth doing regardless of which view ships: gitopsPrForEnv() today matches a PR by target env alone. A matrix cell needs to know which release a pending PR is for, not just which env — parse the image tag back out of the PR title's already-present "... @ <imageTag>" suffix and match on both, so a cell only lights up amber when it's this row's release that's pending.