Skip to content

feat: add ability to deploy Docker Swarm stacks from Git repo with GitOps updates (#2386)#2412

Merged
kmendell merged 10 commits into
getarcaneapp:mainfrom
SplinterHead:feat-swarm-stacks-from-git
Apr 21, 2026
Merged

feat: add ability to deploy Docker Swarm stacks from Git repo with GitOps updates (#2386)#2412
kmendell merged 10 commits into
getarcaneapp:mainfrom
SplinterHead:feat-swarm-stacks-from-git

Conversation

@SplinterHead
Copy link
Copy Markdown
Contributor

@SplinterHead SplinterHead commented Apr 20, 2026

Checklist

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

What This PR Implements

This PR implements full GitOps synchronization support for Docker Swarm Stacks in Arcane. Previously, Swarm stacks could only be created via the web UI or templates natively. This addition brings Swarm deployments up strictly to parity with local projects, allowing users to select a Git repository as the source of truth for their Swarm stack configurations, complete with external .env, configs, and secrets resolution natively from the repository.

Addresses #2386

Fixes:

Changes Made

  • Database Schema Updates: Added the target_type column to the gitops_syncs database model and created corresponding 047 SQL schema migrations for both PostgreSQL and SQLite. This explicitly categorizes sync targets as either project or swarm_stack.
  • Backend Swarm Sync Routing: Hand-wired GitOps syncs to branch directly into performSwarmStackSync dynamically without requiring new huma handler endpoints. Constructor injected SwarmService safely into GitOpsSyncService.
  • Swarm File Resolution (WorkingDir): Fixed a significant issue holding back Swarm stack replication by passing the cloned Git repository folder explicitly down to libswarm.DeployStack as WorkingDir. This allows Swarm stacks to seamlessly load .env variables and related configs: directly from sibling Git files.
  • Docker API Bug Mitigation: Handled the native Docker Swarm daemon service does not have a previous spec bug that surfaces when doing rolling registry updates. Intercepted the crash and seamlessly retried the update with a cleared registry map (matching the official Docker CLI architecture).
  • Frontend Svelte 5 Refinements: Exposed parameter-driven linking for targetTypes from the "Stacks" dashboard, injecting it into GitOpsSyncFormSheet gracefully using strict Svelte 5 $props() and $derived() reactivity without polluting global stores.

Testing

  • Dev environment starts successfully
  • Backend tests pass: just test backend
  • Frontend type checks pass: just lint frontend
  • Manually tested: Triggered a GitOps sync locally parsing Swarm configuration files containing .env mappings; verified Docker daemon retry-logic successfully updated existing specs natively lacking registry metadata.

AI Tool Used

AI Tool: Google DeepMind Antigravity
Assistance Level: Significant

Additional Notes

Supersedes #2234 as this will keep Swarm Stacks as a first class citizen and maintain them as separate entities ( docker compose up vs docker stack deploy )

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 adds GitOps synchronization support for Docker Swarm stacks, bringing them to parity with project-based syncs. It adds a target_type column to gitops_syncs, wires SwarmService into GitOpsSyncService, passes the cloned repo path as WorkingDir to resolve .env/configs from sibling files, and includes a retry workaround for the Docker daemon "service does not have a previous spec" bug.

The three concerns raised in prior review threads (dead-code swarm_stack guard in performSingleFileSyncInternal, missing Internal suffix, and $state-in-$effect) all appear addressed in this revision.

Confidence Score: 5/5

Safe to merge; all previous P0/P1 concerns are resolved and the two remaining findings are non-blocking P2 suggestions.

No P0 or P1 findings remain. The two open comments are both P2: a missing exhaustive switch for TargetType (silent fallthrough is safe but ambiguous) and a missing QueryRegistry=false on the Docker retry path (only relevant for private-registry always-resolve mode). Neither blocks functionality for the primary user path.

backend/pkg/libarcane/swarm/stack_deploy_engine.go — Docker retry path; backend/internal/services/gitops_sync_service.go — TargetType dispatch.

Comments Outside Diff (1)

  1. frontend/src/routes/(app)/environments/[id]/gitops/+page.svelte, line 87-90 (link)

    P1 dialogTargetType not reset when opening the edit dialog

    openCreateSyncDialog writes to dialogTargetType, but openEditSyncDialog never resets it. If a user lands on this page via ?action=create&targetType=swarm_stack (triggering a swarm-stack create), then without reloading navigates to edit an existing project sync, the stale 'swarm_stack' value is forwarded to the dialog and included in the update payload (targetType: 'swarm_stack'). The backend's UpdateSync handler will then overwrite target_type to "swarm_stack" on a project-type sync, causing the next sync run to call performSwarmStackSyncInternal on a record that was never a swarm stack.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: frontend/src/routes/(app)/environments/[id]/gitops/+page.svelte
    Line: 87-90
    
    Comment:
    **`dialogTargetType` not reset when opening the edit dialog**
    
    `openCreateSyncDialog` writes to `dialogTargetType`, but `openEditSyncDialog` never resets it. If a user lands on this page via `?action=create&targetType=swarm_stack` (triggering a swarm-stack create), then without reloading navigates to edit an existing *project* sync, the stale `'swarm_stack'` value is forwarded to the dialog and included in the update payload (`targetType: 'swarm_stack'`). The backend's `UpdateSync` handler will then overwrite `target_type` to `"swarm_stack"` on a project-type sync, causing the next sync run to call `performSwarmStackSyncInternal` on a record that was never a swarm stack.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: backend/internal/services/gitops_sync_service.go
Line: 502-504

Comment:
**No validation of `TargetType` value**

Any arbitrary string stored in `target_type` silently falls through to the project-sync path. If an API client sends `"swarm_Stack"` (capitalisation difference) or any other unrecognised value, the sync will silently run as a project sync instead of failing clearly. Adding a guard at the branch point would make the contract explicit.

```go
switch sync.TargetType {
case "swarm_stack":
    return s.performSwarmStackSyncInternal(syncCtx, sync, id, actor, result, source)
case "", "project":
    // fall through to existing logic
default:
    return result, s.failSync(ctx, id, result, sync, actor, "Unknown target type", fmt.Sprintf("unknown targetType %q", sync.TargetType))
}
```

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

---

This is a comment left during a code review.
Path: backend/pkg/libarcane/swarm/stack_deploy_engine.go
Line: 692-697

Comment:
**Retry leaves `QueryRegistry` unchanged**

When the "previous spec" error fires, only `RegistryAuthFrom` is cleared. The Docker CLI reference implementation also sets `QueryRegistry = false` on the retry path. If `shouldQueryRegistryOnUpdate` returned `true` (e.g. `always` resolve-image mode), the retry still asks Docker to query the registry with no credentials — which may surface a different error for private-registry images, masking the original fix.

```go
if strings.Contains(err.Error(), "service does not have a previous spec") {
    opts.RegistryAuthFrom = ""
    opts.QueryRegistry = false
    if _, retryErr := dockerClient.ServiceUpdate(ctx, existing.ID, opts); retryErr != nil {
        return fmt.Errorf("failed to update swarm service %s: %w", spec.Name, retryErr)
    }
    return nil
}
```

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

Reviews (3): Last reviewed commit: "fix: clean up duplicate sql migrations f..." | Re-trigger Greptile

Comment thread frontend/src/routes/(app)/environments/[id]/gitops/+page.svelte Outdated
Comment thread backend/internal/services/gitops_sync_service.go Outdated
Comment thread backend/internal/services/gitops_sync_service.go Outdated
- Reset dialogTargetType when opening the edit dialog to prevent create-mode
  state from leaking into sync updates.
- Rename performSingleFileSync to performSingleFileSyncInternal to follow
  internal function naming conventions.
- Remove redundant targetType logging in the single file sync path.
Copy link
Copy Markdown
Member

@kmendell kmendell left a comment

Choose a reason for hiding this comment

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

LGTM, Thanks!

@kmendell kmendell enabled auto-merge (squash) April 21, 2026 19:48
@kmendell kmendell merged commit 3150c0f into getarcaneapp:main Apr 21, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants