mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-01 20:59:45 +09:00
f44e64be81
Fixes several gaps in the approval of fork pull request runs: 1. Approving a run that was cancelled while awaiting approval revived its cancelled jobs. Such a run is no longer treated as awaiting approval by the merge box, run page, approve actions and API, and rerunning it approves it. 2. Approval no longer revives jobs cancelled while the run was pending, no longer lets two jobs sharing a concurrency group cancel each other, and re-emits the run so jobs needing a cancelled job get resolved. 3. An unapproved run applies its workflow-level concurrency only once approved. 4. For workflows from the pull request, both the event actor and the pull request author must be trusted to skip approval. Workflows from the default branch, like `issue_comment`, still only check the actor. --------- Co-authored-by: silverwind <me@silverwind.io>
191 lines
8.8 KiB
Go
191 lines
8.8 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"testing"
|
|
|
|
actions_model "gitea.dev/models/actions"
|
|
"gitea.dev/models/db"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unittest"
|
|
user_model "gitea.dev/models/user"
|
|
"gitea.dev/modules/test"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestApproveRuns(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
var emittedRunIDs []int64
|
|
defer test.MockVariableValue(&EmitJobsIfReadyByRun, func(runID int64) error {
|
|
emittedRunIDs = append(emittedRunIDs, runID)
|
|
return nil
|
|
})()
|
|
|
|
insertRun := func(index int64, status actions_model.Status, needApproval bool, approvedBy int64) *actions_model.ActionRun {
|
|
run := &actions_model.ActionRun{
|
|
Title: "approve-run", RepoID: repo.ID, OwnerID: repo.OwnerID, WorkflowID: "test.yaml", Index: index,
|
|
TriggerUserID: doer.ID, Ref: "refs/heads/main",
|
|
CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0", Event: "push", TriggerEvent: "push",
|
|
Status: status, NeedApproval: needApproval, ApprovedBy: approvedBy,
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), run))
|
|
return run
|
|
}
|
|
insertJob := func(run *actions_model.ActionRun, status actions_model.Status, needs ...string) *actions_model.ActionRunJob {
|
|
job := &actions_model.ActionRunJob{
|
|
RunID: run.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, CommitSHA: run.CommitSHA,
|
|
Name: "job1", Attempt: 1, JobID: "job1", Status: status,
|
|
RunsOn: []string{"ubuntu-latest"}, Needs: needs,
|
|
WorkflowPayload: minimalWorkflowPayload("job1"),
|
|
}
|
|
require.NoError(t, db.Insert(t.Context(), job))
|
|
return job
|
|
}
|
|
|
|
t.Run("approve unblocks a job with no dependencies", func(t *testing.T) {
|
|
run := insertRun(1001, actions_model.StatusBlocked, true, 0)
|
|
job := insertJob(run, actions_model.StatusBlocked)
|
|
|
|
approved, err := ApproveRuns(t.Context(), repo, doer, []int64{run.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, approved, 1)
|
|
assert.False(t, approved[0].NeedApproval)
|
|
assert.Equal(t, doer.ID, approved[0].ApprovedBy)
|
|
|
|
assert.Equal(t, actions_model.StatusWaiting, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}).Status)
|
|
})
|
|
|
|
t.Run("approve skips a job whose if is false", func(t *testing.T) {
|
|
defer test.MockVariableValue(&EmitJobsIfReadyByRun, func(int64) error { return nil })()
|
|
run := insertRun(1009, actions_model.StatusBlocked, true, 0)
|
|
job := insertJob(run, actions_model.StatusBlocked)
|
|
job.WorkflowPayload = []byte("jobs:\n job1:\n if: false\n")
|
|
_, err := actions_model.UpdateRunJob(t.Context(), job, nil, "workflow_payload")
|
|
require.NoError(t, err)
|
|
|
|
_, err = ApproveRuns(t.Context(), repo, doer, []int64{run.ID})
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, actions_model.StatusSkipped, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}).Status)
|
|
})
|
|
|
|
t.Run("a job with unmet dependencies stays blocked", func(t *testing.T) {
|
|
run := insertRun(1002, actions_model.StatusBlocked, true, 0)
|
|
job := insertJob(run, actions_model.StatusBlocked, "some-other-job")
|
|
|
|
approved, err := ApproveRuns(t.Context(), repo, doer, []int64{run.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, approved, 1)
|
|
assert.False(t, approved[0].NeedApproval)
|
|
|
|
assert.Equal(t, actions_model.StatusBlocked, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}).Status)
|
|
})
|
|
|
|
t.Run("approval never revives cancelled runs or jobs and re-emits approved runs", func(t *testing.T) {
|
|
cancelledRun := insertRun(1006, actions_model.StatusCancelled, true, 0)
|
|
cancelledRunJob := insertJob(cancelledRun, actions_model.StatusCancelled)
|
|
blockedRun := insertRun(1007, actions_model.StatusBlocked, true, 0)
|
|
cancelledJob := insertJob(blockedRun, actions_model.StatusCancelled)
|
|
emittedRunIDs = nil
|
|
|
|
approved, err := ApproveRuns(t.Context(), repo, doer, []int64{cancelledRun.ID, blockedRun.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, approved, 2)
|
|
assert.True(t, approved[0].NeedApproval)
|
|
assert.Zero(t, approved[0].ApprovedBy)
|
|
assert.False(t, approved[1].NeedApproval)
|
|
assert.Equal(t, []int64{blockedRun.ID}, emittedRunIDs)
|
|
|
|
assert.Equal(t, actions_model.StatusCancelled, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: cancelledRunJob.ID}).Status)
|
|
assert.Equal(t, actions_model.StatusCancelled, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: cancelledJob.ID}).Status)
|
|
})
|
|
|
|
t.Run("approval starts one of two blocked jobs sharing a concurrency group", func(t *testing.T) {
|
|
run := insertRun(1008, actions_model.StatusBlocked, true, 0)
|
|
jobs := []*actions_model.ActionRunJob{insertJob(run, actions_model.StatusBlocked), insertJob(run, actions_model.StatusBlocked)}
|
|
for _, job := range jobs {
|
|
job.RawConcurrency, job.ConcurrencyGroup, job.IsConcurrencyEvaluated = "group: siblings", "siblings", true
|
|
_, err := actions_model.UpdateRunJob(t.Context(), job, nil, "raw_concurrency", "concurrency_group", "is_concurrency_evaluated")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
_, err := ApproveRuns(t.Context(), repo, doer, []int64{run.ID})
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []actions_model.Status{actions_model.StatusWaiting, actions_model.StatusCancelled}, []actions_model.Status{
|
|
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: jobs[0].ID}).Status,
|
|
unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: jobs[1].ID}).Status,
|
|
})
|
|
})
|
|
|
|
t.Run("workflow concurrency cancels peers on approval, not on insertion", func(t *testing.T) {
|
|
previousRun := &actions_model.ActionRun{RepoID: 4, OwnerID: 1, Index: 9001, TriggerUserID: 1, TriggerEvent: "push", EventPayload: "{}", Status: actions_model.StatusWaiting}
|
|
require.NoError(t, db.Insert(t.Context(), previousRun))
|
|
previousAttempt := &actions_model.ActionRunAttempt{RepoID: 4, RunID: previousRun.ID, Attempt: 1, Status: actions_model.StatusWaiting, ConcurrencyGroup: "shared", ConcurrencyCancel: true}
|
|
require.NoError(t, db.Insert(t.Context(), previousAttempt))
|
|
previousJob := &actions_model.ActionRunJob{RunID: previousRun.ID, RunAttemptID: previousAttempt.ID, RepoID: 4, OwnerID: 1, JobID: "deploy", AttemptJobID: 1, Status: actions_model.StatusWaiting}
|
|
require.NoError(t, db.Insert(t.Context(), previousJob))
|
|
|
|
content := []byte(`on: pull_request
|
|
concurrency:
|
|
group: shared
|
|
cancel-in-progress: true
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: shared
|
|
cancel-in-progress: true
|
|
steps:
|
|
- run: echo hi
|
|
`)
|
|
run := &actions_model.ActionRun{
|
|
RepoID: 4, OwnerID: 1, WorkflowID: "test.yaml", TriggerUserID: 2,
|
|
Ref: "refs/pull/1/head", CommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
Event: "pull_request", TriggerEvent: "pull_request", EventPayload: "{}", NeedApproval: true,
|
|
WorkflowRepoID: 4, WorkflowCommitSHA: "c2d72f548424103f01ee1dc02889c1e2bff816b0",
|
|
}
|
|
require.NoError(t, PrepareRunAndInsert(t.Context(), content, run, nil))
|
|
assert.Equal(t, actions_model.StatusWaiting, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: previousJob.ID}).Status)
|
|
|
|
_, err := ApproveRuns(t.Context(), unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}), doer, []int64{run.ID})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, actions_model.StatusCancelled, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: previousJob.ID}).Status)
|
|
jobs := runJobs(t, run.ID, run.LatestAttemptID)
|
|
require.Len(t, jobs, 1)
|
|
assert.Equal(t, actions_model.StatusWaiting, jobs[0].Status)
|
|
})
|
|
|
|
t.Run("re-approving an approved run is a no-op", func(t *testing.T) {
|
|
run := insertRun(1005, actions_model.StatusRunning, false, 4)
|
|
job := insertJob(run, actions_model.StatusRunning)
|
|
|
|
approved, err := ApproveRuns(t.Context(), repo, doer, []int64{run.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, approved, 1)
|
|
assert.EqualValues(t, 4, approved[0].ApprovedBy, "approver must not be overwritten")
|
|
assert.Equal(t, actions_model.StatusRunning, unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRunJob{ID: job.ID}).Status, "started job must not be reset to waiting")
|
|
})
|
|
|
|
t.Run("approving several runs returns them in the requested order", func(t *testing.T) {
|
|
run1 := insertRun(1003, actions_model.StatusBlocked, true, 0)
|
|
insertJob(run1, actions_model.StatusBlocked)
|
|
run2 := insertRun(1004, actions_model.StatusBlocked, true, 0)
|
|
insertJob(run2, actions_model.StatusBlocked)
|
|
|
|
approved, err := ApproveRuns(t.Context(), repo, doer, []int64{run2.ID, run1.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, approved, 2)
|
|
assert.Equal(t, run2.ID, approved[0].ID)
|
|
assert.Equal(t, run1.ID, approved[1].ID)
|
|
assert.False(t, approved[0].NeedApproval)
|
|
assert.False(t, approved[1].NeedApproval)
|
|
})
|
|
}
|