From 7d123945613c5ec0030e74aeb3206dfd0fea895d Mon Sep 17 00:00:00 2001 From: Mitrahsoft Date: Sat, 31 Oct 2026 00:24:07 +0530 Subject: [PATCH] fix(actions): clear commit status links when a run is deleted (#40088) Deleting an Actions run (UI, API or `RUN_RETENTION_DAYS`) left its commit statuses linking to the deleted run, so "Details" gave a 404. `DeleteRun` now clears the `target_url` of the run's statuses and of the commit's status summary on the run's commit, in the same transaction. States are kept, so required status checks and merging are unaffected. This differs from GitHub, which deletes the checks together with the run. Fixes https://github.com/go-gitea/gitea/issues/40079 Co-authored-by: silverwind --- services/actions/cleanup.go | 13 ++++++++++++- tests/integration/actions_delete_run_test.go | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/services/actions/cleanup.go b/services/actions/cleanup.go index 7c2fedc7e65..da33e155fa6 100644 --- a/services/actions/cleanup.go +++ b/services/actions/cleanup.go @@ -10,6 +10,7 @@ import ( actions_model "gitea.dev/models/actions" "gitea.dev/models/db" + git_model "gitea.dev/models/git" actions_module "gitea.dev/modules/actions" "gitea.dev/modules/container" "gitea.dev/modules/log" @@ -243,7 +244,17 @@ func DeleteRun(ctx context.Context, run *actions_model.ActionRun) error { if err := CleanupEphemeralRunners(ctx); err != nil { return err } - return db.DeleteBeans(ctx, recordsToDelete...) + if err := db.DeleteBeans(ctx, recordsToDelete...); err != nil { + return err + } + _, commitID, _ := getCommitStatusEventNameAndCommitID(run) // no statuses were created if this fails + cond := builder.Eq{"repo_id": repoID, "sha": commitID}.And(builder.Like{"target_url", fmt.Sprintf("/actions/runs/%d/jobs/", run.ID)}) + for _, bean := range []any{&git_model.CommitStatus{}, &git_model.CommitStatusSummary{}} { + if _, err := db.GetEngine(ctx).Where(cond).Cols("target_url").Update(bean); err != nil { + return err + } + } + return nil }); err != nil { return err } diff --git a/tests/integration/actions_delete_run_test.go b/tests/integration/actions_delete_run_test.go index b577457eb6e..f7b732de691 100644 --- a/tests/integration/actions_delete_run_test.go +++ b/tests/integration/actions_delete_run_test.go @@ -14,8 +14,11 @@ import ( runnerv1 "gitea.dev/actionslib/runner/v1" actions_model "gitea.dev/models/actions" auth_model "gitea.dev/models/auth" + "gitea.dev/models/db" + git_model "gitea.dev/models/git" "gitea.dev/models/unittest" user_model "gitea.dev/models/user" + "gitea.dev/modules/commitstatus" "gitea.dev/modules/json" "gitea.dev/routers/web/repo/actions" @@ -91,6 +94,7 @@ jobs: createWorkflowFile(t, token, user2.Name, apiRepo.Name, testCase.treePath, opts) var runID int64 + var commitID string for i := 0; i < len(testCase.outcomes); i++ { task := runner.fetchTask(t) jobName := getTaskJobNameByTaskID(t, token, user2.Name, apiRepo.Name, task.Id) @@ -102,6 +106,7 @@ jobs: assert.NoError(t, err) run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{RepoID: apiRepo.ID, Index: parsedRunIndex}) runID = run.ID + commitID = run.CommitSHA } jobs, err := actions_model.GetLatestAttemptJobsByRepoAndRunID(t.Context(), apiRepo.ID, runID) @@ -127,6 +132,15 @@ jobs: req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID)) session.MakeRequest(t, req, http.StatusOK) + statuses, err := git_model.GetLatestCommitStatus(t.Context(), apiRepo.ID, commitID, db.ListOptionsAll) + assert.NoError(t, err) + assert.Len(t, statuses, 3) + for _, status := range statuses { + assert.Equal(t, commitstatus.CommitStatusSuccess, status.State) + assert.Empty(t, status.TargetURL) + } + assert.Empty(t, unittest.AssertExistsAndLoadBean(t, &git_model.CommitStatusSummary{RepoID: apiRepo.ID, SHA: commitID}).TargetURL) + req = NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/delete", user2.Name, apiRepo.Name, runID)) session.MakeRequest(t, req, http.StatusNotFound)