Skip to content

Commit dd1e187

Browse files
committed
feat: Add Actions API rerun endpoints for runs and jobs
1 parent 3b250ba commit dd1e187

6 files changed

Lines changed: 559 additions & 112 deletions

File tree

routers/api/v1/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,7 +1271,9 @@ func Routes() *web.Router {
12711271
m.Group("/{run}", func() {
12721272
m.Get("", repo.GetWorkflowRun)
12731273
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
1274+
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
12741275
m.Get("/jobs", repo.ListWorkflowRunJobs)
1276+
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
12751277
m.Get("/artifacts", repo.GetArtifactsOfRun)
12761278
})
12771279
})

routers/api/v1/repo/action.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,160 @@ func GetWorkflowRun(ctx *context.APIContext) {
11581158
ctx.JSON(http.StatusOK, convertedRun)
11591159
}
11601160

1161+
// RerunWorkflowRun Reruns an entire workflow run.
1162+
func RerunWorkflowRun(ctx *context.APIContext) {
1163+
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun repository rerunWorkflowRun
1164+
// ---
1165+
// summary: Reruns an entire workflow run
1166+
// produces:
1167+
// - application/json
1168+
// parameters:
1169+
// - name: owner
1170+
// in: path
1171+
// description: owner of the repo
1172+
// type: string
1173+
// required: true
1174+
// - name: repo
1175+
// in: path
1176+
// description: name of the repository
1177+
// type: string
1178+
// required: true
1179+
// - name: run
1180+
// in: path
1181+
// description: id of the run
1182+
// type: integer
1183+
// required: true
1184+
// responses:
1185+
// "201":
1186+
// "$ref": "#/responses/WorkflowRun"
1187+
// "400":
1188+
// "$ref": "#/responses/error"
1189+
// "403":
1190+
// "$ref": "#/responses/forbidden"
1191+
// "404":
1192+
// "$ref": "#/responses/notFound"
1193+
// "422":
1194+
// "$ref": "#/responses/validationError"
1195+
1196+
runID := ctx.PathParamInt64("run")
1197+
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
1198+
if errors.Is(err, util.ErrNotExist) {
1199+
ctx.APIErrorNotFound(err)
1200+
return
1201+
} else if err != nil {
1202+
ctx.APIErrorInternal(err)
1203+
return
1204+
}
1205+
1206+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
1207+
if err != nil {
1208+
ctx.APIErrorInternal(err)
1209+
return
1210+
}
1211+
1212+
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, nil); err != nil {
1213+
handleWorkflowRerunError(ctx, err)
1214+
return
1215+
}
1216+
1217+
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
1218+
if err != nil {
1219+
ctx.APIErrorInternal(err)
1220+
return
1221+
}
1222+
ctx.JSON(http.StatusCreated, convertedRun)
1223+
}
1224+
1225+
// RerunWorkflowJob Reruns a specific workflow job in a run.
1226+
func RerunWorkflowJob(ctx *context.APIContext) {
1227+
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
1228+
// ---
1229+
// summary: Reruns a specific workflow job in a run
1230+
// produces:
1231+
// - application/json
1232+
// parameters:
1233+
// - name: owner
1234+
// in: path
1235+
// description: owner of the repo
1236+
// type: string
1237+
// required: true
1238+
// - name: repo
1239+
// in: path
1240+
// description: name of the repository
1241+
// type: string
1242+
// required: true
1243+
// - name: run
1244+
// in: path
1245+
// description: id of the run
1246+
// type: integer
1247+
// required: true
1248+
// - name: job_id
1249+
// in: path
1250+
// description: id of the job
1251+
// type: integer
1252+
// required: true
1253+
// responses:
1254+
// "201":
1255+
// "$ref": "#/responses/WorkflowJob"
1256+
// "400":
1257+
// "$ref": "#/responses/error"
1258+
// "403":
1259+
// "$ref": "#/responses/forbidden"
1260+
// "404":
1261+
// "$ref": "#/responses/notFound"
1262+
// "422":
1263+
// "$ref": "#/responses/validationError"
1264+
1265+
runID := ctx.PathParamInt64("run")
1266+
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
1267+
if errors.Is(err, util.ErrNotExist) {
1268+
ctx.APIErrorNotFound(err)
1269+
return
1270+
} else if err != nil {
1271+
ctx.APIErrorInternal(err)
1272+
return
1273+
}
1274+
1275+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
1276+
if err != nil {
1277+
ctx.APIErrorInternal(err)
1278+
return
1279+
}
1280+
1281+
jobID := ctx.PathParamInt64("job_id")
1282+
var targetJob *actions_model.ActionRunJob
1283+
for _, job := range jobs {
1284+
if job.ID == jobID {
1285+
targetJob = job
1286+
break
1287+
}
1288+
}
1289+
if targetJob == nil {
1290+
ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
1291+
return
1292+
}
1293+
1294+
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
1295+
handleWorkflowRerunError(ctx, err)
1296+
return
1297+
}
1298+
1299+
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
1300+
if err != nil {
1301+
ctx.APIErrorInternal(err)
1302+
return
1303+
}
1304+
ctx.JSON(http.StatusCreated, convertedJob)
1305+
}
1306+
1307+
func handleWorkflowRerunError(ctx *context.APIContext, err error) {
1308+
if errors.Is(err, util.ErrInvalidArgument) {
1309+
ctx.APIError(http.StatusBadRequest, err)
1310+
return
1311+
}
1312+
ctx.APIErrorInternal(err)
1313+
}
1314+
11611315
// ListWorkflowRunJobs Lists all jobs for a workflow run.
11621316
func ListWorkflowRunJobs(ctx *context.APIContext) {
11631317
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run}/jobs repository listWorkflowRunJobs

routers/web/repo/actions/view.go

Lines changed: 13 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,6 @@ import (
3636
notify_service "code.gitea.io/gitea/services/notify"
3737

3838
"github.com/nektos/act/pkg/model"
39-
"go.yaml.in/yaml/v4"
40-
"xorm.io/builder"
4139
)
4240

4341
func getRunIndex(ctx *context_module.Context) int64 {
@@ -431,130 +429,33 @@ func Rerun(ctx *context_module.Context) {
431429
return
432430
}
433431

434-
// reset run's start and stop time
435-
run.PreviousDuration = run.Duration()
436-
run.Started = 0
437-
run.Stopped = 0
438-
run.Status = actions_model.StatusWaiting
439-
440-
vars, err := actions_model.GetVariablesOfRun(ctx, run)
432+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
441433
if err != nil {
442-
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
443-
return
444-
}
445-
446-
if run.RawConcurrency != "" {
447-
var rawConcurrency model.RawConcurrency
448-
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
449-
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
450-
return
451-
}
452-
453-
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil)
454-
if err != nil {
455-
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
456-
return
457-
}
458-
459-
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
460-
if err != nil {
461-
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
462-
return
463-
}
464-
}
465-
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
466-
ctx.ServerError("UpdateRun", err)
467-
return
468-
}
469-
470-
if err := run.LoadAttributes(ctx); err != nil {
471-
ctx.ServerError("run.LoadAttributes", err)
434+
ctx.ServerError("GetRunJobsByRunID", err)
472435
return
473436
}
474-
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
475-
476-
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
477-
if ctx.Written() {
437+
if len(jobs) == 0 {
438+
ctx.NotFound(nil)
478439
return
479440
}
480441

481-
isRunBlocked := run.Status == actions_model.StatusBlocked
482-
if jobIndexStr == "" { // rerun all jobs
483-
for _, j := range jobs {
484-
// if the job has needs, it should be set to "blocked" status to wait for other jobs
485-
shouldBlockJob := len(j.Needs) > 0 || isRunBlocked
486-
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
487-
ctx.ServerError("RerunJob", err)
488-
return
489-
}
442+
var targetJob *actions_model.ActionRunJob
443+
if jobIndexStr != "" {
444+
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
445+
targetJob = jobs[jobIndex]
446+
} else {
447+
targetJob = jobs[0]
490448
}
491-
ctx.JSONOK()
492-
return
493449
}
494450

495-
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
496-
497-
for _, j := range rerunJobs {
498-
// jobs other than the specified one should be set to "blocked" status
499-
shouldBlockJob := j.JobID != job.JobID || isRunBlocked
500-
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
501-
ctx.ServerError("RerunJob", err)
502-
return
503-
}
451+
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
452+
ctx.ServerError("RerunWorkflowRunJobs", err)
453+
return
504454
}
505455

506456
ctx.JSONOK()
507457
}
508458

509-
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
510-
status := job.Status
511-
if !status.IsDone() {
512-
return nil
513-
}
514-
515-
job.TaskID = 0
516-
job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
517-
job.Started = 0
518-
job.Stopped = 0
519-
520-
job.ConcurrencyGroup = ""
521-
job.ConcurrencyCancel = false
522-
job.IsConcurrencyEvaluated = false
523-
if err := job.LoadRun(ctx); err != nil {
524-
return err
525-
}
526-
527-
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
528-
if err != nil {
529-
return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
530-
}
531-
532-
if job.RawConcurrency != "" && !shouldBlock {
533-
err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars, nil)
534-
if err != nil {
535-
return fmt.Errorf("evaluate job concurrency: %w", err)
536-
}
537-
538-
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
539-
if err != nil {
540-
return err
541-
}
542-
}
543-
544-
if err := db.WithTx(ctx, func(ctx context.Context) error {
545-
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
546-
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
547-
return err
548-
}); err != nil {
549-
return err
550-
}
551-
552-
actions_service.CreateCommitStatusForRunJobs(ctx, job.Run, job)
553-
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
554-
555-
return nil
556-
}
557-
558459
func Logs(ctx *context_module.Context) {
559460
runIndex := getRunIndex(ctx)
560461
jobIndex := ctx.PathParamInt64("job")

0 commit comments

Comments
 (0)