Repository navigation
feat(deploy): stage a release now and activate it later (Harper 5.3+) - #1876
Merged
dawsontoth merged 5 commits intoOct 12, 2026
Merged
dawsontoth merged 5 commits into
dawsontoth merged 5 commits into
Conversation
Contributor
There was a problem hiding this comment.
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.
This was referenced Oct 11, 2026
Merged
dawsontoth
added this pull request to stack #1901
October 11, 2026 21:25
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
force-pushed
the
claude/1787-stage-and-activate
branch
from
October 12, 2026 16:45
391cd02 to
1b1bbdb
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
⊙ 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_componentwithactivate: falsebuilds, installs and keeps a release on every node without making it live, anddeploy_component { project, deployment_id }makes it live later with nothing to install (harperv5.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.💡 Solution
All three request shapes come from one
deployRequestBodyover aDeployRequestunion, 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:activate: falseand norestart; Harper refusesrestartalongside it (v5.3.0:components/operationsValidation.js:517-524).package, install options andactivateare refused alongsidedeployment_id(:613-616). It goes through the same mutation, so feat(deploy): follow a rolling deploy's restart job, and show canary certification #1868 follows its restart job like any rolling deploy.Gating. Both are offered only when
registration_inforeads 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 acceptsactivate: falseas an unknown field and makes the release live (harpercomponents/operations.js:1014-1035explains 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:
DeployToastOptionsis no longer exported, the leftover minor from #1868's review.⚖️ Alternatives
stageandactivatemutations. 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.✅ Verification
deployRequestBodypins the three exact bodies, andonDeployComponentSubmitthat the buffered and streamed paths send the same one;supportsStagedDeploymentsrefuses 5.2.x, 5.3.0 prereleases, unreadable and missing versions;ActivateStagedReleaseactivates by id only once confirmed, and offers nothing without staging;DeployProgressends a stage at Staged;reportStagedandreportDeployFailure.deployOutcome.test.tsbuilds its streams through the new body, andNewApplication.test.tsxstubs the version read.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).request(tsc -bpasses),DeployPhasehad no importers, andNewApplication.test.tsxmocksuseImportApplication, 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 theirgit 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