Skip to content

feat(deployments): filter and page the list, show staged releases and how each node installed - #1874

Merged
dawsontoth merged 8 commits into
claude/1785-deploy-canary-job-followingfrom
claude/1786-deployments-page-history
Oct 12, 2026
Merged

dawsontoth merged 8 commits into
claude/1785-deploy-canary-job-followingfrom
claude/1786-deployments-page-history

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

⊙ Problem

Stacked on #1868 (#1785), which stacks on #1824 → #1821 → #1817 → #1812. This diff is only the one commit above #1868. The #1787 PR stacks on this one.

Config → Deployments was built for 5.1's records. Since then a deploy can be staged and activated later (5.3.0), each node's install is compared with the origin's (5.3.1), and a restarting deploy is certified (5.4). Studio's types had none of it and staged was not a status it knew, so a staged record read as still in flight: the list polled every 2s for as long as the page was open, and the detail view tried to tail it. The list fetched the newest 100 records with no filter or paging (Harper sets no default limit: harper components/deploymentOperations.ts:90, so a component with a long history was simply cut off), and the detail view still showed Restorable and Rollback of, which no Harper version sets: rollback_of is always null (components/deploymentRecorder.ts:152, every tag since 5.1.0), and restorable was never on the row.

❓ Your call: the half of #1786 people most want, what is live on each node and what they can go back to, cannot be built yet: nothing in Harper reports it (HarperFast/harper#3059, open). Inferring it from list_deployments and the retention count would show targets an activation then 404s on, so I left it out rather than ship a guess. What remains is still worth shipping now, because the staged-record polling and the cut-off list are bugs on any 5.3 cluster today.

💡 Solution

The status badge knows staged. A row click keeps the filters when it opens the detail, and an empty page says the filters matched nothing rather than that there are no deployments.

⚖️ Alternatives

  • Infer the live and kept releases from the newest success per component and deployment.stagingRetention.maxCount. Kept releases share that count with staged builds, a release made live before 5.3.0 or from a file: directory is not kept, and a node prunes on its own schedule, so the inference would name ids that answer 404. Report which deployment is live on each node, and which releases each node keeps harper#3059 is the real fix.
  • Filter on the client. The list is paged on the server, so a client filter would only filter the current page.

Not done (needs Harper)

Refs #1786

✅ Verification

  • Tests: deploymentPolling.test.ts and types.test.ts pin staged as terminal for both polls; deploymentsSearch.test.ts the URL validation and the 5.3 status gate; deploymentDescriptions.test.ts the source and install-comparison text, including a pre-5.3.1 record that says nothing; index.test.tsx that the URL's filters and page reach list_deployments, that paging follows the server's total, that the component filter resets the page, and that staged and activation rows read as such.
  • Gates, Node 24.21.0: npx vitest run (exit 0; the pre-commit hook ran the full suite), npx tsc -b (exit 0), npx oxlint --format stylish . (exit 0), npx dprint check (exit 0).
  • End to end: not observable here; no 5.3 cluster with staged records was available, and the shared dev-server port was in use by other sessions.
  • Cross-model review: three rounds, codex (graded) and gemini. Round 1's findings (stale clickable rows under keepPreviousData, a saved staged filter on pre-5.3, focus loss and a blur-then-page race in the component filter) are fixed in the second and third commits; round 3 resolved all of them and left one test-comment nit. I left rolled_back out of the status filter on purpose: no Harper version writes it (finish('rolled_back') is never called). Gemini's round-2 blocker, that useDeploymentsAvailable does not exist, was wrong: it is exported from useSupportsDeploymentSSE.ts and tsc -b passes. The Cursor legs could not run (the 1Password SSH agent refused their git fetch), and the Harper domain leg failed authentication.

gemini-code-assist feedback. Declined: dropping .trim() from the component field's sync effect. The applied filter is already trimmed, and the change would normalize the field only when the filter changes. Resubmitting web while web is applied changes nothing, so the effect never runs. The new head is only a merge of the updated #1868; this PR's own diff is unchanged.

🤖 Generated with Claude Code

Related PRs: #1868 overlaps (base of this stack), #1824 overlaps (base of #1868; same deployments page and tests)

Complexity: medium

Review-Coverage: authored=claude; ran=gemini,codex; blocked=cursor-composer(no-receipt),domain(auth); declined=cursor-grok,cursor-kimi,cursor-muse; rounds=3; full=1 @ 627b7d3

Review-Attention: read ~8m (raised: degraded review) @ 627b7d3

@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 introduces support for staged deployments (Harper 5.3.0+) and peer install comparison fingerprints (Harper 5.3.1+). It updates the deployments list view with pagination, filtering by component and status, and enhances the deployment detail view to show installation sources, activation references, and detailed peer replication results. Feedback is provided on a minor improvement in the project input synchronization logic to ensure leading and trailing spaces are correctly normalized upon form submission.

const projectInput = useRef<HTMLInputElement>(null);
useEffect(() => {
// Follows a filter set from outside the field, such as the editor's link, without remounting it.
if (projectInput.current && projectInput.current.value.trim() !== (project ?? '')) {

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.

medium

Using .trim() in the comparison projectInput.current.value.trim() !== (project ?? '') prevents the input field from being normalized/trimmed when the form is submitted. Since project is already trimmed when applied, comparing the raw value directly with project allows the input field to be automatically cleaned of leading/trailing spaces upon submission.

Suggested change
if (projectInput.current && projectInput.current.value.trim() !== (project ?? '')) {
if (projectInput.current && projectInput.current.value !== (project ?? '')) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Declining. The field keeping what was typed isn't a defect: the filter itself is trimmed before it is applied (index.tsx:79). The suggested comparison would also normalize only when the filter changes. Resubmitting web while web is applied is a no-op at :80, so project doesn't change, the [project] effect (:98-103) doesn't run, and the padding stays. Normalizing every time would mean rewriting the field in onSubmit, which is a separate UX call for the reviewer.

🤖 Generated by Claude Code (Anthropic Claude Opus 5.5); posted via @dawsontoth.

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 69.89% 11019 / 15766
🔵 Statements 70.05% 11758 / 16785
🔵 Functions 63.87% 2886 / 4518
🔵 Branches 65.33% 8552 / 13090
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/applications/components/DeployProgress/DeployProgress.tsx 84.21% 75.64% 83.33% 84.21% 56, 103-135, 281
src/features/instance/applications/components/NewApplication/ImportInstructions.tsx 62.5% 100% 50% 62.5% 33, 91-128
src/features/instance/applications/components/NewApplication/schema.ts 100% 100% 100% 100%
src/features/instance/applications/hooks/useSupportsDeploymentSSE.ts 40% 66.66% 50% 40% 34-53
src/features/instance/applications/modals/reportDeployHealth.ts 81.81% 69.23% 100% 80.95% 61-65, 70-89
src/features/instance/config/routes.ts 67.39% 100% 51.61% 67.39% 19, 26, 34, 40, 47, 53, 60, 67, 73, 81, 88, 95, 101, 108, 114
src/features/instance/config/deployments/deploymentsSearch.ts 100% 100% 100% 100%
src/features/instance/config/deployments/index.tsx 85.45% 78.94% 70.58% 88.23% 89-90, 94, 95, 101, 106-109, 147, 186
src/features/instance/config/deployments/components/DeploymentDetail.tsx 0% 0% 0% 0% 21-226
src/features/instance/config/deployments/components/deploymentStatusBadge.tsx 100% 50% 100% 100%
src/features/instance/config/deployments/constants/tableDefinition.tsx 100% 83.33% 100% 100%
src/features/instance/config/deployments/functions/deploymentSource.ts 100% 100% 100% 100%
src/features/instance/config/deployments/functions/describePeerInstall.ts 100% 85.71% 100% 100%
src/integrations/api/instance/applications/deployComponent.ts 94.44% 100% 75% 94.44% 104-106
src/integrations/api/instance/applications/deployComponentStream.ts 83.33% 61.9% 100% 83.33% 65-69
src/integrations/api/instance/deployments/types.ts 100% 100% 100% 100%
src/integrations/api/sse/errors.ts 100% 100% 100% 100%
src/integrations/api/sse/streamOperation.ts 77.77% 75% 55.55% 78.68% 54, 128, 163-182
Generated in workflow #2209 for commit 23beb6e by the Vitest Coverage Report Action

dawsontoth and others added 8 commits October 12, 2026 12:43
… how each node installed

The list filters by component and status, pages by the server's total, and
shows each record's id, source (package, payload hash, or the release an
activation made live) and user. A staged record (Harper 5.3+) is terminal
and reads as waiting to be activated, so the list no longer polls it every
2s. The detail view shows the origin's install source, the activation's
source release, and each peer's install comparison (5.3.1+), and drops the
Restorable and Rollback-of fields no Harper version sets. The editor's
Application menu links to the component's deployments.

Refs #1786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ilter where staging is absent

The list no longer keeps the previous page's rows on screen, clickable, while
the next page loads. A saved ?status=staged link opened against an instance
older than 5.3 no longer asks for a status it cannot have. The component
filter applies on Enter only, so leaving the field to click Next does not
reset the page under the click, and it follows an outside change without
remounting the field.

Refs #1786

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

A saved ?status=staged link opened against Harper older than 5.3 left the
URL claiming a filter while the list showed every status. Once the version
is known, the page replaces the URL without it.

Refs #1786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Redeploy and Import offer Stage only where the instance reads 5.3.0 or
later: it sends activate: false and no restart, and reports the staged
deployment id without a health check. A staged record on the Deployments
page has an Activate action, which sends deploy_component with its
deployment_id and a rolling restart through the same mutation, so it is
followed like any other rolling deploy; the progress panel ends a stage at
Staged. All three request shapes come from one deployRequestBody, and the
editor's Application menu links to a component's staged releases.

Refs #1787

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

Activate awaits the mutation instead of passing it callbacks, which React
Query drops once the detail unmounts, so the report and the refresh still
happen for an activation someone closed the dialog on. The mutation is keyed
by entity and deployment, so a detail reopened mid-rollout shows it pending
and offers no second activation. A stream that ends early says the outcome
is unknown instead of reporting a failure.

Refs #1787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The activation reports every outcome itself, so it opts out of the global
mutation toast, which would have called an early-ended stream a failure
alongside the local 'may not be activated'. It logs the error with
console.error as that handler did, so Datadog still sees it.

Refs #1787

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…after it may have started

When the SSE deploy got no event stream, onDeployComponentSubmit fell back
to the buffered deploy_component on any SSEUnsupportedError: a gateway
502/503/504, a 2xx that was not a stream, a dropped connection, or the
120s idle watchdog firing before headers. Harper starts an SSE operation as
soon as it accepts it and answers with a 200 stream, so each of those can
follow a deploy that is already running, and the fallback deployed the same
package a second time (a second install and rolling restart, or two deploys
of one project racing).

SSEUnsupportedError now carries the response status. The deploy falls back
only on a 4xx, which Harper answers for a request it refused before running
it (authentication, permissions, validation). Anything else becomes
SSEInconclusiveError, which the import, redeploy and activate callers
already answer by checking instead of resending.

Closes #1889

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
deployComponentStream's contract still told every caller to retry on SSEUnsupportedError, which is the resend this branch removes. Also trims two comments that restated the design note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dawsontoth
dawsontoth force-pushed the claude/1786-deployments-page-history branch from 627b7d3 to 23beb6e Compare October 12, 2026 16:45
@dawsontoth
dawsontoth merged commit 377d6b0 into stage Oct 12, 2026
6 checks passed
@dawsontoth
dawsontoth deleted the claude/1786-deployments-page-history branch October 12, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant