feat(deploy): async compose-up for instant UI feedback (v0.28.2)
Deploy API now returns immediately after validation + config save. docker compose up -d runs in a background goroutine so the UI shows progress during image pulls instead of blocking for 30-60s. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -271,41 +271,61 @@ func (m *Manager) DeployStack(req DeployRequest) (string, error) {
|
||||
m.checkLocalImages(req.StackName, stackDir)
|
||||
}
|
||||
|
||||
// Update in-memory stack state BEFORE compose up so the UI reflects
|
||||
// "deployed" immediately (compose up can take 30-60s for image pulls).
|
||||
// If compose up fails, we revert both disk and in-memory state below.
|
||||
// Update in-memory stack state and mark as deploying. The compose-up
|
||||
// runs in a goroutine so the API can return immediately and the UI
|
||||
// shows progress via polling (image pull can take 30-60s).
|
||||
m.mu.Lock()
|
||||
if s, ok := m.stacks[req.StackName]; ok {
|
||||
s.Deployed = true
|
||||
s.Deploying = true
|
||||
s.DeployError = ""
|
||||
s.AppConfig = appCfg
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Run docker compose up -d
|
||||
// Run docker compose up -d asynchronously
|
||||
go m.runComposeDeploy(req.StackName, stackDir, env, appCfg)
|
||||
|
||||
return deployWarning, nil
|
||||
}
|
||||
|
||||
// runComposeDeploy executes docker compose up -d in background.
|
||||
// On success it refreshes status; on failure it reverts the deploy state.
|
||||
func (m *Manager) runComposeDeploy(name, stackDir string, env map[string]string, appCfg *AppConfig) {
|
||||
start := time.Now()
|
||||
_, composeErr := m.composeExecWithEnv(stackDir, env, "up", "-d")
|
||||
|
||||
if composeErr != nil {
|
||||
m.logger.Printf("[ERROR] Stack %s deploy failed after %.1fs: %v", req.StackName, time.Since(start).Seconds(), composeErr)
|
||||
m.logger.Printf("[ERROR] Stack %s deploy failed after %.1fs: %v", name, time.Since(start).Seconds(), composeErr)
|
||||
// Revert in-memory state
|
||||
m.mu.Lock()
|
||||
if s, ok := m.stacks[req.StackName]; ok {
|
||||
if s, ok := m.stacks[name]; ok {
|
||||
s.Deployed = false
|
||||
s.Deploying = false
|
||||
s.DeployError = composeErr.Error()
|
||||
s.AppConfig = nil
|
||||
}
|
||||
m.mu.Unlock()
|
||||
// Revert disk state — keep app.yaml for debugging but mark as not deployed
|
||||
appCfg.Deployed = false
|
||||
_ = SaveAppConfig(stackDir, appCfg)
|
||||
return "", fmt.Errorf("docker compose up failed: %w", composeErr)
|
||||
return
|
||||
}
|
||||
|
||||
m.logger.Printf("[INFO] Stack %s deployed successfully (took %.1fs)", req.StackName, time.Since(start).Seconds())
|
||||
m.logger.Printf("[INFO] Stack %s deployed successfully (took %.1fs)", name, time.Since(start).Seconds())
|
||||
|
||||
// Clear deploying flag
|
||||
m.mu.Lock()
|
||||
if s, ok := m.stacks[name]; ok {
|
||||
s.Deploying = false
|
||||
}
|
||||
m.mu.Unlock()
|
||||
|
||||
// Post-deploy container state check (async, non-blocking)
|
||||
deployEnv := m.stackEnv(stackDir)
|
||||
m.logPostStartStatus(req.StackName, stackDir, deployEnv)
|
||||
m.logPostStartStatus(name, stackDir, deployEnv)
|
||||
|
||||
return deployWarning, m.RefreshStatus()
|
||||
_ = m.RefreshStatus()
|
||||
}
|
||||
|
||||
// UpdateStackConfig updates non-locked fields for a deployed stack.
|
||||
|
||||
Reference in New Issue
Block a user