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
@@ -207,6 +207,47 @@ func TestResolveUses(t *testing.T) {
})
}
func TestCheckRunJobLimit(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
const (
runID = 900100
attemptA = 910001
attemptB = 910002
)
seed := func(attemptID int64, n int) {
for i := range n {
name := fmt.Sprintf("job-%d-%d", attemptID, i)
require.NoError(t, db.Insert(t.Context(), &actions_model.ActionRunJob{
RunID: runID,
RunAttemptID: attemptID,
RepoID: 1,
OwnerID: 1,
CommitSHA: "abcdef",
Name: name,
JobID: name,
AttemptJobID: attemptID*1000 + int64(i),
Status: actions_model.StatusBlocked,
}))
}
}
seed(attemptA, 5)
seed(attemptB, 3) // a different attempt of the same run must not count toward attempt A
limit := actions_model.MaxJobNumPerRun
// attempt A already holds 5 jobs: filling up to the cap is allowed, one more is rejected.
require.NoError(t, checkRunJobLimit(t.Context(), runID, attemptA, limit-5))
require.ErrorContains(t, checkRunJobLimit(t.Context(), runID, attemptA, limit-4), "maximum")
require.ErrorContains(t, checkRunJobLimit(t.Context(), runID, attemptA, limit), "maximum")
// the count is scoped to the attempt: attempt B only holds 3 jobs, so attempt A's 5 must not leak in.
require.NoError(t, checkRunJobLimit(t.Context(), runID, attemptB, limit-3))
require.ErrorContains(t, checkRunJobLimit(t.Context(), runID, attemptB, limit-2), "maximum")
}
func TestUndoExpansion(t *testing.T) {
require.NoError(t, unittest.PrepareTestDatabase())
ctx := t.Context()