diff --git a/options/locale/custom_keys.json b/options/locale/custom_keys.json index 95b85990a9..52097ace8c 100644 --- a/options/locale/custom_keys.json +++ b/options/locale/custom_keys.json @@ -518,6 +518,10 @@ "actions.runners.task_list.actions": "Actions", "actions.runners.job_cancelled": "Job cancelled successfully", "actions.runners.job_not_cancelable": "Job cannot be cancelled (not in waiting or blocked state)", + "actions.runners.cancel_job": "Cancel Job", + "actions.runners.cancel_all_jobs": "Cancel All", + "actions.runners.confirm_cancel_all": "Are you sure you want to cancel all waiting jobs?", + "actions.runners.jobs_cancelled": "%d job(s) cancelled successfully", "actions.runners.waiting_duration": "Waiting", "actions.runners.runner_match": "Runners", "actions.runners.stuck": "Stuck", diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index d8995ecc3a..0a697ac1fc 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -3778,6 +3778,10 @@ "actions.runners.task_list.actions": "Actions", "actions.runners.job_cancelled": "Job cancelled successfully", "actions.runners.job_not_cancelable": "Job cannot be cancelled (not in waiting or blocked state)", + "actions.runners.cancel_job": "Cancel Job", + "actions.runners.cancel_all_jobs": "Cancel All", + "actions.runners.confirm_cancel_all": "Are you sure you want to cancel all waiting jobs?", + "actions.runners.jobs_cancelled": "%d job(s) cancelled successfully", "actions.runners.waiting_duration": "Waiting", "actions.runners.runner_match": "Runners", "actions.runners.stuck": "Stuck", diff --git a/routers/web/shared/actions/runners.go b/routers/web/shared/actions/runners.go index 926f7f1274..b34d621af5 100644 --- a/routers/web/shared/actions/runners.go +++ b/routers/web/shared/actions/runners.go @@ -936,37 +936,114 @@ func canRunnerSatisfyLabels(runner *actions_model.ActionRunner, requiredLabels [ // CancelWaitingJob cancels a waiting job from the queue func CancelWaitingJob(ctx *context.Context) { jobID := ctx.FormInt64("job_id") + label := ctx.FormString("label") + + // Build redirect URL preserving label filter + redirectURL := setting.AppSubURL + "/-/admin/actions/runners/queue" + if label != "" { + redirectURL += "?label=" + url.QueryEscape(label) + } + if jobID == 0 { ctx.Flash.Error("Invalid job ID") - ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners/queue") + ctx.Redirect(redirectURL) return } job, err := actions_model.GetRunJobByID(ctx, jobID) if err != nil { ctx.Flash.Error("Job not found") - ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners/queue") + ctx.Redirect(redirectURL) return } // Only cancel jobs that are waiting or blocked if job.Status != actions_model.StatusWaiting && job.Status != actions_model.StatusBlocked { ctx.Flash.Warning(ctx.Tr("actions.runners.job_not_cancelable")) - ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners/queue") + ctx.Redirect(redirectURL) return } - // Cancel the job + // Cancel the job directly - set status to cancelled + var cancelledCount int64 if err := db.WithTx(ctx, func(txCtx go_context.Context) error { - _, err := actions_model.CancelJobs(txCtx, []*actions_model.ActionRunJob{job}) - return err + job.Status = actions_model.StatusCancelled + job.Stopped = timeutil.TimeStampNow() + + n, err := actions_model.UpdateRunJob(txCtx, job, nil, "status", "stopped") + if err != nil { + return err + } + cancelledCount = n + return nil }); err != nil { log.Error("CancelWaitingJob: failed to cancel job %d: %v", jobID, err) - ctx.Flash.Error("Failed to cancel job") - ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners/queue") + ctx.Flash.Error("Failed to cancel job: " + err.Error()) + ctx.Redirect(redirectURL) + return + } + + if cancelledCount == 0 { + ctx.Flash.Warning("Job was not cancelled - it may have already started or been modified") + ctx.Redirect(redirectURL) return } ctx.Flash.Success(ctx.Tr("actions.runners.job_cancelled")) - ctx.Redirect(setting.AppSubURL + "/-/admin/actions/runners/queue") + ctx.Redirect(redirectURL) +} + +// CancelAllWaitingJobs cancels all waiting jobs, optionally filtered by label +func CancelAllWaitingJobs(ctx *context.Context) { + label := ctx.FormString("label") + + // Build redirect URL preserving label filter + redirectURL := setting.AppSubURL + "/-/admin/actions/runners/queue" + if label != "" { + redirectURL += "?label=" + url.QueryEscape(label) + } + + // Get all waiting jobs for the label + jobs, err := actions_model.GetWaitingJobsByLabel(ctx, label) + if err != nil { + log.Error("CancelAllWaitingJobs: failed to get jobs: %v", err) + ctx.Flash.Error("Failed to get waiting jobs") + ctx.Redirect(redirectURL) + return + } + + if len(jobs) == 0 { + ctx.Flash.Info(ctx.Tr("actions.runners.no_waiting_jobs")) + ctx.Redirect(redirectURL) + return + } + + // Cancel all jobs + var cancelledCount int64 + if err := db.WithTx(ctx, func(txCtx go_context.Context) error { + for _, job := range jobs { + // Only cancel waiting or blocked jobs + if job.Status != actions_model.StatusWaiting && job.Status != actions_model.StatusBlocked { + continue + } + + job.Status = actions_model.StatusCancelled + job.Stopped = timeutil.TimeStampNow() + + n, err := actions_model.UpdateRunJob(txCtx, job, nil, "status", "stopped") + if err != nil { + return err + } + cancelledCount += n + } + return nil + }); err != nil { + log.Error("CancelAllWaitingJobs: failed to cancel jobs: %v", err) + ctx.Flash.Error("Failed to cancel jobs: " + err.Error()) + ctx.Redirect(redirectURL) + return + } + + ctx.Flash.Success(ctx.Tr("actions.runners.jobs_cancelled", cancelledCount)) + ctx.Redirect(redirectURL) } diff --git a/routers/web/web.go b/routers/web/web.go index aac07af46d..dfa63e1656 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -519,6 +519,7 @@ func registerWebRoutes(m *web.Router) { m.Get("/queue-depth", shared_actions.QueueDepthJSON) m.Get("/queue", shared_actions.WaitingJobs) m.Post("/queue/cancel", shared_actions.CancelWaitingJob) + m.Post("/queue/cancel-all", shared_actions.CancelAllWaitingJobs) m.Get("/{runnerid}/status", shared_actions.RunnerStatusJSON) m.Post("/reset_registration_token", shared_actions.ResetRunnerRegistrationToken) }) diff --git a/templates/admin/runners/waiting_jobs.tmpl b/templates/admin/runners/waiting_jobs.tmpl index d8c518a27a..b211b9f8fc 100644 --- a/templates/admin/runners/waiting_jobs.tmpl +++ b/templates/admin/runners/waiting_jobs.tmpl @@ -5,9 +5,20 @@ {{if .Label}} {{.Label}} {{end}} - - - +
+ {{if .Jobs}} +
+ {{$.CsrfTokenHtml}} + + +
+ {{end}} + + + +
{{if .Jobs}} @@ -77,8 +88,9 @@
{{$.CsrfTokenHtml}} -