perf(actions): batch per-row queries in run, job and cancellation paths (#39723)

Several Actions code paths issued one or more queries per listed row.
They now load in batch, with unchanged output.

- API job list: tasks, steps and runners load in batch
- API run list: latest attempts and their trigger users load in batch
- Repository runs page: the jobs of all active runs load in one query
- Cancelling previous runs and replacing concurrency group entries fetch
their jobs in one query

Per call with 50 rows on SQLite, base vs head:

| Path | Queries | Time |
| --- | --- | --- |
| API job list | 254 → 6 | 12.9 → 1.9 ms |
| API run list | 305 → 207 | 11.7 → 6.2 ms |
| Runs page job loading | 100 → 1 | 6.5 → 0.47 ms |
| `CancelPreviousJobs` | 546 → 496 | ~120 ms, per-job writes dominate |

The remaining run list queries come from per-run repository conversion,
which this PR doesn't change.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
authored and GitHub committed 2026-10-17 09:47:17 +02:00
1 parent efb0b34395
commit 3058f5ddf9
10 files changed
+241 -121

No files matched your search

+16 -14
View File
@@ -15,6 +15,7 @@ import (
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
user_model "gitea.dev/models/user"
"gitea.dev/modules/container"
"gitea.dev/modules/git"
"gitea.dev/modules/json"
"gitea.dev/modules/log"
@@ -408,26 +409,27 @@ func CancelPreviousJobsByRunConcurrency(ctx context.Context, attempt *ActionRunA
return nil, nil
}
var jobsToCancel []*ActionRunJob
attempts, jobs, err := getConcurrencyEntriesToReplace(ctx, attempt.RepoID, attempt.ConcurrencyGroup, attempt.ConcurrencyCancel)
if err != nil {
return nil, fmt.Errorf("find concurrent runs and jobs: %w", err)
}
jobs = slices.DeleteFunc(jobs, func(job *ActionRunJob) bool { return job.RunID == attempt.RunID })
jobsToCancel = append(jobsToCancel, jobs...)
// cancel runs in the same concurrency group
for _, concurrentAttempt := range attempts {
if concurrentAttempt.RunID == attempt.RunID {
continue
}
jobs, err := GetRunJobsByRunAndAttemptID(ctx, concurrentAttempt.RunID, concurrentAttempt.ID)
if err != nil {
return nil, fmt.Errorf("find run %d attempt %d jobs: %w", concurrentAttempt.RunID, concurrentAttempt.ID, err)
}
jobsToCancel = append(jobsToCancel, jobs...)
attempts = slices.DeleteFunc(attempts, func(concurrentAttempt *ActionRunAttempt) bool { return concurrentAttempt.RunID == attempt.RunID })
attemptJobs, err := getRunJobsByAttempts(ctx, attempts)
if err != nil {
return nil, fmt.Errorf("find concurrent attempt jobs: %w", err)
}
return CancelJobs(ctx, jobsToCancel, false)
return CancelJobs(ctx, append(jobs, attemptJobs...), false)
}
func getRunJobsByAttempts(ctx context.Context, attempts []*ActionRunAttempt) (jobs []*ActionRunJob, _ error) {
for chunk := range slices.Chunk(attempts, db.DefaultMaxInSize) {
attemptIDs := container.FilterSlice(chunk, func(attempt *ActionRunAttempt) (int64, bool) { return attempt.ID, true })
if err := db.GetEngine(ctx).In("run_attempt_id", attemptIDs).Find(&jobs); err != nil {
return nil, err
}
}
return jobs, nil
}
+29 -43
View File
@@ -271,6 +271,24 @@ func GetLatestAttemptJobsByRun(ctx context.Context, run *ActionRun) (ActionJobLi
return jobs, nil
}
func GetLatestAttemptJobsByRuns(ctx context.Context, runs []*ActionRun) (map[int64]ActionJobList, error) {
result := make(map[int64]ActionJobList, len(runs))
if len(runs) == 0 {
return result, nil
}
attemptIDs := container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { return run.LatestAttemptID, run.LatestAttemptID != 0 })
legacyRunIDs := container.FilterSlice(runs, func(run *ActionRun) (int64, bool) { return run.ID, run.LatestAttemptID == 0 })
cond := builder.In("run_attempt_id", attemptIDs).Or(builder.Eq{"run_attempt_id": 0}.And(builder.In("run_id", legacyRunIDs)))
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).Where(cond).OrderBy("id").Find(&jobs); err != nil {
return nil, err
}
for _, job := range jobs {
result[job.RunID] = append(result[job.RunID], job)
}
return result, nil
}
// GetAllRunJobsByRepoAndRunID returns all jobs for a run across all attempts.
func GetAllRunJobsByRepoAndRunID(ctx context.Context, repoID, runID int64) (ActionJobList, error) {
var jobs []*ActionRunJob
@@ -685,43 +703,18 @@ func hasFailFastMatrixFailure(jobs []*ActionRunJob) bool {
// It's useful when a new run is triggered, and all previous runs needn't be continued anymore.
func CancelPreviousJobs(ctx context.Context, repoID int64, ref, workflowID string, event webhook_module.HookEventType) ([]*ActionRunJob, error) {
// Find all runs in the specified repository, reference, and workflow with non-final status
runs, total, err := db.FindAndCount[ActionRun](ctx, FindRunOptions{
runIDs := builder.Select("id").From("action_run").Where(FindRunOptions{
RepoID: repoID,
Ref: ref,
WorkflowID: workflowID,
TriggerEvent: event,
Status: []Status{StatusRunning, StatusWaiting, StatusBlocked, StatusCancelling},
})
if err != nil {
}.ToConds())
var jobs []*ActionRunJob
if err := db.GetEngine(ctx).Where(builder.In("run_id", runIDs)).Find(&jobs); err != nil {
return nil, err
}
// If there are no runs found, there's no need to proceed with cancellation, so return nil.
if total == 0 {
return nil, nil
}
cancelledJobs := make([]*ActionRunJob, 0, total)
// Iterate over each found run and cancel its associated jobs.
for _, run := range runs {
// Find all jobs associated with the current run.
jobs, err := db.Find[ActionRunJob](ctx, FindRunJobOptions{
RunID: run.ID,
})
if err != nil {
return cancelledJobs, err
}
cjs, err := CancelJobs(ctx, jobs, false)
if err != nil {
return cancelledJobs, err
}
cancelledJobs = append(cancelledJobs, cjs...)
}
// Return nil to indicate successful cancellation of all running and waiting jobs.
return cancelledJobs, nil
return CancelJobs(ctx, jobs, false)
}
// GetAncestorCallerIDs returns the IDs of the reusable workflow callers the job is nested in.
@@ -738,7 +731,7 @@ func GetAncestorCallerIDs(ctx context.Context, job *ActionRunJob) (container.Set
return ids, nil
}
func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) (jobsToCancel []*ActionRunJob, _ error) {
func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob) ([]*ActionRunJob, error) {
if job.RawConcurrency == "" {
return nil, nil
}
@@ -758,21 +751,14 @@ func CancelPreviousJobsByJobConcurrency(ctx context.Context, job *ActionRunJob)
return nil, err
}
jobs = slices.DeleteFunc(jobs, func(j *ActionRunJob) bool { return j.ID == job.ID || callerIDs.Contains(j.ID) })
jobsToCancel = append(jobsToCancel, jobs...)
// cancel runs in the same concurrency group
for _, attempt := range attempts {
if attempt.ID == job.RunAttemptID {
continue
}
jobs, err := GetRunJobsByRunAndAttemptID(ctx, attempt.RunID, attempt.ID)
if err != nil {
return nil, fmt.Errorf("find run %d attempt %d jobs: %w", attempt.RunID, attempt.ID, err)
}
jobsToCancel = append(jobsToCancel, jobs...)
attempts = slices.DeleteFunc(attempts, func(attempt *ActionRunAttempt) bool { return attempt.ID == job.RunAttemptID })
attemptJobs, err := getRunJobsByAttempts(ctx, attempts)
if err != nil {
return nil, fmt.Errorf("find concurrent attempt jobs: %w", err)
}
return CancelJobs(ctx, jobsToCancel, false)
return CancelJobs(ctx, append(jobs, attemptJobs...), false)
}
// CancelJobs cancels every cancellable job it is given, force skipping the graceful cancelling
+45
View File
@@ -467,3 +467,48 @@ func TestCancelJobs_CallerWaitsForCancellingChild(t *testing.T) {
}
assert.Equal(t, StatusCancelled, unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: run.ID}).Status)
}
func TestBatchLoadAndCancelRunJobs(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
legacyRun := &ActionRun{RepoID: 4, OwnerID: 1, Index: 9600, TriggerUserID: 1, Ref: "refs/heads/batch", Status: StatusRunning}
run := &ActionRun{RepoID: 4, OwnerID: 1, Index: 9601, TriggerUserID: 1, Ref: "refs/heads/batch", Status: StatusRunning}
require.NoError(t, db.Insert(ctx, legacyRun, run))
require.NoError(t, db.Insert(ctx, &ActionRunJob{RunID: legacyRun.ID, RepoID: legacyRun.RepoID, OwnerID: legacyRun.OwnerID}))
oldAttempt := &ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 1, TriggerUserID: 1}
latestAttempt := &ActionRunAttempt{RepoID: run.RepoID, RunID: run.ID, Attempt: 2, TriggerUserID: 2}
require.NoError(t, db.Insert(ctx, oldAttempt, latestAttempt))
for _, attemptID := range []int64{oldAttempt.ID, latestAttempt.ID, latestAttempt.ID} {
require.NoError(t, db.Insert(ctx, &ActionRunJob{RunID: run.ID, RunAttemptID: attemptID, RepoID: run.RepoID, OwnerID: run.OwnerID}))
}
run.LatestAttemptID = latestAttempt.ID
require.NoError(t, UpdateRun(ctx, run, "latest_attempt_id"))
runs := RunList{legacyRun, run}
jobsByRun, err := GetLatestAttemptJobsByRuns(ctx, runs)
require.NoError(t, err)
for _, listedRun := range runs {
expected, err := GetLatestAttemptJobsByRun(ctx, listedRun)
require.NoError(t, err)
require.NotEmpty(t, expected)
assert.Equal(t, expected, jobsByRun[listedRun.ID])
}
attempts, err := runs.GetLatestAttempts(ctx)
require.NoError(t, err)
require.Len(t, attempts, 1)
assert.EqualValues(t, 2, attempts[latestAttempt.ID].TriggerUser.ID)
chunkedAttempts := []*ActionRunAttempt{oldAttempt}
for i := range db.DefaultMaxInSize {
chunkedAttempts = append(chunkedAttempts, &ActionRunAttempt{ID: -int64(i + 1)})
}
attemptJobs, err := getRunJobsByAttempts(ctx, append(chunkedAttempts, latestAttempt))
require.NoError(t, err)
assert.Len(t, attemptJobs, 3)
cancelled, err := CancelPreviousJobs(ctx, run.RepoID, run.Ref, "", "")
require.NoError(t, err)
assert.Len(t, cancelled, 4)
}
+17
View File
@@ -5,6 +5,8 @@ package actions
import (
"context"
"maps"
"slices"
"gitea.dev/models/db"
repo_model "gitea.dev/models/repo"
@@ -42,6 +44,21 @@ func (runs RunList) LoadTriggerUser(ctx context.Context) error {
return nil
}
// GetLatestAttempts returns the latest attempts of the runs keyed by attempt ID
func (runs RunList) GetLatestAttempts(ctx context.Context) (map[int64]*ActionRunAttempt, error) {
attemptIDs := container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
return run.LatestAttemptID, run.LatestAttemptID != 0
})
attempts := make(map[int64]*ActionRunAttempt, len(attemptIDs))
if len(attemptIDs) == 0 {
return attempts, nil
}
if err := db.GetEngine(ctx).In("id", attemptIDs).Find(&attempts); err != nil {
return nil, err
}
return attempts, ActionRunAttemptList(slices.Collect(maps.Values(attempts))).LoadTriggerUser(ctx)
}
func (runs RunList) LoadRepos(ctx context.Context) error {
repoIDs := container.FilterSlice(runs, func(run *ActionRun) (int64, bool) {
return run.RepoID, run.Repo == nil
+8
View File
@@ -207,6 +207,14 @@ func init() {
db.RegisterModel(&ActionRunner{})
}
func GetRunnersMapByIDs(ctx context.Context, ids []int64) (map[int64]*ActionRunner, error) {
runners := make(map[int64]*ActionRunner, len(ids))
if len(ids) == 0 {
return runners, nil
}
return runners, db.GetEngine(ctx).In("id", ids).Find(&runners)
}
// FindRunnerOptions
// ownerID == 0 and repoID == 0 means any runner including global runners
// repoID != 0 and WithAvailable == false means any runner for the given repo
+24
View File
@@ -9,6 +9,7 @@ import (
"gitea.dev/models/db"
"gitea.dev/modules/container"
"gitea.dev/modules/timeutil"
"gitea.dev/modules/util"
"xorm.io/builder"
)
@@ -41,6 +42,29 @@ func (tasks TaskList) LoadJobs(ctx context.Context) error {
return jobsList.LoadAttributes(ctx, true)
}
func (tasks TaskList) LoadSteps(ctx context.Context) error {
taskIDs := container.FilterSlice(tasks, func(task *ActionTask) (int64, bool) {
return task.ID, task.Steps == nil
})
if len(taskIDs) == 0 {
return nil
}
var steps []*ActionTaskStep
if err := db.GetEngine(ctx).In("task_id", taskIDs).OrderBy("task_id, `index`").Find(&steps); err != nil {
return err
}
stepsByTask := make(map[int64][]*ActionTaskStep, len(taskIDs))
for _, step := range steps {
stepsByTask[step.TaskID] = append(stepsByTask[step.TaskID], step)
}
for _, task := range tasks {
if task.Steps == nil {
task.Steps = util.SliceNilAsEmpty(stepsByTask[task.ID])
}
}
return nil
}
func (tasks TaskList) LoadAttributes(ctx context.Context) error {
return tasks.LoadJobs(ctx)
}
+15 -21
View File
@@ -90,27 +90,16 @@ func ListJobs(ctx *context.APIContext, ownerID, repoID, runID int64, runAttemptI
return
}
res.Entries = make([]*api.ActionWorkflowJob, len(jobs))
isRepoLevel := repoID != 0 && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == repoID
for i := range jobs {
var repository *repo_model.Repository
if isRepoLevel {
repository = ctx.Repo.Repository
} else {
if jobs[i].Run == nil || jobs[i].Run.Repo == nil {
ctx.APIErrorInternal(fmt.Errorf("job %d is missing its run or repository", jobs[i].ID))
return
}
repository = jobs[i].Run.Repo
}
convertedWorkflowJob, err := convert.ToActionWorkflowJob(ctx, repository, nil, jobs[i])
if err != nil {
ctx.APIErrorInternal(err)
for _, job := range jobs {
if job.Run == nil || job.Run.Repo == nil {
ctx.APIErrorInternal(fmt.Errorf("job %d is missing its run or repository", job.ID))
return
}
res.Entries[i] = convertedWorkflowJob
}
res.Entries, err = convert.ToActionWorkflowJobs(ctx, jobs)
if err != nil {
ctx.APIErrorInternal(err)
return
}
ctx.SetLinkHeader(total, listOptions.PageSize)
ctx.SetTotalCountHeader(total)
@@ -224,6 +213,12 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string)
return
}
latestAttempts, err := runList.GetLatestAttempts(ctx)
if err != nil {
ctx.APIErrorInternal(err)
return
}
res.Entries = make([]*api.ActionWorkflowRun, 0, len(runs))
for _, run := range runs {
if run.Repo == nil {
@@ -232,8 +227,7 @@ func ListRuns(ctx *context.APIContext, ownerID, repoID int64, workflowID string)
total--
continue
}
// TODO: load run attempts in batch
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, nil, excludePullRequests)
convertedRun, err := convert.ToActionWorkflowRun(ctx, run, latestAttempts[run.LatestAttemptID], excludePullRequests)
if err != nil {
ctx.APIErrorInternal(err)
return
+11 -10
View File
@@ -543,17 +543,18 @@ func (data *actionRunListData) processActionRuns(ctx *context.Context) bool {
return false
}
activeRuns := slices.DeleteFunc(slices.Clone(data.ActionRuns), func(run *actions_model.ActionRun) bool {
return !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked)
})
jobsByRun, err := actions_model.GetLatestAttemptJobsByRuns(ctx, activeRuns)
if err != nil {
ctx.ServerError("GetLatestAttemptJobsByRuns", err)
return false
}
data.RunErrors = make(map[int64]string)
for _, run := range data.ActionRuns {
if !run.Status.In(actions_model.StatusWaiting, actions_model.StatusRunning, actions_model.StatusBlocked) {
continue
}
jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(ctx, run.RepoID, run.ID)
if err != nil {
ctx.ServerError("GetRunJobsByRunID", err)
return false
}
for _, job := range jobs {
for _, run := range activeRuns {
for _, job := range jobsByRun[run.ID] {
// A deferred matrix is unresolvable until its needs finish, so the whole per-job block
// is skipped: parsing the payload would report a valid workflow as invalid.
if job.IsMatrixDeferred || !job.Status.In(actions_model.StatusWaiting, actions_model.StatusBlocked, actions_model.StatusPending) {
+13 -6
View File
@@ -15,6 +15,7 @@ import (
"gitea.dev/models/unittest"
"gitea.dev/modules/git"
"gitea.dev/modules/git/gitcmd"
api "gitea.dev/modules/structs"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
@@ -126,7 +127,7 @@ func TestToActionWorkflowRun_UsesTriggerEvent(t *testing.T) {
assert.Equal(t, "schedule", apiRun.Event)
}
func TestToActionWorkflowJob_StepStatusIsIndependentOfJobStatus(t *testing.T) {
func TestToActionWorkflowJob_StepStatusAndBatchParity(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()
@@ -142,10 +143,11 @@ func TestToActionWorkflowJob_StepStatusIsIndependentOfJobStatus(t *testing.T) {
require.NoError(t, db.Insert(ctx, run))
task := &actions_model.ActionTask{
ID: 900102,
JobID: 9001,
RepoID: 2,
Status: actions_model.StatusFailure,
ID: 900102,
JobID: 9001,
RepoID: 2,
RunnerID: 34346,
Status: actions_model.StatusFailure,
}
require.NoError(t, db.Insert(ctx, task))
@@ -162,8 +164,8 @@ func TestToActionWorkflowJob_StepStatusIsIndependentOfJobStatus(t *testing.T) {
require.NoError(t, db.Insert(ctx, job))
require.NoError(t, db.Insert(ctx,
&actions_model.ActionTaskStep{TaskID: task.ID, RepoID: 2, Index: 0, Name: "step-success", Status: actions_model.StatusSuccess},
&actions_model.ActionTaskStep{TaskID: task.ID, RepoID: 2, Index: 1, Name: "step-failure", Status: actions_model.StatusFailure},
&actions_model.ActionTaskStep{TaskID: task.ID, RepoID: 2, Index: 0, Name: "step-success", Status: actions_model.StatusSuccess},
))
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
@@ -172,10 +174,15 @@ func TestToActionWorkflowJob_StepStatusIsIndependentOfJobStatus(t *testing.T) {
require.NoError(t, err)
require.Len(t, apiJob.Steps, 2)
assert.Equal(t, "runner_to_be_deleted-user", apiJob.RunnerName)
assert.Equal(t, "completed", apiJob.Steps[0].Status, "step 0 status")
assert.Equal(t, "success", apiJob.Steps[0].Conclusion, "step 0 conclusion (succeeded before the failure)")
assert.Equal(t, "completed", apiJob.Steps[1].Status, "step 1 status")
assert.Equal(t, "failure", apiJob.Steps[1].Conclusion, "step 1 conclusion")
apiJobs, err := ToActionWorkflowJobs(ctx, []*actions_model.ActionRunJob{job})
require.NoError(t, err)
assert.Equal(t, []*api.ActionWorkflowJob{apiJob}, apiJobs)
}
func TestToActionsStatus(t *testing.T) {
+63 -27
View File
@@ -491,11 +491,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
return nil, err
}
status, conclusion := ToRunActionsStatus(job.Run, job.Status)
var runnerID int64
var runnerName string
var steps []*api.ActionWorkflowStep
var runner *actions_model.ActionRunner
if effectiveTaskID := job.EffectiveTaskID(); effectiveTaskID != 0 {
if task == nil {
task, _, err = db.GetByID[actions_model.ActionTask](ctx, effectiveTaskID)
@@ -505,28 +501,68 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
}
if task != nil {
if task.Steps == nil {
task.Steps, err = actions_model.GetTaskStepsByTaskID(ctx, task.ID)
if err != nil {
return nil, err
}
task.Steps = util.SliceNilAsEmpty(task.Steps)
}
runnerID = task.RunnerID
if runner, ok, _ := db.GetByID[actions_model.ActionRunner](ctx, runnerID); ok {
runnerName = runner.Name
}
for i, step := range task.Steps {
stepStatus, stepConclusion := ToActionsStatus(step.Status)
steps = append(steps, &api.ActionWorkflowStep{
Name: step.Name,
Number: int64(i),
Status: stepStatus,
Conclusion: stepConclusion,
StartedAt: step.Started.AsTime().UTC(),
CompletedAt: step.Stopped.AsTime().UTC(),
})
if err := (actions_model.TaskList{task}).LoadSteps(ctx); err != nil {
return nil, err
}
runner, _, _ = db.GetByID[actions_model.ActionRunner](ctx, task.RunnerID)
}
}
return toActionWorkflowJob(ctx, repo, task, runner, job), nil
}
// ToActionWorkflowJobs requires job.Run.Repo to be loaded
func ToActionWorkflowJobs(ctx context.Context, jobs []*actions_model.ActionRunJob) ([]*api.ActionWorkflowJob, error) {
tasks, err := actions_model.GetTasksMapByIDs(ctx, container.FilterSlice(jobs, func(job *actions_model.ActionRunJob) (int64, bool) {
taskID := job.EffectiveTaskID()
return taskID, taskID != 0
}))
if err != nil {
return nil, err
}
taskList := actions_model.TaskList(slices.Collect(maps.Values(tasks)))
if err := taskList.LoadSteps(ctx); err != nil {
return nil, err
}
runners, err := actions_model.GetRunnersMapByIDs(ctx, container.FilterSlice(taskList, func(task *actions_model.ActionTask) (int64, bool) {
return task.RunnerID, task.RunnerID != 0
}))
if err != nil {
return nil, err
}
result := make([]*api.ActionWorkflowJob, len(jobs))
for i, job := range jobs {
task := tasks[job.EffectiveTaskID()]
var runner *actions_model.ActionRunner
if task != nil {
runner = runners[task.RunnerID]
}
result[i] = toActionWorkflowJob(ctx, job.Run.Repo, task, runner, job)
}
return result, nil
}
func toActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task *actions_model.ActionTask, runner *actions_model.ActionRunner, job *actions_model.ActionRunJob) *api.ActionWorkflowJob {
status, conclusion := ToRunActionsStatus(job.Run, job.Status)
var runnerID int64
var runnerName string
var steps []*api.ActionWorkflowStep
if task != nil && job.EffectiveTaskID() != 0 {
runnerID = task.RunnerID
if runner != nil {
runnerName = runner.Name
}
for i, step := range task.Steps {
stepStatus, stepConclusion := ToActionsStatus(step.Status)
steps = append(steps, &api.ActionWorkflowStep{
Name: step.Name,
Number: int64(i),
Status: stepStatus,
Conclusion: stepConclusion,
StartedAt: step.Started.AsTime().UTC(),
CompletedAt: step.Stopped.AsTime().UTC(),
})
}
}
@@ -551,7 +587,7 @@ func ToActionWorkflowJob(ctx context.Context, repo *repo_model.Repository, task
CreatedAt: job.Created.AsTime().UTC(),
StartedAt: job.Started.AsTime().UTC(),
CompletedAt: job.Stopped.AsTime().UTC(),
}, nil
}
}
func getActionWorkflowEntry(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, refName git.RefName, folder string, entry *git.TreeEntry) *api.ActionWorkflow {