Skip to content

feat(deploy): stage a release now and activate it later (Harper 5.3+) - #1876

Merged
dawsontoth merged 5 commits into
claude/1786-deployments-page-historyfrom
claude/1787-stage-and-activate
Oct 12, 2026
Merged

dawsontoth merged 5 commits into
claude/1786-deployments-page-historyfrom
claude/1787-stage-and-activate

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

⊙ Problem

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

Since Harper 5.3.0, deploy_component with activate: false builds, installs and keeps a release on every node without making it live, and deploy_component { project, deployment_id } makes it live later with nothing to install (harper v5.3.0:components/operations.js:638). Studio could do neither, so the slow, failure-prone part of a deploy always happened at the moment it went live.

❓ Your call: #1787 leaves the UX unspecified beyond "a Stage only option" and "an Activate action", so this is the minimal slice: a switch in the two dialogs, an Activate button on a staged record's detail, and an editor link to the component's staged releases. A staged-releases panel inside the editor, or Activate on the list rows, would be more direct; tell me if either is wanted. Rollback to a kept release (#1232) uses the same call and is not built here.

💡 Solution

All three request shapes come from one deployRequestBody over a DeployRequest union, which the mutation now takes in place of the separate fields and sends on both transports (the stream takes the same body). Harper's validator refuses the wrong mix outright, so the shapes are exact:

Gating. Both are offered only when registration_info reads 5.3.0 or later, and an unreadable version offers neither. That is the opposite of how #1868 gates certification, on purpose: a node older than 5.3.0 accepts activate: false as an unknown field and makes the release live (harper components/operations.js:1014-1035 explains the hazard), so offering it on a guess risks doing the opposite of what was asked. The design note records why.

Stage only. The Redeploy dialog gets a switch that only counts where staging exists; so does Import (form, schema, request). A stage succeeds only once every other node confirmed staging it (operations.js:1023-1035), and it restarts nothing, so it is reported as staged with its id and no health check, from the progress panel, the buffered path, and an import, which also skips opening the not-yet-live application in the tree. The panel ends a stage at Staged (phases, result flag), and a stream that ends early says the stage may not have finished rather than health-checking a component nothing changed.

Activate. A staged record's detail shows Activate, which asks first and then sends the activation; it is hidden where staging isn't known to exist. The mutation is keyed by entity and deployment, so a detail closed and reopened mid-rollout shows it pending and offers no second activation, and it is awaited rather than given callbacks, which React Query drops once the detail unmounts. It reports every outcome itself, an early-ended stream as unknown, not failed, so it opts out of the global toast and logs to Datadog the way that handler did (hook argument). The editor's Application menu links to the component's staged releases. Failures go through a shared reportDeployFailure, and a stage's warnings get their own title.

Also: DeployToastOptions is no longer exported, the leftover minor from #1868's review.

⚖️ Alternatives

  • Separate stage and activate mutations. They would repeat the transport choice, the stream fallback and the job following; one request union keeps the payload rules in one place, where the tests pin them.
  • Gate on the response, as certification is. A stage's danger is in the request, so there is nothing to wait for: by the time an older node answers, it has already deployed live.

✅ Verification

  • Tests: deployRequestBody pins the three exact bodies, and onDeployComponentSubmit that the buffered and streamed paths send the same one; supportsStagedDeployments refuses 5.2.x, 5.3.0 prereleases, unreadable and missing versions; ActivateStagedRelease activates by id only once confirmed, and offers nothing without staging; DeployProgress ends a stage at Staged; reportStaged and reportDeployFailure. deployOutcome.test.ts builds its streams through the new body, and NewApplication.test.tsx stubs the version read.
  • Gates, Node 24.21.0: npx vitest run (407 files, 3,827 tests, exit 0; the pre-commit hook ran it again), 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 was available to stage on, and the shared dev-server port was in use by other sessions.
  • Cross-model review: three rounds, codex (graded) and gemini. Round 1 found that the Activate callbacks were dropped when the detail closed mid-rollout, letting a reopened detail send a second activation; round 2 that an early-ended stream was toasted as a failure by the global handler next to the local warning. Both are fixed and round 3 returned LGTM. Gemini's round-1 blockers were wrong on evidence: every caller passes request (tsc -b passes), DeployPhase had no importers, and NewApplication.test.tsx mocks useImportApplication, so the omitted hook is never called. I declined one minor: a staged import leaves the import form in place rather than navigating, and its toast links the record. The Cursor legs could not run (the 1Password SSH agent refused their git fetch), and the Harper domain leg failed authentication.

Refs #1787 #1232

🤖 Generated with Claude Code

Related PRs: #1874 overlaps (base of this stack), #1868 overlaps (base of #1874), #1841 overlaps (changes the onDeployComponentSubmit arguments it wraps), #1871 overlaps (deploy failure toasts in the same callers)

Complexity: medium

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

Review-Attention: study ~15m (critical: schema.ts; raised: degraded review) @ 2d5c9e0

@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 staging and activating application deployments on Harper 5.3+ instances. It adds a 'Stage only' option to the import and redeploy interfaces, allowing users to install releases across nodes without immediately making them live. It also adds an 'Activate' button to the deployment details view to transition staged releases to live. The deploy_component API has been refactored to handle these new request types (deploy, stage, and activate), and comprehensive unit tests have been added to verify the new behavior. There are no review comments, so I have no feedback to provide.

@github-actions

github-actions Bot commented Oct 11, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 69.98% 11087 / 15842
🔵 Statements 70.14% 11829 / 16863
🔵 Functions 64.13% 2908 / 4534
🔵 Branches 65.35% 8586 / 13138
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/applications/components/DeployProgress/DeployProgress.tsx 84.21% 76.54% 83.33% 84.21% 57, 106-138, 284
src/features/instance/applications/components/NewApplication/ImportInstructions.tsx 60% 70% 42.85% 60% 37, 95-132, 166-176
src/features/instance/applications/components/NewApplication/schema.ts 100% 100% 100% 100%
src/features/instance/applications/hooks/useSupportsDeploymentSSE.ts 30.76% 66.66% 40% 30.76% 33-63
src/features/instance/applications/modals/reportDeployHealth.ts 84% 71.42% 100% 83.33% 62-66, 71-90
src/features/instance/config/deployments/components/ActivateStagedRelease.tsx 100% 78.57% 100% 100%
src/features/instance/config/deployments/components/DeploymentDetail.tsx 0% 0% 0% 0% 22-230
src/integrations/api/instance/applications/deployComponent.ts 95.83% 100% 83.33% 95.83% 117-121
src/integrations/api/instance/applications/deployComponentStream.ts 84.21% 57.89% 100% 84.21% 66-70
src/integrations/api/sse/errors.ts 100% 100% 100% 100%
src/integrations/api/sse/streamOperation.ts 79.36% 75% 66.66% 78.68% 129, 164-183
Generated in workflow #2218 for commit 377d6b0 by the Vitest Coverage Report Action

dawsontoth and others added 5 commits October 12, 2026 12:43
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/1787-stage-and-activate branch from 391cd02 to 1b1bbdb Compare October 12, 2026 16:45
@dawsontoth
dawsontoth merged commit 377d6b0 into stage Oct 12, 2026
6 checks passed
@dawsontoth
dawsontoth deleted the claude/1787-stage-and-activate branch October 12, 2026 16:45
dawsontoth added a commit that referenced this pull request Oct 13, 2026
…ailed

The rebase onto stage brought in a write surface this PR's caller audit
predates: activating a staged release (#1876's chain). Its mutation opts
out of the global toast, like import and redeploy since #1871, so an
activation that timed out or got a 504 showed "Failed to activate <app>"
with axios's own text and no warning that it may have gone through.

deployFailureTitle (new, beside the deploy reporters) is the one place a
deploy's failure title is decided: "Couldn't confirm <app> was activated"
for a write with no verdict, "Failed to activate <app>" otherwise. The
import form and the redeploy modal, which the rebase had given their own
copies of that rule, now use it too. reportDeployFailure shows the
unknown-outcome copy, and the activation's progress panel says so as
well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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