Repository navigation
feat(previews): turn on and manage PR previews from Studio - #2045
dawsontoth wants to merge 11 commits into
Conversation
A super user on Harper 5.4+ can now enable previews of an application, see their status and what's live, delete one, disable them, and read what to commit. Studio calls the cluster's own operations, so previews stay the cluster's: enable_previews, disable_previews and get_preview_policy, plus get_components and drop_component. The floor is 5.4.0-alpha.2: alpha.1 was cut before harper#3617 and has no such operations. A private repository's token is sealed in the browser, as the CLI does it, rather than sending people to a checkout. The repository lookup lives in integrations/github so Deploy from GitHub (#1788) can share it. Secrets custody's key source moves to a hook the Secrets page and this panel both use. Closes #2044 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A drop replicates from any page of a cluster, so an instance page no longer leaves the preview on the other nodes. - The workflow uses the cleanup environment the policy pins. - A partial drop no longer toasts success, and a drop or disable with no answer reads as outcome unknown instead of as a failure. - A failed refetch no longer removes the panel and its rerun. - The sealed token leaves the mutation cache, and the panel says so when it can't tell which key to seal with. - The form drops an id left behind by an earlier repository, and goes back to main when the next one has no default branch to offer. - A deep link waits for the tree before showing a preview's panel. - Prerelease clusters are told which version to install, and the GitHub rule's date is the one GitHub now gives: 2026-12-02. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A name GitHub didn't find went stale at once, so each return to the tab spent another of the 60 lookups an hour every process at this address shares. Only a new name, or a minute passing, spends one now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Enable's answer is the policy, so a failed read right after it no longer flips the switch back off. - A token stored but not granted reads as "Not granted", and disable still mentions it. - A drop waits past Harper's own limit for a peer, so a hung peer comes back as that peer's result rather than as no answer. - A disable's per-node result carries what became of branched storage. - A setup with no grant claims no data mode or limit. - The page says no secret is stored in GitHub, which a sealed token on the cluster doesn't contradict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
workflow_ref is compared byte for byte, and a private repository is invisible to the anonymous lookup, so enable could send the name as typed and leave a policy no CI token would ever match. The form now takes the id and name together from one gh line, and refuses a line for another repository. - A prerelease cluster's workflow installs that cluster's version, since harper@^5.4 matches no prerelease. - A disable that landed reads as off even if the read after it fails. - The token leaves the mutation cache before the grant goes out. - A disable's per-node result shows Harper's own message. - The GitHub lookup gives up after 10 seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A live list that could not be read says so, rather than "None yet". - A sealed and granted token stays sealed on the page if the read after it fails, so the workflow keeps credential=true. - An application with no previews reads no secrets. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setQueryData doesn't cancel a fetch already running, so a read started before enable answered could land after it and show previews as off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces the "PR Previews" feature for Harper 5.4+, enabling users to configure, enable, disable, and manage pull request previews directly from Harper Studio. It adds several new components, hooks, API integrations, and tests under src/features/instance/config/previews and related directories, while updating existing navigation, routing, and operations catalogs. Feedback on the changes highlights a high-severity issue in EnablePreviewsForm.tsx where a validation error on the hidden repositoryIdentity field can persist and block form submission without any visible feedback when switching to a public repository. It is recommended to clear this error when needsId becomes false and ensure the schema validation is updated accordingly.
…able Pins that a stale setError on repositoryIdentity, hidden once the repository is found, doesn't stop the next submit: react-hook-form replaces errors with the resolver's on submit, and the schema puts no rule on that field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The workflow waits for the secrets read before showing, so a file copied early can't leave out a sealed token's credential=true, and it says so when the read fails. - The enable form waits for the cluster's details instead of briefly saying it doesn't know the cluster's URL. - A preview's Application menu opens the previews of the application it belongs to, rather than a panel that can't do anything for it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The workflow the panel shows is now HarperFast/deploy-action's examples/preview.yaml, with the policy's application, branch and environments, instead of a copy of harper's shell starter. The action holds the deploy/drop lifecycle, keeps the PR comment and Deployment, and picks the CLI version, so the panel no longer reads the cluster's version to pin one. Only the deploy job names domain, credential and branched_databases, since a drop reads none of them. It names @v1, which exists once HarperFast/deploy-action#1 merges and is tagged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
app, domain and both environment names went into the YAML unquoted, so a host suffix holding a newline could add a step to a job that holds id-token: write. They now go through JSON.stringify, as the branch already did. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
⊙ Problem
Harper 5.4 can run a preview for each PR of an application, but turning that on takes the CLI, run from a checkout. Studio can't enable previews, show what's running, or remove them. Closes #2044.
🤔 Does this need a full panel, not a status page and a CLI command?
The epic's goal is one toggle in Studio plus one workflow file. Status plus
harper preview setupwould be far smaller, but the CLI needs a checkout of the repository. Most of the panel is the form, and the states Harper can report.💡 Solution
A new Previews page, under Config and in the editor's Application menu, for super users on 5.4+. It calls the cluster directly.
🤔 Private repositories: the panel seals the token
The CLI needs a checkout, which breaks one toggle and one workflow file. Studio already encrypts secrets in the browser, so the panel does what
harper preview setup credential=truedoes:set_secret, thengrant_secretto<app>-pr-*. GitHub answers a private repository with a 404, so the panel then asks for a token.🤔 Repository id: one lookup, in
integrations/github#1788 isn't built, so the lookup sits outside this page for that panel to import.
workflow_refis compared byte for byte, so the spelling must be GitHub's too. For a repository GitHub won't show, the form takes the id and name from onegh apiline.🤔 The floor is 5.4.0-alpha.2, not 5.4
5.4.0-alpha.1was cut before harper#3617, so it has noenable_previews.🤔 The workflow runs deploy-action, so this merges after it
The file to commit uses
HarperFast/deploy-action@v1, which exists once HarperFast/deploy-action#1 merges and is tagged.🤔 Deleting from one instance's page drops the preview everywhere
A preview is on every node, so any page with a cluster in its route sends
replicated: true.📸 Screenshots
Taken against a local Harper built from
main(its version set to 5.4.0-alpha.2), with a sampleshopapp and two previews deployed by hand.Off: turning previews on opens the form. The repository's id and default branch come from GitHub.
On: the policy, the workflow to commit with its repository variable and environments, and the live previews.
Turning it off asks first, and says what it removes.
⚖️ Alternatives
🔧 Changes
previewandhost; replication.ts addskeepReplicationFailures; secrets.ts drops a sealed value from the mutation cache.✅ Verification
In the running app, against Harper
mainpatched to alpha.2:harperfast/studio.gitenabled asHarperFast/studio, id219919692, branchstage. A private name enabled with GitHub's spelling from theghline, and a line for another repository was refused.EPERMdrop gave the real 500, and the rerun finished it.5.4.0-alpha.1the nav entry is gone and a bookmark shows a notice.shop-pr-*.Tests
Not verified: a real Actions run or OIDC exchange, a multi-node cluster, Fabric sign-in, and Central Manager custody.
Known limits, and review findings left open
workflowinput, so a second application in one repository gets Harper's 409.enablePreviewsalways returns the full policy.🤖 Generated with Claude Code
Related PRs: #2031 overlaps (both add to the Config nav and routes), 2 others independent
Complexity: complicated
Late Grok findings
21 surviving (highest minor) — Grok reviewed d92c4c7
src/features/instance/config/previews/LivePreviews.tsx:118—toast.success("Dropped …")fires even when a peer failed and was kept underkeepReplicationFailures, or when Harper says the branched copy "was left in place". The inline alert says the preview was not fully dropped while the toast says it was. The confirm atLivePreviews.tsx:172also hides ansrc/features/instance/config/previews/DisablePreviews.tsx:46—describeDropnever readsDroppedPreview.message. When a node's drop text says the branched storage was kept, the row still reads "dropped", so a disable hides the retained database copy.src/features/instance/config/previews/starterPreviewWorkflow.ts:95— The cleanup job hardcodespreview-cleanup, but step 3 atPreviewWorkflowInstructions.tsx:35tells the user to createpolicy.cleanupEnvironment, and the deploy job already honourspolicy.environment. If the policy names a different cleanup environment, the cleanup OIDC token never matches thesrc/features/instance/config/previews/EnablePreviewsForm.tsx:174— When the lookup is anything other thanfound(private repo, 403/429, network error), the typed spelling is sent.DESIGN.md:55-56saysworkflow_refis compared byte for byte, so enablingharperfast/studiosucceeds but no Actions token fromHarperFast/studioever matches.src/features/instance/config/previews/EnablePreviewsForm.tsx:80—useAudienceignores whether the cluster query is still pending or has failed (retry: false). On a Fabric Connect session the auth-store URL is the proxy's, so the form is replaced by "doesn't know this cluster's operations URL" (:187-196). That happens briefly while loading, and stays until asrc/features/instance/config/previews/PreviewCredential.tsx:37—clusterInfoUnavailableis discarded. When the cluster lookup fails,knownstays false, so Seal token (:108) is disabled with no explanation.src/features/instance/config/previews/EnablePreviewsForm.tsx:38—repositoryIdis validated by the schema even while its field is hidden (:221). Type a bad id, then change the repository to one that is found: the field disappears, zod still fails, and submit does nothing with no visible error.src/features/instance/config/previews/ApplicationPreviews.tsx:39— If a refetch of the policy fails,isErrorreplaces the whole panel, including the "Run disable again" alert. Separately, after enable the switch briefly snaps off:setShowEnableForm(false)runs beforerefetch()lands.src/features/instance/config/previews/PreviewWorkflowInstructions.tsx:109— The 2026-11-02 enforcement date is reported to have moved to 2026-12-02. That source is past my knowledge cutoff, so I couldn't verify it; keep the finding until someone checks the changelog.src/features/instance/config/previews/starterPreviewWorkflow.ts:38—appanddomaingo into the shellrun:andenvironment:lines unquoted. AhostSuffixwith a space or#corrupts the committed file. Disputed in degree: the super user who commits the file is the one who enters the value.src/features/instance/config/previews/PreviewPolicyStatus.tsx:53— For an incomplete policy withgrant: null, the status still shows "Shared" and "The cluster's default", describing a grant that doesn't exist.src/features/instance/config/previews/EnablePreviewsForm.tsx:140— The default branch from repo A survives a switch to repo B when B isn't found, becausesetValuedoesn't mark the field dirty.src/features/instance/config/previews/index.tsx:64— While the tree is loading,previewOfis undefined, so a bookmarked preview project mountsApplicationPreviewsand fetches its policy. Also,'preview' in entrydisagrees with the!entry.previewtest inpreviewableApplications.src/features/instance/config/previews/index.tsx:55— The notice says "5.4 or later" while the floor is5.4.0-alpha.2, so an alpha.1 instance gets a confusing message.src/features/instance/config/previews/LivePreviews.tsx:28— A timeout or network error is titled "was not fully dropped" (and "did not finish disabling" for disable), when the actual outcome is unknown.src/features/instance/config/previews/PreviewCredential.tsx:59— Ifset_secretsucceeds andgrant_secretfails, a row is left with no grant. Disputed in degree: the badge correctly reads "Not set", and retrying redoes both calls.src/features/instance/config/previews/ApplicationPreviews.tsx:34—list_secretsis fetched even when previews are off. This is not a hot path.src/features/instance/config/previews/PreviewWorkflowInstructions.tsx:67—policy.repositorygoes into hrefs raw, whilePreviewPolicyStatusandLivePreviewsrun it throughparseGitHubRepository. Thehttps://github.com/prefix is fixed, so the worst case is a wrong path.src/features/instance/config/previews/ApplicationPreviews.tsx:17— Relative imports have no extensions, against the Harper checklist. Disputed: the Studio repo is extensionless throughout (54 relative imports underconfig/alone).src/features/instance/config/previews/index.tsx:18— Several added comments restate their identifiers: here,PreviewCredential.tsx:20,PreviewPolicyStatus.tsx:18,PreviewWorkflowInstructions.tsx:14,previewsSearch.ts:2,usePreviewsAvailable.ts:7,previews.ts:30andpreviews.ts:201-217.src/features/instance/config/previews/PreviewCredential.tsx:59— The plaintext PAT stays in the MutationCache variables until GC. This is howuseSetSecretalready treats every secret, andmutationErrorHandlerignores the variables.Not standing: 1 merged, 1 accepted-known. Grok ran 17m (17m from launch to report); the fold adjudication (
cursor-grok@d92c4c7b18ce-8e283a17, claude) took 3m.Late Grok findings
1 surviving (highest minor) — Grok reviewed 8eaf8f2
src/features/instance/config/previews/ApplicationPreviews.tsx:114— if the policy refetch fails after a successful enable, the form closes over a cached "off" policy. The switch flips off and the form unmounts while the cluster has previews on, and the Enable button can be clicked again while the refetch runs.Not standing: none. Grok ran 16m (16m from launch to report); the fold adjudication (
cursor-grok@8eaf8f20c7d7-59c95f64, claude) took 1m.Deferred findings
f1d6177ca3e258b0nit maintainability —src/features/instance/config/previews/PageNotice.tsx— shared PageNotice added while identical local copies remain — filed Share one PageNotice across the Trust Policies, Firewall and Previews pages #2053Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex,cursor-grok; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=9; full=7 @ c132f42
Review-Attention: deep ~35m (critical: replication.ts; sensitive: index.tsx, useSecretsKeySource.ts +1; decisions: do-less-alternative, setup-state-from-problem-text, workflow-delegates-to-deploy-action, token-sealed-in-browser, optimistic-cache-seeding, previews-under-config, json-quoting-over-hostname-validation, seal-credential-in-browser, workflow-via-deploy-action, seed-cache-from-mutation-answer) @ c132f42