mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-26 02:58:51 +01:00
Feature: Add button to re-run failed jobs in Actions (#36924)
Fixes #35997 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
parent
ee009ebec8
commit
b22123ef86
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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"}}"
|
||||
|
||||
49
templates/swagger/v1_json.tmpl
generated
49
templates/swagger/v1_json.tmpl
generated
@ -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": [
|
||||
|
||||
@ -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({
|
||||
<button class="ui basic small compact button red" @click="cancelRun()" v-else-if="run.canCancel">
|
||||
{{ locale.cancel }}
|
||||
</button>
|
||||
<button class="ui basic small compact button link-action" :data-url="`${run.link}/rerun`" v-else-if="run.canRerun">
|
||||
{{ locale.rerun_all }}
|
||||
</button>
|
||||
<template v-else-if="run.canRerun">
|
||||
<div v-if="run.canRerunFailed" class="ui small compact buttons">
|
||||
<button class="ui basic small compact button link-action" :data-url="`${run.link}/rerun-failed`">
|
||||
{{ locale.rerun_failed }}
|
||||
</button>
|
||||
<div class="ui basic small compact dropdown icon button">
|
||||
<SvgIcon name="octicon-triangle-down" :size="14"/>
|
||||
<div class="menu">
|
||||
<div class="item link-action" :data-url="`${run.link}/rerun`">
|
||||
{{ locale.rerun_all }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button v-else class="ui basic small compact button link-action" :data-url="`${run.link}/rerun`">
|
||||
{{ locale.rerun_all }}
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-commit-summary">
|
||||
|
||||
@ -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'),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user