feat(actions): add bulk cancel functionality for waiting jobs
All checks were successful
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m38s
Build and Release / Lint (push) Successful in 5m26s
Build and Release / Unit Tests (push) Successful in 5m42s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m2s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m34s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m22s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 6m55s
All checks were successful
Build and Release / Create Release (push) Successful in 1s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 2m38s
Build and Release / Lint (push) Successful in 5m26s
Build and Release / Unit Tests (push) Successful in 5m42s
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Successful in 3m2s
Build and Release / Build Binaries (amd64, darwin, macos) (push) Successful in 4m34s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Successful in 9h4m22s
Build and Release / Build Binaries (arm64, darwin, macos) (push) Successful in 4m55s
Build and Release / Build Binary (linux/arm64) (push) Successful in 6m55s
Add "Cancel All" button to waiting jobs view to cancel all waiting/blocked jobs at once, with optional label filtering. Improve individual job cancellation to preserve label filter in redirect URL and provide better error feedback.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -5,9 +5,20 @@
|
||||
{{if .Label}}
|
||||
<span class="ui blue label">{{.Label}}</span>
|
||||
{{end}}
|
||||
<a href="{{.Link}}" class="ui right">
|
||||
<button class="ui tiny button">{{svg "octicon-arrow-left" 14}} {{ctx.Locale.Tr "actions.runners.back_to_runners"}}</button>
|
||||
</a>
|
||||
<div class="ui right">
|
||||
{{if .Jobs}}
|
||||
<form action="{{AppSubUrl}}/-/admin/actions/runners/queue/cancel-all" method="post" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="label" value="{{.Label}}">
|
||||
<button class="ui tiny red button" type="submit" onclick="return confirm('{{ctx.Locale.Tr "actions.runners.confirm_cancel_all"}}');">
|
||||
{{svg "octicon-x" 14}} {{ctx.Locale.Tr "actions.runners.cancel_all_jobs"}}
|
||||
</button>
|
||||
</form>
|
||||
{{end}}
|
||||
<a href="{{.Link}}">
|
||||
<button class="ui tiny button">{{svg "octicon-arrow-left" 14}} {{ctx.Locale.Tr "actions.runners.back_to_runners"}}</button>
|
||||
</a>
|
||||
</div>
|
||||
</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .Jobs}}
|
||||
@@ -77,8 +88,9 @@
|
||||
<form action="{{AppSubUrl}}/-/admin/actions/runners/queue/cancel" method="post" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="job_id" value="{{.ID}}">
|
||||
<button class="ui tiny red button" type="submit" title="{{ctx.Locale.Tr "cancel"}}">
|
||||
{{svg "octicon-x" 14}}
|
||||
<input type="hidden" name="label" value="{{$.Label}}">
|
||||
<button class="ui tiny red button" type="submit">
|
||||
{{ctx.Locale.Tr "actions.runners.cancel_job"}}
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
|
||||
Reference in New Issue
Block a user