mirror of
https://github.com/go-gitea/gitea
synced 2025-12-06 21:08:25 +00:00
#35783 fixes an actions rerun bug. Due to this bug, some runs may be incorrectly marked as `StatusWaiting` even though all the jobs are in done status. These runs cannot be run or cancelled. This PR adds a new doctor command to fix the inconsistent run status. ``` gitea doctor check --run fix-actions-unfinished-run-status --fix ``` Thanks to @ChristopherHX for the test.
172 lines
4.4 KiB
Go
172 lines
4.4 KiB
Go
// Copyright 2024 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package doctor
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
actions_model "code.gitea.io/gitea/models/actions"
|
|
"code.gitea.io/gitea/models/db"
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
|
unit_model "code.gitea.io/gitea/models/unit"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
repo_service "code.gitea.io/gitea/services/repository"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
func disableMirrorActionsUnit(ctx context.Context, logger log.Logger, autofix bool) error {
|
|
var reposToFix []*repo_model.Repository
|
|
|
|
for page := 1; ; page++ {
|
|
repos, _, err := repo_model.SearchRepository(ctx, repo_model.SearchRepoOptions{
|
|
ListOptions: db.ListOptions{
|
|
PageSize: repo_model.RepositoryListDefaultPageSize,
|
|
Page: page,
|
|
},
|
|
Mirror: optional.Some(true),
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("SearchRepository: %w", err)
|
|
}
|
|
if len(repos) == 0 {
|
|
break
|
|
}
|
|
|
|
for _, repo := range repos {
|
|
if repo.UnitEnabled(ctx, unit_model.TypeActions) {
|
|
reposToFix = append(reposToFix, repo)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(reposToFix) == 0 {
|
|
logger.Info("Found no mirror with actions unit enabled")
|
|
} else {
|
|
logger.Warn("Found %d mirrors with actions unit enabled", len(reposToFix))
|
|
}
|
|
if !autofix || len(reposToFix) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, repo := range reposToFix {
|
|
if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit_model.Type{unit_model.TypeActions}); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
logger.Info("Fixed %d mirrors with actions unit enabled", len(reposToFix))
|
|
|
|
return nil
|
|
}
|
|
|
|
func fixUnfinishedRunStatus(ctx context.Context, logger log.Logger, autofix bool) error {
|
|
total := 0
|
|
inconsistent := 0
|
|
fixed := 0
|
|
|
|
cond := builder.In("status", []actions_model.Status{
|
|
actions_model.StatusWaiting,
|
|
actions_model.StatusRunning,
|
|
actions_model.StatusBlocked,
|
|
}).And(builder.Lt{"updated": timeutil.TimeStampNow().AddDuration(-setting.Actions.ZombieTaskTimeout)})
|
|
|
|
err := db.Iterate(
|
|
ctx,
|
|
cond,
|
|
func(ctx context.Context, run *actions_model.ActionRun) error {
|
|
total++
|
|
|
|
jobs, err := actions_model.GetRunJobsByRunID(ctx, run.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("GetRunJobsByRunID: %w", err)
|
|
}
|
|
expected := actions_model.AggregateJobStatus(jobs)
|
|
if expected == run.Status {
|
|
return nil
|
|
}
|
|
|
|
inconsistent++
|
|
logger.Warn("Run %d (repo_id=%d, index=%d) has status %s, expected %s", run.ID, run.RepoID, run.Index, run.Status, expected)
|
|
|
|
if !autofix {
|
|
return nil
|
|
}
|
|
|
|
run.Started, run.Stopped = getRunTimestampsFromJobs(run, expected, jobs)
|
|
run.Status = expected
|
|
|
|
if err := actions_model.UpdateRun(ctx, run, "status", "started", "stopped"); err != nil {
|
|
return fmt.Errorf("UpdateRun: %w", err)
|
|
}
|
|
fixed++
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
logger.Critical("Unable to iterate unfinished runs: %v", err)
|
|
return err
|
|
}
|
|
|
|
if inconsistent == 0 {
|
|
logger.Info("Checked %d unfinished runs; all statuses are consistent.", total)
|
|
return nil
|
|
}
|
|
|
|
if autofix {
|
|
logger.Info("Checked %d unfinished runs; fixed %d of %d runs.", total, fixed, inconsistent)
|
|
} else {
|
|
logger.Warn("Checked %d unfinished runs; found %d runs need to be fixed", total, inconsistent)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getRunTimestampsFromJobs(run *actions_model.ActionRun, newStatus actions_model.Status, jobs actions_model.ActionJobList) (started, stopped timeutil.TimeStamp) {
|
|
started = run.Started
|
|
if (newStatus.IsRunning() || newStatus.IsDone()) && started.IsZero() {
|
|
var earliest timeutil.TimeStamp
|
|
for _, job := range jobs {
|
|
if job.Started > 0 && (earliest.IsZero() || job.Started < earliest) {
|
|
earliest = job.Started
|
|
}
|
|
}
|
|
started = earliest
|
|
}
|
|
|
|
stopped = run.Stopped
|
|
if newStatus.IsDone() && stopped.IsZero() {
|
|
var latest timeutil.TimeStamp
|
|
for _, job := range jobs {
|
|
if job.Stopped > latest {
|
|
latest = job.Stopped
|
|
}
|
|
}
|
|
stopped = latest
|
|
}
|
|
|
|
return started, stopped
|
|
}
|
|
|
|
func init() {
|
|
Register(&Check{
|
|
Title: "Disable the actions unit for all mirrors",
|
|
Name: "disable-mirror-actions-unit",
|
|
IsDefault: false,
|
|
Run: disableMirrorActionsUnit,
|
|
Priority: 9,
|
|
})
|
|
Register(&Check{
|
|
Title: "Fix inconsistent status for unfinished actions runs",
|
|
Name: "fix-actions-unfinished-run-status",
|
|
IsDefault: false,
|
|
Run: fixUnfinishedRunStatus,
|
|
Priority: 9,
|
|
})
|
|
}
|