mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-13 06:20:24 +00:00
**Backport:** #11063
Fixes #11030.
When a `strategy.matrix` needs to be evaluated on the output of another job, it can become evaluated into an empty set of jobs. In this case, and assuming no other jobs in the run are active, the run should reach a settled state. The logic to check the other jobs in the run and determine if this state has been hit needs to be explicitly added to the job emitter.
To accomplish this change, this action run state logic was extracted out of `UpdateRunJobWithoutNotification` where it could be reused.
(cherry picked from commit c198082975)
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11072
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net>
Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
393 lines
13 KiB
Go
393 lines
13 KiB
Go
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package actions
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"forgejo.org/models/db"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/models/unittest"
|
|
"forgejo.org/modules/cache"
|
|
|
|
"code.forgejo.org/forgejo/runner/v12/act/jobparser"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestGetRunBefore(t *testing.T) {
|
|
}
|
|
|
|
func TestSetConcurrencyGroup(t *testing.T) {
|
|
run := ActionRun{}
|
|
run.SetConcurrencyGroup("abc123")
|
|
assert.Equal(t, "abc123", run.ConcurrencyGroup)
|
|
run.SetConcurrencyGroup("ABC123") // case should collapse in SetConcurrencyGroup
|
|
assert.Equal(t, "abc123", run.ConcurrencyGroup)
|
|
}
|
|
|
|
func TestSetDefaultConcurrencyGroup(t *testing.T) {
|
|
run := ActionRun{
|
|
Ref: "refs/heads/main",
|
|
WorkflowID: "testing",
|
|
TriggerEvent: "pull_request",
|
|
}
|
|
run.SetDefaultConcurrencyGroup()
|
|
assert.Equal(t, "refs/heads/main_testing_pull_request__auto", run.ConcurrencyGroup)
|
|
run = ActionRun{
|
|
Ref: "refs/heads/main",
|
|
WorkflowID: "TESTING", // case should collapse in SetDefaultConcurrencyGroup
|
|
TriggerEvent: "pull_request",
|
|
}
|
|
run.SetDefaultConcurrencyGroup()
|
|
assert.Equal(t, "refs/heads/main_testing_pull_request__auto", run.ConcurrencyGroup)
|
|
}
|
|
|
|
func TestRepoNumOpenActions(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
err := cache.Init()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Repo 1", func(t *testing.T) {
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
|
clearRepoRunCountCache(t.Context(), repo)
|
|
assert.Equal(t, 0, RepoNumOpenActions(t.Context(), repo.ID))
|
|
})
|
|
|
|
t.Run("Repo 4", func(t *testing.T) {
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4})
|
|
clearRepoRunCountCache(t.Context(), repo)
|
|
assert.Equal(t, 0, RepoNumOpenActions(t.Context(), repo.ID))
|
|
})
|
|
|
|
t.Run("Repo 63", func(t *testing.T) {
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 63})
|
|
clearRepoRunCountCache(t.Context(), repo)
|
|
assert.Equal(t, 1, RepoNumOpenActions(t.Context(), repo.ID))
|
|
})
|
|
|
|
t.Run("Cache Invalidation", func(t *testing.T) {
|
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 63})
|
|
assert.Equal(t, 1, RepoNumOpenActions(t.Context(), repo.ID))
|
|
|
|
err = db.DeleteBeans(t.Context(), &ActionRun{RepoID: repo.ID})
|
|
require.NoError(t, err)
|
|
|
|
// Even though we've deleted ActionRun, expecting that the number of open runs is still 1 (cached)
|
|
assert.Equal(t, 1, RepoNumOpenActions(t.Context(), repo.ID))
|
|
|
|
// Now that we clear the cache, computation should be performed
|
|
clearRepoRunCountCache(t.Context(), repo)
|
|
assert.Equal(t, 0, RepoNumOpenActions(t.Context(), repo.ID))
|
|
})
|
|
}
|
|
|
|
func TestActionRun_GetRunsNotDoneByRepoIDAndPullRequestPosterID(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
repoID := int64(10)
|
|
pullRequestID := int64(3)
|
|
pullRequestPosterID := int64(30)
|
|
|
|
runDone := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
Status: StatusSuccess,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runDone, nil))
|
|
|
|
unrelatedUser := int64(5)
|
|
runNotByPoster := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: unrelatedUser,
|
|
Status: StatusRunning,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runNotByPoster, nil))
|
|
|
|
unrelatedRepository := int64(6)
|
|
runNotInTheSameRepository := &ActionRun{
|
|
RepoID: unrelatedRepository,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
Status: StatusSuccess,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runNotInTheSameRepository, nil))
|
|
|
|
for _, status := range []Status{StatusUnknown, StatusWaiting, StatusRunning} {
|
|
t.Run(fmt.Sprintf("%s", status), func(t *testing.T) {
|
|
runNotDone := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
Status: status,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runNotDone, nil))
|
|
runs, err := GetRunsNotDoneByRepoIDAndPullRequestPosterID(t.Context(), repoID, pullRequestPosterID)
|
|
require.NoError(t, err)
|
|
require.Len(t, runs, 1)
|
|
run := runs[0]
|
|
assert.Equal(t, runNotDone.ID, run.ID)
|
|
assert.Equal(t, runNotDone.Status, run.Status)
|
|
unittest.AssertSuccessfulDelete(t, run)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActionRun_NeedApproval(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
pullRequestPosterID := int64(4)
|
|
repoID := int64(10)
|
|
pullRequestID := int64(2)
|
|
runDoesNotNeedApproval := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runDoesNotNeedApproval, nil))
|
|
unrelatedRepository := int64(6)
|
|
runNotInTheSameRepository := &ActionRun{
|
|
RepoID: unrelatedRepository,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
NeedApproval: true,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runNotInTheSameRepository, nil))
|
|
unrelatedPullRequest := int64(3)
|
|
runNotInTheSamePullRequest := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: unrelatedPullRequest,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
NeedApproval: true,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runNotInTheSamePullRequest, nil))
|
|
|
|
t.Run("HasRunThatNeedApproval is false", func(t *testing.T) {
|
|
has, err := HasRunThatNeedApproval(t.Context(), repoID, pullRequestID)
|
|
require.NoError(t, err)
|
|
assert.False(t, has)
|
|
})
|
|
|
|
runNeedApproval := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
NeedApproval: true,
|
|
}
|
|
require.NoError(t, InsertRun(t.Context(), runNeedApproval, nil))
|
|
|
|
t.Run("HasRunThatNeedApproval is true", func(t *testing.T) {
|
|
has, err := HasRunThatNeedApproval(t.Context(), repoID, pullRequestID)
|
|
require.NoError(t, err)
|
|
assert.True(t, has)
|
|
})
|
|
|
|
assertApprovalEqual := func(t *testing.T, expected, actual *ActionRun) {
|
|
t.Helper()
|
|
assert.Equal(t, expected.RepoID, actual.RepoID)
|
|
assert.Equal(t, expected.PullRequestID, actual.PullRequestID)
|
|
assert.Equal(t, expected.PullRequestPosterID, actual.PullRequestPosterID)
|
|
assert.Equal(t, expected.NeedApproval, actual.NeedApproval)
|
|
}
|
|
|
|
t.Run("GetRunsThatNeedApproval", func(t *testing.T) {
|
|
runs, err := GetRunsThatNeedApprovalByRepoIDAndPullRequestID(t.Context(), repoID, pullRequestID)
|
|
require.NoError(t, err)
|
|
require.Len(t, runs, 1)
|
|
assertApprovalEqual(t, runNeedApproval, runs[0])
|
|
})
|
|
}
|
|
|
|
func TestActionRun_IncompleteMatrix(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
pullRequestPosterID := int64(4)
|
|
repoID := int64(10)
|
|
pullRequestID := int64(2)
|
|
runDoesNotNeedApproval := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
}
|
|
|
|
workflowRaw := []byte(`
|
|
jobs:
|
|
job2:
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
matrix:
|
|
dim1: "${{ fromJSON(needs.other-job.outputs.some-output) }}"
|
|
steps:
|
|
- run: true
|
|
`)
|
|
workflows, err := jobparser.Parse(workflowRaw, false, jobparser.WithJobOutputs(map[string]map[string]string{}))
|
|
require.NoError(t, err)
|
|
require.True(t, workflows[0].IncompleteMatrix) // must be set for this test scenario to be valid
|
|
|
|
require.NoError(t, InsertRun(t.Context(), runDoesNotNeedApproval, workflows))
|
|
|
|
jobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: runDoesNotNeedApproval.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
job := jobs[0]
|
|
|
|
// Expect job with an incomplete matrix to be StatusBlocked:
|
|
assert.Equal(t, StatusBlocked, job.Status)
|
|
}
|
|
|
|
func TestActionRun_IncompleteRunsOn(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
pullRequestPosterID := int64(4)
|
|
repoID := int64(10)
|
|
pullRequestID := int64(2)
|
|
runDoesNotNeedApproval := &ActionRun{
|
|
RepoID: repoID,
|
|
PullRequestID: pullRequestID,
|
|
PullRequestPosterID: pullRequestPosterID,
|
|
}
|
|
|
|
workflowRaw := []byte(`
|
|
jobs:
|
|
job2:
|
|
runs-on: ${{ needs.other-job.outputs.some-output }}
|
|
steps:
|
|
- run: true
|
|
`)
|
|
workflows, err := jobparser.Parse(workflowRaw, false, jobparser.WithJobOutputs(map[string]map[string]string{}), jobparser.SupportIncompleteRunsOn())
|
|
require.NoError(t, err)
|
|
require.True(t, workflows[0].IncompleteRunsOn) // must be set for this test scenario to be valid
|
|
|
|
require.NoError(t, InsertRun(t.Context(), runDoesNotNeedApproval, workflows))
|
|
|
|
jobs, err := db.Find[ActionRunJob](t.Context(), FindRunJobOptions{RunID: runDoesNotNeedApproval.ID})
|
|
require.NoError(t, err)
|
|
require.Len(t, jobs, 1)
|
|
job := jobs[0]
|
|
|
|
// Expect job with an incomplete runs-on to be StatusBlocked:
|
|
assert.Equal(t, StatusBlocked, job.Status)
|
|
}
|
|
|
|
func TestComputeRunStatus(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
|
|
t.Run("no changes", func(t *testing.T) {
|
|
run, columns, err := ComputeRunStatus(t.Context(), 791)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusSuccess, run.Status)
|
|
assert.NotContains(t, columns, "status")
|
|
assert.EqualValues(t, 1683636528, run.Started)
|
|
assert.NotContains(t, columns, "started")
|
|
assert.EqualValues(t, 1683636626, run.Stopped)
|
|
assert.NotContains(t, columns, "stopped")
|
|
})
|
|
|
|
t.Run("change status", func(t *testing.T) {
|
|
job := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: 192})
|
|
job.Status = StatusFailure
|
|
affected, err := db.GetEngine(t.Context()).Cols("status").ID(job.ID).Update(job)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
run, columns, err := ComputeRunStatus(t.Context(), 791)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusFailure, run.Status)
|
|
assert.Contains(t, columns, "status")
|
|
assert.NotContains(t, columns, "started")
|
|
assert.NotContains(t, columns, "stopped")
|
|
})
|
|
|
|
t.Run("won't change started if not running", func(t *testing.T) {
|
|
job := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: 192})
|
|
job.Status = StatusBlocked
|
|
affected, err := db.GetEngine(t.Context()).Cols("status").ID(job.ID).Update(job)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
preRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: 791})
|
|
preRun.Started = 0
|
|
affected, err = db.GetEngine(t.Context()).Cols("started").ID(preRun.ID).Update(preRun)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
run, columns, err := ComputeRunStatus(t.Context(), 791)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusBlocked, run.Status)
|
|
assert.EqualValues(t, 0, run.Started)
|
|
assert.Contains(t, columns, "status")
|
|
assert.NotContains(t, columns, "started")
|
|
assert.NotContains(t, columns, "stopped")
|
|
})
|
|
|
|
t.Run("change started", func(t *testing.T) {
|
|
// Need the job to be "Running" for started to appear to change
|
|
job := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: 192})
|
|
job.Status = StatusRunning
|
|
affected, err := db.GetEngine(t.Context()).Cols("status").ID(job.ID).Update(job)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
preRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: 791})
|
|
preRun.Started = 0
|
|
affected, err = db.GetEngine(t.Context()).Cols("started").ID(preRun.ID).Update(preRun)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
run, columns, err := ComputeRunStatus(t.Context(), 791)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusRunning, run.Status)
|
|
assert.NotEqualValues(t, 0, run.Started)
|
|
assert.Contains(t, columns, "status")
|
|
assert.Contains(t, columns, "started")
|
|
assert.NotContains(t, columns, "stopped")
|
|
})
|
|
|
|
t.Run("won't change stopped if not done", func(t *testing.T) {
|
|
job := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: 192})
|
|
job.Status = StatusRunning
|
|
affected, err := db.GetEngine(t.Context()).Cols("status").ID(job.ID).Update(job)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
preRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: 791})
|
|
preRun.Stopped = 0
|
|
affected, err = db.GetEngine(t.Context()).Cols("stopped").ID(preRun.ID).Update(preRun)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
run, columns, err := ComputeRunStatus(t.Context(), 791)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusRunning, run.Status)
|
|
assert.EqualValues(t, 0, run.Stopped)
|
|
assert.Contains(t, columns, "status")
|
|
assert.NotContains(t, columns, "stopped")
|
|
})
|
|
|
|
t.Run("change stopped", func(t *testing.T) {
|
|
// Need the job to be some version of Done for stopped to appear to change
|
|
job := unittest.AssertExistsAndLoadBean(t, &ActionRunJob{ID: 192})
|
|
job.Status = StatusSuccess
|
|
affected, err := db.GetEngine(t.Context()).Cols("status").ID(job.ID).Update(job)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
preRun := unittest.AssertExistsAndLoadBean(t, &ActionRun{ID: 791})
|
|
preRun.Stopped = 0
|
|
affected, err = db.GetEngine(t.Context()).Cols("stopped").ID(preRun.ID).Update(preRun)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, 1, affected)
|
|
|
|
run, columns, err := ComputeRunStatus(t.Context(), 791)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, StatusSuccess, run.Status)
|
|
assert.NotEqualValues(t, 0, run.Stopped)
|
|
assert.NotContains(t, columns, "status")
|
|
assert.NotContains(t, columns, "started")
|
|
assert.Contains(t, columns, "stopped")
|
|
})
|
|
}
|