Skip to content

Commit 054eb6d

Browse files
bircniwxiaoguangGiteaBot
authored
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>
1 parent 56f23f6 commit 054eb6d

9 files changed

Lines changed: 575 additions & 178 deletions

File tree

models/repo/repo.go

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -422,52 +422,37 @@ func (repo *Repository) UnitEnabled(ctx context.Context, tp unit.Type) bool {
422422
return false
423423
}
424424

425-
// MustGetUnit always returns a RepoUnit object
425+
// MustGetUnit always returns a RepoUnit object even if the unit doesn't exist (not enabled)
426426
func (repo *Repository) MustGetUnit(ctx context.Context, tp unit.Type) *RepoUnit {
427427
ru, err := repo.GetUnit(ctx, tp)
428428
if err == nil {
429429
return ru
430430
}
431-
431+
if !errors.Is(err, util.ErrNotExist) {
432+
setting.PanicInDevOrTesting("Failed to get unit %v for repository %d: %v", tp, repo.ID, err)
433+
}
434+
ru = &RepoUnit{RepoID: repo.ID, Type: tp}
432435
switch tp {
433436
case unit.TypeExternalWiki:
434-
return &RepoUnit{
435-
Type: tp,
436-
Config: new(ExternalWikiConfig),
437-
}
437+
ru.Config = new(ExternalWikiConfig)
438438
case unit.TypeExternalTracker:
439-
return &RepoUnit{
440-
Type: tp,
441-
Config: new(ExternalTrackerConfig),
442-
}
439+
ru.Config = new(ExternalTrackerConfig)
443440
case unit.TypePullRequests:
444-
return &RepoUnit{
445-
Type: tp,
446-
Config: new(PullRequestsConfig),
447-
}
441+
ru.Config = new(PullRequestsConfig)
448442
case unit.TypeIssues:
449-
return &RepoUnit{
450-
Type: tp,
451-
Config: new(IssuesConfig),
452-
}
443+
ru.Config = new(IssuesConfig)
453444
case unit.TypeActions:
454-
return &RepoUnit{
455-
Type: tp,
456-
Config: new(ActionsConfig),
457-
}
445+
ru.Config = new(ActionsConfig)
458446
case unit.TypeProjects:
459-
cfg := new(ProjectsConfig)
460-
cfg.ProjectsMode = ProjectsModeNone
461-
return &RepoUnit{
462-
Type: tp,
463-
Config: cfg,
464-
}
447+
ru.Config = new(ProjectsConfig)
448+
default: // other units don't have config
465449
}
466-
467-
return &RepoUnit{
468-
Type: tp,
469-
Config: new(UnitConfig),
450+
if ru.Config != nil {
451+
if err = ru.Config.FromDB(nil); err != nil {
452+
setting.PanicInDevOrTesting("Failed to load default config for unit %v of repository %d: %v", tp, repo.ID, err)
453+
}
470454
}
455+
return ru
471456
}
472457

473458
// GetUnit returns a RepoUnit object

models/repo/repo_unit.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,8 @@ type ProjectsConfig struct {
241241

242242
// FromDB fills up a ProjectsConfig from serialized format.
243243
func (cfg *ProjectsConfig) FromDB(bs []byte) error {
244+
// TODO: remove GetProjectsMode, only use ProjectsMode
245+
cfg.ProjectsMode = ProjectsModeAll
244246
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
245247
}
246248

routers/api/v1/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1259,7 +1259,9 @@ func Routes() *web.Router {
12591259
m.Group("/{run}", func() {
12601260
m.Get("", repo.GetWorkflowRun)
12611261
m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun)
1262+
m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun)
12621263
m.Get("/jobs", repo.ListWorkflowRunJobs)
1264+
m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob)
12631265
m.Get("/artifacts", repo.GetArtifactsOfRun)
12641266
})
12651267
})

routers/api/v1/repo/action.go

Lines changed: 159 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"net/http"
1414
"net/url"
15+
"slices"
1516
"strconv"
1617
"strings"
1718
"time"
@@ -1103,6 +1104,33 @@ func ActionsEnableWorkflow(ctx *context.APIContext) {
11031104
ctx.Status(http.StatusNoContent)
11041105
}
11051106

1107+
func getCurrentRepoActionRunByID(ctx *context.APIContext) *actions_model.ActionRun {
1108+
runID := ctx.PathParamInt64("run")
1109+
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
1110+
if errors.Is(err, util.ErrNotExist) {
1111+
ctx.APIErrorNotFound(err)
1112+
return nil
1113+
} else if err != nil {
1114+
ctx.APIErrorInternal(err)
1115+
return nil
1116+
}
1117+
return run
1118+
}
1119+
1120+
func getCurrentRepoActionRunJobsByID(ctx *context.APIContext) (*actions_model.ActionRun, actions_model.ActionJobList) {
1121+
run := getCurrentRepoActionRunByID(ctx)
1122+
if ctx.Written() {
1123+
return nil, nil
1124+
}
1125+
1126+
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
1127+
if err != nil {
1128+
ctx.APIErrorInternal(err)
1129+
return nil, nil
1130+
}
1131+
return run, jobs
1132+
}
1133+
11061134
// GetWorkflowRun Gets a specific workflow run.
11071135
func GetWorkflowRun(ctx *context.APIContext) {
11081136
// swagger:operation GET /repos/{owner}/{repo}/actions/runs/{run} repository GetWorkflowRun
@@ -1134,24 +1162,144 @@ func GetWorkflowRun(ctx *context.APIContext) {
11341162
// "404":
11351163
// "$ref": "#/responses/notFound"
11361164

1137-
runID := ctx.PathParamInt64("run")
1138-
job, has, err := db.GetByID[actions_model.ActionRun](ctx, runID)
1165+
run := getCurrentRepoActionRunByID(ctx)
1166+
if ctx.Written() {
1167+
return
1168+
}
1169+
1170+
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
11391171
if err != nil {
11401172
ctx.APIErrorInternal(err)
11411173
return
11421174
}
1175+
ctx.JSON(http.StatusOK, convertedRun)
1176+
}
11431177

1144-
if !has || job.RepoID != ctx.Repo.Repository.ID {
1145-
ctx.APIErrorNotFound(util.ErrNotExist)
1178+
// RerunWorkflowRun Reruns an entire workflow run.
1179+
func RerunWorkflowRun(ctx *context.APIContext) {
1180+
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun repository rerunWorkflowRun
1181+
// ---
1182+
// summary: Reruns an entire workflow run
1183+
// produces:
1184+
// - application/json
1185+
// parameters:
1186+
// - name: owner
1187+
// in: path
1188+
// description: owner of the repo
1189+
// type: string
1190+
// required: true
1191+
// - name: repo
1192+
// in: path
1193+
// description: name of the repository
1194+
// type: string
1195+
// required: true
1196+
// - name: run
1197+
// in: path
1198+
// description: id of the run
1199+
// type: integer
1200+
// required: true
1201+
// responses:
1202+
// "201":
1203+
// "$ref": "#/responses/WorkflowRun"
1204+
// "400":
1205+
// "$ref": "#/responses/error"
1206+
// "403":
1207+
// "$ref": "#/responses/forbidden"
1208+
// "404":
1209+
// "$ref": "#/responses/notFound"
1210+
// "422":
1211+
// "$ref": "#/responses/validationError"
1212+
1213+
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
1214+
if ctx.Written() {
11461215
return
11471216
}
11481217

1149-
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, job)
1218+
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, nil); err != nil {
1219+
handleWorkflowRerunError(ctx, err)
1220+
return
1221+
}
1222+
1223+
convertedRun, err := convert.ToActionWorkflowRun(ctx, ctx.Repo.Repository, run)
11501224
if err != nil {
11511225
ctx.APIErrorInternal(err)
11521226
return
11531227
}
1154-
ctx.JSON(http.StatusOK, convertedRun)
1228+
ctx.JSON(http.StatusCreated, convertedRun)
1229+
}
1230+
1231+
// RerunWorkflowJob Reruns a specific workflow job in a run.
1232+
func RerunWorkflowJob(ctx *context.APIContext) {
1233+
// swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob
1234+
// ---
1235+
// summary: Reruns a specific workflow job in a run
1236+
// produces:
1237+
// - application/json
1238+
// parameters:
1239+
// - name: owner
1240+
// in: path
1241+
// description: owner of the repo
1242+
// type: string
1243+
// required: true
1244+
// - name: repo
1245+
// in: path
1246+
// description: name of the repository
1247+
// type: string
1248+
// required: true
1249+
// - name: run
1250+
// in: path
1251+
// description: id of the run
1252+
// type: integer
1253+
// required: true
1254+
// - name: job_id
1255+
// in: path
1256+
// description: id of the job
1257+
// type: integer
1258+
// required: true
1259+
// responses:
1260+
// "201":
1261+
// "$ref": "#/responses/WorkflowJob"
1262+
// "400":
1263+
// "$ref": "#/responses/error"
1264+
// "403":
1265+
// "$ref": "#/responses/forbidden"
1266+
// "404":
1267+
// "$ref": "#/responses/notFound"
1268+
// "422":
1269+
// "$ref": "#/responses/validationError"
1270+
1271+
run, jobs := getCurrentRepoActionRunJobsByID(ctx)
1272+
if ctx.Written() {
1273+
return
1274+
}
1275+
1276+
jobID := ctx.PathParamInt64("job_id")
1277+
jobIdx := slices.IndexFunc(jobs, func(job *actions_model.ActionRunJob) bool { return job.ID == jobID })
1278+
if jobIdx == -1 {
1279+
ctx.APIErrorNotFound(util.NewNotExistErrorf("workflow job with id %d", jobID))
1280+
return
1281+
}
1282+
1283+
targetJob := jobs[jobIdx]
1284+
if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil {
1285+
handleWorkflowRerunError(ctx, err)
1286+
return
1287+
}
1288+
1289+
convertedJob, err := convert.ToActionWorkflowJob(ctx, ctx.Repo.Repository, nil, targetJob)
1290+
if err != nil {
1291+
ctx.APIErrorInternal(err)
1292+
return
1293+
}
1294+
ctx.JSON(http.StatusCreated, convertedJob)
1295+
}
1296+
1297+
func handleWorkflowRerunError(ctx *context.APIContext, err error) {
1298+
if errors.Is(err, util.ErrInvalidArgument) {
1299+
ctx.APIError(http.StatusBadRequest, err)
1300+
return
1301+
}
1302+
ctx.APIErrorInternal(err)
11551303
}
11561304

11571305
// ListWorkflowRunJobs Lists all jobs for a workflow run.
@@ -1198,9 +1346,7 @@ func ListWorkflowRunJobs(ctx *context.APIContext) {
11981346
// "404":
11991347
// "$ref": "#/responses/notFound"
12001348

1201-
repoID := ctx.Repo.Repository.ID
1202-
1203-
runID := ctx.PathParamInt64("run")
1349+
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
12041350

12051351
// Avoid the list all jobs functionality for this api route to be used with a runID == 0.
12061352
if runID <= 0 {
@@ -1300,10 +1446,8 @@ func GetArtifactsOfRun(ctx *context.APIContext) {
13001446
// "404":
13011447
// "$ref": "#/responses/notFound"
13021448

1303-
repoID := ctx.Repo.Repository.ID
13041449
artifactName := ctx.Req.URL.Query().Get("name")
1305-
1306-
runID := ctx.PathParamInt64("run")
1450+
repoID, runID := ctx.Repo.Repository.ID, ctx.PathParamInt64("run")
13071451

13081452
artifacts, total, err := db.FindAndCount[actions_model.ActionArtifact](ctx, actions_model.FindArtifactsOptions{
13091453
RepoID: repoID,
@@ -1364,15 +1508,11 @@ func DeleteActionRun(ctx *context.APIContext) {
13641508
// "404":
13651509
// "$ref": "#/responses/notFound"
13661510

1367-
runID := ctx.PathParamInt64("run")
1368-
run, err := actions_model.GetRunByRepoAndID(ctx, ctx.Repo.Repository.ID, runID)
1369-
if errors.Is(err, util.ErrNotExist) {
1370-
ctx.APIErrorNotFound(err)
1371-
return
1372-
} else if err != nil {
1373-
ctx.APIErrorInternal(err)
1511+
run := getCurrentRepoActionRunByID(ctx)
1512+
if ctx.Written() {
13741513
return
13751514
}
1515+
13761516
if !run.Status.IsDone() {
13771517
ctx.APIError(http.StatusBadRequest, "this workflow run is not done")
13781518
return

0 commit comments

Comments
 (0)