From b032c2b8e86f145d461e2889eed5049d14b5bce2 Mon Sep 17 00:00:00 2001 From: Tanish Date: Mon, 19 Jan 2026 19:56:40 +0530 Subject: [PATCH 1/6] added stopDeployment handler --- .../handlers/deployments/stopDeployment.go | 37 +++++++++++++++++++ server/docker/image.go | 2 + 2 files changed, 39 insertions(+) create mode 100644 server/api/handlers/deployments/stopDeployment.go diff --git a/server/api/handlers/deployments/stopDeployment.go b/server/api/handlers/deployments/stopDeployment.go new file mode 100644 index 0000000..8c0a6b8 --- /dev/null +++ b/server/api/handlers/deployments/stopDeployment.go @@ -0,0 +1,37 @@ +package deployments + +import ( + "encoding/json" + "net/http" + + "github.com/corecollectives/mist/api/handlers" + "github.com/corecollectives/mist/models" +) + +type stopDeployment struct { + DeploymentID int64 `json:"deploymentId"` +} + +func StopDeployment(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + handlers.SendResponse(w, http.StatusMethodNotAllowed, false, nil, "Method not allowed", "Only POST method is allowed") + return + } + var req stopDeployment + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + handlers.SendResponse(w, http.StatusBadRequest, false, nil, "Invalid request body", "Could not parse JSON") + return + } + + deployment, err := models.GetDeploymentByID(req.DeploymentID) + if err != nil { + handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to get deployment details", err.Error()) + return + } + err = models.UpdateDeploymentStatus(req.DeploymentID, "stopped", "pending", deployment.Progress, deployment.ErrorMessage) + if err != nil { + handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to stop deployment", err.Error()) + return + } + handlers.SendResponse(w, http.StatusOK, true, nil, "Deployment stopped successfully", "") +} diff --git a/server/docker/image.go b/server/docker/image.go index cff75f9..0a00fec 100644 --- a/server/docker/image.go +++ b/server/docker/image.go @@ -55,6 +55,8 @@ func BuildImage(imageTag, contextPath string, envVars map[string]string, logfile } return nil + + // legacy exec method // // From c26de867a1bafdaf5742aff5322780017b6ab3b4 Mon Sep 17 00:00:00 2001 From: Tanish Date: Mon, 19 Jan 2026 20:32:02 +0530 Subject: [PATCH 2/6] added getDeploymentStatus function --- server/models/deployment.go | 14 ++++++++++++++ server/queue/deployQueue.go | 1 + 2 files changed, 15 insertions(+) diff --git a/server/models/deployment.go b/server/models/deployment.go index d02fadb..1e2fe7d 100644 --- a/server/models/deployment.go +++ b/server/models/deployment.go @@ -207,6 +207,20 @@ func UpdateContainerInfo(depID int64, containerID, containerName, imageTag strin return db.Model(&Deployment{}).Where("id = ?", depID).Updates(updates).Error } +func GetDeploymentStatus(depID int64) (string, error) { + var status string + result := db.Model(&Deployment{}).Select("status").Where("id = ?", depID).Scan(&status) + if result.Error != nil { + return "", result.Error + } + + if result.RowsAffected == 0 { + return "", gorm.ErrRecordNotFound + } + + return status, nil +} + //############################################################################################################# //ARCHIVED CODE BELOW------> diff --git a/server/queue/deployQueue.go b/server/queue/deployQueue.go index 15d1c8d..523b34a 100644 --- a/server/queue/deployQueue.go +++ b/server/queue/deployQueue.go @@ -41,6 +41,7 @@ func (q *Queue) StartWorker(db *gorm.DB) { go func() { defer q.wg.Done() for id := range q.jobs { + q.HandleWork(id, db) } From 17834a1a385048f537b7914b2d8dc5c92402972f Mon Sep 17 00:00:00 2001 From: Tanish Date: Mon, 19 Jan 2026 23:46:43 +0530 Subject: [PATCH 3/6] added a way to stop ongoing deployments --- server/api/RegisterRoutes.go | 1 + .../handlers/deployments/stopDeployment.go | 9 +++++- server/docker/container.go | 10 +++--- server/docker/deployer.go | 19 ++++++++--- server/docker/deployerMain.go | 5 +-- server/docker/image.go | 24 ++++++++------ server/git/clone.go | 5 +-- server/github/cloneRepo.go | 6 ++-- server/queue/ctxManager.go | 32 +++++++++++++++++++ server/queue/deployQueue.go | 11 ++++++- server/queue/handleWork.go | 10 ++++-- 11 files changed, 101 insertions(+), 31 deletions(-) create mode 100644 server/queue/ctxManager.go diff --git a/server/api/RegisterRoutes.go b/server/api/RegisterRoutes.go index a3ab4f4..c4a5d22 100644 --- a/server/api/RegisterRoutes.go +++ b/server/api/RegisterRoutes.go @@ -96,6 +96,7 @@ func RegisterRoutes(mux *http.ServeMux) { mux.Handle("POST /api/deployments/create", middleware.AuthMiddleware()(http.HandlerFunc(deployments.AddDeployHandler))) mux.Handle("POST /api/deployments/getByAppId", middleware.AuthMiddleware()(http.HandlerFunc(deployments.GetByApplicationID))) mux.Handle("GET /api/deployments/logs", middleware.AuthMiddleware()(http.HandlerFunc(deployments.GetCompletedDeploymentLogsHandler))) + mux.Handle("POST /api/deployments/stopDep", middleware.AuthMiddleware()(http.HandlerFunc(deployments.StopDeployment))) mux.Handle("GET /api/templates/list", middleware.AuthMiddleware()(http.HandlerFunc(templates.ListServiceTemplates))) mux.Handle("GET /api/templates/get", middleware.AuthMiddleware()(http.HandlerFunc(templates.GetServiceTemplateByName))) diff --git a/server/api/handlers/deployments/stopDeployment.go b/server/api/handlers/deployments/stopDeployment.go index 8c0a6b8..61115f6 100644 --- a/server/api/handlers/deployments/stopDeployment.go +++ b/server/api/handlers/deployments/stopDeployment.go @@ -6,6 +6,7 @@ import ( "github.com/corecollectives/mist/api/handlers" "github.com/corecollectives/mist/models" + "github.com/corecollectives/mist/queue" ) type stopDeployment struct { @@ -33,5 +34,11 @@ func StopDeployment(w http.ResponseWriter, r *http.Request) { handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to stop deployment", err.Error()) return } - handlers.SendResponse(w, http.StatusOK, true, nil, "Deployment stopped successfully", "") + wasRunning := queue.Cancel(req.DeploymentID) + message := "Deployment marked as stopped" + if wasRunning { + message = "Deployment aborted immediately" + } + + handlers.SendResponse(w, http.StatusOK, true, nil, message, "") } diff --git a/server/docker/container.go b/server/docker/container.go index 5b9a948..1e3f9ed 100644 --- a/server/docker/container.go +++ b/server/docker/container.go @@ -253,9 +253,9 @@ func ContainerExists(name string) bool { // return true } -func RunContainer(app *models.App, imageTag, containerName string, domains []string, Port int, envVars map[string]string, logfile *os.File) error { +func RunContainer(ctx context.Context, app *models.App, imageTag, containerName string, domains []string, Port int, envVars map[string]string, logfile *os.File) error { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() cli, err := client.New(client.FromEnv) @@ -388,7 +388,7 @@ func RunContainer(app *models.App, imageTag, containerName string, domains []str hostConfig.Resources.Memory = int64(*app.MemoryLimit) * 1024 * 1024 } - resp, err := cli.ContainerCreate(ctx, client.ContainerCreateOptions{ + resp, err := cli.ContainerCreate(timeoutCtx, client.ContainerCreateOptions{ Name: containerName, Config: &config, HostConfig: &hostConfig, @@ -397,7 +397,7 @@ func RunContainer(app *models.App, imageTag, containerName string, domains []str return fmt.Errorf("failed to create container: %w", err) } - _, err = cli.ContainerStart(ctx, resp.ID, client.ContainerStartOptions{}) + _, err = cli.ContainerStart(timeoutCtx, resp.ID, client.ContainerStartOptions{}) if err != nil { return fmt.Errorf("failed to start container: %w", err) } @@ -661,7 +661,7 @@ func RecreateContainer(app *models.App) error { return fmt.Errorf("failed to stop/remove container: %w", err) } - if err := RunContainer(app, imageTag, containerName, domains, port, envVars, nil); err != nil { + if err := RunContainer(ctx, app, imageTag, containerName, domains, port, envVars, nil); err != nil { return fmt.Errorf("failed to run container: %w", err) } diff --git a/server/docker/deployer.go b/server/docker/deployer.go index 2f34af5..06aaf51 100644 --- a/server/docker/deployer.go +++ b/server/docker/deployer.go @@ -1,6 +1,7 @@ package docker import ( + "context" "encoding/json" "fmt" "os" @@ -14,7 +15,7 @@ import ( "gorm.io/gorm" ) -func DeployApp(dep *models.Deployment, app *models.App, appContextPath, imageTag, containerName string, db *gorm.DB, logfile *os.File, logger *utils.DeploymentLogger) error { +func DeployApp(ctx context.Context, dep *models.Deployment, app *models.App, appContextPath, imageTag, containerName string, db *gorm.DB, logfile *os.File, logger *utils.DeploymentLogger) error { logger.Info("Starting deployment process") @@ -94,7 +95,11 @@ func DeployApp(dep *models.Deployment, app *models.App, appContextPath, imageTag "image": imageName, }) - if err := PullDockerImage(imageName, logfile); err != nil { + if err := PullDockerImage(ctx, imageName, logfile); err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Docker image pull canceled") + return ctx.Err() + } logger.Error(err, "Docker image pull failed") dep.Status = "failed" dep.Stage = "failed" @@ -118,7 +123,11 @@ func DeployApp(dep *models.Deployment, app *models.App, appContextPath, imageTag models.UpdateDeploymentStatus(dep.ID, "building", "building", 50, nil) logger.Info("Building Docker image with environment variables") - if err := BuildImage(imageTag, appContextPath, envVars, logfile); err != nil { + if err := BuildImage(ctx, imageTag, appContextPath, envVars, logfile); err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Docker image build canceled") + return ctx.Err() + } logger.Error(err, "Docker image build failed") dep.Status = "failed" dep.Stage = "failed" @@ -141,7 +150,7 @@ func DeployApp(dep *models.Deployment, app *models.App, appContextPath, imageTag models.UpdateDeploymentStatus(dep.ID, "deploying", "deploying", 80, nil) logger.Info("Stopping existing container if exists") - err = StopRemoveContainer(containerName, logfile) + err = StopRemoveContainer( containerName, logfile) if err != nil { logger.Error(err, "Failed to stop/remove existing container") dep.Status = "failed" @@ -162,7 +171,7 @@ func DeployApp(dep *models.Deployment, app *models.App, appContextPath, imageTag "appType": app.AppType, }) - if err := RunContainer(app, imageTag, containerName, domains, port, envVars, logfile); err != nil { + if err := RunContainer(ctx, app, imageTag, containerName, domains, port, envVars, logfile); err != nil { logger.Error(err, "Failed to run container") dep.Status = "failed" dep.Stage = "failed" diff --git a/server/docker/deployerMain.go b/server/docker/deployerMain.go index 1300316..7b89e37 100644 --- a/server/docker/deployerMain.go +++ b/server/docker/deployerMain.go @@ -1,6 +1,7 @@ package docker import ( + "context" "fmt" "os" "path/filepath" @@ -11,7 +12,7 @@ import ( "gorm.io/gorm" ) -func DeployerMain(Id int64, db *gorm.DB, logFile *os.File, logger *utils.DeploymentLogger) (string, error) { +func DeployerMain(ctx context.Context, Id int64, db *gorm.DB, logFile *os.File, logger *utils.DeploymentLogger) (string, error) { dep, err := LoadDeployment(Id, db) if err != nil { logger.Error(err, "Failed to load deployment") @@ -44,7 +45,7 @@ func DeployerMain(Id int64, db *gorm.DB, logFile *os.File, logger *utils.Deploym imageTag := dep.CommitHash containerName := fmt.Sprintf("app-%d", app.ID) - err = DeployApp(dep, &app, appContextPath, imageTag, containerName, db, logFile, logger) + err = DeployApp(ctx, dep, &app, appContextPath, imageTag, containerName, db, logFile, logger) if err != nil { logger.Error(err, "DeployApp failed") dep.Status = "failed" diff --git a/server/docker/image.go b/server/docker/image.go index 0a00fec..4420db9 100644 --- a/server/docker/image.go +++ b/server/docker/image.go @@ -12,8 +12,8 @@ import ( "github.com/rs/zerolog/log" ) -func BuildImage(imageTag, contextPath string, envVars map[string]string, logfile *os.File) error { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) +func BuildImage(ctx context.Context, imageTag, contextPath string, envVars map[string]string, logfile *os.File) error { + timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute) defer cancel() cli, err := client.New(client.FromEnv) if err != nil { @@ -41,11 +41,14 @@ func BuildImage(imageTag, contextPath string, envVars map[string]string, logfile log.Info().Str("image_tag", imageTag).Msg("Building Docker image") - resp, err := cli.ImageBuild(ctx, buildCtx, buildOptions) + resp, err := cli.ImageBuild(timeoutCtx, buildCtx, buildOptions) if err != nil { - if ctx.Err() == context.DeadlineExceeded { + if timeoutCtx.Err() == context.DeadlineExceeded { return fmt.Errorf("image build timed out after 15 minutes") } + if timeoutCtx.Err() == context.Canceled { + return context.Canceled + } return err } defer resp.Body.Close() @@ -55,8 +58,6 @@ func BuildImage(imageTag, contextPath string, envVars map[string]string, logfile } return nil - - // legacy exec method // // @@ -90,8 +91,8 @@ func BuildImage(imageTag, contextPath string, envVars map[string]string, logfile // return nil } -func PullDockerImage(imageName string, logfile *os.File) error { - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) +func PullDockerImage(ctx context.Context, imageName string, logfile *os.File) error { + timeoutCtx, cancel := context.WithTimeout(ctx, 15*time.Minute) defer cancel() cli, err := client.New(client.FromEnv) @@ -100,11 +101,14 @@ func PullDockerImage(imageName string, logfile *os.File) error { } log.Debug().Str("image_name", imageName).Msg("pulling image") - resp, err := cli.ImagePull(ctx, imageName, client.ImagePullOptions{}) + resp, err := cli.ImagePull(timeoutCtx, imageName, client.ImagePullOptions{}) if err != nil { - if ctx.Err() == context.DeadlineExceeded { + if timeoutCtx.Err() == context.DeadlineExceeded { return fmt.Errorf("image pull timed out after 15 minutes") } + if timeoutCtx.Err() == context.Canceled { + return context.Canceled + } return err } defer resp.Close() diff --git a/server/git/clone.go b/server/git/clone.go index 1ccaed7..6331113 100644 --- a/server/git/clone.go +++ b/server/git/clone.go @@ -1,6 +1,7 @@ package git import ( + "context" "fmt" "os" @@ -9,12 +10,12 @@ import ( "github.com/rs/zerolog/log" ) -func CloneRepo(url string, branch string, logFile *os.File, path string) error { +func CloneRepo(ctx context.Context, url string, branch string, logFile *os.File, path string) error { _, err := fmt.Fprintf(logFile, "[GIT]: Cloning into %s\n", path) if err != nil { log.Warn().Msg("error logging into log file") } - _, err = git.PlainClone(path, &git.CloneOptions{ + _, err = git.PlainCloneContext(ctx, path, &git.CloneOptions{ URL: url, // Progress: logFile, ReferenceName: plumbing.NewBranchReferenceName(branch), diff --git a/server/github/cloneRepo.go b/server/github/cloneRepo.go index 9a6f5f0..19f0019 100644 --- a/server/github/cloneRepo.go +++ b/server/github/cloneRepo.go @@ -11,7 +11,7 @@ import ( "github.com/rs/zerolog/log" ) -func CloneRepo(appId int64, logFile *os.File) error { +func CloneRepo(ctx context.Context, appId int64, logFile *os.File) error { log.Info().Int64("app_id", appId).Msg("Starting repository clone") userId, err := models.GetUserIDByAppID(appId) @@ -71,7 +71,7 @@ func CloneRepo(appId int64, logFile *os.File) error { log.Info().Str("clone_url", cloneURL).Str("branch", branch).Str("path", path).Msg("Cloning repository") - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() // old command implementation @@ -87,7 +87,7 @@ func CloneRepo(appId int64, logFile *os.File) error { // } // new git sdk implementation - err = git.CloneRepo(repoURL, branch, logFile, path) + err = git.CloneRepo(ctx, repoURL, branch, logFile, path) if err != nil { if ctx.Err() == context.DeadlineExceeded { return fmt.Errorf("git clone timed out after 10 minutes") diff --git a/server/queue/ctxManager.go b/server/queue/ctxManager.go new file mode 100644 index 0000000..ae7c2af --- /dev/null +++ b/server/queue/ctxManager.go @@ -0,0 +1,32 @@ +package queue + +import ( + "context" + "sync" +) + +var activeDeployments = make(map[int64]context.CancelFunc) +var mu sync.Mutex + +func Register(id int64, cancel context.CancelFunc) { + mu.Lock() + defer mu.Unlock() + activeDeployments[id] = cancel +} + +func Unregister(id int64) { + mu.Lock() + defer mu.Unlock() + delete(activeDeployments, id) +} + +func Cancel(id int64) bool { + mu.Lock() + defer mu.Unlock() + if cancel, exists := activeDeployments[id]; exists { + cancel() + delete(activeDeployments, id) + return true + } + return false +} diff --git a/server/queue/deployQueue.go b/server/queue/deployQueue.go index 523b34a..994f1f7 100644 --- a/server/queue/deployQueue.go +++ b/server/queue/deployQueue.go @@ -5,6 +5,7 @@ import ( "fmt" "sync" + "github.com/corecollectives/mist/models" "github.com/rs/zerolog/log" "gorm.io/gorm" ) @@ -41,7 +42,15 @@ func (q *Queue) StartWorker(db *gorm.DB) { go func() { defer q.wg.Done() for id := range q.jobs { - + status, err := models.GetDeploymentStatus(id) + if err != nil { + log.Error().Err(err).Msg("Failed to get deployment status") + continue + } + if status == "stopped" { + log.Info().Msgf("Deployment %d has been stopped before processing, skipping", id) + continue + } q.HandleWork(id, db) } diff --git a/server/queue/handleWork.go b/server/queue/handleWork.go index 3b09c8c..b2f91d6 100644 --- a/server/queue/handleWork.go +++ b/server/queue/handleWork.go @@ -1,6 +1,7 @@ package queue import ( + "context" "fmt" "sync" @@ -27,6 +28,11 @@ func (q *Queue) HandleWork(id int64, db *gorm.DB) { } }() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + Register(id, cancel) + defer Unregister(id) + appId, err := models.GetAppIDByDeploymentID(id) if err != nil { errMsg := fmt.Sprintf("Failed to get app ID: %v", err) @@ -79,7 +85,7 @@ func (q *Queue) HandleWork(id int64, db *gorm.DB) { logger.Info("Cloning repository") models.UpdateDeploymentStatus(id, "cloning", "cloning", 20, nil) - err = github.CloneRepo(appId, logFile) + err = github.CloneRepo(ctx, appId, logFile) if err != nil { logger.Error(err, "Failed to clone repository") errMsg := fmt.Sprintf("Failed to clone repository: %v", err) @@ -92,7 +98,7 @@ func (q *Queue) HandleWork(id int64, db *gorm.DB) { logger.Info("Skipping git clone for database app") } - _, err = docker.DeployerMain(id, db, logFile, logger) + _, err = docker.DeployerMain(ctx, id, db, logFile, logger) if err != nil { logger.Error(err, "Deployment failed") errMsg := fmt.Sprintf("Deployment failed: %v", err) From a755549a0c62e0ea121d40711ebae123d5d4372f Mon Sep 17 00:00:00 2001 From: 07calc Date: Tue, 20 Jan 2026 20:02:49 +0530 Subject: [PATCH 4/6] feat: stop deployment --- bun.lock | 14 +++++ .../deployments/deployment-list.tsx | 60 ++++++++++++++++--- .../deployments/deployment-monitor.tsx | 44 +++++++++++++- dash/src/services/deployments.service.ts | 17 ++++++ package.json | 6 ++ 5 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 bun.lock create mode 100644 package.json diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a03a348 --- /dev/null +++ b/bun.lock @@ -0,0 +1,14 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "devDependencies": { + "baseline-browser-mapping": "^2.9.15", + }, + }, + }, + "packages": { + "baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="], + } +} diff --git a/dash/src/components/deployments/deployment-list.tsx b/dash/src/components/deployments/deployment-list.tsx index 6e9639f..a513d96 100644 --- a/dash/src/components/deployments/deployment-list.tsx +++ b/dash/src/components/deployments/deployment-list.tsx @@ -5,13 +5,14 @@ import { Button } from "@/components/ui/button" import { toast } from "sonner" import { DeploymentMonitor } from "@/components/deployments" import type { Deployment, App } from "@/types" -import { Loader2, Clock, CheckCircle2, XCircle, PlayCircle, AlertCircle } from "lucide-react" +import { Loader2, Clock, CheckCircle2, XCircle, PlayCircle, AlertCircle, Square } from "lucide-react" import { deploymentsService } from "@/services" export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => { const [deployments, setDeployments] = useState([]) const [loading, setLoading] = useState(true) const [deploying, setDeploying] = useState(false) + const [stoppingIds, setStoppingIds] = useState>(new Set()) const [selectedDeployment, setSelectedDeployment] = useState(null) const fetchDeployments = async () => { @@ -51,6 +52,25 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => fetchDeployments() } + const handleStop = async (deploymentId: number) => { + if (stoppingIds.has(deploymentId)) return + try { + setStoppingIds(prev => new Set(prev).add(deploymentId)) + await deploymentsService.stopDeployment(deploymentId) + toast.success('Deployment stopped') + fetchDeployments() + } catch (error) { + console.error('Stop deployment error:', error) + toast.error(error instanceof Error ? error.message : 'Failed to stop deployment') + } finally { + setStoppingIds(prev => { + const next = new Set(prev) + next.delete(deploymentId) + return next + }) + } + } + useEffect(() => { fetchDeployments() @@ -99,6 +119,10 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => } } + const canStopDeployment = (deployment: Deployment) => { + return ['pending', 'building', 'deploying', 'cloning'].includes(deployment.status) + } + return ( <> {/* Deployment Monitor */} @@ -226,14 +250,32 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => - +
+ {canStopDeployment(d) && ( + + )} + +
))} diff --git a/dash/src/components/deployments/deployment-monitor.tsx b/dash/src/components/deployments/deployment-monitor.tsx index 3f761fd..eae7164 100644 --- a/dash/src/components/deployments/deployment-monitor.tsx +++ b/dash/src/components/deployments/deployment-monitor.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Sheet, SheetContent, @@ -6,8 +6,10 @@ import { SheetTitle, } from '@/components/ui/sheet'; import { Badge } from '@/components/ui/badge'; -import { Terminal, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Terminal, CheckCircle2, XCircle, AlertCircle, Loader2, Square } from 'lucide-react'; import { useDeploymentMonitor } from '@/hooks'; +import { deploymentsService } from '@/services'; import { LogLine } from '@/components/logs/log-line'; import { cn } from '@/lib/utils'; import { toast } from 'sonner'; @@ -23,6 +25,7 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P const bottomRef = useRef(null); const completedRef = useRef(false); + const [stopping, setStopping] = useState(false); const { logs, status, error, isConnected, isLoading, isLive, reset } = useDeploymentMonitor({ deploymentId, @@ -52,6 +55,20 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P onClose(); }; + const handleStop = async () => { + if (stopping) return; + try { + setStopping(true); + await deploymentsService.stopDeployment(deploymentId); + toast.success('Deployment stopped'); + } catch (err) { + console.error('Stop deployment error:', err); + toast.error(err instanceof Error ? err.message : 'Failed to stop deployment'); + } finally { + setStopping(false); + } + }; + const getStatusInfo = () => { const statusValue = status?.status || 'pending'; @@ -91,6 +108,11 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P } }; + const canStop = () => { + const statusValue = status?.status || 'pending'; + return ['pending', 'building', 'deploying', 'cloning'].includes(statusValue); + }; + const statusInfo = getStatusInfo(); return ( @@ -127,6 +149,24 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P #{deploymentId} + + {/* Stop Button */} + {canStop() && ( + + )} diff --git a/dash/src/services/deployments.service.ts b/dash/src/services/deployments.service.ts index 66c1608..426c494 100644 --- a/dash/src/services/deployments.service.ts +++ b/dash/src/services/deployments.service.ts @@ -68,4 +68,21 @@ export const deploymentsService = { const host = window.location.host; return `${protocol}//${host}${API_BASE}/deployments/logs/stream?id=${deploymentId}`; }, + + /** + * Stop a deployment + */ + async stopDeployment(deploymentId: number): Promise { + const response = await fetch(`${API_BASE}/deployments/stopDep`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify({ deploymentId }), + }); + + const data = await response.json(); + if (!response.ok) { + throw new Error(data.message || data.error || 'Failed to stop deployment'); + } + }, }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..0f7714c --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": {}, + "devDependencies": { + "baseline-browser-mapping": "^2.9.15" + } +} \ No newline at end of file From 8568a09916512d0fd503c27c6c80ce553749d3e1 Mon Sep 17 00:00:00 2001 From: 07calc Date: Tue, 20 Jan 2026 20:03:12 +0530 Subject: [PATCH 5/6] fix: remove root bun.lock --- bun.lock | 14 -------------- package.json | 6 ------ 2 files changed, 20 deletions(-) delete mode 100644 bun.lock delete mode 100644 package.json diff --git a/bun.lock b/bun.lock deleted file mode 100644 index a03a348..0000000 --- a/bun.lock +++ /dev/null @@ -1,14 +0,0 @@ -{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "devDependencies": { - "baseline-browser-mapping": "^2.9.15", - }, - }, - }, - "packages": { - "baseline-browser-mapping": ["baseline-browser-mapping@2.9.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-kX8h7K2srmDyYnXRIppo4AH/wYgzWVCs+eKr3RusRSQ5PvRYoEFmR/I0PbdTjKFAoKqp5+kbxnNTFO9jOfSVJg=="], - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 0f7714c..0000000 --- a/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": {}, - "devDependencies": { - "baseline-browser-mapping": "^2.9.15" - } -} \ No newline at end of file From 9785aa307e834e80d31be44cd8f9193418d399d0 Mon Sep 17 00:00:00 2001 From: 07calc Date: Tue, 20 Jan 2026 21:38:55 +0530 Subject: [PATCH 6/6] fix: showing dep failed when stopped --- .../deployments/deployment-list.tsx | 53 +++++++++++++++---- .../deployments/deployment-monitor.tsx | 40 +++++++++++--- dash/src/hooks/use-deployment-monitor.ts | 15 +++++- .../handlers/deployments/getCompletedLogs.go | 2 +- .../handlers/deployments/stopDeployment.go | 33 +++++++++++- server/docker/deployer.go | 10 +++- server/docker/deployerMain.go | 5 +- server/git/clone.go | 3 ++ server/models/deployment.go | 4 ++ server/queue/handleWork.go | 12 +++++ 10 files changed, 154 insertions(+), 23 deletions(-) diff --git a/dash/src/components/deployments/deployment-list.tsx b/dash/src/components/deployments/deployment-list.tsx index a513d96..b44f58d 100644 --- a/dash/src/components/deployments/deployment-list.tsx +++ b/dash/src/components/deployments/deployment-list.tsx @@ -5,8 +5,9 @@ import { Button } from "@/components/ui/button" import { toast } from "sonner" import { DeploymentMonitor } from "@/components/deployments" import type { Deployment, App } from "@/types" -import { Loader2, Clock, CheckCircle2, XCircle, PlayCircle, AlertCircle, Square } from "lucide-react" +import { Loader2, Clock, CheckCircle2, XCircle, PlayCircle, AlertCircle, Square, SquareSlash } from "lucide-react" import { deploymentsService } from "@/services" +import { cn } from "@/lib/utils" export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => { const [deployments, setDeployments] = useState([]) @@ -79,14 +80,14 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => return () => clearInterval(interval) }, [appId]) - // Helper to get status icon and color + // Helper to get status badge with styles const getStatusBadge = (deployment: Deployment) => { const { status, stage } = deployment switch (status) { case 'success': return ( - + Success @@ -102,25 +103,47 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => case 'deploying': case 'cloning': return ( - + {stage.charAt(0).toUpperCase() + stage.slice(1)} ) case 'pending': return ( - + Pending ) + case 'stopped': + return ( + + + Stopped + + ) default: return {status} } } + // Check if deployment can be stopped const canStopDeployment = (deployment: Deployment) => { - return ['pending', 'building', 'deploying', 'cloning'].includes(deployment.status) + return ['pending', 'building', 'deploying', 'cloning'].includes(deployment.status) && deployment.status !== 'stopped' + } + + // Get border class based on status + const getDeploymentBorderClass = (status: string) => { + switch (status) { + case 'success': + return 'border-green-200 dark:border-green-800 bg-green-50/50 dark:bg-green-950/20' + case 'failed': + return 'border-red-200 dark:border-red-800 bg-red-50/50 dark:bg-red-950/20' + case 'stopped': + return 'border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-900/20' + default: + return 'border-border bg-muted/20 hover:bg-muted/30' + } } return ( @@ -183,7 +206,10 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => {deployments.map((d) => (
@@ -194,7 +220,7 @@ export const DeploymentsTab = ({ appId, app }: { appId: number; app?: App }) => {/* Progress indicator for in-progress deployments */} - {d.status !== 'success' && d.status !== 'failed' && d.progress > 0 && ( + {d.status !== 'success' && d.status !== 'failed' && d.status !== 'stopped' && d.progress > 0 && (

)} - {d.error_message && ( + {/* Show error message for failed deployments */} + {d.error_message && d.status === 'failed' && (

{d.error_message}

)} + + {/* Show stopped message for stopped deployments */} + {d.status === 'stopped' && ( +

+ + Deployment was stopped by user +

+ )}
diff --git a/dash/src/components/deployments/deployment-monitor.tsx b/dash/src/components/deployments/deployment-monitor.tsx index eae7164..40fded0 100644 --- a/dash/src/components/deployments/deployment-monitor.tsx +++ b/dash/src/components/deployments/deployment-monitor.tsx @@ -7,7 +7,7 @@ import { } from '@/components/ui/sheet'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; -import { Terminal, CheckCircle2, XCircle, AlertCircle, Loader2, Square } from 'lucide-react'; +import { Terminal, CheckCircle2, XCircle, AlertCircle, Loader2, Square, CircleSlash, SquareSlash } from 'lucide-react'; import { useDeploymentMonitor } from '@/hooks'; import { deploymentsService } from '@/services'; import { LogLine } from '@/components/logs/log-line'; @@ -99,9 +99,15 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P icon: , label: 'Pending', }; + case 'stopped': + return { + color: 'bg-slate-500 text-white', + icon: , + label: 'Stopped', + }; default: return { - color: 'bg-gray-500 text-white', + color: 'bg-slate-500 text-white', icon: null, label: statusValue, }; @@ -110,7 +116,12 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P const canStop = () => { const statusValue = status?.status || 'pending'; - return ['pending', 'building', 'deploying', 'cloning'].includes(statusValue); + return ['pending', 'building', 'deploying', 'cloning'].includes(statusValue) && statusValue !== 'stopped'; + }; + + const isTerminalStatus = () => { + const statusValue = status?.status || ''; + return ['success', 'failed', 'stopped'].includes(statusValue); }; const statusInfo = getStatusInfo(); @@ -183,8 +194,8 @@ export const DeploymentMonitor = ({ deploymentId, open, onClose, onComplete }: P
- {/* Progress Bar */} - {status.status !== 'success' && status.status !== 'failed' && ( + {/* Progress Bar - Hide for terminal statuses */} + {!isTerminalStatus() && (
)} - {/* Error Banner - Only show for live deployments */} - {error && isLive && ( + {/* Stopped Banner */} + {status?.status === 'stopped' && ( +
+ + + Deployment Stopped + + {status.duration && ( + + Stopped at {status.progress}% after {status.duration}s + + )} +
+ )} + + {/* Error Banner - Only show for live deployments with failed status */} + {error && isLive && status?.status === 'failed' && (
diff --git a/dash/src/hooks/use-deployment-monitor.ts b/dash/src/hooks/use-deployment-monitor.ts index a74a2f6..78b687a 100644 --- a/dash/src/hooks/use-deployment-monitor.ts +++ b/dash/src/hooks/use-deployment-monitor.ts @@ -219,6 +219,8 @@ export const useDeploymentMonitor = ({ progress: deployment.progress, message: deployment.status === 'success' ? 'Deployment completed successfully' + : deployment.status === 'stopped' + ? 'Deployment was stopped by user' : 'Deployment failed', error_message: deployment.error_message, duration: deployment.duration, @@ -226,6 +228,8 @@ export const useDeploymentMonitor = ({ if (deployment.status === 'failed' && deployment.error_message) { setError(deployment.error_message); + } else if (deployment.status === 'stopped') { + setError('Deployment was stopped by user'); } setIsLoading(false); @@ -302,6 +306,13 @@ export const useDeploymentMonitor = ({ setError(statusData.error_message); onError?.(statusData.error_message); } + + // Handle stopped status + if (statusData.status === 'stopped' && !hasCompletedRef.current) { + console.log('[DeploymentMonitor] Deployment stopped by user'); + hasCompletedRef.current = true; + setError('Deployment was stopped by user'); + } break; } @@ -335,8 +346,8 @@ export const useDeploymentMonitor = ({ // Check the current status from state to decide on reconnection setStatus((currentStatus) => { - // Don't reconnect if deployment is complete - if (currentStatus?.status === 'success' || currentStatus?.status === 'failed') { + // Don't reconnect if deployment is complete (success, failed, or stopped) + if (currentStatus?.status === 'success' || currentStatus?.status === 'failed' || currentStatus?.status === 'stopped') { console.log('[DeploymentMonitor] Deployment complete, not reconnecting'); return currentStatus; } diff --git a/server/api/handlers/deployments/getCompletedLogs.go b/server/api/handlers/deployments/getCompletedLogs.go index e843acf..745965b 100644 --- a/server/api/handlers/deployments/getCompletedLogs.go +++ b/server/api/handlers/deployments/getCompletedLogs.go @@ -68,7 +68,7 @@ func GetCompletedDeploymentLogsHandler(w http.ResponseWriter, r *http.Request) { return } - if dep.Status != "success" && dep.Status != "failed" { + if dep.Status != "success" && dep.Status != "failed" && dep.Status != "stopped" { handlers.SendResponse(w, http.StatusBadRequest, false, nil, "deployment is still in progress, use WebSocket endpoint", "") return } diff --git a/server/api/handlers/deployments/stopDeployment.go b/server/api/handlers/deployments/stopDeployment.go index 61115f6..b9e9d66 100644 --- a/server/api/handlers/deployments/stopDeployment.go +++ b/server/api/handlers/deployments/stopDeployment.go @@ -3,10 +3,15 @@ package deployments import ( "encoding/json" "net/http" + "os" + "path/filepath" + "strconv" "github.com/corecollectives/mist/api/handlers" + "github.com/corecollectives/mist/constants" "github.com/corecollectives/mist/models" "github.com/corecollectives/mist/queue" + "github.com/rs/zerolog/log" ) type stopDeployment struct { @@ -26,19 +31,43 @@ func StopDeployment(w http.ResponseWriter, r *http.Request) { deployment, err := models.GetDeploymentByID(req.DeploymentID) if err != nil { + log.Error().Err(err).Int64("deployment_id", req.DeploymentID).Msg("Failed to get deployment details") handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to get deployment details", err.Error()) return } - err = models.UpdateDeploymentStatus(req.DeploymentID, "stopped", "pending", deployment.Progress, deployment.ErrorMessage) + + logPath := filepath.Join(constants.Constants["LogPath"].(string), deployment.CommitHash+strconv.FormatInt(req.DeploymentID, 10)+"_build_logs") + + file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to stop deployment", err.Error()) + log.Error().Err(err).Int64("deployment_id", req.DeploymentID).Str("log_path", logPath).Msg("Failed to open log file") + handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to open log file", err.Error()) + return + } + defer file.Close() + + logLine := "deployment was stopped by user\n" + _, err = file.WriteString(logLine) + if err != nil { + log.Error().Err(err).Int64("deployment_id", req.DeploymentID).Str("log_path", logPath).Msg("Failed to write to log file") + handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to write to log file", err.Error()) return } + wasRunning := queue.Cancel(req.DeploymentID) message := "Deployment marked as stopped" if wasRunning { message = "Deployment aborted immediately" } + errorMsg := "deployment stopped by user" + err = models.UpdateDeploymentStatus(req.DeploymentID, "stopped", "stopped", deployment.Progress, &errorMsg) + if err != nil { + log.Error().Err(err).Int64("deployment_id", req.DeploymentID).Msg("Failed to update deployment status") + handlers.SendResponse(w, http.StatusInternalServerError, false, nil, "Failed to stop deployment", err.Error()) + return + } + + log.Info().Int64("deployment_id", req.DeploymentID).Bool("was_running", wasRunning).Msg("Deployment stopped successfully") handlers.SendResponse(w, http.StatusOK, true, nil, message, "") } diff --git a/server/docker/deployer.go b/server/docker/deployer.go index 06aaf51..d892c8d 100644 --- a/server/docker/deployer.go +++ b/server/docker/deployer.go @@ -150,8 +150,12 @@ func DeployApp(ctx context.Context, dep *models.Deployment, app *models.App, app models.UpdateDeploymentStatus(dep.ID, "deploying", "deploying", 80, nil) logger.Info("Stopping existing container if exists") - err = StopRemoveContainer( containerName, logfile) + err = StopRemoveContainer(containerName, logfile) if err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Container stop/remove canceled") + return ctx.Err() + } logger.Error(err, "Failed to stop/remove existing container") dep.Status = "failed" dep.Stage = "failed" @@ -172,6 +176,10 @@ func DeployApp(ctx context.Context, dep *models.Deployment, app *models.App, app }) if err := RunContainer(ctx, app, imageTag, containerName, domains, port, envVars, logfile); err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Container run canceled") + return ctx.Err() + } logger.Error(err, "Failed to run container") dep.Status = "failed" dep.Stage = "failed" diff --git a/server/docker/deployerMain.go b/server/docker/deployerMain.go index 7b89e37..2a854bb 100644 --- a/server/docker/deployerMain.go +++ b/server/docker/deployerMain.go @@ -47,6 +47,10 @@ func DeployerMain(ctx context.Context, Id int64, db *gorm.DB, logFile *os.File, err = DeployApp(ctx, dep, &app, appContextPath, imageTag, containerName, db, logFile, logger) if err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Deployment cancelled by user") + return "", context.Canceled + } logger.Error(err, "DeployApp failed") dep.Status = "failed" dep.Stage = "failed" @@ -58,7 +62,6 @@ func DeployerMain(ctx context.Context, Id int64, db *gorm.DB, logFile *os.File, logger.Info("Deployment completed successfully") - // Run automatic cleanup if enabled in settings settings, err := models.GetSystemSettings() if err != nil { logger.Warn(fmt.Sprintf("Failed to get system settings for cleanup: %v", err)) diff --git a/server/git/clone.go b/server/git/clone.go index 6331113..1f9865f 100644 --- a/server/git/clone.go +++ b/server/git/clone.go @@ -22,6 +22,9 @@ func CloneRepo(ctx context.Context, url string, branch string, logFile *os.File, SingleBranch: true, }) if err != nil { + if ctx.Err() == context.Canceled { + return fmt.Errorf("deployment stopped by user") + } return err } diff --git a/server/models/deployment.go b/server/models/deployment.go index 1e2fe7d..242b06b 100644 --- a/server/models/deployment.go +++ b/server/models/deployment.go @@ -1,6 +1,7 @@ package models import ( + "fmt" "time" "github.com/corecollectives/mist/utils" @@ -178,6 +179,9 @@ func UpdateDeploymentStatus(depID int64, status, stage string, progress int, err updates["duration"] = &duration } } + if errorMsg != nil { + fmt.Println("updated dep status: ", *errorMsg) + } return db.Model(d).Updates(updates).Error } func MarkDeploymentStarted(depID int64) error { diff --git a/server/queue/handleWork.go b/server/queue/handleWork.go index b2f91d6..76ff2ab 100644 --- a/server/queue/handleWork.go +++ b/server/queue/handleWork.go @@ -87,6 +87,12 @@ func (q *Queue) HandleWork(id int64, db *gorm.DB) { err = github.CloneRepo(ctx, appId, logFile) if err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Deployment cancelled by user") + errMsg := "deployment stopped by user" + models.UpdateDeploymentStatus(id, "stopped", "stopped", dep.Progress, &errMsg) + return + } logger.Error(err, "Failed to clone repository") errMsg := fmt.Sprintf("Failed to clone repository: %v", err) models.UpdateDeploymentStatus(id, "failed", "failed", 0, &errMsg) @@ -100,6 +106,12 @@ func (q *Queue) HandleWork(id int64, db *gorm.DB) { _, err = docker.DeployerMain(ctx, id, db, logFile, logger) if err != nil { + if ctx.Err() == context.Canceled { + logger.Info("Deployment cancelled by user") + errMsg := "deployment stopped by user" + models.UpdateDeploymentStatus(id, "stopped", "stopped", dep.Progress, &errMsg) + return + } logger.Error(err, "Deployment failed") errMsg := fmt.Sprintf("Deployment failed: %v", err) models.UpdateDeploymentStatus(id, "failed", "failed", 0, &errMsg)