mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
[v15.0/forgejo] refactor: move rerun logic to services (#12537)
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/12141 Move the logic for handling reruns of Forgejo Action workflows and individual jobs to services. That is a prerequisite for adding the corresponding HTTP API endpoints. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. All work and communication must conform to Forgejo's [AI Agreement](https://codeberg.org/forgejo/governance/src/branch/main/AIAgreement.md). There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [x] `make pr-go` before pushing ### Tests for JavaScript changes (can be removed for Go changes) - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [x] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. *The decision if the pull request will be shown in the release notes is up to the mergers / release team.* The content of the `release-notes/<pull request number>.md` file will serve as the basis for the release notes. If the file does not exist, the title of the pull request will be used instead. Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12537 Reviewed-by: Andreas Ahlenstorf <aahlenst@noreply.codeberg.org> Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
This commit is contained in:
parent
4e40eede03
commit
2d982942f5
12 changed files with 1122 additions and 140 deletions
|
|
@ -257,19 +257,33 @@ func (run *ActionRun) IsDispatchedRun() bool {
|
|||
return run.TriggerEvent == "workflow_dispatch"
|
||||
}
|
||||
|
||||
// IsRunnable indicates whether this ActionRun can generally be run.
|
||||
func (run *ActionRun) IsRunnable() bool {
|
||||
// IsValid indicates whether this ActionRun is valid and can be run.
|
||||
func (run *ActionRun) IsValid() bool {
|
||||
return run.PreExecutionErrorCode == 0 && run.PreExecutionError == ""
|
||||
}
|
||||
|
||||
// CanBeRerun indicates whether this ActionRun can be rerun.
|
||||
func (run *ActionRun) CanBeRerun() bool {
|
||||
if !run.IsRunnable() {
|
||||
if !run.IsValid() {
|
||||
return false
|
||||
}
|
||||
return run.Status.IsDone()
|
||||
}
|
||||
|
||||
func (run *ActionRun) PrepareNextAttempt() error {
|
||||
if run.Status != StatusUnknown && !run.Status.IsDone() {
|
||||
return fmt.Errorf("cannot prepare next attempt because run %d is active: %s", run.ID, run.Status.String())
|
||||
}
|
||||
|
||||
run.PreviousDuration = run.Duration()
|
||||
|
||||
run.Status = StatusWaiting
|
||||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionsCountOpenCacheKey(repoID int64) string {
|
||||
return fmt.Sprintf("Actions:CountOpenActionRuns:%d", repoID)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,12 +141,16 @@ func (job *ActionRunJob) PrepareNextAttempt(initialStatus Status) error {
|
|||
}
|
||||
|
||||
// CanBeRerun answers whether this ActionRunJob can be rerun. Returns true if it is done and the Run it belongs to
|
||||
// is runnable. Returns false in all other cases, including when Run is nil.
|
||||
func (job *ActionRunJob) CanBeRerun() bool {
|
||||
if job.Run == nil || !job.Run.IsRunnable() {
|
||||
return false
|
||||
// is valid. Returns false in all other cases.
|
||||
func (job *ActionRunJob) CanBeRerun(ctx context.Context) (bool, error) {
|
||||
if err := job.LoadRun(ctx); err != nil {
|
||||
return false, fmt.Errorf("cannot load run %d of job %d: %w", job.RunID, job.ID, err)
|
||||
}
|
||||
return job.Status.IsDone()
|
||||
|
||||
if !job.Run.IsValid() {
|
||||
return false, nil
|
||||
}
|
||||
return job.Status.IsDone(), nil
|
||||
}
|
||||
|
||||
func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) {
|
||||
|
|
|
|||
|
|
@ -428,9 +428,10 @@ func TestAllNeedsExist(t *testing.T) {
|
|||
|
||||
func TestActionRunJob_CanBeRerun(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
job ActionRunJob
|
||||
canBeRerun bool
|
||||
name string
|
||||
job ActionRunJob
|
||||
canBeRerun bool
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "job with unknown status",
|
||||
|
|
@ -468,9 +469,9 @@ func TestActionRunJob_CanBeRerun(t *testing.T) {
|
|||
canBeRerun: false,
|
||||
},
|
||||
{
|
||||
name: "ActionRun is nil",
|
||||
job: ActionRunJob{Run: nil, Status: StatusSuccess},
|
||||
canBeRerun: false,
|
||||
name: "ActionRun is nil",
|
||||
job: ActionRunJob{ID: 12, Run: nil, Status: StatusSuccess},
|
||||
expectedError: "cannot load run 0 of job 12",
|
||||
},
|
||||
{
|
||||
name: "with busy run but completed job",
|
||||
|
|
@ -489,7 +490,15 @@ func TestActionRunJob_CanBeRerun(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.canBeRerun, testCase.job.CanBeRerun())
|
||||
result, err := testCase.job.CanBeRerun(t.Context())
|
||||
|
||||
if testCase.expectedError == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.ErrorContains(t, err, testCase.expectedError)
|
||||
}
|
||||
|
||||
assert.Equal(t, testCase.canBeRerun, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -93,27 +93,27 @@ func TestIsManualRun(t *testing.T) {
|
|||
assert.False(t, pushRun.IsDispatchedRun())
|
||||
}
|
||||
|
||||
func TestActionRun_IsRunnable(t *testing.T) {
|
||||
func TestActionRun_IsValid(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
run ActionRun
|
||||
isRunnable bool
|
||||
name string
|
||||
run ActionRun
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
name: "valid run",
|
||||
run: ActionRun{},
|
||||
isRunnable: true,
|
||||
name: "valid run",
|
||||
run: ActionRun{},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
name: "with pre-execution error",
|
||||
run: ActionRun{PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingOutput},
|
||||
isRunnable: false,
|
||||
name: "with pre-execution error",
|
||||
run: ActionRun{PreExecutionErrorCode: ErrorCodeIncompleteRunsOnMissingOutput},
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
assert.Equal(t, testCase.isRunnable, testCase.run.IsRunnable())
|
||||
assert.Equal(t, testCase.isValid, testCase.run.IsValid())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -314,11 +314,16 @@ func getViewResponse(ctx *app_context.Context, req *ViewRequest, runIndex, jobIn
|
|||
// Ah, another job is still running. Keep the frontend polling enabled then.
|
||||
done = false
|
||||
}
|
||||
canBeRerun, err := v.CanBeRerun(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return nil
|
||||
}
|
||||
resp.State.Run.Jobs = append(resp.State.Run.Jobs, &ViewJob{
|
||||
ID: v.ID,
|
||||
Name: v.Name,
|
||||
Status: v.Status.String(),
|
||||
CanRerun: v.CanBeRerun() && ctx.Repo.CanWrite(unit.TypeActions),
|
||||
CanRerun: canBeRerun && ctx.Repo.CanWrite(unit.TypeActions),
|
||||
Duration: v.Duration().String(),
|
||||
})
|
||||
}
|
||||
|
|
@ -495,120 +500,48 @@ func Rerun(ctx *app_context.Context) {
|
|||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if jobIndexStr == "" && !run.CanBeRerun() {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.workflow.rerun_impossible"))
|
||||
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
|
||||
}
|
||||
|
||||
// reset run's start and stop time when it is done
|
||||
if run.Status.IsDone() {
|
||||
run.PreviousDuration = run.Duration()
|
||||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
if err := actions_service.UpdateRun(ctx, run, "started", "stopped", "previous_duration"); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
var rerunJobs []*actions_model.ActionRunJob
|
||||
if jobIndexStr == "" { // Rerun the entire workflow.
|
||||
rerunJobs, err = actions_service.RerunAllJobs(ctx, run)
|
||||
} else { // Rerun a single job
|
||||
job, _ := getRunJobs(ctx, runIndex, jobIndex)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
rerunJobs, err = actions_service.RerunJob(ctx, job)
|
||||
}
|
||||
|
||||
job, jobs := getRunJobs(ctx, runIndex, jobIndex)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
if jobIndexStr == "" { // rerun all jobs
|
||||
var redirectURL string
|
||||
for _, j := range jobs {
|
||||
if !j.CanBeRerun() {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.workflow.job_rerun_impossible"))
|
||||
return
|
||||
}
|
||||
|
||||
// if the job has needs, it should be set to "blocked" status to wait for other jobs
|
||||
shouldBlock := len(j.Needs) > 0
|
||||
if err := rerunJob(ctx, j, shouldBlock); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if redirectURL == "" {
|
||||
redirectURL, err = j.HTMLURL(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, actions_service.ErrRerunWorkflowInvalid) ||
|
||||
errors.Is(err, actions_service.ErrRerunWorkflowStillRunning) {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.workflow.rerun_impossible"))
|
||||
return
|
||||
}
|
||||
|
||||
if redirectURL != "" {
|
||||
ctx.JSON(http.StatusOK, &redirectObject{Redirect: redirectURL})
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "unable to determine redirectURL for job rerun")
|
||||
if errors.Is(err, actions_service.ErrRerunWorkflowDisabled) {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.workflow.disabled"))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rerunJobs := actions_service.GetAllRerunJobs(job, jobs)
|
||||
|
||||
var redirectURL string
|
||||
for _, j := range rerunJobs {
|
||||
if !j.CanBeRerun() {
|
||||
if errors.Is(err, actions_service.ErrRerunJobStillRunning) {
|
||||
ctx.JSONError(ctx.Locale.Tr("actions.workflow.job_rerun_impossible"))
|
||||
return
|
||||
}
|
||||
|
||||
// jobs other than the specified one should be set to "blocked" status
|
||||
shouldBlock := j.JobID != job.JobID
|
||||
if err := rerunJob(ctx, j, shouldBlock); err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
if j.JobID == job.JobID {
|
||||
redirectURL, err = j.HTMLURL(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if redirectURL != "" {
|
||||
ctx.JSON(http.StatusOK, &redirectObject{Redirect: redirectURL})
|
||||
} else {
|
||||
ctx.Error(http.StatusInternalServerError, "unable to determine redirectURL for job rerun")
|
||||
}
|
||||
}
|
||||
|
||||
func rerunJob(ctx *app_context.Context, job *actions_model.ActionRunJob, shouldBlock bool) error {
|
||||
status := job.Status
|
||||
if !status.IsDone() {
|
||||
return nil
|
||||
if len(rerunJobs) == 0 {
|
||||
ctx.Error(http.StatusInternalServerError, "no jobs were rerun")
|
||||
return
|
||||
}
|
||||
|
||||
initialStatus := actions_model.StatusWaiting
|
||||
if shouldBlock {
|
||||
initialStatus = actions_model.StatusBlocked
|
||||
}
|
||||
if err := job.PrepareNextAttempt(initialStatus); err != nil {
|
||||
return err
|
||||
redirectURL, err := rerunJobs[0].HTMLURL(ctx)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
_, err := actions_service.UpdateRunJob(ctx, job, builder.Eq{"status": status}, "handle", "attempt", "task_id", "status", "started", "stopped")
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actions_service.CreateCommitStatus(ctx, job)
|
||||
return nil
|
||||
ctx.JSON(http.StatusOK, &redirectObject{Redirect: redirectURL})
|
||||
}
|
||||
|
||||
func Logs(ctx *app_context.Context) {
|
||||
|
|
|
|||
|
|
@ -554,7 +554,7 @@ func TestActionsRerun(t *testing.T) {
|
|||
runIndex: 138575,
|
||||
jobIndex: 1,
|
||||
expectedCode: 400,
|
||||
expectedBody: "{\"errorMessage\":\"actions.workflow.job_rerun_impossible\",\"renderFormat\":\"html\"}\n",
|
||||
expectedBody: "{\"errorMessage\":\"actions.workflow.rerun_impossible\",\"renderFormat\":\"html\"}\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
|
|
|
|||
82
services/actions/TestRerun_RerunAllJobs/action_run.yml
Normal file
82
services/actions/TestRerun_RerunAllJobs/action_run.yml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
- id: 455620
|
||||
title: Completed workflow
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 110
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: 1ca7f0f9-81e7-4e27-85eb-b080c33d32d3
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 1 # success
|
||||
version: 4
|
||||
started: 1776279254
|
||||
stopped: 1776279265
|
||||
previous_duration: 0
|
||||
created: 1776263479
|
||||
updated: 1776279265
|
||||
|
||||
- id: 455630
|
||||
title: Running workflow
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 111
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 6 # running
|
||||
version: 2
|
||||
started: 1776281360
|
||||
stopped: 0
|
||||
previous_duration: 0
|
||||
created: 1776281359
|
||||
updated: 1776281367
|
||||
|
||||
- id: 455640
|
||||
title: Invalid workflow
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: invalid.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 112
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: 4055b3488bf960c9c9f5c1eb2d84e346faaad7dc
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 2 # failure
|
||||
version: 2
|
||||
started: 0
|
||||
stopped: 0
|
||||
previous_duration: 0
|
||||
created: 1776282729
|
||||
updated: 1776282729
|
||||
pre_execution_error_code: 1
|
||||
pre_execution_error_details:
|
||||
- "yaml: unmarshal errors:\n line 7: cannot unmarshal !!map into []*model.Step"
|
||||
|
||||
- id: 455650
|
||||
title: Workflow with dependent jobs
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: dependent.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 113
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 1 # success
|
||||
version: 2
|
||||
started: 1776331635
|
||||
stopped: 1776331721
|
||||
previous_duration: 0
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
227
services/actions/TestRerun_RerunAllJobs/action_run_job.yml
Normal file
227
services/actions/TestRerun_RerunAllJobs/action_run_job.yml
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
# Jobs of completed workflow run.
|
||||
- id: 683880
|
||||
run_id: 455620
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: caller
|
||||
attempt: 1
|
||||
handle: 7ecf26bd-408d-485f-b162-f349f5fe95bc
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
caller:
|
||||
name: caller
|
||||
runs-on: []
|
||||
if: false
|
||||
__metadata:
|
||||
workflow_call_inputs:
|
||||
greet_target: Mona the Octocat
|
||||
workflow_call_id: d4f799b5a5d27e303ab97a546708c2aca6c1672b3bbc5defa0d73b251bca7955
|
||||
job_id: caller
|
||||
needs: ["caller.callee"]
|
||||
runs_on: []
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 0
|
||||
stopped: 0
|
||||
created: 1776263479
|
||||
updated: 1776279265
|
||||
- id: 683881
|
||||
run_id: 455620
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: callee
|
||||
attempt: 1
|
||||
handle: 24920992-6701-48cc-872d-d017b33ee90e
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_call:
|
||||
inputs:
|
||||
greet_target:
|
||||
default: Mona the Octocat
|
||||
type: string
|
||||
jobs:
|
||||
caller.callee:
|
||||
name: callee
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "Hello ${{ inputs.greet_target }}"
|
||||
__metadata:
|
||||
workflow_call_parent: d4f799b5a5d27e303ab97a546708c2aca6c1672b3bbc5defa0d73b251bca7955
|
||||
job_id: caller.callee
|
||||
needs: null
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 989
|
||||
status: 1 # success
|
||||
started: 1776279254
|
||||
stopped: 1776279264
|
||||
created: 1776263479
|
||||
updated: 1776279264
|
||||
|
||||
# Jobs of running workflow run.
|
||||
- id: 683890
|
||||
run_id: 455630
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: 41569413fe943e3f4e8e9625e14c1e01d85581af
|
||||
name: caller
|
||||
attempt: 1
|
||||
handle: 0185f661-3b6b-4f49-8a56-f6585c9c396e
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
caller:
|
||||
name: caller
|
||||
runs-on: []
|
||||
if: false
|
||||
__metadata:
|
||||
workflow_call_inputs:
|
||||
greet_target: Mona the Octocat
|
||||
workflow_call_id: d4f799b5a5d27e303ab97a546708c2aca6c1672b3bbc5defa0d73b251bca7955
|
||||
job_id: caller
|
||||
needs: ["caller.callee"]
|
||||
runs_on: []
|
||||
task_id: 0
|
||||
status: 7 # blocked
|
||||
started: 0
|
||||
stopped: 0
|
||||
created: 1776281359
|
||||
updated: 1776281359
|
||||
- id: 683891
|
||||
run_id: 455630
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: 41569413fe943e3f4e8e9625e14c1e01d85581af
|
||||
name: callee
|
||||
attempt: 1
|
||||
handle: 4ee3bdf7-69f6-4174-9ffd-eb4a250188bc
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_call:
|
||||
inputs:
|
||||
greet_target:
|
||||
default: Mona the Octocat
|
||||
type: string
|
||||
jobs:
|
||||
caller.callee:
|
||||
name: callee
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "Hello ${{ inputs.greet_target }}"
|
||||
__metadata:
|
||||
workflow_call_parent: d4f799b5a5d27e303ab97a546708c2aca6c1672b3bbc5defa0d73b251bca7955
|
||||
job_id: caller.callee
|
||||
needs: null
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 989
|
||||
status: 6 # running
|
||||
started: 1776281360
|
||||
stopped: 0
|
||||
created: 1776281359
|
||||
updated: 1776281367
|
||||
|
||||
# Jobs of invalid workflow.
|
||||
- id: 683900
|
||||
run_id: 455640
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: 4055b3488bf960c9c9f5c1eb2d84e346faaad7dc
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: c60533ff-61b3-4f6f-9788-260911cda6ed
|
||||
workflow_payload: ""
|
||||
job_id:
|
||||
needs: []
|
||||
runs_on: []
|
||||
task_id: 0
|
||||
status: 2 # failure
|
||||
started: 0
|
||||
stopped: 0
|
||||
created: 1776282729
|
||||
updated: 1776282729
|
||||
|
||||
# Jobs of workflow with dependent jobs.
|
||||
- id: 683910
|
||||
run_id: 455650
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
name: lint
|
||||
attempt: 1
|
||||
handle: 0a721b46-36d1-4540-b070-d559cf87515b
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Linting"
|
||||
job_id: lint
|
||||
needs: null
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331495
|
||||
stopped: 1776331523
|
||||
created: 1776331495
|
||||
updated: 1776331523
|
||||
- id: 683911
|
||||
run_id: 455650
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
name: build
|
||||
attempt: 1
|
||||
handle: eec6b880-b013-4426-9361-8a4ad491ae91
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Building"
|
||||
job_id: build
|
||||
needs: [lint]
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331693
|
||||
stopped: 1776331721
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
- id: 683912
|
||||
run_id: 455650
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: b6398b81-f644-4f4b-9ed9-0c596c090b2f
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Testing"
|
||||
job_id: test
|
||||
needs: [build]
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331665
|
||||
stopped: 1776331693
|
||||
created: 1776331495
|
||||
updated: 1776331693
|
||||
82
services/actions/TestRerun_RerunJob/action_run.yml
Normal file
82
services/actions/TestRerun_RerunJob/action_run.yml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
- id: 455620
|
||||
title: Completed workflow
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 110
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: 1ca7f0f9-81e7-4e27-85eb-b080c33d32d3
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 1 # success
|
||||
version: 4
|
||||
started: 1776279254
|
||||
stopped: 1776279265
|
||||
previous_duration: 0
|
||||
created: 1776263479
|
||||
updated: 1776279265
|
||||
|
||||
- id: 455630
|
||||
title: Running workflow
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: test.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 111
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 6 # running
|
||||
version: 2
|
||||
started: 1776281360
|
||||
stopped: 0
|
||||
previous_duration: 0
|
||||
created: 1776281359
|
||||
updated: 1776281367
|
||||
|
||||
- id: 455640
|
||||
title: Invalid workflow
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: invalid.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 112
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: 4055b3488bf960c9c9f5c1eb2d84e346faaad7dc
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 2 # failure
|
||||
version: 2
|
||||
started: 0
|
||||
stopped: 0
|
||||
previous_duration: 0
|
||||
created: 1776282729
|
||||
updated: 1776282729
|
||||
pre_execution_error_code: 1
|
||||
pre_execution_error_details:
|
||||
- "yaml: unmarshal errors:\n line 7: cannot unmarshal !!map into []*model.Step"
|
||||
|
||||
- id: 455650
|
||||
title: Workflow with dependent jobs
|
||||
repo_id: 62 # test_workflows
|
||||
owner_id: 2 # user2
|
||||
workflow_id: dependent.yaml
|
||||
workflow_directory: .forgejo/workflows
|
||||
index: 113
|
||||
trigger_user_id: 2
|
||||
ref: refs/heads/main
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
event: push
|
||||
event_payload: "{}"
|
||||
status: 6 # running
|
||||
version: 2
|
||||
started: 1776331635
|
||||
stopped: 0
|
||||
previous_duration: 0
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
244
services/actions/TestRerun_RerunJob/action_run_job.yml
Normal file
244
services/actions/TestRerun_RerunJob/action_run_job.yml
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
# Jobs of completed workflow run.
|
||||
- id: 683880
|
||||
run_id: 455620
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: caller
|
||||
attempt: 1
|
||||
handle: 7ecf26bd-408d-485f-b162-f349f5fe95bc
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
caller:
|
||||
name: caller
|
||||
runs-on: []
|
||||
if: false
|
||||
__metadata:
|
||||
workflow_call_inputs:
|
||||
greet_target: Mona the Octocat
|
||||
workflow_call_id: d4f799b5a5d27e303ab97a546708c2aca6c1672b3bbc5defa0d73b251bca7955
|
||||
job_id: caller
|
||||
needs: ["caller.callee"]
|
||||
runs_on: []
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 0
|
||||
stopped: 0
|
||||
created: 1776263479
|
||||
updated: 1776279265
|
||||
- id: 683881
|
||||
run_id: 455620
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: callee
|
||||
attempt: 1
|
||||
handle: 24920992-6701-48cc-872d-d017b33ee90e
|
||||
workflow_payload: |
|
||||
"on":
|
||||
workflow_call:
|
||||
inputs:
|
||||
greet_target:
|
||||
default: Mona the Octocat
|
||||
type: string
|
||||
jobs:
|
||||
caller.callee:
|
||||
name: callee
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: |
|
||||
echo "Hello ${{ inputs.greet_target }}"
|
||||
__metadata:
|
||||
workflow_call_parent: d4f799b5a5d27e303ab97a546708c2aca6c1672b3bbc5defa0d73b251bca7955
|
||||
job_id: caller.callee
|
||||
needs: null
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 989
|
||||
status: 1 # success
|
||||
started: 1776279254
|
||||
stopped: 1776279264
|
||||
created: 1776263479
|
||||
updated: 1776279264
|
||||
|
||||
# Jobs of running workflow run.
|
||||
- id: 683590
|
||||
run_id: 455630
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: lint
|
||||
attempt: 1
|
||||
handle: d114a733-b364-4c08-b15d-77b6cc21a616
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Linting"
|
||||
job_id: lint
|
||||
needs: null
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331495
|
||||
stopped: 1776331523
|
||||
created: 1776331495
|
||||
updated: 1776331523
|
||||
- id: 683591
|
||||
run_id: 455630
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: build
|
||||
attempt: 1
|
||||
handle: afd58a3e-76e9-467a-9a9d-9d6250fcc905
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Building"
|
||||
job_id: build
|
||||
needs: [lint]
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331693
|
||||
stopped: 1776331721
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
- id: 683592
|
||||
run_id: 455630
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: e36900942550acf94ddd8c0a3b91cc0165b4bd02
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: 2d9438cb-5067-4aff-9adb-9c1265fb42d5
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Testing"
|
||||
job_id: test
|
||||
needs: [build]
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 6 # StatusRunning
|
||||
started: 1776331665
|
||||
stopped: 0
|
||||
created: 1776331495
|
||||
updated: 1776331693
|
||||
|
||||
|
||||
# Jobs of invalid workflow.
|
||||
- id: 683900
|
||||
run_id: 455640
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: 4055b3488bf960c9c9f5c1eb2d84e346faaad7dc
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: c60533ff-61b3-4f6f-9788-260911cda6ed
|
||||
workflow_payload: ""
|
||||
job_id:
|
||||
needs: []
|
||||
runs_on: []
|
||||
task_id: 0
|
||||
status: 2 # failure
|
||||
started: 0
|
||||
stopped: 0
|
||||
created: 1776282729
|
||||
updated: 1776282729
|
||||
|
||||
# Jobs of successful workflow run with dependent jobs.
|
||||
- id: 683910
|
||||
run_id: 455650
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
name: lint
|
||||
attempt: 1
|
||||
handle: 0a721b46-36d1-4540-b070-d559cf87515b
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
lint:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Linting"
|
||||
job_id: lint
|
||||
needs: null
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331495
|
||||
stopped: 1776331523
|
||||
created: 1776331495
|
||||
updated: 1776331523
|
||||
- id: 683911
|
||||
run_id: 455650
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
name: build
|
||||
attempt: 1
|
||||
handle: eec6b880-b013-4426-9361-8a4ad491ae91
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Building"
|
||||
job_id: build
|
||||
needs: []
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331693
|
||||
stopped: 1776331721
|
||||
created: 1776331495
|
||||
updated: 1776331721
|
||||
- id: 683912
|
||||
run_id: 455650
|
||||
repo_id: 62
|
||||
owner_id: 2
|
||||
commit_sha: ae36c1d75cc82cb9f9e54a86c8137ed05c3bd66e
|
||||
name: test
|
||||
attempt: 1
|
||||
handle: b6398b81-f644-4f4b-9ed9-0c596c090b2f
|
||||
workflow_payload: |
|
||||
"on":
|
||||
push:
|
||||
jobs:
|
||||
test:
|
||||
name: test
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "Testing"
|
||||
job_id: test
|
||||
needs: [build]
|
||||
runs_on: ["ubuntu-latest"]
|
||||
task_id: 0
|
||||
status: 1 # success
|
||||
started: 1776331665
|
||||
stopped: 1776331693
|
||||
created: 1776331495
|
||||
updated: 1776331693
|
||||
|
|
@ -4,10 +4,30 @@
|
|||
package actions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/db"
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/modules/container"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrRerunWorkflowInvalid signals that the workflow cannot be run because it is invalid, for example, due to syntax
|
||||
// errors.
|
||||
ErrRerunWorkflowInvalid = errors.New("workflow is invalid")
|
||||
// ErrRerunWorkflowDisabled indicates that the workflow cannot be run because it has been disabled by the user or
|
||||
// Forgejo.
|
||||
ErrRerunWorkflowDisabled = errors.New("workflow is disabled")
|
||||
// ErrRerunWorkflowStillRunning signals that the workflow cannot be rerun because at least one job is still running.
|
||||
ErrRerunWorkflowStillRunning = errors.New("workflow is still running")
|
||||
// ErrRerunJobStillRunning signals that the job cannot be rerun because it is still running.
|
||||
ErrRerunJobStillRunning = errors.New("job is still running")
|
||||
)
|
||||
|
||||
// GetAllRerunJobs get all jobs that need to be rerun when job should be rerun
|
||||
|
|
@ -16,22 +36,154 @@ func GetAllRerunJobs(job *actions_model.ActionRunJob, allJobs []*actions_model.A
|
|||
rerunJobsIDSet := make(container.Set[string])
|
||||
rerunJobsIDSet.Add(job.JobID)
|
||||
|
||||
for {
|
||||
found := false
|
||||
for _, j := range allJobs {
|
||||
if rerunJobsIDSet.Contains(j.JobID) {
|
||||
continue
|
||||
}
|
||||
if slices.ContainsFunc(j.Needs, rerunJobsIDSet.Contains) {
|
||||
found = true
|
||||
rerunJobs = append(rerunJobs, j)
|
||||
rerunJobsIDSet.Add(j.JobID)
|
||||
}
|
||||
for _, j := range allJobs {
|
||||
if rerunJobsIDSet.Contains(j.JobID) {
|
||||
continue
|
||||
}
|
||||
if !found {
|
||||
break
|
||||
if slices.ContainsFunc(j.Needs, rerunJobsIDSet.Contains) {
|
||||
rerunJobs = append(rerunJobs, j)
|
||||
rerunJobsIDSet.Add(j.JobID)
|
||||
}
|
||||
}
|
||||
|
||||
return rerunJobs
|
||||
}
|
||||
|
||||
// RerunAllJobs reruns all jobs of the given run and returns them. For it to succeed, the workflow must be valid, and the
|
||||
// previous run must have completed.
|
||||
func RerunAllJobs(ctx context.Context, run *actions_model.ActionRun) ([]*actions_model.ActionRunJob, error) {
|
||||
if !run.IsValid() {
|
||||
return nil, ErrRerunWorkflowInvalid
|
||||
}
|
||||
if !run.Status.IsDone() {
|
||||
return nil, ErrRerunWorkflowStillRunning
|
||||
}
|
||||
|
||||
if err := run.LoadRepo(ctx); err != nil {
|
||||
return nil, fmt.Errorf("cannot load repo of run %d: %w", run.ID, err)
|
||||
}
|
||||
|
||||
actionsConfig := run.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if actionsConfig.IsWorkflowDisabled(run.WorkflowID) {
|
||||
return nil, ErrRerunWorkflowDisabled
|
||||
}
|
||||
|
||||
var rerunJobs []*actions_model.ActionRunJob
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
if run.Status != actions_model.StatusUnknown && !run.Status.IsDone() {
|
||||
return fmt.Errorf("cannot prepare next attempt because run %d is active: %s", run.ID, run.Status.String())
|
||||
}
|
||||
|
||||
run.PreviousDuration = run.Duration()
|
||||
|
||||
run.Status = actions_model.StatusWaiting
|
||||
run.Started = 0
|
||||
run.Stopped = 0
|
||||
|
||||
// The columns have to be specified here to work around a xorm quirk: It won't update columns that are set to
|
||||
// their zero value without AllCols().
|
||||
if err := UpdateRun(ctx, run, "status", "started", "stopped", "previous_duration"); err != nil {
|
||||
return fmt.Errorf("cannot update run %d: %w", run.ID, err)
|
||||
}
|
||||
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load jobs of run %d: %w", run.ID, err)
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
initialStatus := actions_model.StatusWaiting
|
||||
if len(job.Needs) > 0 {
|
||||
initialStatus = actions_model.StatusBlocked
|
||||
}
|
||||
|
||||
if err := rerunSingleJob(ctx, job, initialStatus); err != nil {
|
||||
return fmt.Errorf("could not rerun job %d of run %d: %w", job.ID, run.ID, err)
|
||||
}
|
||||
|
||||
rerunJobs = append(rerunJobs, job)
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rerunJobs, nil
|
||||
}
|
||||
|
||||
// RerunJob reruns the given job and all its dependent jobs. It returns all jobs that were rerun. For it to succeed, the
|
||||
// workflow that defines this job must be valid, and the previous run must have completed. Dependent jobs that have not
|
||||
// completed yet are ignored.
|
||||
func RerunJob(ctx context.Context, job *actions_model.ActionRunJob) ([]*actions_model.ActionRunJob, error) {
|
||||
if err := job.LoadAttributes(ctx); err != nil {
|
||||
return nil, fmt.Errorf("cannot load attributes of job %d: %w", job.ID, err)
|
||||
}
|
||||
if !job.Run.IsValid() {
|
||||
return nil, ErrRerunWorkflowInvalid
|
||||
}
|
||||
|
||||
actionsConfig := job.Run.Repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()
|
||||
if actionsConfig.IsWorkflowDisabled(job.Run.WorkflowID) {
|
||||
return nil, ErrRerunWorkflowDisabled
|
||||
}
|
||||
|
||||
if !job.Status.IsDone() {
|
||||
return nil, ErrRerunJobStillRunning
|
||||
}
|
||||
|
||||
var rerunJobs []*actions_model.ActionRunJob
|
||||
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||
jobs, err := actions_model.GetRunJobsByRunID(ctx, job.RunID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not load jobs of run %d: %w", job.RunID, err)
|
||||
}
|
||||
|
||||
for _, jobToRerun := range GetAllRerunJobs(job, jobs) {
|
||||
canBeRerun, err := jobToRerun.CanBeRerun(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot determine whether job %d can be rerun: %w", jobToRerun.ID, err)
|
||||
}
|
||||
|
||||
// Skipping jobs that cannot be rerun is wrong. They should be cancelled and rerun, instead, because they
|
||||
// are dependent jobs and the old results might be worthless, anyway. But we keep that behaviour for now,
|
||||
// because changing it requires more rework.
|
||||
if !canBeRerun {
|
||||
continue
|
||||
}
|
||||
|
||||
// The job that should be rerun cannot be blocked, even if it has needs.
|
||||
initialStatus := actions_model.StatusWaiting
|
||||
if len(jobToRerun.Needs) > 0 && jobToRerun.ID != job.ID {
|
||||
initialStatus = actions_model.StatusBlocked
|
||||
}
|
||||
|
||||
if err := rerunSingleJob(ctx, jobToRerun, initialStatus); err != nil {
|
||||
return fmt.Errorf("cannot rerun job %d: %w", jobToRerun.ID, err)
|
||||
}
|
||||
rerunJobs = append(rerunJobs, jobToRerun)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rerunJobs, nil
|
||||
}
|
||||
|
||||
func rerunSingleJob(ctx context.Context, job *actions_model.ActionRunJob, initialStatus actions_model.Status) error {
|
||||
oldStatus := job.Status
|
||||
|
||||
if err := job.PrepareNextAttempt(initialStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The columns have to be specified here to work around a xorm quirk: It won't update columns that are set to their
|
||||
// zero value without AllCols().
|
||||
if _, err := UpdateRunJob(ctx, job, builder.Eq{"status": oldStatus}, "handle", "attempt", "task_id", "status", "started", "stopped"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
CreateCommitStatus(ctx, job)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@ package actions
|
|||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
actions_model "forgejo.org/models/actions"
|
||||
"forgejo.org/models/unit"
|
||||
"forgejo.org/models/unittest"
|
||||
"forgejo.org/modules/timeutil"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetAllRerunJobs(t *testing.T) {
|
||||
func TestRerun_GetAllRerunJobs(t *testing.T) {
|
||||
job1 := &actions_model.ActionRunJob{JobID: "job1"}
|
||||
job2 := &actions_model.ActionRunJob{JobID: "job2", Needs: []string{"job1"}}
|
||||
job3 := &actions_model.ActionRunJob{JobID: "job3", Needs: []string{"job2"}}
|
||||
|
|
@ -46,3 +51,233 @@ func TestGetAllRerunJobs(t *testing.T) {
|
|||
assert.ElementsMatch(t, tc.rerunJobs, rerunJobs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRerun_RerunAllJobs(t *testing.T) {
|
||||
t.Run("Reruns completed workflow", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunAllJobs")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455620})
|
||||
|
||||
rerunJobs, err := RerunAllJobs(t.Context(), run)
|
||||
require.NoError(t, err)
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455620})
|
||||
|
||||
assert.Equal(t, actions_model.StatusWaiting, run.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
||||
assert.Equal(t, 11*time.Second, run.PreviousDuration)
|
||||
|
||||
assert.Len(t, rerunJobs, 2)
|
||||
assert.Equal(t, int64(683880), rerunJobs[0].ID)
|
||||
assert.Equal(t, int64(2), rerunJobs[0].Attempt)
|
||||
assert.Equal(t, actions_model.StatusBlocked, rerunJobs[0].Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), rerunJobs[0].Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), rerunJobs[0].Stopped)
|
||||
|
||||
assert.Equal(t, int64(683881), rerunJobs[1].ID)
|
||||
assert.Equal(t, int64(2), rerunJobs[1].Attempt)
|
||||
assert.Equal(t, actions_model.StatusWaiting, rerunJobs[1].Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), rerunJobs[1].Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), rerunJobs[1].Stopped)
|
||||
})
|
||||
|
||||
t.Run("Error if workflow running", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunAllJobs")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455630})
|
||||
|
||||
rerunJobs, err := RerunAllJobs(t.Context(), run)
|
||||
require.ErrorContains(t, err, "workflow is still running")
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455630})
|
||||
|
||||
assert.Equal(t, actions_model.StatusRunning, run.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(1776281360), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
||||
assert.Equal(t, time.Duration(0), run.PreviousDuration)
|
||||
|
||||
assert.Empty(t, rerunJobs)
|
||||
})
|
||||
|
||||
t.Run("Error if workflow invalid", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunAllJobs")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455640})
|
||||
|
||||
rerunJobs, err := RerunAllJobs(t.Context(), run)
|
||||
require.ErrorContains(t, err, "workflow is invalid")
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455640})
|
||||
|
||||
assert.Equal(t, actions_model.StatusFailure, run.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), run.Stopped)
|
||||
assert.Equal(t, time.Duration(0), run.PreviousDuration)
|
||||
|
||||
assert.Empty(t, rerunJobs)
|
||||
})
|
||||
|
||||
t.Run("Error if workflow disabled", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunAllJobs")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455620})
|
||||
|
||||
// Disable workflow
|
||||
require.NoError(t, run.LoadAttributes(t.Context()))
|
||||
actionsConfig := run.Repo.MustGetUnit(t.Context(), unit.TypeActions).ActionsConfig()
|
||||
actionsConfig.DisableWorkflow(run.WorkflowID)
|
||||
|
||||
rerunJobs, err := RerunAllJobs(t.Context(), run)
|
||||
require.ErrorContains(t, err, "workflow is disabled")
|
||||
|
||||
run = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 455620})
|
||||
|
||||
assert.Equal(t, actions_model.StatusSuccess, run.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(1776279254), run.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(1776279265), run.Stopped)
|
||||
assert.Equal(t, time.Duration(0), run.PreviousDuration)
|
||||
|
||||
assert.Empty(t, rerunJobs)
|
||||
})
|
||||
}
|
||||
|
||||
func TestRerun_RerunJob(t *testing.T) {
|
||||
t.Run("Rerun independent job", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683910})
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, rerunJobs, 1)
|
||||
assert.Equal(t, job.ID, rerunJobs[0].ID)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683910})
|
||||
|
||||
assert.Equal(t, int64(2), job.Attempt)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Stopped)
|
||||
})
|
||||
|
||||
t.Run("Rerun job needed by others", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683911})
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, rerunJobs, 2)
|
||||
assert.Equal(t, int64(683911), rerunJobs[0].ID)
|
||||
assert.Equal(t, int64(683912), rerunJobs[1].ID)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683911})
|
||||
|
||||
assert.Equal(t, int64(2), job.Attempt)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Stopped)
|
||||
|
||||
dependentJob := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683912})
|
||||
|
||||
assert.Equal(t, int64(2), dependentJob.Attempt)
|
||||
assert.Equal(t, actions_model.StatusBlocked, dependentJob.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), dependentJob.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), dependentJob.Stopped)
|
||||
})
|
||||
|
||||
t.Run("Rerun job with needs", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683912})
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Len(t, rerunJobs, 1)
|
||||
assert.Equal(t, int64(683912), rerunJobs[0].ID)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683912})
|
||||
|
||||
assert.Len(t, job.Needs, 1)
|
||||
assert.Equal(t, int64(2), job.Attempt)
|
||||
assert.Equal(t, actions_model.StatusWaiting, job.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Stopped)
|
||||
})
|
||||
|
||||
t.Run("Error if workflow invalid", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683900})
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.ErrorContains(t, err, "workflow is invalid")
|
||||
assert.Empty(t, rerunJobs)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683900})
|
||||
|
||||
assert.Equal(t, int64(1), job.Attempt)
|
||||
assert.Equal(t, actions_model.StatusFailure, job.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Stopped)
|
||||
})
|
||||
|
||||
t.Run("Error if workflow disabled", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683881})
|
||||
|
||||
// Disable workflow
|
||||
require.NoError(t, job.LoadAttributes(t.Context()))
|
||||
actionsConfig := job.Run.Repo.MustGetUnit(t.Context(), unit.TypeActions).ActionsConfig()
|
||||
actionsConfig.DisableWorkflow(job.Run.WorkflowID)
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.ErrorContains(t, err, "workflow is disabled")
|
||||
assert.Empty(t, rerunJobs)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683881})
|
||||
|
||||
assert.Equal(t, int64(1), job.Attempt)
|
||||
assert.Equal(t, actions_model.StatusSuccess, job.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(1776279254), job.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(1776279264), job.Stopped)
|
||||
})
|
||||
|
||||
t.Run("Error if job still running", func(t *testing.T) {
|
||||
defer unittest.OverrideFixtures("services/actions/TestRerun_RerunJob")()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
job := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683592})
|
||||
|
||||
rerunJobs, err := RerunJob(t.Context(), job)
|
||||
|
||||
require.ErrorContains(t, err, "job is still running")
|
||||
assert.Empty(t, rerunJobs)
|
||||
|
||||
job = unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: 683592})
|
||||
|
||||
assert.Equal(t, int64(1), job.Attempt)
|
||||
assert.Equal(t, actions_model.StatusRunning, job.Status)
|
||||
assert.Equal(t, timeutil.TimeStamp(1776331665), job.Started)
|
||||
assert.Equal(t, timeutil.TimeStamp(0), job.Stopped)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue