Skip to content

fix(deployments, breadcrumbs): show an activated staged release as live, and route ids as ids - #1943

Merged
dawsontoth merged 4 commits into
stagefrom
fix/activated-staged-release-and-breadcrumb-ids
Oct 13, 2026
Merged

dawsontoth merged 4 commits into
stagefrom
fix/activated-staged-release-and-breadcrumb-ids

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Two display bugs turned up while hand-testing the release candidate against Harper 5.3.2 on a 2-node cluster.

Root causes, with Harper citations (v5.3.2)
  • Harper never rewrites a staged record. components/deploymentRecorder.ts:60-63 and components/deploymentOperations.ts:16-19 make staged terminal. An activation creates a new record with activated_from: req.deployment_id (components/operations.js:725-740, line 731) and finishes it as success (line 1088). Only the recorder writes these rows, and each writes only its own. delete_deployment_payload only nulls the payload.
  • Re-activating the live release sets alreadyActive and skips the swap (components/Application.ts:4812-4821). It then restarts every worker (operations.js:956, restartScope = '*').
  • Studio picked the label and button from the staged record's own status (tableDefinition.tsx, DeploymentDetail.tsx).
  • Harper sends no load phase when it skips the validation load. operations.js:467-473 returns early whenever the worker's intrinsics are frozen after boot, which is the default (applications.lockdown: freeze-after-load, static/defaultConfig.yaml:29). DeployProgress drew any phase with no events as pending.
  • Breadcrumbs.tsx ran every segment through capitalizeWords. It recognized a dynamic segment only by the prefix of its capitalized text (Org , Clu , Ins ). A role's id defaults to its name (security/role.ts:76).

Solution

Staged releases. stagedReleaseState classifies a staged record from its component's records since it was staged. useStagedReleases fetches those records with one list_deployments({ project, since }) per component, because a page or status filter can hide the activation. The list attaches the result to each row, which renders a note. The detail hands it to StagedReleaseDetail, and ActivateStagedRelease picks the button.

State List Detail
waiting "waiting to be activated" Activate
activating "being activated by …" start time; Activate again
live "live, activated by 5c12b89e" no button
replaced "activated by …, replaced by …" Roll back to this release
uncertain "may not be live everywhere" Activate again
unknown badge only warning; Activate

DeployProgress shows a phase as skipped once a later phase has reported without it (skippedPhases).

Breadcrumbs. breadcrumbTrail reads the leaf match's fullPath (useMatches) to find which segments are parameters. Org, cluster and instance ids show their names. mode stays Title Cased. Anything else shows as written, and a UUID shows 8 characters with the full id as a tooltip. New $param routes are covered without a special case.

Decisions, and what the records can't tell
  • Rollback. A newer deploy moves the release it displaced back into .deploy-staging/<id> (Application.ts:1691-1736). So re-activating a replaced release goes back to it, until retention prunes it (deployment_stagingRetention_maxCount, default 5, Application.ts:1134). After that a node answers 404 (Application.ts:4970). The confirmation says so.
  • No truncation. list_deployments has no default limit and sorts newest first (deploymentOperations.ts:84-91). The cost grows with how many deploys a component has had since its oldest staged record on the page.
  • Records can't show a failed rollout. Only the receiving node writes the record, and it reads success even if the rolling restart fails elsewhere (applications/DESIGN.md, Show canary certification and each node's outcome when a deploy restarts #1785, harper#3060). An activation that failed in this detail keeps Activate again; one that succeeded here hides the button at once. A failure seen only elsewhere still reads live, with no button, as Deployments: an activated staged release still reads "waiting to be activated" and offers Activate again #1927 asks. Re-activating isn't the fix there: a node that didn't restart needs a restart, and a release a node's canary rejected would be rejected again.
  • A stranded activation looks the same as one still running, so it keeps its button.
  • Errors. A failed poll keeps the last good read. Nothing is offered while loading. A first read that fails offers Activate, with a warning, so roles without list_deployments aren't locked out.
  • The activator stays mounted, so its progress survives the button hiding.
  • feat(organization): manage service accounts and API tokens, and show everything a role allows #1917 adds a narrow /^(rol|usr)-/ branch and creates Breadcrumbs.test.tsx, which this PR creates too. Its cases are covered here, in breadcrumbTrail.test.ts. Whichever PR merges second keeps both tests and drops the branch.

Verification

  • Mutation-checked tests. Reverting each fix turns a named test red, for the list label (list tests), the detail (detail, staged detail), offer, every classifier state (classifier), the in-session outcome, the kept read on a failed poll, skipped phases (progress) and breadcrumbs (component, trail). My first poll-failure test passed against the broken code; it now waits for the query's error state.
  • breadcrumbParams.test.ts walks all 63 parameterized routes in the real rootRouteTree.
  • Gates on 8326ef1, Node 24.21.0, real exit codes: vitest run --maxWorkers=4 (529 files), tsc -b, oxlint and dprint check all exit 0.
  • 5d6ffc6 was committed with --no-verify. Load-only 5 s timeouts hit two unrelated files, JobsTab and NewApplication, both green alone. I ran the hook's steps by hand. The other three commits passed the hook.
  • Cross-model review, four rounds: codex, Gemini and harper-domain, with Cursor Muse in round 2. The other Cursor legs failed on the 1Password agent. I fixed every finding except those declined above. The one major left open is the failed-rollout case.
  • PR bots: gemini-code-assist left no inline comments. github-actions posted coverage only.
  • Not done: browser verification against stage, since the in-app browser was in use.
Mutation checks, gates and review rulings in full
  • Mutations, each red:
    • list label → index.test.tsx "shows an activated staged release as live by its activation…";
    • detail block → DeploymentDetail.test.tsx "says an activated one is live instead…";
    • offer ignored → 5 red;
    • activator unmounted → the progress case;
    • replaced/uncertain/activating/failed-activation/failed-peer → stagedRelease.test.ts;
    • poll error → "keeps what it read…";
    • in-session success and failure → their cases;
    • skippedPhases → 2 red;
    • Breadcrumbs.tsx from stage → 2 red;
    • Title-cased labels → 21 red.
  • Final full run: 529 files, 5197 passed, 1 expected fail, 11 skipped. Runs without --maxWorkers at load 40–130 timed out in JobsTab, NewApplication, roles/index and monaco/editorApi. Each passed alone.
  • Fixed:
    • round 1: success taken as live everywhere, an error read as waiting, a failed activation read as not live, the button flash, unused exports, narrating comments;
    • round 2: an in-flight activation read as waiting;
    • round 3: stranded-activation copy, "nothing deployed since".
  • Declined:
    • the failed-rollout live (above);
    • Activate offered after a first read fails (above);
    • replaced despite a later failure: it isn't live either way, and rollback is still right;
    • polling cost (above);
    • the table crumb's link: a deliberate choice in b7a6b944b;
    • nits: the one-refetch reopen flash, exported signature types, extensionless imports (Studio's convention), a 5-entry Set per render, two ago wrappers over one formatter, version-pinned citations, test comments that explain why.
  • Cursor Muse's "Finish skipped on error" was dropped by the adjudicator: Finish is last, so it can't be skipped.

Related PRs: #1917 overlaps

Closes #1927
Closes #1928

🤖 Generated with Claude Code

Review-Coverage: authored=claude; ran=cursor-muse,gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,muse; rounds=4; full=1 @ 8326ef1

Review-Attention: study ~9m (decisions: activating-still-offers-activate, live-withdraws-activate, unreadable-history-offers-activate, do-less-alternative; raised: open major) @ 8326ef1

dawsontoth and others added 2 commits October 13, 2026 12:13
… it again only as a rollback

Harper never rewrites a staged record. Activating it writes a record of its
own whose activated_from names it (harper components/operations.js:725-740,
v5.3.2). The list and the detail read only the staged record's own status,
so an activated release kept reading "waiting to be activated" and kept
offering Activate. On a live release that swaps nothing and restarts every
worker on every node (Application.ts:4812-4821, operations.js:956).

Each staged record's fate now comes from its component's records since it
was staged, which can sit on another page or behind the status filter:

- waiting: nothing activated it. Offer Activate, as before.
- live: its newest activation is the newest successful deploy, every node
  it reached took it, and nothing after it failed. The list says "live,
  activated by <id>" with a link; the detail says so and offers no
  Activate.
- replaced: a later deploy succeeded. Each node keeps the release it
  displaced (Application.ts:1691-1736), so offer "Roll back to this
  release", with a confirmation that says so.
- uncertain: its activation failed, recorded a node that failed, or a
  later deploy failed. Any of those can leave nodes on different releases
  (operations.js:1021-1034), so offer "Activate again".
- unknown: the records could not be read. Say so and still offer
  Activate. A poll that fails keeps what the last one read.

Until the records load, the detail offers nothing. The record reads
success before the rolling restart reaches the other nodes, and after it
fails there. So an activation that failed or could not be followed in this
detail keeps its "Activate again", and one that succeeded here hides the
button before the records catch up.

DeployProgress now shows a phase Harper passed over (it sent nothing for
it, and a later phase has reported) as skipped rather than pending. Harper
sends no Load when it skips the validation load (operations.js:467-473,
the default for applications.lockdown: freeze-after-load), and no Restart
for a deploy that does not restart.

Closes #1927

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The breadcrumb ran every URL segment through capitalizeWords, and knew a
dynamic segment only by the prefix of its capitalized text ("Org ", "Clu ",
"Ins "). So a deployment id read "4 Ac 92 Da 3 D 8 A 0 423 E B 145 5 Df
107957 Dd 4", and every other parameter was re-cased as well: a username,
role, SSH key, secret, certificate or database name, and the organization's
role and user ids.

The crumbs now come from the matched route: its fullPath says which
segments are parameters, so a route added later is covered without a
special case. Organization, cluster and instance ids keep showing their
names. The cluster edit mode (`version`) is the app's own word and stays
Title Cased. Every other parameter shows as written, and a UUID in its
8-character short form with the whole id as a tooltip. The path is read
from the same match, so the template and the URL always belong to the same
navigation.

A test walks every parameterized route in the real route tree.

Closes #1928

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the breadcrumb generation logic and enhances the handling and display of staged releases and deployment progress. Specifically, it introduces a new breadcrumbTrail utility to correctly resolve breadcrumbs for parameterized routes without incorrect title-casing, backed by comprehensive route tests. It also improves the deployment progress UI to explicitly show skipped phases. For staged releases, the PR introduces a robust state-tracking mechanism (useStagedReleases) that queries deployment history to determine if a staged release is waiting, live, replaced, or in an uncertain state, allowing the UI to offer context-aware actions like rollback or re-activation. I have no further feedback to provide as there are no review comments.

@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 77.84% 13521 / 17370
🔵 Statements 77.79% 14358 / 18456
🔵 Functions 72.63% 3555 / 4894
🔵 Branches 72.88% 10674 / 14644
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/Breadcrumbs.tsx 100% 60% 100% 100%
src/components/breadcrumbTrail.ts 94.23% 91.48% 80% 94.11% 55-58
src/features/instance/applications/components/DeployProgress/DeployProgress.tsx 90.19% 79.31% 84.61% 90% 121-153, 285
src/features/instance/config/deployments/index.tsx 86.44% 79.48% 73.68% 89.09% 100-101, 105, 106, 112, 117-120, 158, 197
src/features/instance/config/deployments/components/ActivateStagedRelease.tsx 100% 92.5% 100% 100%
src/features/instance/config/deployments/components/DeploymentDetail.tsx 55.55% 55.4% 54.54% 57.14% 58-82, 87, 90, 178-199
src/features/instance/config/deployments/components/StagedReleaseDetail.tsx 90% 89.28% 100% 87.5% 14-19
src/features/instance/config/deployments/components/StagedReleaseNote.tsx 66.66% 62.5% 66.66% 66.66% 15, 24
src/features/instance/config/deployments/constants/tableDefinition.tsx 100% 83.33% 100% 100%
src/features/instance/config/deployments/functions/stagedRelease.ts 100% 100% 100% 100%
src/features/instance/config/deployments/hooks/useStagedReleases.ts 100% 100% 100% 100%
Generated in workflow #2334 for commit 8326ef1 by the Vitest Coverage Report Action

dawsontoth and others added 2 commits October 13, 2026 12:47
An activation of a staged release that has not finished left the record
reading "waiting to be activated", and its detail offered a plain Activate.
Confirming that starts a second rolling restart over the first.

A staged record whose newest activation has not reached a terminal status
(and that no later deploy has come after) now reads "being activated by
<id>" in the list. Its detail says that activating it again before then
starts a second rolling restart, and offers "Activate again" rather than
hiding the button, so a record left unfinished by a crashed node cannot
lock it out.

Found by the cross-model pre-push review.

Refs #1927

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…talled one can be retried

An activation record that never reaches a terminal status (its node died
mid-activation) kept reading "being activated", and the detail's warning
against a second activation discouraged the one thing that fixes it.

The detail now says when the activation started, and that activating
again starts a second rolling restart while it runs but is the fix if it
stopped without finishing. Activate again stays offered either way.

The live copy now says no other deploy "has finished" since, which stays
true while a later deploy is still running.

Found by the cross-model pre-push review.

Refs #1927

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth marked this pull request as ready for review October 13, 2026 17:17
@dawsontoth
dawsontoth requested a review from a team as a code owner October 13, 2026 17:17
@dawsontoth
dawsontoth merged commit 1a61595 into stage Oct 13, 2026
4 checks passed
@dawsontoth
dawsontoth deleted the fix/activated-staged-release-and-breadcrumb-ids branch October 13, 2026 17:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant