Skip to content

feat(previews): turn on and manage PR previews from Studio - #2045

Open
dawsontoth wants to merge 11 commits into
stagefrom
claude/previews-panel-a6da02
Open

dawsontoth wants to merge 11 commits into
stagefrom
claude/previews-panel-a6da02

Conversation

@dawsontoth

@dawsontoth dawsontoth commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor
A beaver pulls one lever that raises a row of small tents while woodland friends peek in and an otter folds one away

⊙ 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 setup would 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.

  • Turn previews on with a repository, branch, data mode and limits. Harper's 409 shows as it comes.
  • See the status, each problem Harper reports, the live previews, and what to commit.
  • Delete a preview and see each node's result.
  • Disable previews after a confirmation. If it stops partway, the page says why and offers a rerun.

🤔 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=true does: set_secret, then grant_secret to <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_ref is 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 one gh api line.

🤔 The floor is 5.4.0-alpha.2, not 5.4

5.4.0-alpha.1 was cut before harper#3617, so it has no enable_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 sample shop app and two previews deployed by hand.

Off: turning previews on opens the form. The repository's id and default branch come from GitHub.

The Previews page for shop with the enable form filled in: repository HarperFast/harper-ecommerce-template found with its id, branch main, branched data, a seven-day life and a preview domain

On: the policy, the workflow to commit with its repository variable and environments, and the live previews.

The Previews page for shop once enabled: status, the same-site warning, the preview.yaml workflow that runs HarperFast/deploy-action@v1, and two live previews with delete buttons

Turning it off asks first, and says what it removes.

The confirmation dialog: Disable previews of shop?, explaining it revokes the CI identity and drops every preview

⚖️ Alternatives

  • A panel inside the editor — it ties into the editor's opened-file state. A Config route is linkable, like View Deployments.

🔧 Changes

✅ Verification

In the running app, against Harper main patched to alpha.2:

  • harperfast/studio.git enabled as HarperFast/studio, id 219919692, branch stage. A private name enabled with GitHub's spelling from the gh line, and a line for another repository was refused.
  • Harper's real 409 showed inline. A forced EPERM drop gave the real 500, and the rerun finished it.
  • On 5.4.0-alpha.1 the nav entry is gone and a bookmark shows a notice.
  • A token sealed against a scratch test custody decrypted to the pasted value, granted to shop-pr-*.
  • Gate: 626 test files, tsc, oxlint, dprint. Guards mutation-checked.
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
  • There is no workflow input, so a second application in one repository gets Harper's 409.
  • The enable answer seeds the cache unchecked. Harper's enablePreviews always returns the full policy.
  • An unread secrets list counts as no token for that moment.
  • If the components list fails, a deep link to a preview opens its panel.

🤖 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
  • minor — src/features/instance/config/previews/LivePreviews.tsx:118 — toast.success("Dropped …") fires even when a peer failed and was kept under keepReplicationFailures, 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 at LivePreviews.tsx:172 also hides an
  • minor — src/features/instance/config/previews/DisablePreviews.tsx:46 — describeDrop never reads DroppedPreview.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.
  • minor — src/features/instance/config/previews/starterPreviewWorkflow.ts:95 — The cleanup job hardcodes preview-cleanup, but step 3 at PreviewWorkflowInstructions.tsx:35 tells the user to create policy.cleanupEnvironment, and the deploy job already honours policy.environment. If the policy names a different cleanup environment, the cleanup OIDC token never matches the
  • minor — src/features/instance/config/previews/EnablePreviewsForm.tsx:174 — When the lookup is anything other than found (private repo, 403/429, network error), the typed spelling is sent. DESIGN.md:55-56 says workflow_ref is compared byte for byte, so enabling harperfast/studio succeeds but no Actions token from HarperFast/studio ever matches.
  • minor — src/features/instance/config/previews/EnablePreviewsForm.tsx:80 — useAudience ignores 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 a
  • minor — src/features/instance/config/previews/PreviewCredential.tsx:37 — clusterInfoUnavailable is discarded. When the cluster lookup fails, known stays false, so Seal token (:108) is disabled with no explanation.
  • minor — src/features/instance/config/previews/EnablePreviewsForm.tsx:38 — repositoryId is 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.
  • minor — src/features/instance/config/previews/ApplicationPreviews.tsx:39 — If a refetch of the policy fails, isError replaces the whole panel, including the "Run disable again" alert. Separately, after enable the switch briefly snaps off: setShowEnableForm(false) runs before refetch() lands.
  • minor — 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.
  • nit — src/features/instance/config/previews/starterPreviewWorkflow.ts:38 — app and domain go into the shell run: and environment: lines unquoted. A hostSuffix with a space or # corrupts the committed file. Disputed in degree: the super user who commits the file is the one who enters the value.
  • nit — src/features/instance/config/previews/PreviewPolicyStatus.tsx:53 — For an incomplete policy with grant: null, the status still shows "Shared" and "The cluster's default", describing a grant that doesn't exist.
  • nit — 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, because setValue doesn't mark the field dirty.
  • nit — src/features/instance/config/previews/index.tsx:64 — While the tree is loading, previewOf is undefined, so a bookmarked preview project mounts ApplicationPreviews and fetches its policy. Also, 'preview' in entry disagrees with the !entry.preview test in previewableApplications.
  • nit — src/features/instance/config/previews/index.tsx:55 — The notice says "5.4 or later" while the floor is 5.4.0-alpha.2, so an alpha.1 instance gets a confusing message.
  • nit — 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.
  • nit — src/features/instance/config/previews/PreviewCredential.tsx:59 — If set_secret succeeds and grant_secret fails, a row is left with no grant. Disputed in degree: the badge correctly reads "Not set", and retrying redoes both calls.
  • nit — src/features/instance/config/previews/ApplicationPreviews.tsx:34 — list_secrets is fetched even when previews are off. This is not a hot path.
  • nit — src/features/instance/config/previews/PreviewWorkflowInstructions.tsx:67 — policy.repository goes into hrefs raw, while PreviewPolicyStatus and LivePreviews run it through parseGitHubRepository. The https://github.com/ prefix is fixed, so the worst case is a wrong path.
  • nit — 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 under config/ alone).
  • nit — 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:30 and previews.ts:201-217.
  • nit — src/features/instance/config/previews/PreviewCredential.tsx:59 — The plaintext PAT stays in the MutationCache variables until GC. This is how useSetSecret already treats every secret, and mutationErrorHandler ignores 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
  • minor — 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

Review-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

dawsontoth and others added 7 commits October 30, 2026 12:51
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>

@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 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.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 82.62% 17291 / 20928
🔵 Statements 82.51% 18353 / 22242
🔵 Functions 78.11% 4732 / 6058
🔵 Branches 77.74% 13957 / 17952
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/features/instance/config/index.tsx 15.78% 0% 0% 15.78% 38-251
src/features/instance/config/routes.ts 67.27% 100% 51.35% 67.27% 23, 30, 38, 44, 51, 57, 64, 71, 78, 84, 92, 99, 107, 114, 120, 127, 134, 140
src/features/instance/config/previews/ApplicationPreviews.tsx 97.05% 97.14% 85.71% 97.05% 148
src/features/instance/config/previews/DisablePreviews.tsx 100% 84.61% 100% 100%
src/features/instance/config/previews/EnablePreviewsForm.tsx 98.59% 95.71% 100% 98.57% 164
src/features/instance/config/previews/LivePreviews.tsx 92.85% 88.7% 81.81% 92.85% 134, 158, 192
src/features/instance/config/previews/PageNotice.tsx 100% 100% 100% 100%
src/features/instance/config/previews/PreviewCredential.tsx 95.23% 100% 77.77% 95.12% 84-85
src/features/instance/config/previews/PreviewPolicyStatus.tsx 100% 100% 100% 100%
src/features/instance/config/previews/PreviewWarnings.tsx 100% 100% 100% 100%
src/features/instance/config/previews/PreviewWorkflowInstructions.tsx 80% 81.81% 33.33% 80% 27, 73-105
src/features/instance/config/previews/index.tsx 92% 96.15% 60% 91.66% 40-44, 87
src/features/instance/config/previews/previewAudience.ts 100% 100% 100% 100%
src/features/instance/config/previews/previewsSearch.ts 100% 100% 100% 100%
src/features/instance/config/previews/starterPreviewWorkflow.ts 100% 100% 100% 100%
src/features/instance/config/previews/usePreviewsAvailable.ts 100% 100% 100% 100%
src/features/instance/config/roles/operations/operationsCatalog.ts 100% 97.36% 100% 100%
src/features/instance/config/secrets/index.tsx 0% 0% 0% 0% 26-164
src/features/instance/secrets/useSecretsKeySource.ts 100% 100% 100% 100%
src/integrations/api/replication.ts 100% 100% 100% 100%
src/integrations/api/instance/applications/getComponents.ts 100% 100% 100% 100%
src/integrations/api/instance/previews/previews.ts 100% 96.77% 100% 100%
src/integrations/api/instance/secrets/secrets.ts 100% 94.73% 100% 100%
src/integrations/github/lookUpGitHubRepository.ts 100% 100% 100% 100%
Generated in workflow #2505 for commit c132f42 by the Vitest Coverage Report Action

…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>
@dawsontoth
dawsontoth marked this pull request as ready for review November 3, 2026 13:41
@dawsontoth
dawsontoth requested a review from a team as a code owner November 3, 2026 13:41

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 Reviewed with Muse

dawsontoth and others added 3 commits November 3, 2026 10:12
- 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>
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.

PR previews: Previews panel for an application

2 participants