Skip to content

feat: deployment sync — follow a source deployment with eager propagation - #44

Merged
HahaSula merged 24 commits into
mainfrom
feat/deployment-sync
Jul 5, 2026
Merged

feat: deployment sync — follow a source deployment with eager propagation#44
HahaSula merged 24 commits into
mainfrom
feat/deployment-sync

Conversation

@HahaSula

@HahaSula HahaSula commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the deployment sync feature specced out in #39: a deployment (target) can follow a source deployment. Saving the source eagerly propagates its content to every registered target, and a synced target is frozen (read-only) until explicitly unlinked.

This closes out the full spec in #39 including the four rounds of design revisions in that issue's comments (role-exclusivity flat tree, 🟢/🔴/🟠/⚫ status system in the sync modals, path validation against charts/, and the API contract).

  • server/lib/sync.js — registry CRUD (sync.yaml), role-exclusivity enforcement, path safety validation
  • server/routes/sync.jsGET/POST/DELETE /api/v2/sync per the contract in feat: deployment sync — follow a source deployment with eager propagation #39
  • server/routes/deployments.js — eager sync on save, auto-unlink on delete; also fixes a pre-existing bug where DELETE never removed folder-mode deployments (only a legacy sibling filename)
  • server/routes/folders.js — new GET /folders/deployments recursive listing, used by the sync modals' candidate pickers (kept separate from the lazy-loaded tree)
  • DeploymentTree (the actually-active sidebar — DeploymentSelector.jsx turned out to be unused dead code) — source/synced badges, right-click Sync to/Sync from/Unlink/Delete menu gated by role
  • SyncToModal / SyncFromModal / DeleteSourceModal — new components implementing the 🟢/🔴/🟠/⚫ status system and Keep/Delete flow from the spec
  • AlertUserView / AlertTable / AlertOverviewWorkspace — frozen read-only state with a "Synced from X" banner

Notable fixes found along the way (not sync-specific)

  • DELETE /:chart/:deployment only ever removed a legacy-named sibling file, silently leaving folder-mode deployments (Chart.yaml + values.yaml directly in the folder) fully intact on disk
  • A stale-closure bug in DeploymentTree where tree node handlers frozen at an earlier render would refresh against an outdated expandedKeys, wiping out already-loaded children after a mutation — fixed with a ref
  • Filed fix: folder API allows paths outside deployments/, can collide with charts/ #42 separately for a related, broader pre-existing gap: the folder API doesn't restrict paths to under deployments/, so it can collide with charts/ (this PR only adds the narrower guard needed for sync's own paths)

Test plan

  • npm test — 315/316 passing (the one failure is pre-existing and unrelated: a git-locale string match issue in tests/unit/git-lib.test.js)
  • npm run lint — clean
  • npm run build — succeeds
  • npm run test:e2e — 52/52 passing, including new tests/e2e/deployment-sync.spec.js covering Sync to via the UI, frozen read-only state, Unlink sync, and eager propagation

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a V2 “charts sync” API with sync registry lookup plus sync/unlink operations, and a GET endpoint to list deployment folders with chart/alert counts.
    • Introduced sync-aware UI flows (Sync to/Sync from, unlink, delete) with synced status badges and read-only synced editing.
  • Bug Fixes
    • Enforced safe path validation and role exclusivity; synchronized deployments are now properly frozen/read-only and preview avoids save requests.
    • Improved deletion cleanup for both folder-mode and legacy sibling-file layouts, with eager-sync safety checks.
  • Tests
    • Expanded unit, integration, and E2E coverage for sync/eager-sync propagation, concurrency, failure modes, and new endpoints.

HahaSula added 9 commits July 1, 2026 16:46
Pure registry CRUD for the deployment-sync feature (#39): read/write
sync.yaml, query source/target relationships, and mutate the registry
for sync/unlink operations. Enforces the flat-tree role-exclusivity
rule (a deployment can't be both a source and a target) and rejects
unsafe paths (traversal, absolute, or rooted at the charts dir).

Part of #39, stage 1/8 (see issue for full plan).
GET/POST/DELETE /api/v2/sync per the contract in #39: full-registry
query, per-source/per-target lookup, create-or-switch a sync, and
unlink. POST validates both paths (traversal/absolute/charts-dir
rejection, isDeployment check) and copies source content into the
target, creating it if it doesn't exist yet.

Part of #39, stage 2/8.
POST /:chart/:deployment now propagates saved content to every
registered target when the deployment is itself a sync source
(folder-mode only, matched by the existing `folder` query param).
Each target write is best-effort — one failing target doesn't undo
the source's own successful save.

DELETE /:chart/:deployment auto-removes the deployment from the sync
registry first if it's currently a target, so deleting it never
leaves a dangling registry entry.

Part of #39, stage 3/8.
GET /api/v2/folders/deployments walks the whole gitops tree (skipping
CHARTS_DIR) and returns every recognized deployment as a flat list,
for the Sync to/from modals' candidate pickers. Deliberately
uncached and separate from the lazy-loaded folder tree — it only
runs when a sync modal opens, not on every navigation, so a fresh
scan each time is simpler than maintaining an index.

Also adds the frontend chartApi.js client for both this endpoint and
the sync registry API from stage 2 (getSyncRegistry, getSyncTargets,
getSyncSource, createSync, unlinkSync, listAllDeployments).

Part of #39, stage 4/8.
The DELETE route only ever removed a legacy-named sibling file
(<deployment>-values.yaml), which doesn't exist for folder-mode
deployments (Chart.yaml + values.yaml living directly in the folder)
— so deleting one silently left it fully intact on disk. Now it
checks for a direct values.yaml and, if present, removes the whole
directory instead.

Found while wiring up a delete action for the deployment-sync feature
(#39) — needed a working delete before DeleteSourceModal could make
sense. Pre-existing bug, not introduced by sync.
…tTree

DeploymentTree — the real active sidebar (DeploymentSelector.jsx
turned out to be unused dead code) — now fetches the sync registry
alongside the folder tree and shows a source/synced badge on every
deployment node. Right-click menu is gated by role per Issue #39's
flat-tree rule: targets get Sync from.../Unlink sync (no Sync to,
they can't also be a source); sources get Sync to... (no Sync from,
they can't also be a target); independent nodes get both.

New modals:
- SyncToModal: multi-select targets with the 🟢/🔴/🟠/⚫ status system,
  ack checkboxes gate Confirm, best-effort per-target submission
- SyncFromModal: single source picker with the mirrored ⚫ condition
  (disabled if the candidate is itself already a target)
- DeleteSourceModal: per-target Keep/Delete, composed from the
  existing DELETE /:chart/:deployment and new DELETE /sync calls

refreshAll() deliberately isn't memoized with useCallback — an
earlier version froze expandedKeys in a stale closure, so handlers
fired after the tree had been expanded would refresh against the
mount-time (empty) expansion set.

Part of #39, stage 5/8.
Selecting a deployment now checks getSyncSource() and, if it's a
synced target, disables every input across both Single and Overview
modes (AlertTable gets a readOnly prop; the Common Values fields are
disabled directly) and shows a "Synced from X" banner. Save stays
disabled the same way it already was — readOnly inputs mean `dirty`
can never become true, so no special-case Save logic was needed.

DeploymentTree now takes an onSyncChange callback, fired after any
sync mutation, so the banner updates immediately if the currently
open deployment's sync state changes from the sidebar rather than
only refreshing on reselect.

Preview already worked correctly against frozen deployments without
changes: it only calls handleSave() first when dirty, which frozen
inputs never trigger.

Part of #39, stage 6/8.
…tests

Tree node titles embed handlers (Dropdown onClick -> handleUnlink ->
refreshAll) that get frozen at whichever render first built that
specific node — typically much earlier than when it's actually
clicked. refreshAll was reading expandedKeys from that frozen
closure, so after further navigation it would refresh using a stale
(often empty) expansion set and silently wipe out already-loaded
children. Fixed by reading from expandedKeysRef.current instead,
which every closure shares as the same ref object regardless of
which render created it.

Found via a genuinely flaky E2E "Unlink sync" test — reproduced with
a throwaway debug spec + console logging before landing on the ref
fix (also caught along the way: the E2E server only picks up source
changes after `npm run build`, since it serves the static dist/
bundle, not a live dev server).

Adds tests/e2e/deployment-sync.spec.js covering: Sync to... via the
UI creating a target and showing the synced badge, frozen read-only
state with the "Synced from" banner, Unlink sync restoring editability,
and eager sync propagation from an edited-and-saved source to its
target.

Part of #39, stage 7/8.
refreshAll and expandPath were awaiting getSyncRegistry() and each
loadChildren() call sequentially, adding enough latency to flake 3
pre-existing E2E tests (nested-deployment.spec.js) that click through
the tree shortly after page load without a robustness buffer. Running
them concurrently via Promise.all brings initial load time back down
and fixes the flakiness — confirmed via a full Playwright run (52/52
passing) after the change.

Part of #39, stage 7/8.
@rophy

rophy commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

PR Preview Bot

Preview environment failed to start.

...(truncated)
#21 sending tarball
#21 sending tarball 1.4s done
#21 DONE 1.7s

#22 [app] importing to docker
#22 loading layer 78b85a9eb4dd 463.05kB / 463.05kB 0.2s done
#22 loading layer beb54eda713c 1.42kB / 1.42kB 0.1s done
#22 loading layer ec815f496a56 12.53kB / 12.53kB 0.1s done
#22 loading layer fb287530f66e 3.29kB / 3.29kB 0.1s done
#22 loading layer 4b3e0cb8c5df 337B / 337B 0.1s done
#22 DONE 0.2s

#23 [app] resolving provenance for metadata file
#23 DONE 0.0s
 app  Built
 Container null-ptr-exception-rulemgmt-pr-44-app-1  Recreate
 Container null-ptr-exception-rulemgmt-pr-44-app-1  Recreated
 Container null-ptr-exception-rulemgmt-pr-44-app-1  Starting
Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint null-ptr-exception-rulemgmt-pr-44-app-1 (0dad69923e38177e9922d70687d224bc320e21fd117810c878fa30b90d697f64): Bind for 127.0.0.1:12101 failed: port is already allocated

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds sync registry storage and APIs, sync-aware deployment save/delete behavior, deployment discovery, read-only UI handling for synced deployments, and test coverage across unit, integration, and end-to-end flows.

Changes

Deployment Sync Feature

Layer / File(s) Summary
Sync registry core logic
server/lib/sync.js, tests/unit/sync.test.js
Adds sync.yaml persistence, registry helpers, path safety and normalization, sync/unlink mutation logic, deployment-directory detection, and unit tests.
Sync API endpoints and wiring
server.js, server/routes/sync.js, tests/integration/sync-api.test.js
Mounts /api/v2/sync and implements GET/POST/DELETE handlers for sync registry queries, sync creation, and unlinking.
Deployment save/delete sync integration
server/routes/deployments.js, tests/integration/deployments-sync.test.js, tests/integration/deployments-api.test.js
Propagates saved values to sync targets, blocks writes to synced targets, and unlinks targets during delete while handling folder-mode and legacy deletion layouts.
Folder deployment enumeration
server/routes/folders.js, tests/integration/folders-deployments.test.js
Adds recursive deployment discovery and GET /api/v2/folders/deployments with metadata extraction.
Frontend sync API client
src/utils/chartApi.js
Adds wrappers for deployment listing and sync registry/query/mutation endpoints.
Sync and delete modal components
src/components/SyncToModal.jsx, src/components/SyncFromModal.jsx, src/components/DeleteSourceModal.jsx
Adds modals for syncing targets, syncing from a source, and choosing keep/delete behavior when deleting a sync source.
Deployment tree sync badges and actions
src/components/DeploymentTree.jsx
Adds sync status badges, context-menu actions, registry-aware refresh, and sync-change callbacks.
Read-only propagation to alert views
src/components/AlertOverviewWorkspace.jsx, src/components/AlertTable.jsx, src/pages/AlertUserView.jsx
Adds read-only props and disables editing/saving when a deployment is synced.
End-to-end sync test suite
tests/e2e/deployment-sync.spec.js
Adds Playwright coverage for sync creation, read-only synced views, unlinking, propagation, and source deletion flows.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant DeploymentTree
  participant SyncToModal
  participant SyncAPI
  participant SyncRegistry
  participant Filesystem

  User->>DeploymentTree: choose "Sync to..."
  DeploymentTree->>SyncToModal: open with source
  User->>SyncToModal: select targets and confirm
  SyncToModal->>SyncAPI: POST /api/v2/sync
  SyncAPI->>SyncRegistry: applySync(source, target)
  SyncAPI->>Filesystem: copy values.yaml / Chart.yaml
  SyncAPI->>SyncRegistry: writeSyncRegistry()
  SyncToModal->>DeploymentTree: onSuccess -> refreshAll
Loading
sequenceDiagram
  participant User
  participant AlertUserView
  participant DeploymentsRoute
  participant SyncRegistry
  participant TargetDeployment

  User->>AlertUserView: save source deployment values
  AlertUserView->>DeploymentsRoute: POST /:chart/:deployment?folder=...
  DeploymentsRoute->>SyncRegistry: getTargetsForSource(folder)
  SyncRegistry-->>DeploymentsRoute: targets
  loop each target
    DeploymentsRoute->>TargetDeployment: write values.yaml
  end
Loading

Possibly related issues

Possibly related PRs

Suggested reviewers: rophy

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.92% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the core change: deployment sync with eager propagation from a source to targets.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deployment-sync

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@HahaSula
HahaSula marked this pull request as ready for review July 1, 2026 11:42

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/AlertUserView.jsx (1)

149-161: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guard frozenSource against stale async responses.

Both getSyncSource(...) call sites unconditionally write the resolved value into state. If the user changes deployments while an earlier request is still in flight, the late response can mark the wrong deployment read-only or editable. Please key this off the latest selectedFolder (for example via an effect/request token) before calling setFrozenSource.

Also applies to: 232-236

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/AlertUserView.jsx` around lines 149 - 161, The handleFolderSelect
flow in AlertUserView updates frozenSource from an async getSyncSource(path)
response without checking whether the response is still current, so a late
request can overwrite state for a newer selectedFolder. Add a staleness guard in
the handleFolderSelect and other getSyncSource call site logic (for example a
request token, cancellation flag, or selectedFolder comparison) before calling
setFrozenSource, and only commit the resolved source when it matches the latest
selection.
🧹 Nitpick comments (2)
tests/e2e/deployment-sync.spec.js (2)

89-93: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Brittle input selector could target the wrong field.

page.locator('input.ant-input:visible').first() selects whichever visible Ant input happens to be first in the DOM. If the "Common Values" section renders more than one visible input (or its layout changes), the test could silently assert against the wrong field.

Also applies to: 131-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/deployment-sync.spec.js` around lines 89 - 93, The test is using a
brittle first-match locator for an Ant input, which can target the wrong field
when multiple visible inputs exist. Update the deployment sync e2e assertions to
scope the input lookup to the “Common Values” section or another stable
label/role-based target instead of
page.locator('input.ant-input:visible').first(), and apply the same locator
change in both affected assertion blocks. Use the surrounding section heading
and the existing Save button check to anchor the selector to the intended field.

39-43: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add teardown for the e2e-sync-test fixtures. POST /api/v2/folders/init won’t fail on an already-initialized folder, but this suite still leaves prod, staging, and dev behind, so reruns can inherit stale sync/value state from a previous run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/deployment-sync.spec.js` around lines 39 - 43, Add teardown for the
e2e-sync-test fixtures so reruns start clean instead of inheriting stale state.
Update the deployment-sync suite by adding cleanup alongside the existing
test.beforeAll/initDeployment setup for ROOT/prod, ROOT/staging, and ROOT/dev,
and remove those initialized folders after the suite finishes. Use the existing
initDeployment helper and the suite-level lifecycle hooks in
deployment-sync.spec.js to locate and implement the cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/lib/sync.js`:
- Around line 7-18: Serialize the sync registry updates in readSyncRegistry and
writeSyncRegistry so concurrent POST/DELETE requests for the same gitopsDir
cannot overwrite each other’s changes. Add a per-gitopsDir mutex or queued
update helper around the read-modify-write path that uses readSyncRegistry and
writeSyncRegistry, and make writeSyncRegistry persist via an atomic temp-file
rename for SYNC_FILE instead of writing directly.
- Around line 7-14: readSyncRegistry currently treats every read failure as an
empty registry, which hides malformed or unreadable sync.yaml cases. Update the
catch in readSyncRegistry to only return { syncs: [] } when the file is missing
(the missing-registry case), and rethrow or surface all other errors such as
YAML parse, permission, and I/O failures. Use the existing readSyncRegistry and
SYNC_FILE logic to distinguish ENOENT from other error types so we don’t
silently overwrite valid sync links.
- Around line 51-57: `isSafeSyncPath` currently validates raw strings, so
variants like `cpu/./prod`, `cpu//prod`, and `cpu/prod/` can evade the registry
checks. Canonicalize the candidate path first in `isSafeSyncPath` (using a
normalized/clean relative form) and then perform the `..`, absolute-path, and
`chartsDirName` checks against that normalized value. Make sure the same
canonical form is what later compares against `source === target` and the
role-exclusivity logic in the sync flow.

In `@server/routes/deployments.js`:
- Around line 117-122: The propagation loop in the deployments route is
swallowing write failures, so the endpoint still succeeds even when one or more
target syncs fail. Update the target write logic in the loop that writes each
target’s values file to collect any errors instead of ignoring them, and after
processing all targets return a non-200 response if any propagation failed. Keep
the source save behavior intact, but make the failure visible to the caller when
target writes break.
- Around line 115-121: Revalidate each sync target before using it in the
deployment save flow: `readSyncRegistry()` and `getTargetsForSource()` can
return untrusted raw paths, so the `for (const target of targets)` block in the
deploy route must reject absolute paths and any path that escapes
`req.gitopsDir`. Add a safe path check around the `path.join(req.gitopsDir,
target)` / `fs.mkdir` / `fs.writeFile` sequence so only normalized target
directories under the repo are written to, and skip or error on invalid entries.

In `@server/routes/sync.js`:
- Around line 73-89: The sync flow in server/routes/sync.js overwrites the
target folder before writeSyncRegistry succeeds, which can leave target content
orphaned on failure. Update the sync handler around the sourceDir/targetDir copy
logic to make the target write and registry persistence transactional: back up
the existing target files before writing, and restore them if writeSyncRegistry
throws, or defer applying the target mutation until after the registry write
succeeds. Use the existing sync route logic and writeSyncRegistry as the main
points to adjust.
- Around line 90-92: The sync route error handling currently returns err.message
from fs failures, which can leak absolute server paths to clients. Update the
catch blocks in the sync route handler in server/routes/sync.js to log the
detailed error server-side, then respond with a stable generic 500 JSON error
instead of exposing err.message. Apply the same change in both affected catch
paths so the route behavior is consistent.

In `@src/components/DeleteSourceModal.jsx`:
- Around line 18-32: The handleConfirm flow in DeleteSourceModal.jsx treats
deleteDeployment and unlinkSync as always successful, so the modal can close
with a success message even when a step fails. Update handleConfirm to inspect
each result from deleteDeployment(...) and unlinkSync(...), stop on the first {
ok: false } outcome, and avoid calling message.success, onSuccess, or onClose
unless every step succeeds. Keep setSubmitting(true) at the start and move
setSubmitting(false) into a finally block so it always resets.

In `@src/components/DeploymentTree.jsx`:
- Around line 217-235: Clear the current selection after a successful deployment
delete, since both delete flows currently only call refreshAll() and leave
AlertUserView bound to a removed selectedFolder/selectedChart. Update
handleDeleteClick and the DeleteSourceModal success path to invalidate or
redirect the selection when the deleted node matches the current selection, then
refresh the tree. Use the existing selection/state setters in DeploymentTree.jsx
to reset the active folder/chart consistently for both delete paths.
- Around line 45-73: The deployment actions in DeploymentTree.jsx are only
exposed through the context-menu Dropdown, so add a visible button or similar
trigger alongside the existing menu in the node title rendering. Update the
DeploymentTree JSX around titleContent and the onMenuClick/menuItems setup so
the same sync/unlink/delete items can be opened from a non-context-menu trigger
while keeping the current context menu behavior intact.

In `@src/components/SyncToModal.jsx`:
- Around line 67-79: `SyncToModal` currently adds `newPathTrimmed` directly in
`handleConfirm`, which lets an existing deployment path bypass the same
overwrite checks used for selected rows. Update the `newPath` flow to compare
`newPathTrimmed` against the existing deployment list/targets before confirming,
and treat matches as an existing target that must go through the same red/orange
classification and overwrite acknowledgment gate as `selected` items. Keep
`canConfirm` and `handleConfirm` aligned so the modal cannot submit an
already-synced path without the required confirmation.

In `@src/pages/AlertUserView.jsx`:
- Around line 373-378: Preview in AlertUserView still bypasses the read-only
guard because handlePreview() can call handleSave() while dirty is true even
when frozenSource is set. Update the Preview path in AlertUserView (including
handlePreview and the related Preview button) to respect frozenSource the same
way handleSave does, either by disabling Preview when frozenSource is present or
by preventing handlePreview from invoking save in that state; if appropriate,
also clear dirty when frozenSource becomes truthy.

In `@src/utils/chartApi.js`:
- Around line 188-191: The wrapper functions in chartApi only handle non-OK
responses and still let transport errors from apiFetch or res.json escape.
Update listAllDeployments and the other affected wrappers in this file to use
try/catch around the request/parse path, and return their existing fallback
shapes on network failures as well. Keep the same contracts for each helper: []
for listAllDeployments-style calls, { syncs: [] } for sync loaders, and { ok:
false, error: ... } for action wrappers so callers like the modal Promise.all
flows do not see unhandled rejections.

In `@tests/e2e/deployment-sync.spec.js`:
- Around line 38-142: These deployment sync e2e tests depend on shared mutable
state across test cases, so make the suite explicitly serial by wrapping the
existing deployment sync block with test.describe.serial in
deployment-sync.spec.js. Keep the current test flow and stateful assumptions
intact, but ensure the ordering dependency between the sync, unlink, and
propagation tests is enforced and later tests auto-skip if an earlier step
fails.

---

Outside diff comments:
In `@src/pages/AlertUserView.jsx`:
- Around line 149-161: The handleFolderSelect flow in AlertUserView updates
frozenSource from an async getSyncSource(path) response without checking whether
the response is still current, so a late request can overwrite state for a newer
selectedFolder. Add a staleness guard in the handleFolderSelect and other
getSyncSource call site logic (for example a request token, cancellation flag,
or selectedFolder comparison) before calling setFrozenSource, and only commit
the resolved source when it matches the latest selection.

---

Nitpick comments:
In `@tests/e2e/deployment-sync.spec.js`:
- Around line 89-93: The test is using a brittle first-match locator for an Ant
input, which can target the wrong field when multiple visible inputs exist.
Update the deployment sync e2e assertions to scope the input lookup to the
“Common Values” section or another stable label/role-based target instead of
page.locator('input.ant-input:visible').first(), and apply the same locator
change in both affected assertion blocks. Use the surrounding section heading
and the existing Save button check to anchor the selector to the intended field.
- Around line 39-43: Add teardown for the e2e-sync-test fixtures so reruns start
clean instead of inheriting stale state. Update the deployment-sync suite by
adding cleanup alongside the existing test.beforeAll/initDeployment setup for
ROOT/prod, ROOT/staging, and ROOT/dev, and remove those initialized folders
after the suite finishes. Use the existing initDeployment helper and the
suite-level lifecycle hooks in deployment-sync.spec.js to locate and implement
the cleanup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 90e2a517-d9ac-410a-961f-84e2958c47ce

📥 Commits

Reviewing files that changed from the base of the PR and between 0c3f166 and 2c66562.

📒 Files selected for processing (19)
  • server.js
  • server/lib/sync.js
  • server/routes/deployments.js
  • server/routes/folders.js
  • server/routes/sync.js
  • src/components/AlertOverviewWorkspace.jsx
  • src/components/AlertTable.jsx
  • src/components/DeleteSourceModal.jsx
  • src/components/DeploymentTree.jsx
  • src/components/SyncFromModal.jsx
  • src/components/SyncToModal.jsx
  • src/pages/AlertUserView.jsx
  • src/utils/chartApi.js
  • tests/e2e/deployment-sync.spec.js
  • tests/integration/deployments-api.test.js
  • tests/integration/deployments-sync.test.js
  • tests/integration/folders-deployments.test.js
  • tests/integration/sync-api.test.js
  • tests/unit/sync.test.js

Comment thread server/lib/sync.js
Comment thread server/lib/sync.js Outdated
Comment thread server/lib/sync.js
Comment thread server/routes/deployments.js Outdated
Comment thread server/routes/deployments.js
Comment thread src/components/DeploymentTree.jsx
Comment thread src/components/SyncToModal.jsx
Comment thread src/pages/AlertUserView.jsx
Comment thread src/utils/chartApi.js
Comment thread tests/e2e/deployment-sync.spec.js Outdated
@HahaSula

HahaSula commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Went through CodeRabbit's 14 findings against the actual diff. Scoping one out, fixing the rest here:

  • Deferred to refactor: chartApi.js wrapper functions don't handle network-level fetch failures #45: the chartApi.js network-error-handling finding — every existing wrapper in that file (not just the new sync ones) has the same shape, so it should be a single pass across the whole file rather than patched half-way in this PR.
  • Everything else (path normalization in isSafeSyncPath, target revalidation in the eager-sync propagation loop, SyncToModal's ack-bypass on manually-typed paths, DeleteSourceModal swallowing partial failures, registry read/write atomicity, error message leakage, stale selection after delete, and the E2E nitpicks) will be addressed in follow-up commits on this branch.

Will push fixes incrementally and re-request review once done.

HahaSula added 5 commits July 2, 2026 14:04
…istry paths on write

POST /:chart/:deployment now rejects writes to a folder that's currently a
sync target (409), instead of relying solely on the frontend disabling
Save — a race in AlertUserView's getSyncSource call could otherwise let a
save through before the UI catches up. The eager-sync propagation loop also
re-validates each target path with isSafeSyncPath before writing to it,
since sync.yaml is a plain file in the gitops repo and isn't automatically
trustworthy just because an entry is in the registry.

Found during review of the CodeRabbit findings on this PR.
…can't bypass role exclusivity

isSafeSyncPath validated the raw candidate string, so 'cpu/prod',
'cpu/./prod', 'cpu//prod', and 'cpu/prod/' all pointed at the same
deployment but were treated as different strings by the registry's strict
equality checks (isSource/isTarget/applySync). An unnormalized spelling of
an existing source could therefore slip past the "can't also be a target"
role-exclusivity guard.

Adds normalizeSyncPath and runs it in isSafeSyncPath and at the sync route
boundary (GET/POST/DELETE) before any validation, comparison, or registry
storage, so the registry always holds one canonical spelling per path.

This codebase has shipped two prior path-traversal bugs (#33/#34), which
PR #44's own commit history calls out as the reason path validation here
is a hard requirement, not a nice-to-have.
…yncToModal

The "Add new path" input pushed its value straight into the submit list
without ever going through classify() — typing an existing deployment's
path bypassed the red/orange overwrite-confirmation checkboxes entirely,
letting it be silently overwritten. Manually-typed paths are now
classified the same way as picked-from-list rows: known deployments need
the same ack, syncing to the source itself is blocked, and unrecognized
paths are treated as genuinely new (no ack needed, matching what POST
/sync already does for a target that doesn't exist yet).
… reporting success

handleConfirm ran through every target's delete/unlink call plus the
source's own delete without checking any result, then unconditionally
showed a success message. A failed step now stops the sequence, shows an
error instead of a false success, and skips onSuccess/onClose so the modal
stays open — except the tree is still refreshed if any earlier step did
complete, so the sidebar doesn't show stale badges for targets that were
already unlinked/deleted before the failure. setSubmitting(false) moved to
a finally so it can't get stuck true.
…e suite serial with teardown

- New test: typing an existing deployment's path into SyncToModal's "Add
  new path" field requires the same overwrite ack as picking it from the
  list (covers the fix in the previous commit).
- New test: deleting a sync source honors independent per-target
  Keep/Delete choices — the Delete flow had zero coverage in any layer
  before this (this project has no React component-test convention, so
  E2E is the only place bugs like the ack-bypass or the swallowed-errors
  one in DeleteSourceModal could actually be caught).
- Wrapped the suite in test.describe.serial and added afterAll teardown
  that removes the ROOT deployments and resets sync.yaml. Without it,
  reruns inherit sync state from the previous run (e.g. a target left
  checked+disabled as already-synced), which is what broke the "saving a
  source propagates" test while developing the tests above.
@HahaSula

HahaSula commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/DeleteSourceModal.jsx (1)

11-16: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Effect keys off targets reference, not content.

If the parent (DeploymentTree.jsx) passes a fresh array literal for targets on each render while the modal stays open, this effect re-fires and silently resets any per-target Keep/Delete selections the user already made.

♻️ Possible mitigation
-  useEffect(() => {
-    if (!open) return
-    const initial = {}
-    for (const t of targets) initial[t] = 'keep'
-    setDecisions(initial)
-  }, [open, targets])
+  useEffect(() => {
+    if (!open) return
+    const initial = {}
+    for (const t of targets) initial[t] = 'keep'
+    setDecisions(initial)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [open, JSON.stringify(targets)])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DeleteSourceModal.jsx` around lines 11 - 16, The
initialization effect in DeleteSourceModal is re-running whenever the targets
array reference changes, which can wipe user-made Keep/Delete selections while
the modal remains open. Update the useEffect tied to open and targets so it only
initializes decisions when the modal first opens or when the actual target set
changes meaningfully, and avoid resetting state on every new array instance
passed from DeploymentTree.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/DeleteSourceModal.jsx`:
- Around line 18-53: The handleConfirm flow in DeleteSourceModal only handles
non-OK responses, so thrown errors from deleteDeployment or unlinkSync can
escape without user feedback. Add a catch around the existing try/finally in
handleConfirm, show message.error with the exception details for any unexpected
failure, and keep the current finally behavior for setSubmitting and onSuccess
refresh handling.

---

Nitpick comments:
In `@src/components/DeleteSourceModal.jsx`:
- Around line 11-16: The initialization effect in DeleteSourceModal is
re-running whenever the targets array reference changes, which can wipe
user-made Keep/Delete selections while the modal remains open. Update the
useEffect tied to open and targets so it only initializes decisions when the
modal first opens or when the actual target set changes meaningfully, and avoid
resetting state on every new array instance passed from DeploymentTree.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e93ac72-eb97-453c-9a3b-16eaceec8cbf

📥 Commits

Reviewing files that changed from the base of the PR and between 2c66562 and 36439bc.

📒 Files selected for processing (9)
  • server/lib/sync.js
  • server/routes/deployments.js
  • server/routes/sync.js
  • src/components/DeleteSourceModal.jsx
  • src/components/SyncToModal.jsx
  • tests/e2e/deployment-sync.spec.js
  • tests/integration/deployments-sync.test.js
  • tests/integration/sync-api.test.js
  • tests/unit/sync.test.js
🚧 Files skipped from review as they are similar to previous changes (5)
  • server/routes/deployments.js
  • server/lib/sync.js
  • tests/integration/sync-api.test.js
  • server/routes/sync.js
  • src/components/SyncToModal.jsx

Comment thread src/components/DeleteSourceModal.jsx
…rget content not reference

Two follow-ups from CodeRabbit's re-review of the previous commit:

- handleConfirm only handled { ok: false } results — a rejected promise
  from deleteDeployment/unlinkSync (e.g. a network failure) would escape
  as an unhandled rejection with no feedback shown to the user. Wrapped in
  a catch that reports it via message.error.
- The decisions-initializing effect depended on the targets array
  reference, so a re-render handing down a fresh-but-equal array while the
  modal stayed open would silently wipe out Keep/Delete choices already
  made. Keyed on targets.join('|') instead.
@HahaSula

HahaSula commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/components/DeleteSourceModal.jsx (1)

15-22: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Use a collision-free key for target content.

targets.join('|') can collide for different target sets, e.g. ['a|b', 'c'] and ['a', 'b|c'], leaving stale decisions while the modal is open.

♻️ Proposed fix
-  const targetsKey = targets.join('|')
+  const targetsKey = JSON.stringify(targets)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/DeleteSourceModal.jsx` around lines 15 - 22, The modal reset
logic in DeleteSourceModal is using a non-unique derived key from
targets.join('|'), which can collide for different target arrays and leave stale
decisions in useEffect. Replace the targetsKey dependency with a collision-free
representation based on the full targets contents, and keep the reset logic in
the same effect so it reinitializes decisions correctly whenever the open state
or the actual target set changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/DeleteSourceModal.jsx`:
- Around line 54-59: The catch path in DeleteSourceModal’s delete flow is unsafe
when deleteDeployment/unlinkSync rejects with null or undefined, because
accessing err.message can throw inside the catch. Update the catch block in
DeleteSourceModal to defensively derive the error text from err only when it is
truthy, and fall back to the existing generic failure message so message.error
always receives a safe string.

---

Nitpick comments:
In `@src/components/DeleteSourceModal.jsx`:
- Around line 15-22: The modal reset logic in DeleteSourceModal is using a
non-unique derived key from targets.join('|'), which can collide for different
target arrays and leave stale decisions in useEffect. Replace the targetsKey
dependency with a collision-free representation based on the full targets
contents, and keep the reset logic in the same effect so it reinitializes
decisions correctly whenever the open state or the actual target set changes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3b59a58-3d3f-4400-830f-fc8a473fbc46

📥 Commits

Reviewing files that changed from the base of the PR and between 36439bc and b2bc7de.

📒 Files selected for processing (1)
  • src/components/DeleteSourceModal.jsx

Comment thread src/components/DeleteSourceModal.jsx Outdated
HahaSula and others added 2 commits July 3, 2026 10:07
…writes, registry-first ordering, generic 500s

Addresses four CodeRabbit findings on the sync backend:

- readSyncRegistry now only treats a missing sync.yaml as an empty
  registry; parse/permission/I-O errors surface instead of inviting the
  next write to silently wipe existing links
- withSyncRegistryLock serializes all registry read-modify-write
  sequences (concurrent POST/DELETE could drop each other's changes);
  in-process is sufficient since each user gets their own singleuser pod
- POST /sync persists the registry before touching target files, with a
  best-effort content restore + unlink rollback if the copy fails — a
  failed persist can no longer orphan overwritten target content
- 500 responses log the real error server-side and return a generic
  message instead of fs error text that embeds absolute server paths

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…en Preview guard

Addresses the remaining CodeRabbit UI findings:

- tree node actions get a visible ⋯ button opening the same menu, so
  keyboard/touch users aren't locked out of right-click-only actions
- deleting the currently selected deployment (directly or via the
  delete-source flow) clears the selection instead of leaving the
  editor open on content that no longer exists; DeleteSourceModal now
  reports which paths were actually removed from disk
- Preview on a frozen deployment renders on-disk state directly instead
  of bouncing a save off the server's read-only guard
- DeleteSourceModal's catch tolerates nullish rejection values

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/AlertUserView.jsx (1)

165-178: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard both getSyncSource calls against stale responses. handleFolderSelect and onSyncChange each await getSyncSource(...) and then set frozenSource unconditionally, so a slower earlier request can overwrite the current folder’s sync state. Add a current-folder check before updating state, and handle fetch failures too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/AlertUserView.jsx` around lines 165 - 178, The frozen source update
in handleFolderSelect and onSyncChange is vulnerable to stale async responses
overwriting the current folder’s state. After awaiting getSyncSource(path),
verify the selected folder is still the active one before calling
setFrozenSource, and apply the same guard in both handlers. Also wrap the
getSyncSource call in error handling so failures do not leave the UI in an
inconsistent state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/pages/AlertUserView.jsx`:
- Around line 165-178: The frozen source update in handleFolderSelect and
onSyncChange is vulnerable to stale async responses overwriting the current
folder’s state. After awaiting getSyncSource(path), verify the selected folder
is still the active one before calling setFrozenSource, and apply the same guard
in both handlers. Also wrap the getSyncSource call in error handling so failures
do not leave the UI in an inconsistent state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f3ce4bc2-60bd-4ab5-82d4-d971ddcb470e

📥 Commits

Reviewing files that changed from the base of the PR and between b2bc7de and 48cc19a.

📒 Files selected for processing (8)
  • server/lib/sync.js
  • server/routes/deployments.js
  • server/routes/sync.js
  • src/components/DeleteSourceModal.jsx
  • src/components/DeploymentTree.jsx
  • src/pages/AlertUserView.jsx
  • tests/e2e/deployment-sync.spec.js
  • tests/integration/sync-api.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • server/routes/deployments.js
  • src/components/DeploymentTree.jsx

HahaSula and others added 2 commits July 3, 2026 10:40
readSyncRegistry now (deliberately) treats a corrupt registry as a hard
error, which makes a crash mid-write more costly than before — a
truncated sync.yaml would take the whole sync API down until manually
repaired. Write to a temp file and rename into place so the registry on
disk is always either the old or the new complete content.

The registry-write-failure integration test switches from a read-only
file to a read-only directory, since rename replaces the target
regardless of its file permissions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Rapid folder switches fire overlapping getSyncSource calls, and a
slower earlier response landing last could freeze (or unfreeze) the
wrong folder. Both handleFolderSelect and onSyncChange now go through
refreshFrozenSource, which only lets the response for the most recently
requested folder update state, and tolerates transport failures
(failing open is safe — the server enforces read-only on save).

Also makes the uppercase-folder e2e save test idempotent: refilling the
exact value a previous run already saved doesn't fire React's onChange,
so dirty stayed false and Save stayed disabled on any non-fresh
workspace.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/pages/AlertUserView.jsx (1)

22-27: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Initialize frozenSource for restored folder selections

selectedFolder is restored from session state, but the tree only expands ancestors on mount; it does not re-run handleFolderSelect, so frozenSource stays null until the user reselects the folder or a sync event fires. That leaves a synced deployment looking editable until save time.

💡 Proposed fix
+useEffect(() => {
+  if (selectedFolder) refreshFrozenSource(selectedFolder)
+}, [selectedFolder])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pages/AlertUserView.jsx` around lines 22 - 27, `AlertUserView` restores
`selectedFolder` from session state, but `frozenSource` is never initialized for
that restored selection because `handleFolderSelect` only runs on user
interaction or sync updates. Update the mount/restore flow in `AlertUserView` so
a saved folder selection also initializes `frozenSource` immediately, using the
same logic as `handleFolderSelect`/folder sync handling, to keep the deployment
state read-only when it should be.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/integration/sync-api.test.js`:
- Around line 261-268: The permission-failure case in the sync API test is
brittle under UID 0 and can leave tmpDir read-only if the request throws. Update
the test around request(app).post('/api/v2/sync') to skip or adapt the
chmodSync(tmpDir, 0o555) assertion when running as root, and wrap the read-only
setup/restore in a try/finally so chmodSync(tmpDir, 0o755) always runs even on
failure. Use the existing tmpDir, request(app), and fs.chmodSync calls in this
test to make the cleanup resilient.

---

Outside diff comments:
In `@src/pages/AlertUserView.jsx`:
- Around line 22-27: `AlertUserView` restores `selectedFolder` from session
state, but `frozenSource` is never initialized for that restored selection
because `handleFolderSelect` only runs on user interaction or sync updates.
Update the mount/restore flow in `AlertUserView` so a saved folder selection
also initializes `frozenSource` immediately, using the same logic as
`handleFolderSelect`/folder sync handling, to keep the deployment state
read-only when it should be.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 43baa4a4-9bca-4533-8a68-7dfd0a6f58bb

📥 Commits

Reviewing files that changed from the base of the PR and between 48cc19a and bb72c32.

📒 Files selected for processing (4)
  • server/lib/sync.js
  • src/pages/AlertUserView.jsx
  • tests/e2e/nested-deployment.spec.js
  • tests/integration/sync-api.test.js

Comment thread tests/integration/sync-api.test.js Outdated
…en permission test

- Folders restored from session state never pass through
  handleFolderSelect, so a synced deployment restored on page load
  looked editable until save time — an effect on selectedFolder now
  refreshes frozenSource for that path too
- The registry-write-failure test skips under root (permissions don't
  apply to UID 0) and restores directory permissions in a finally so a
  thrown request can't leave tmpDir unremovable for afterEach

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@HahaSula
HahaSula requested a review from rophy July 3, 2026 05:15
HahaSula and others added 4 commits July 5, 2026 17:39
refreshAll re-inserted expanded-node children in raw expandedKeys order,
but antd keeps descendant keys when an ancestor is collapsed and appends
the ancestor after them on re-expand — so the array can be child-before-
parent. insertChildren against a not-yet-populated parent is a silent
no-op, which left expanded nodes empty after any sync/unlink/delete
refresh until a full page reload. Sort keys by depth before inserting.

E2E covers the collapse → re-expand → sync sequence and asserts the
nested deployment stays visible with its source badge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
On win32, path.normalize rewrites '/' to '\', so sync.yaml stored
non-portable '\'-separated paths — a file that gets committed to the
gitops repo and read on Linux — and every registry comparison against
'/'-separated tree paths silently missed (no badges, broken auto-unlink,
8 failing sync-api tests on Windows). It also let 'charts\evil' slip
past the charts-dir check, which splits on '/', while path.join still
resolved it into the charts directory. Fold backslashes into '/' and use
path.posix.normalize so both spellings canonicalize identically.

Also skip the registry-write-failure test on Windows, where chmod on a
directory is a no-op and the write succeeds anyway (same reason it
already skips under root).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ches

The selectedFolder effect rebuilt the tree down to just the selected
folder's ancestor chain on every selection — the loaded children of every
other expanded branch were dropped, and antd's internal loadedKeys then
blocked loadData from refetching them, so those branches re-expanded
empty until a full page reload.

Fold the effect into refreshAll: it now takes ensureKeys (ancestors that
must end up expanded) and unions them with the user's current expansion
set instead of replacing it. The mount/refreshKey effect skips when a
folder is selected so two concurrent refreshes can't race, with the loser
clobbering the winner's tree with a staler expansion set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Unsaved edits live only in React state, and both switching deployments
and reloading dropped them silently — with sync in the picture the stakes
are higher, since an eagerly-propagated target can't be re-edited to
reconstruct what was lost. Folder switches (tree click or new-deployment
create) now go through a Discard/Keep-editing confirmation while dirty,
and a beforeunload warning covers reload/close. Frozen folders skip the
guard: their inputs are disabled, so a lingering dirty flag there can't
be real work.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@HahaSula
HahaSula merged commit b1e0367 into main Jul 5, 2026
4 checks passed
@rophy
rophy deleted the feat/deployment-sync branch July 5, 2026 13:46
HahaSula added a commit that referenced this pull request Jul 6, 2026
Bump version from 1.3.0 to 1.4.0 to trigger a new image build and
GitHub Release via CI. Since 1.3.0: deployment sync (#44), alert
overview mode with column filters (#37), promtool validation (#41,
#43), plus several fixes — enough new features to warrant a minor
bump per this project's convention (1.2.0 -> 1.3.0 similarly bumped
minor for the Gitea migration).
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.

2 participants