From b22123ef862247f36baec556f25872d04c7cfbcd Mon Sep 17 00:00:00 2001 From: bircni Date: Sat, 21 Mar 2026 22:27:13 +0100 Subject: [PATCH] Feature: Add button to re-run failed jobs in Actions (#36924) Fixes #35997 --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: wxiaoguang --- options/locale/locale_en-US.json | 1 + routers/api/v1/api.go | 1 + routers/api/v1/repo/action.go | 50 ++++++++++- routers/web/devtest/mock_actions.go | 1 + routers/web/repo/actions/view.go | 66 +++++++++++---- routers/web/web.go | 1 + services/actions/rerun.go | 87 +++++++++++++------ services/actions/rerun_test.go | 97 ++++++++++++++++++++++ templates/repo/actions/view_component.tmpl | 1 + templates/swagger/v1_json.tmpl | 49 +++++++++++ web_src/js/components/RepoActionView.vue | 22 ++++- web_src/js/features/repo-actions.ts | 1 + 12 files changed, 332 insertions(+), 45 deletions(-) diff --git a/options/locale/locale_en-US.json b/options/locale/locale_en-US.json index 5a5148a146..1eafb3c9d1 100644 --- a/options/locale/locale_en-US.json +++ b/options/locale/locale_en-US.json @@ -81,6 +81,7 @@ "retry": "Retry", "rerun": "Re-run", "rerun_all": "Re-run all jobs", + "rerun_failed": "Re-run failed jobs", "save": "Save", "add": "Add", "add_all": "Add All", diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 560894b798..454bae73db 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1259,6 +1259,7 @@ func Routes() *web.Router { m.Get("", repo.GetWorkflowRun) m.Delete("", reqToken(), reqRepoWriter(unit.TypeActions), repo.DeleteActionRun) m.Post("/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowRun) + m.Post("/rerun-failed-jobs", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunFailedWorkflowRun) m.Get("/jobs", repo.ListWorkflowRunJobs) m.Post("/jobs/{job_id}/rerun", reqToken(), reqRepoWriter(unit.TypeActions), repo.RerunWorkflowJob) m.Get("/artifacts", repo.GetArtifactsOfRun) diff --git a/routers/api/v1/repo/action.go b/routers/api/v1/repo/action.go index d7e51b8046..d704092051 100644 --- a/routers/api/v1/repo/action.go +++ b/routers/api/v1/repo/action.go @@ -1255,7 +1255,7 @@ func RerunWorkflowRun(ctx *context.APIContext) { return } - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, nil); err != nil { + if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs); err != nil { handleWorkflowRerunError(ctx, err) return } @@ -1268,6 +1268,52 @@ func RerunWorkflowRun(ctx *context.APIContext) { ctx.JSON(http.StatusCreated, convertedRun) } +// RerunFailedWorkflowRun Reruns all failed jobs in a workflow run. +func RerunFailedWorkflowRun(ctx *context.APIContext) { + // swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/rerun-failed-jobs repository rerunFailedWorkflowRun + // --- + // summary: Reruns all failed jobs in a workflow run + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repository + // type: string + // required: true + // - name: run + // in: path + // description: id of the run + // type: integer + // required: true + // responses: + // "201": + // "$ref": "#/responses/empty" + // "400": + // "$ref": "#/responses/error" + // "403": + // "$ref": "#/responses/forbidden" + // "404": + // "$ref": "#/responses/notFound" + // "422": + // "$ref": "#/responses/validationError" + + run, jobs := getCurrentRepoActionRunJobsByID(ctx) + if ctx.Written() { + return + } + + if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil { + handleWorkflowRerunError(ctx, err) + return + } + + ctx.Status(http.StatusCreated) +} + // RerunWorkflowJob Reruns a specific workflow job in a run. func RerunWorkflowJob(ctx *context.APIContext) { // swagger:operation POST /repos/{owner}/{repo}/actions/runs/{run}/jobs/{job_id}/rerun repository rerunWorkflowJob @@ -1321,7 +1367,7 @@ func RerunWorkflowJob(ctx *context.APIContext) { } targetJob := jobs[jobIdx] - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil { + if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetAllRerunJobs(targetJob, jobs)); err != nil { handleWorkflowRerunError(ctx, err) return } diff --git a/routers/web/devtest/mock_actions.go b/routers/web/devtest/mock_actions.go index 0dd33425dc..ac3483239a 100644 --- a/routers/web/devtest/mock_actions.go +++ b/routers/web/devtest/mock_actions.go @@ -75,6 +75,7 @@ func MockActionsRunsJobs(ctx *context.Context) { resp.State.Run.CanCancel = runID == 10 resp.State.Run.CanApprove = runID == 20 resp.State.Run.CanRerun = runID == 30 + resp.State.Run.CanRerunFailed = runID == 30 resp.State.Run.CanDeleteArtifact = true resp.State.Run.WorkflowID = "workflow-id" resp.State.Run.WorkflowLink = "./workflow-link" diff --git a/routers/web/repo/actions/view.go b/routers/web/repo/actions/view.go index 2685dd8857..6a48a2daa3 100644 --- a/routers/web/repo/actions/view.go +++ b/routers/web/repo/actions/view.go @@ -122,6 +122,7 @@ type ViewResponse struct { CanCancel bool `json:"canCancel"` CanApprove bool `json:"canApprove"` // the run needs an approval and the doer has permission to approve CanRerun bool `json:"canRerun"` + CanRerunFailed bool `json:"canRerunFailed"` CanDeleteArtifact bool `json:"canDeleteArtifact"` Done bool `json:"done"` WorkflowID string `json:"workflowID"` @@ -238,6 +239,14 @@ func ViewPost(ctx *context_module.Context) { resp.State.Run.CanApprove = run.NeedApproval && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanRerun = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) resp.State.Run.CanDeleteArtifact = run.Status.IsDone() && ctx.Repo.CanWrite(unit.TypeActions) + if resp.State.Run.CanRerun { + for _, job := range jobs { + if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled { + resp.State.Run.CanRerunFailed = true + break + } + } + } resp.State.Run.Done = run.Status.IsDone() resp.State.Run.WorkflowID = run.WorkflowID resp.State.Run.WorkflowLink = run.WorkflowLink() @@ -398,6 +407,22 @@ func convertToViewModel(ctx context.Context, locale translation.Locale, cursors return viewJobs, logs, nil } +// checkRunRerunAllowed checks whether a rerun is permitted for the given run, +// writing the appropriate JSON error to ctx and returning false when it is not. +func checkRunRerunAllowed(ctx *context_module.Context, run *actions_model.ActionRun) bool { + if !run.Status.IsDone() { + ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done")) + return false + } + cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) + cfg := cfgUnit.ActionsConfig() + if cfg.IsWorkflowDisabled(run.WorkflowID) { + ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled")) + return false + } + return true +} + // Rerun will rerun jobs in the given run // If jobIDStr is a blank string, it means rerun all jobs func Rerun(ctx *context_module.Context) { @@ -408,26 +433,39 @@ func Rerun(ctx *context_module.Context) { return } - // rerun is not allowed if the run is not done - if !run.Status.IsDone() { - ctx.JSONError(ctx.Locale.Tr("actions.runs.not_done")) + if !checkRunRerunAllowed(ctx, run) { return } - // can not rerun job when workflow is disabled - cfgUnit := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypeActions) - cfg := cfgUnit.ActionsConfig() - if cfg.IsWorkflowDisabled(run.WorkflowID) { - ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled")) - return - } - - var targetJob *actions_model.ActionRunJob // nil means rerun all jobs + var jobsToRerun []*actions_model.ActionRunJob if ctx.PathParam("job") != "" { - targetJob = currentJob + jobsToRerun = actions_service.GetAllRerunJobs(currentJob, jobs) + } else { + jobsToRerun = jobs } - if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobs, targetJob); err != nil { + if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, jobsToRerun); err != nil { + ctx.ServerError("RerunWorkflowRunJobs", err) + return + } + + ctx.JSONOK() +} + +// RerunFailed reruns all failed jobs in the given run +func RerunFailed(ctx *context_module.Context) { + runID := getRunID(ctx) + + run, jobs, _ := getRunJobsAndCurrentJob(ctx, runID) + if ctx.Written() { + return + } + + if !checkRunRerunAllowed(ctx, run) { + return + } + + if err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, actions_service.GetFailedRerunJobs(jobs)); err != nil { ctx.ServerError("RerunWorkflowRunJobs", err) return } diff --git a/routers/web/web.go b/routers/web/web.go index 8da7609994..fd5276e4e0 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1529,6 +1529,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Get("/artifacts/{artifact_name}", actions.ArtifactsDownloadView) m.Delete("/artifacts/{artifact_name}", reqRepoActionsWriter, actions.ArtifactsDeleteView) m.Post("/rerun", reqRepoActionsWriter, actions.Rerun) + m.Post("/rerun-failed", reqRepoActionsWriter, actions.RerunFailed) }) m.Group("/workflows/{workflow_name}", func() { m.Get("/badge.svg", webAuth.AllowBasic, webAuth.AllowOAuth2, actions.GetWorkflowBadge) diff --git a/services/actions/rerun.go b/services/actions/rerun.go index 5177b90d61..1596d9bfc5 100644 --- a/services/actions/rerun.go +++ b/services/actions/rerun.go @@ -20,7 +20,27 @@ import ( "xorm.io/builder" ) -// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun +// GetFailedRerunJobs returns all failed jobs and their downstream dependent jobs that need to be rerun +func GetFailedRerunJobs(allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob { + rerunJobIDSet := make(container.Set[int64]) + var jobsToRerun []*actions_model.ActionRunJob + + for _, job := range allJobs { + if job.Status == actions_model.StatusFailure || job.Status == actions_model.StatusCancelled { + for _, j := range GetAllRerunJobs(job, allJobs) { + if !rerunJobIDSet.Contains(j.ID) { + rerunJobIDSet.Add(j.ID) + jobsToRerun = append(jobsToRerun, j) + } + } + } + } + + return jobsToRerun +} + +// GetAllRerunJobs returns the target job and all jobs that transitively depend on it. +// Downstream jobs are included regardless of their current status. func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.ActionRunJob) []*actions_model.ActionRunJob { rerunJobs := []*actions_model.ActionRunJob{job} rerunJobsIDSet := make(container.Set[string]) @@ -49,12 +69,12 @@ func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.A return rerunJobs } -// RerunWorkflowRunJobs reruns all done jobs of a workflow run, -// or reruns a selected job and all of its downstream jobs when targetJob is specified. -func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob, targetJob *actions_model.ActionRunJob) error { - // Rerun is not allowed if the run is not done. +// prepareRunRerun validates the run, resets its state, handles concurrency, persists the +// updated run, and fires a status-update notification. +// It returns isRunBlocked (true when the run itself is held by a concurrency group). +func prepareRunRerun(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobs []*actions_model.ActionRunJob) (isRunBlocked bool, err error) { if !run.Status.IsDone() { - return util.NewInvalidArgumentErrorf("this workflow run is not done") + return false, util.NewInvalidArgumentErrorf("this workflow run is not done") } cfgUnit := repo.MustGetUnit(ctx, unit.TypeActions) @@ -62,7 +82,7 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run // Rerun is not allowed when workflow is disabled. cfg := cfgUnit.ActionsConfig() if cfg.IsWorkflowDisabled(run.WorkflowID) { - return util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID) + return false, util.NewInvalidArgumentErrorf("workflow %s is disabled", run.WorkflowID) } // Reset run's timestamps and status. @@ -73,31 +93,31 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run vars, err := actions_model.GetVariablesOfRun(ctx, run) if err != nil { - return fmt.Errorf("get run %d variables: %w", run.ID, err) + return false, fmt.Errorf("get run %d variables: %w", run.ID, err) } if run.RawConcurrency != "" { var rawConcurrency model.RawConcurrency if err := yaml.Unmarshal([]byte(run.RawConcurrency), &rawConcurrency); err != nil { - return fmt.Errorf("unmarshal raw concurrency: %w", err) + return false, fmt.Errorf("unmarshal raw concurrency: %w", err) } if err := EvaluateRunConcurrencyFillModel(ctx, run, &rawConcurrency, vars, nil); err != nil { - return err + return false, err } run.Status, err = PrepareToStartRunWithConcurrency(ctx, run) if err != nil { - return err + return false, err } } if err := actions_model.UpdateRun(ctx, run, "started", "stopped", "previous_duration", "status", "concurrency_group", "concurrency_cancel"); err != nil { - return err + return false, err } if err := run.LoadAttributes(ctx); err != nil { - return err + return false, err } for _, job := range jobs { @@ -106,23 +126,38 @@ func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run notify_service.WorkflowRunStatusUpdate(ctx, run.Repo, run.TriggerUser, run) - isRunBlocked := run.Status == actions_model.StatusBlocked + return run.Status == actions_model.StatusBlocked, nil +} - if targetJob == nil { - for _, job := range jobs { - // If the job has needs, it should be blocked to wait for its dependencies. - shouldBlockJob := len(job.Needs) > 0 || isRunBlocked - if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil { - return err - } - } +// RerunWorkflowRunJobs reruns the given jobs of a workflow run. +// jobsToRerun must include all jobs to be rerun (the target job and its transitively dependent jobs). +// A job is blocked (waiting for dependencies) if the run itself is blocked or if any of its +// needs are also being rerun. +func RerunWorkflowRunJobs(ctx context.Context, repo *repo_model.Repository, run *actions_model.ActionRun, jobsToRerun []*actions_model.ActionRunJob) error { + if len(jobsToRerun) == 0 { return nil } - rerunJobs := GetAllRerunJobs(targetJob, jobs) - for _, job := range rerunJobs { - // Jobs other than the selected one should wait for dependencies. - shouldBlockJob := job.JobID != targetJob.JobID || isRunBlocked + isRunBlocked, err := prepareRunRerun(ctx, repo, run, jobsToRerun) + if err != nil { + return err + } + + rerunJobIDs := make(container.Set[string]) + for _, j := range jobsToRerun { + rerunJobIDs.Add(j.JobID) + } + + for _, job := range jobsToRerun { + shouldBlockJob := isRunBlocked + if !shouldBlockJob { + for _, need := range job.Needs { + if rerunJobIDs.Contains(need) { + shouldBlockJob = true + break + } + } + } if err := rerunWorkflowJob(ctx, job, shouldBlockJob); err != nil { return err } diff --git a/services/actions/rerun_test.go b/services/actions/rerun_test.go index a98de7b788..3b4dc5483f 100644 --- a/services/actions/rerun_test.go +++ b/services/actions/rerun_test.go @@ -4,11 +4,14 @@ package actions import ( + "context" "testing" actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/util" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetAllRerunJobs(t *testing.T) { @@ -46,3 +49,97 @@ func TestGetAllRerunJobs(t *testing.T) { assert.ElementsMatch(t, tc.rerunJobs, rerunJobs) } } + +func TestGetFailedRerunJobs(t *testing.T) { + // IDs must be non-zero to distinguish jobs in the dedup set. + makeJob := func(id int64, jobID string, status actions_model.Status, needs ...string) *actions_model.ActionRunJob { + return &actions_model.ActionRunJob{ID: id, JobID: jobID, Status: status, Needs: needs} + } + + t.Run("no failed jobs returns empty", func(t *testing.T) { + jobs := []*actions_model.ActionRunJob{ + makeJob(1, "job1", actions_model.StatusSuccess), + makeJob(2, "job2", actions_model.StatusSkipped, "job1"), + } + assert.Empty(t, GetFailedRerunJobs(jobs)) + }) + + t.Run("single failed job with no dependents", func(t *testing.T) { + job1 := makeJob(1, "job1", actions_model.StatusFailure) + job2 := makeJob(2, "job2", actions_model.StatusSuccess) + jobs := []*actions_model.ActionRunJob{job1, job2} + + result := GetFailedRerunJobs(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1}, result) + }) + + t.Run("failed job pulls in downstream dependents", func(t *testing.T) { + // job1 failed; job2 depends on job1 (skipped); job3 depends on job2 (skipped) + job1 := makeJob(1, "job1", actions_model.StatusFailure) + job2 := makeJob(2, "job2", actions_model.StatusSkipped, "job1") + job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job2") + job4 := makeJob(4, "job4", actions_model.StatusSuccess) // unrelated, must not appear + jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4} + + result := GetFailedRerunJobs(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3}, result) + }) + + t.Run("multiple independent failed jobs each pull in their own dependents", func(t *testing.T) { + // job1 failed -> job3 depends on job1 + // job2 failed -> job4 depends on job2 + job1 := makeJob(1, "job1", actions_model.StatusFailure) + job2 := makeJob(2, "job2", actions_model.StatusFailure) + job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1") + job4 := makeJob(4, "job4", actions_model.StatusSkipped, "job2") + jobs := []*actions_model.ActionRunJob{job1, job2, job3, job4} + + result := GetFailedRerunJobs(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3, job4}, result) + }) + + t.Run("shared downstream dependent is not duplicated", func(t *testing.T) { + // job1 and job2 both failed; job3 depends on both + job1 := makeJob(1, "job1", actions_model.StatusFailure) + job2 := makeJob(2, "job2", actions_model.StatusFailure) + job3 := makeJob(3, "job3", actions_model.StatusSkipped, "job1", "job2") + jobs := []*actions_model.ActionRunJob{job1, job2, job3} + + result := GetFailedRerunJobs(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2, job3}, result) + assert.Len(t, result, 3) // job3 must appear exactly once + }) + + t.Run("successful downstream job of a failed job is still included", func(t *testing.T) { + // job1 failed; job2 succeeded but depends on job1 — downstream is always rerun + // regardless of its own status (GetAllRerunJobs includes all transitive dependents) + job1 := makeJob(1, "job1", actions_model.StatusFailure) + job2 := makeJob(2, "job2", actions_model.StatusSuccess, "job1") + jobs := []*actions_model.ActionRunJob{job1, job2} + + result := GetFailedRerunJobs(jobs) + assert.ElementsMatch(t, []*actions_model.ActionRunJob{job1, job2}, result) + }) +} + +func TestRerunValidation(t *testing.T) { + runningRun := &actions_model.ActionRun{Status: actions_model.StatusRunning} + + t.Run("RerunWorkflowRunJobs rejects a non-done run", func(t *testing.T) { + jobs := []*actions_model.ActionRunJob{ + {ID: 1, JobID: "job1"}, + } + err := RerunWorkflowRunJobs(context.Background(), nil, runningRun, jobs) + require.Error(t, err) + assert.ErrorIs(t, err, util.ErrInvalidArgument) + }) + + t.Run("RerunWorkflowRunJobs rejects a non-done run when failed jobs exist", func(t *testing.T) { + jobs := []*actions_model.ActionRunJob{ + {ID: 1, JobID: "job1", Status: actions_model.StatusFailure}, + } + err := RerunWorkflowRunJobs(context.Background(), nil, runningRun, GetFailedRerunJobs(jobs)) + require.Error(t, err) + assert.ErrorIs(t, err, util.ErrInvalidArgument) + }) +} diff --git a/templates/repo/actions/view_component.tmpl b/templates/repo/actions/view_component.tmpl index 457159ef54..9fa77d8a3f 100644 --- a/templates/repo/actions/view_component.tmpl +++ b/templates/repo/actions/view_component.tmpl @@ -7,6 +7,7 @@ data-locale-cancel="{{ctx.Locale.Tr "actions.runs.cancel"}}" data-locale-rerun="{{ctx.Locale.Tr "rerun"}}" data-locale-rerun-all="{{ctx.Locale.Tr "rerun_all"}}" + data-locale-rerun-failed="{{ctx.Locale.Tr "rerun_failed"}}" data-locale-runs-scheduled="{{ctx.Locale.Tr "actions.runs.scheduled"}}" data-locale-runs-commit="{{ctx.Locale.Tr "actions.runs.commit"}}" data-locale-runs-pushed-by="{{ctx.Locale.Tr "actions.runs.pushed_by"}}" diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index fab51203d1..20db48b91a 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -5578,6 +5578,55 @@ } } }, + "/repos/{owner}/{repo}/actions/runs/{run}/rerun-failed-jobs": { + "post": { + "tags": [ + "repository" + ], + "summary": "Reruns all failed jobs in a workflow run", + "operationId": "rerunFailedWorkflowRun", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repository", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "id of the run", + "name": "run", + "in": "path", + "required": true + } + ], + "responses": { + "201": { + "$ref": "#/responses/empty" + }, + "400": { + "$ref": "#/responses/error" + }, + "403": { + "$ref": "#/responses/forbidden" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "422": { + "$ref": "#/responses/validationError" + } + } + } + }, "/repos/{owner}/{repo}/actions/secrets": { "get": { "produces": [ diff --git a/web_src/js/components/RepoActionView.vue b/web_src/js/components/RepoActionView.vue index 8f3fd097b3..3c4f6d5273 100644 --- a/web_src/js/components/RepoActionView.vue +++ b/web_src/js/components/RepoActionView.vue @@ -147,6 +147,7 @@ export default defineComponent({ canCancel: false, canApprove: false, canRerun: false, + canRerunFailed: false, canDeleteArtifact: false, done: false, workflowID: '', @@ -512,9 +513,24 @@ export default defineComponent({ - +
diff --git a/web_src/js/features/repo-actions.ts b/web_src/js/features/repo-actions.ts index fca41c0c66..7fa0461786 100644 --- a/web_src/js/features/repo-actions.ts +++ b/web_src/js/features/repo-actions.ts @@ -19,6 +19,7 @@ export function initRepositoryActionView() { cancel: el.getAttribute('data-locale-cancel'), rerun: el.getAttribute('data-locale-rerun'), rerun_all: el.getAttribute('data-locale-rerun-all'), + rerun_failed: el.getAttribute('data-locale-rerun-failed'), scheduled: el.getAttribute('data-locale-runs-scheduled'), commit: el.getAttribute('data-locale-runs-commit'), pushedBy: el.getAttribute('data-locale-runs-pushed-by'),