Repository navigation
fix(deployments, breadcrumbs): show an activated staged release as live, and route ids as ids - #1943
Conversation
… 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>
There was a problem hiding this comment.
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.
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>
Problem
Two display bugs turned up while hand-testing the release candidate against Harper 5.3.2 on a 2-node cluster.
4 Ac 92 Da 3 D 8 A 0 …, and so did usernames, role, secret, key, certificate and database names, and the organization's role and user ids.Root causes, with Harper citations (v5.3.2)
components/deploymentRecorder.ts:60-63andcomponents/deploymentOperations.ts:16-19makestagedterminal. An activation creates a new record withactivated_from: req.deployment_id(components/operations.js:725-740, line 731) and finishes it assuccess(line 1088). Only the recorder writes these rows, and each writes only its own.delete_deployment_payloadonly nulls the payload.alreadyActiveand skips the swap (components/Application.ts:4812-4821). It then restarts every worker (operations.js:956,restartScope = '*').status(tableDefinition.tsx,DeploymentDetail.tsx).loadphase when it skips the validation load.operations.js:467-473returns early whenever the worker's intrinsics are frozen after boot, which is the default (applications.lockdown: freeze-after-load,static/defaultConfig.yaml:29).DeployProgressdrew any phase with no events as pending.Breadcrumbs.tsxran every segment throughcapitalizeWords. 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.
stagedReleaseStateclassifies a staged record from its component's records since it was staged.useStagedReleasesfetches those records with onelist_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 toStagedReleaseDetail, andActivateStagedReleasepicks the button.5c12b89e"DeployProgressshows a phase as skipped once a later phase has reported without it (skippedPhases).Breadcrumbs.
breadcrumbTrailreads the leaf match'sfullPath(useMatches) to find which segments are parameters. Org, cluster and instance ids show their names.modestays Title Cased. Anything else shows as written, and a UUID shows 8 characters with the full id as a tooltip. New$paramroutes are covered without a special case.Decisions, and what the records can't tell
.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.list_deploymentshas 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.successeven 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 readslive, 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.list_deploymentsaren't locked out./^(rol|usr)-/branch and createsBreadcrumbs.test.tsx, which this PR creates too. Its cases are covered here, inbreadcrumbTrail.test.ts. Whichever PR merges second keeps both tests and drops the branch.Verification
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'serrorstate.breadcrumbParams.test.tswalks all 63 parameterized routes in the realrootRouteTree.8326ef1, Node 24.21.0, real exit codes:vitest run --maxWorkers=4(529 files),tsc -b,oxlintanddprint checkall exit 0.5d6ffc6was committed with--no-verify. Load-only 5 s timeouts hit two unrelated files,JobsTabandNewApplication, both green alone. I ran the hook's steps by hand. The other three commits passed the hook.gemini-code-assistleft no inline comments.github-actionsposted coverage only.Mutation checks, gates and review rulings in full
index.test.tsx"shows an activated staged release as live by its activation…";DeploymentDetail.test.tsx"says an activated one is live instead…";offerignored → 5 red;replaced/uncertain/activating/failed-activation/failed-peer →stagedRelease.test.ts;skippedPhases→ 2 red;Breadcrumbs.tsxfromstage→ 2 red;--maxWorkersat load 40–130 timed out inJobsTab,NewApplication,roles/indexandmonaco/editorApi. Each passed alone.successtaken as live everywhere, an error read as waiting, a failed activation read as not live, the button flash, unused exports, narrating comments;live(above);replaceddespite a later failure: it isn't live either way, and rollback is still right;b7a6b944b;Setper render, twoagowrappers over one formatter, version-pinned citations, test comments that explain why.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