feat: Add Actions API rerun endpoints for runs and jobs (#36768)

This PR adds official REST API endpoints to rerun Gitea Actions workflow
runs and individual jobs:

* POST /api/v1/repos/{owner}/{repo}/actions/runs/{run}/rerun
* POST /api/v1/repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun

It reuses the existing rerun behavior from the web UI and exposes it
through stable API routes.

---------

Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: Giteabot <teabot@gitea.io>
This commit is contained in:
Nicolas
2026-03-02 22:34:06 +01:00
committed by GitHub
parent 56f23f623a
commit 054eb6d8a5
9 changed files with 580 additions and 183 deletions

View File

@@ -36,8 +36,6 @@ import (
notify_service "code.gitea.io/gitea/services/notify"
"github.com/nektos/act/pkg/model"
"go.yaml.in/yaml/v4"
"xorm.io/builder"
)
func getRunIndex(ctx *context_module.Context) int64 {
@@ -53,7 +51,7 @@ func getRunIndex(ctx *context_module.Context) int64 {
func View(ctx *context_module.Context) {
ctx.Data["PageIsActions"] = true
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
jobIndex := ctx.PathParamInt("job")
ctx.Data["RunIndex"] = runIndex
ctx.Data["JobIndex"] = jobIndex
ctx.Data["ActionsURL"] = ctx.Repo.RepoLink + "/actions"
@@ -211,7 +209,7 @@ func getActionsViewArtifacts(ctx context.Context, repoID, runIndex int64) (artif
func ViewPost(ctx *context_module.Context) {
req := web.GetForm(ctx).(*ViewRequest)
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
jobIndex := ctx.PathParamInt("job")
current, jobs := getRunJobs(ctx, runIndex, jobIndex)
if ctx.Written() {
@@ -405,11 +403,8 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors
// If jobIndexStr is a blank string, it means rerun all jobs
func Rerun(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndexStr := ctx.PathParam("job")
var jobIndex int64
if jobIndexStr != "" {
jobIndex, _ = strconv.ParseInt(jobIndexStr, 10, 64)
}
jobIndexHas := ctx.PathParam("job") != ""
jobIndex := ctx.PathParamInt("job")
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
@@ -431,130 +426,29 @@ func Rerun(ctx *context_module.Context) {
return
}
// reset run's start and stop time
run.PreviousDuration = run.Duration()
run.Started = 0
run.Stopped = 0
run.Status = actions_model.StatusWaiting
vars, err := actions_model.GetVariablesOfRun(ctx, run)
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
if err != nil {
ctx.ServerError("GetVariablesOfRun", fmt.Errorf("get run %d variables: %w", run.ID, err))
ctx.ServerError("GetRunJobsByRunID", err)
return
}
if run.RawConcurrency != "" {
var rawConcurrency model.RawConcurrency
if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil {
ctx.ServerError("UnmarshalRawConcurrency", fmt.Errorf("unmarshal raw concurrency: %w", err))
return
}
err = actions_service.EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil)
if err != nil {
ctx.ServerError("EvaluateRunConcurrencyFillModel", err)
return
}
run.Status, err = actions_service.PrepareToStartRunWithConcurrency(ctx, run)
if err != nil {
ctx.ServerError("PrepareToStartRunWithConcurrency", err)
var targetJob *actions_model.ActionRunJob // nil means rerun all jobs
if jobIndexHas {
if jobIndex < 0 || jobIndex >= len(jobs) {
ctx.JSONError(ctx.Locale.Tr("error.not_found"))
return
}
targetJob = jobs[jobIndex] // only rerun the selected job
}
if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil {
ctx.ServerError("UpdateRun", err)
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
ctx.ServerError("RerunWorkflowRunJobs", err)
return
}
if err := run.LoadAttributes(ctx); err != nil {
ctx.ServerError("run.LoadAttributes", err)
return
}
notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run)
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
if ctx.Written() {
return
}
isRunBlocked := run.Status == actions_model.StatusBlocked
if jobIndexStr == "" { // rerun all jobs
for _, j := range jobs {
// if the job has needs, it should be set to "blocked" status to wait for other jobs
shouldBlockJob := len(j.Needs) > 0 || isRunBlocked
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
ctx.ServerError("RerunJob", err)
return
}
}
ctx.JSONOK()
return
}
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
for _, j := range rerunJobs {
// jobs other than the specified one should be set to "blocked" status
shouldBlockJob := j.JobID != job.JobID || isRunBlocked
if err := rerunJob(ctx, j, shouldBlockJob); err != nil {
ctx.ServerError("RerunJob", err)
return
}
}
ctx.JSONOK()
}
func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
status := job.Status
if !status.IsDone() {
return nil
}
job.TaskID = 0
job.Status = util.Iif(shouldBlock, actions_model.StatusBlocked, actions_model.StatusWaiting)
job.Started = 0
job.Stopped = 0
job.ConcurrencyGroup = ""
job.ConcurrencyCancel = false
job.IsConcurrencyEvaluated = false
if err := job.LoadRun(ctx); err != nil {
return err
}
vars, err := actions_model.GetVariablesOfRun(ctx, job.Run)
if err != nil {
return fmt.Errorf("get run %d variables: %w", job.Run.ID, err)
}
if job.RawConcurrency != "" && !shouldBlock {
err = actions_service.EvaluateJobConcurrencyFillModel(ctx, job.Run, job, vars, nil)
if err != nil {
return fmt.Errorf("evaluate job concurrency: %w", err)
}
job.Status, err = actions_service.PrepareToStartJobWithConcurrency(ctx, job)
if err != nil {
return err
}
}
if err := db.WithTx(ctx, func(ctx context.Context) error {
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
return err
}); err != nil {
return err
}
actions_service.CreateCommitStatusForRunJobs(ctx, job.Run, job)
notify_service.WorkflowJobStatusUpdate(ctx, job.Run.Repo, job.Run.TriggerUser, job, nil)
return nil
}
func Logs(ctx *context_module.Context) {
runIndex := getRunIndex(ctx)
jobIndex := ctx.PathParamInt64("job")
@@ -715,7 +609,7 @@ func Delete(ctx *context_module.Context) {
// getRunJobs gets the jobs of runIndex, and returns jobs[jobIndex], jobs.
// Any error will be written to the ctx.
// It never returns a nil job of an empty jobs, if the jobIndex is out of range, it will be treated as 0.
func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
func getRunJobs(ctx *context_module.Context, runIndex int64, jobIndex int) (*actions_model.ActionRunJob, []*actions_model.ActionRunJob) {
run, err := actions_model.GetRunByIndex(ctx, ctx.Repo.Repository.ID, runIndex)
if err != nil {
if errors.Is(err, util.ErrNotExist) {
@@ -740,7 +634,7 @@ func getRunJobs(ctx *context_module.Context, runIndex, jobIndex int64) (*actions
v.Run = run
}
if jobIndex >= 0 && jobIndex < int64(len(jobs)) {
if jobIndex >= 0 && jobIndex < len(jobs) {
return jobs[jobIndex], jobs
}
return jobs[0], jobs