fix(actions): correctness and hardening fixes (#38518)

Various fixes to actions

1. **Cap total jobs per run in reusable-workflow expansion** — only
nesting depth was capped, so fan-out + nested reusable workflows could
explode job-row inserts and exhaust the DB from a single push. Now
enforces `MaxJobNumPerRun` in the insert path.
2. **Reject rerun-failed when a run has no failed jobs** — an empty job
list meant "re-run everything", so `rerun-failed` on a green run re-ran
all jobs. Now errors (web + API).
3. **Don't adopt external commit statuses into the legacy hash** — the
pre-#35699 Context-only hash matched API-posted statuses too, collapsing
two same-named workflows into one check. Now limited to Actions-user
rows.
4. **Don't cut post-cancel cleanup short in `StopEndlessTasks`** — the
sweep force-stopped just-cancelled jobs mid-cleanup. Now targets
`StatusRunning` only; stalled cancels stay covered by `StopZombieTasks`.
5. **Avoid redundant run reload in `GenerateGiteaContext`** — resolving
`github.triggering_actor` reloaded the run already passed in. Now loads
only the trigger user via new `ActionRunAttempt.LoadTriggerUser`.

---------

Co-authored-by: Zettat123 <zettat123@gmail.com>
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
bircni
2026-07-25 18:07:40 +02:00
committed by GitHub
parent 7a1941e384
commit 69f0a10364
15 changed files with 292 additions and 26 deletions
+17 -12
View File
@@ -566,9 +566,6 @@ func ViewPost(ctx *context_module.Context) {
}
func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse, run *actions_model.ActionRun, attempt *actions_model.ActionRunAttempt, jobs []*actions_model.ActionRunJob) {
// Latest when the run has no attempts yet (legacy) or the viewed attempt is the run's latest.
isLatestAttempt := run.LatestAttemptID == 0 || (attempt != nil && attempt.ID == run.LatestAttemptID)
resp.State.Run.RepoID = ctx.Repo.Repository.ID
resp.State.Run.Index = run.Index
// the title for the "run" is from the commit message
@@ -577,8 +574,12 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
resp.State.Run.Link = run.Link()
resp.State.Run.ViewLink = getRunViewLink(run, attempt)
resp.State.Run.Attempts = make([]*ViewRunAttempt, 0)
// Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts and summaries all
// share run_attempt_id=0, so passing 0 here scopes to this run's legacy rows only.
var runAttemptID int64
var effectiveStatus actions_model.Status
if attempt != nil {
runAttemptID = attempt.ID
effectiveStatus = attempt.Status
resp.State.Run.RunAttempt = attempt.Attempt
resp.State.Run.Duration = attempt.Duration().String()
@@ -588,6 +589,9 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
resp.State.Run.Duration = run.Duration().String()
resp.State.Run.TriggeredAt = run.Created.AsTime().Unix()
}
// Latest when the run has no attempts yet (legacy) or the viewed attempt is the run's latest.
isLatestAttempt := run.LatestAttemptID == 0 || runAttemptID == run.LatestAttemptID
resp.State.Run.Status = effectiveStatus.String()
resp.State.Run.Done = effectiveStatus.IsDone()
@@ -647,7 +651,7 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
Status: runAttempt.Status.String(),
Done: runAttempt.Status.IsDone(),
Link: getRunViewLink(run, runAttempt),
Current: runAttempt.ID == attempt.ID,
Current: runAttempt.ID == runAttemptID,
Latest: runAttempt.ID == run.LatestAttemptID,
TriggeredAt: runAttempt.Created.AsTime().Unix(),
TriggerUserName: runAttempt.TriggerUser.GetDisplayName(),
@@ -672,13 +676,6 @@ func fillViewRunResponseSummary(ctx *context_module.Context, resp *ViewResponse,
resp.State.Run.PullRequest = refInfo.PullRequest
resp.State.Run.TriggerEvent = run.TriggerEvent
// Legacy runs (LatestAttemptID == 0) have no attempt; their artifacts and summaries all
// share run_attempt_id=0, so passing 0 here scopes to this run's legacy rows only.
var runAttemptID int64
if attempt != nil {
runAttemptID = attempt.ID
}
// Each step's markdown is rendered independently so an unclosed construct
// in one step can't bleed into the next.
// On a single-job view only that job's summaries are needed; the run view shows all.
@@ -1006,7 +1003,15 @@ func RerunFailed(ctx *context_module.Context) {
return
}
if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, actions_service.GetFailedJobsForRerun(jobs)); err != nil {
// An empty job list means "re-run the whole run" to RerunWorkflowRunJobs, which is right for the plain
// rerun button but wrong here, so a direct POST on a fully successful run cannot re-run everything.
failedJobs := actions_service.GetFailedJobsForRerun(jobs)
if len(failedJobs) == 0 {
ctx.JSONError(ctx.Locale.Tr("actions.runs.no_failed_jobs"))
return
}
if _, err := actions_service.RerunWorkflowRunJobs(ctx, ctx.Repo.Repository, run, ctx.Doer, failedJobs); err != nil {
handleWorkflowRerunError(ctx, err)
return
}