Skip to content

fix(deploy): consolidate deployment detection into single source of truth#3606

Merged
waleedlatif1 merged 11 commits intostagingfrom
waleedlatif1/fix-deploy-detection
Mar 16, 2026
Merged

fix(deploy): consolidate deployment detection into single source of truth#3606
waleedlatif1 merged 11 commits intostagingfrom
waleedlatif1/fix-deploy-detection

Conversation

@waleedlatif1
Copy link
Collaborator

@waleedlatif1 waleedlatif1 commented Mar 16, 2026

Summary

  • Consolidates deployment detection into a single source of truth — one query hook (useDeploymentInfo), one mutation hook (useDeployWorkflow), one server function (checkNeedsRedeployment), one invalidation helper (invalidateDeploymentQueries)
  • Editor header uses client-side comparison via hasWorkflowChanged for instant badge updates with zero API calls during editing. The deployed state snapshot is fetched once via React Query and refreshed after deploy/undeploy/version-activate mutations.
  • Child workflow badges use server-side useDeploymentInfo with 30s staleTime (they don't have Zustand state for client comparison)
  • Removes 503 lines of duplicate/dead code: useDeployedState hook (110 lines), useChildDeploymentStatus, useDeployChildWorkflow, fetchChildDeploymentStatus, inline comparison logic in deploy/status routes

Before

  • Two separate code paths for deployment detection (editor header vs child badges) with different APIs, different caching, different invalidation → inconsistent "needs redeploy" signals (false positives)
  • Child badge made 2 API calls per render (/status + /deployments) with staleTime: 0 and cache: 'no-store'
  • Inline comparison logic duplicated in both /deploy and /status route handlers

After

  • One shared checkNeedsRedeployment() for both server endpoints
  • One useDeploymentInfo hook shared by all badge consumers (30s staleTime, React Query deduplication)
  • Editor header: pure useMemo client comparison — zero network during editing
  • All mutations use invalidateDeploymentQueries() for consistent cache refresh

Test plan

  • Deploy a workflow → button shows "Live"
  • Edit a block value → button instantly shows "Update"
  • Move a block (position only) → button stays "Live" (positions excluded by comparison)
  • Click Update → redeploy → button shows "Live"
  • Verify no API calls to /api/workflows/{id}/deploy during editing (Network tab)
  • Child workflow badges show correct "undeployed"/"redeploy" status
  • Tool-input deploy badges work correctly
  • TypeScript compiles clean (bunx tsc --noEmit)

@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all 50 Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To keep getting Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 16, 2026 9:26pm

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR eliminates two separate, inconsistent deployment-detection code paths by consolidating them into a single server-side checkNeedsRedeployment utility and a shared client-side React Query layer (useDeploymentInfo, useDeployedWorkflowState, useDeployWorkflow, invalidateDeploymentQueries). The result is ~503 lines removed, one fewer API call per child workflow badge render, and consistent cache invalidation across all mutation paths.

Key changes:

  • Server: checkNeedsRedeployment in app/api/workflows/utils.ts is now the single source of truth for both /deploy (GET) and /status route handlers.
  • Client — editor header: useDeployedState (manual fetch + local state) replaced by useDeployedWorkflowState (React Query with 30s staleTime + AbortSignal); change comparison stays client-side via useMemo in useChangeDetection.
  • Client — child/tool badges: useChildDeploymentStatus (2 API calls, staleTime 0) replaced by the shared useDeploymentInfo (1 API call, 30s staleTime with React Query deduplication).
  • Mutations: All three mutation hooks (useDeployWorkflow, useUndeployWorkflow, useActivateDeploymentVersion) now call invalidateDeploymentQueries for consistent cache refresh.
  • Three style-level issues found: isFetchingDeployedState in deploy.tsx suppresses the "Update" badge during legitimate background refetches; invalidateDeploymentQueries is not awaited in handleChatDeployed; useRevertToVersion in workflows.ts still manually invalidates the three deployment queries instead of using the new helper.

Confidence Score: 4/5

  • Safe to merge — no correctness bugs found; all issues are minor style/UX edge cases.
  • The refactor is well-structured with no critical logic errors. The isFetchingDeployedState guard causes a brief badge flicker during rare window-focus refetches, and useRevertToVersion bypasses the new invalidateDeploymentQueries helper, but neither impacts correctness. Previous review threads have been resolved. TypeScript compiles clean per the PR author.
  • apps/sim/app/workspace/.../deploy/deploy.tsx (isFetching guard) and apps/sim/hooks/queries/workflows.ts (missed consolidation in useRevertToVersion).

Important Files Changed

Filename Overview
apps/sim/app/api/workflows/utils.ts Adds checkNeedsRedeployment as the single server-side source of truth for redeployment detection, consolidating previously duplicated DB queries from the /deploy and /status routes. Sequential-then-parallel query pattern is correct and efficient.
apps/sim/hooks/queries/deployments.ts Adds useDeployedWorkflowState, invalidateDeploymentQueries, and the deployedState query key. Mutation onSuccess handlers now use invalidateDeploymentQueries for consistent cache invalidation. Minor issue: useActivateDeploymentVersion uses onSettled (not onSuccess) for invalidation — intentional to cover error cases, but means setDeploymentStatus (in onSuccess) and invalidation fire separately, which is fine.
apps/sim/hooks/queries/workflows.ts Removes 144 lines of duplicated useChildDeploymentStatus / useDeployChildWorkflow / fetchChildDeploymentStatus. useRevertToVersion now invalidates deploymentKeys.deployedState but still calls the three deployment invalidations individually instead of using the new invalidateDeploymentQueries helper.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx Replaces the old useDeployedState hook with useDeployedWorkflowState from React Query. The isFetchingDeployedState flag is folded into isLoadingDeployedState to suppress comparison during post-deploy background refetches, but this also suppresses the "Update" badge during unrelated window-focus refetches when stale data is still valid.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/hooks/use-change-detection.ts Adds deployedState as a memo dependency guard, skipping the expensive mergeSubblockStateWithValues computation when no deployed baseline exists. Logic is clean and correct.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx Removes refetchDeployedState prop and all manual post-mutation refetch calls, relying on React Query invalidation from mutation onSuccess. handleChatDeployed calls invalidateDeploymentQueries without awaiting it in an async function.

Sequence Diagram

sequenceDiagram
    participant EditorHeader as Deploy Button (Editor)
    participant ChildBadge as Child/Tool Badge
    participant RQ as React Query Cache
    participant DeployAPI as /api/workflows/[id]/deploy (GET)
    participant StatusAPI as /api/workflows/[id]/status (GET)
    participant DeployedAPI as /api/workflows/[id]/deployed (GET)
    participant Utils as checkNeedsRedeployment()

    Note over EditorHeader: On mount (isDeployed=true)
    EditorHeader->>RQ: useDeployedWorkflowState (staleTime 30s)
    RQ->>DeployedAPI: fetch deployed snapshot
    DeployedAPI-->>RQ: WorkflowState (baseline)
    RQ-->>EditorHeader: deployedState
    EditorHeader->>EditorHeader: useMemo hasWorkflowChanged(current, deployed)

    Note over ChildBadge: On mount
    ChildBadge->>RQ: useDeploymentInfo (staleTime 30s)
    RQ->>DeployAPI: GET /deploy
    DeployAPI->>Utils: checkNeedsRedeployment()
    Utils-->>DeployAPI: needsRedeployment: bool
    DeployAPI-->>RQ: {isDeployed, needsRedeployment, ...}
    RQ-->>ChildBadge: deployment info

    Note over EditorHeader: User clicks Deploy/Update
    EditorHeader->>RQ: useDeployWorkflow.mutateAsync()
    RQ->>DeployAPI: POST /deploy
    DeployAPI-->>RQ: success
    RQ->>RQ: invalidateDeploymentQueries(info+deployedState+versions)
    RQ->>RQ: invalidate workflowKeys.state
    RQ-->>EditorHeader: re-fetch deployedState (new baseline)
    RQ-->>ChildBadge: re-fetch deploymentInfo
Loading

Comments Outside Diff (3)

  1. apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/deploy.tsx, line 47 (link)

    Background refetch suppresses changeDetected while stale data is still valid

    isFetchingDeployedState is true during any background refetch — including the 30-second stale-time window-focus refetch. When it's passed through as isLoadingDeployedState: true, useChangeDetection returns false unconditionally, so the editor header button briefly snaps from "Update" → "Live" for the ~1-2 s the refetch takes, even though the old deployedState in the cache is still a valid comparison baseline.

    This is intentional for the post-deploy scenario (prevents a spurious "Update" flash while the new baseline loads), but it's an over-broad guard for background refetches. Separating the two cases makes the intent explicit:

    // isLoading is only true on the INITIAL fetch (no cached data yet).
    // isFetching is also true on background refetches that still have old data.
    // Pass only isLoading to avoid suppressing changeDetected when valid cached
    // data is present during a background stale-time refresh.
    isLoadingDeployedState: isLoadingDeployedState,

    If the post-deploy "Update" flash is a concern, it can be addressed separately (e.g. an isJustDeployed ref set in the mutation onSuccess) rather than blanket-suppressing all background fetches.

  2. apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal.tsx, line 384 (link)

    invalidateDeploymentQueries result not awaited

    handleChatDeployed is an async function, but invalidateDeploymentQueries (which returns Promise.all([...])) is called without await. While there are no subsequent operations in this callback that depend on the invalidation completing, leaving a floating promise in an async function is easy to miss, silently swallows any rejection, and sets a confusing precedent — especially since the previous code explicitly await-ed refetchDeployedState() at the same call site.

  3. apps/sim/hooks/queries/workflows.ts, line 517-528 (link)

    useRevertToVersion bypasses the new invalidateDeploymentQueries helper

    The three deployment-related invalidateQueries calls on these lines (for deploymentKeys.info, deploymentKeys.deployedState, and deploymentKeys.versions) are exactly what invalidateDeploymentQueries wraps, but this site calls them individually. If a new deployment query key is ever added to that helper it will silently be missed here.

    Consider consolidating to use the shared helper (imported from @/hooks/queries/deployments) alongside the existing workflowKeys.state invalidation — the same way the other mutation hooks in deployments.ts do after this PR.

Last reviewed commit: f7cfa26

@waleedlatif1 waleedlatif1 force-pushed the waleedlatif1/fix-deploy-detection branch from 6f6e99b to 1fcff2b Compare March 16, 2026 19:36
- Remove redundant isLoading check (subset of isPending in RQ v5)
- Make deployedState prop nullable to avoid non-null assertion
- Destructure mutateAsync to eliminate ESLint suppression
- Guard debounced invalidation against stale workflowId on switch
…tion dep

- Remove self-explanatory comments in deploy.tsx, tool-input.tsx, use-child-workflow.ts
- Tighten non-obvious comments in use-change-detection.ts
- Destructure mutate/isPending in WorkflowToolDeployBadge to avoid unstable
  mutation object in useCallback deps (TanStack Query no-unstable-deps pattern)
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all 50 Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To keep getting Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

Avoid running mergeSubblockStateWithValues on every render for
non-deployed workflows where changeDetected always returns false.
@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

bugbot run

@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all 50 Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To keep getting Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

Pre-existing type error — workflow table was used but not imported.
Match the pattern used by all other fetch helpers in the file so
in-flight requests are cancelled on component unmount or query re-trigger.
All three queries (active version, normalized data, workflow variables)
now run concurrently via Promise.all, saving one DB round trip on the
common path.
@waleedlatif1
Copy link
Collaborator Author

@greptile

…ver polling

The lastSaved-based server polling was triggering API calls on every
local store mutation (before socket persistence), wasting requests and
checking stale DB state. Revert the editor header to pure client-side
hasWorkflowChanged comparison — zero network during editing, instant
badge updates. Child workflow badges still use server-side
useDeploymentInfo (they don't have Zustand state).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

bugbot run

@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@waleedlatif1
Copy link
Collaborator Author

@cursor review

@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@waleedlatif1
Copy link
Collaborator Author

@greptile

…fetch

Guard change detection on isFetching (not just isLoading) so the
comparison is suppressed during background refetches after mutations,
preventing a brief Update→Live badge flicker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@waleedlatif1
Copy link
Collaborator Author

Re: transient "Update" badge flash after redeploy (Greptile finding)

Addressed in f7cfa26 — we now destructure isFetching from useDeployedWorkflowState and OR it with isLoading before passing to useChangeDetection. This suppresses the comparison during the ~200-500ms background refetch window after mutation invalidation, preventing the brief Update→Live flicker.

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1 waleedlatif1 merged commit 395a61d into staging Mar 16, 2026
11 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/fix-deploy-detection branch March 16, 2026 21:39
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