refactor: move rerun logic to services (#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.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/12141
Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org>
Co-authored-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
Co-committed-by: Andreas Ahlenstorf <andreas@ahlenstorf.ch>
This commit is contained in:
Andreas Ahlenstorf 2026-04-19 22:08:00 +02:00 committed by Mathieu Fenniak
parent 178a0a25f8
commit 6cd3f0263d
12 changed files with 1122 additions and 140 deletions

View 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

View 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

View 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

View 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

View file

@ -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
}

View file

@ -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)
})
}