Skip to content

feat: upgrade all environments button#2941

Open
kmendell wants to merge 1 commit into
mainfrom
feat/update-all
Open

feat: upgrade all environments button#2941
kmendell wants to merge 1 commit into
mainfrom
feat/update-all

Conversation

@kmendell

@kmendell kmendell commented Jun 14, 2026

Copy link
Copy Markdown
Member

Checklist

  • This PR is not opened from my fork’s main branch

What This PR Implements

Fixes: #2574

Changes Made

Testing Done

AI Tool Used (if applicable)

Additional Context

Disclaimer Greptiles Reviews use AI, make sure to check over its work.

To better help train Greptile on our codebase, if the comment is useful and valid Like the comment, if its not helpful or invalid Dislike

To have Greptile Re-Review the changes, mention greptileai.

Greptile Summary

This PR implements a "Update All" button on the Environments page that triggers a fleet-wide Arcane self-upgrade: the manager upgrades itself first (job persisted as pending_restart), restarts, and then on the next boot resumes upgrading each remote agent sequentially via a new EnvironmentUpdateJob model and migration.

  • Backend: New StartUpdateAll/ResumeUpdateAllOnStartup/runAgentsPhaseInternal orchestration in SystemUpgradeService, backed by a new environment_update_jobs DB table and two API endpoints (POST /environments/{id}/system/upgrade/all, GET .../all/status). Concurrent-call guard via updatingAll atomic + DB-level active-job check.
  • Frontend: New update-all-dialog.svelte with phased UI (confirm → running → finished), live status polling with reconnect handling, and the deleted environment-upgrade-service.ts consolidated into system-upgrade-service.ts.

Confidence Score: 4/5

Safe to merge after fixing the dialog race condition; backend orchestration is solid.

The backend orchestration, persistence, and concurrency guards are well thought out. The single issue that needs attention is in the dialog: if the user dismisses the dialog (via Escape or the Close button) while the initial triggerUpdateAll() POST is still in flight, handleConfirm resumes after the await and unconditionally sets pollActive = true, restarting background polling against a closed dialog for the lifetime of the page.

frontend/src/routes/(app)/environments/update-all-dialog.svelte — handleConfirm needs a dismissal guard after the async triggerUpdateAll() call.

Comments Outside Diff (1)

  1. frontend/src/routes/(app)/environments/update-all-dialog.svelte, line 1369-1374 (link)

    P2 $state updated inside $effect

    reset() assigns to phase, job, and reconnecting, all of which are $state. Updating reactive state from inside an $effect is explicitly flagged in the project's Svelte guidelines because it can cause unnecessary re-renders or subtle cycles. The intent here — "reset state when the dialog transitions from closed to open" — is better modelled as a reactive guard in handleConfirm / handleClose, or by factoring the effect so the side-effecting stopPolling() and the state mutations are kept separate.

    Rule Used: What: Avoid updating $state inside $effect blo... (source)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: frontend/src/routes/(app)/environments/update-all-dialog.svelte
    Line: 1369-1374
    
    Comment:
    **`$state` updated inside `$effect`**
    
    `reset()` assigns to `phase`, `job`, and `reconnecting`, all of which are `$state`. Updating reactive state from inside an `$effect` is explicitly flagged in the project's Svelte guidelines because it can cause unnecessary re-renders or subtle cycles. The intent here — "reset state when the dialog transitions from closed to open" — is better modelled as a reactive guard in `handleConfirm` / `handleClose`, or by factoring the effect so the side-effecting `stopPolling()` and the state mutations are kept separate.
    
    **Rule Used:** What: Avoid updating `$state` inside `$effect` blo... ([source](https://app.greptile.com/ofkm/-/custom-context?memory=8e0bee41-b073-4a49-a01c-2c2c8782b420))
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Codex Fix in Claude Code

Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
frontend/src/routes/(app)/environments/update-all-dialog.svelte:71-88
**`handleConfirm` races with dismiss: polling starts after dialog is closed**

`triggerUpdateAll()` is awaited without any guard for mid-flight dismissal. If the user presses Escape (or the Close button) while the initial POST is in flight, `resetState()` sets `pollActive = false` and `phase = 'confirm'`. When the await resumes, the code unconditionally sets `pollActive = true` and calls `schedulePoll()`, restarting background polling even though the dialog is visually closed. Since the component is never conditionally rendered, `onDestroy` won't fire until page navigation, so the polls continue indefinitely. Adding an early-exit check like `if (phase !== 'running') return;` immediately after the `await` (and after the `catch` block) closes the window.

Reviews (3): Last reviewed commit: "feat: upgrade all environments button" | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

kmendell commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@kmendell kmendell marked this pull request as ready for review June 14, 2026 22:25
@kmendell kmendell requested a review from a team June 14, 2026 22:25
@getarcaneappbot

getarcaneappbot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Container images for this PR have been built successfully!

  • Manager: ghcr.io/getarcaneapp/manager:pr-2941
  • Agent: ghcr.io/getarcaneapp/agent:pr-2941

Built from commit d6a6d37

@graphite-app

graphite-app Bot commented Jun 14, 2026

Copy link
Copy Markdown

Graphite Automations

"Warn authors when publishing large PRs" took an action on this PR • (06/14/26)

1 teammate was notified to this PR based on Kyle Mendell's automation.

Comment thread backend/internal/services/system_upgrade_service.go
Comment thread backend/internal/models/environment_update_job.go
Comment thread backend/internal/services/system_upgrade_service.go
Comment on lines +71 to +88
async function handleConfirm() {
phase = 'running';
reconnecting = false;
try {
job = await systemUpgradeService.triggerUpdateAll();
} catch {
toast.error(m.environments_update_all_trigger_failed());
resetState();
return;
}

if (job && (job.status === 'completed' || job.status === 'failed')) {
phase = 'finished';
return;
}

pollActive = true;
schedulePoll();

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.

P1 handleConfirm races with dismiss: polling starts after dialog is closed

triggerUpdateAll() is awaited without any guard for mid-flight dismissal. If the user presses Escape (or the Close button) while the initial POST is in flight, resetState() sets pollActive = false and phase = 'confirm'. When the await resumes, the code unconditionally sets pollActive = true and calls schedulePoll(), restarting background polling even though the dialog is visually closed. Since the component is never conditionally rendered, onDestroy won't fire until page navigation, so the polls continue indefinitely. Adding an early-exit check like if (phase !== 'running') return; immediately after the await (and after the catch block) closes the window.

Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/routes/(app)/environments/update-all-dialog.svelte
Line: 71-88

Comment:
**`handleConfirm` races with dismiss: polling starts after dialog is closed**

`triggerUpdateAll()` is awaited without any guard for mid-flight dismissal. If the user presses Escape (or the Close button) while the initial POST is in flight, `resetState()` sets `pollActive = false` and `phase = 'confirm'`. When the await resumes, the code unconditionally sets `pollActive = true` and calls `schedulePoll()`, restarting background polling even though the dialog is visually closed. Since the component is never conditionally rendered, `onDestroy` won't fire until page navigation, so the polls continue indefinitely. Adding an early-exit check like `if (phase !== 'running') return;` immediately after the `await` (and after the `catch` block) closes the window.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Codex Fix in Claude Code

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.

⚡️ Feature: Update all environments button

2 participants