mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-11 15:12:34 +00:00
Follow #38237 #38237 posts "skipped" commit statuses for every workflow that is not triggered due to a filter (e.g. `paths` or `branches`) mismatch. However, for non-required workflows, creating "skipped" commit statuses for them would generate a lot of noise. To address this issue, this PR adds a check before creating commit status: - For the context that matches any required status check patterns, a "skipped" commit status will be created. The `Required` label can inform users that this status check is required, but has been skipped because of a filter mismatch. - For a non-required context, nothing will be created. NOTE: Reducing noise is a best-effort approach and isn't entirely accurate. When creating commit statuses, it is impossible to predict which branch protection rule will take effect. Therefore, we have to compare the commit status context against the required patterns from all rules. If any rule matches, the context is considered "required".
167 lines
7.1 KiB
Go
167 lines
7.1 KiB
Go
// Copyright 2024 The Gitea Authors.
|
|
// All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package pull
|
|
|
|
import (
|
|
"testing"
|
|
|
|
actions_model "gitea.dev/models/actions"
|
|
"gitea.dev/models/db"
|
|
git_model "gitea.dev/models/git"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unittest"
|
|
"gitea.dev/modules/commitstatus"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestMergeRequiredContextsCommitStatus(t *testing.T) {
|
|
cases := []struct {
|
|
commitStatuses []*git_model.CommitStatus
|
|
requiredContexts []string
|
|
expected commitstatus.CommitStatusState
|
|
}{
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{},
|
|
requiredContexts: []string{},
|
|
expected: commitstatus.CommitStatusPending,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build xxx", State: commitstatus.CommitStatusSkipped},
|
|
},
|
|
requiredContexts: []string{"Build*"},
|
|
expected: commitstatus.CommitStatusSuccess,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build 1", State: commitstatus.CommitStatusSkipped},
|
|
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 3", State: commitstatus.CommitStatusSuccess},
|
|
},
|
|
requiredContexts: []string{"Build*"},
|
|
expected: commitstatus.CommitStatusSuccess,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2t", State: commitstatus.CommitStatusPending},
|
|
},
|
|
requiredContexts: []string{"Build*", "Build 2t*"},
|
|
expected: commitstatus.CommitStatusPending,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2t", State: commitstatus.CommitStatusFailure},
|
|
},
|
|
requiredContexts: []string{"Build*", "Build 2t*"},
|
|
expected: commitstatus.CommitStatusFailure,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2t", State: commitstatus.CommitStatusFailure},
|
|
},
|
|
requiredContexts: []string{"Build*"},
|
|
expected: commitstatus.CommitStatusFailure,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2t", State: commitstatus.CommitStatusSuccess},
|
|
},
|
|
requiredContexts: []string{"Build*", "Build 2t*", "Build 3*"},
|
|
expected: commitstatus.CommitStatusPending,
|
|
},
|
|
{
|
|
commitStatuses: []*git_model.CommitStatus{
|
|
{Context: "Build 1", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2", State: commitstatus.CommitStatusSuccess},
|
|
{Context: "Build 2t", State: commitstatus.CommitStatusSuccess},
|
|
},
|
|
requiredContexts: []string{"Build*", "Build *", "Build 2t*", "Build 1*"},
|
|
expected: commitstatus.CommitStatusSuccess,
|
|
},
|
|
}
|
|
for i, c := range cases {
|
|
assert.Equal(t, c.expected, MergeRequiredContextsCommitStatus(c.commitStatuses, c.requiredContexts), "case %d", i)
|
|
}
|
|
}
|
|
|
|
// TestEffectiveRequiredContexts: every required scoped workflow's stored status-check patterns are appended to the
|
|
// branch protection's configured contexts unconditionally (must-present; the matching is done downstream).
|
|
func TestEffectiveRequiredContexts(t *testing.T) {
|
|
require.NoError(t, unittest.PrepareTestDatabase())
|
|
consumer := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 4}) // owned by user5
|
|
pbOn := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"configured/check"}}
|
|
|
|
t.Run("nil protected branch: nil", func(t *testing.T) {
|
|
got, err := EffectiveRequiredContexts(t.Context(), consumer, nil)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, got)
|
|
})
|
|
|
|
t.Run("status checks disabled, no required scoped: nothing required", func(t *testing.T) {
|
|
pbOff := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"configured/check"}}
|
|
got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOff)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, got) // the rule's own status check is off and no required scoped workflow applies -> nothing gates
|
|
})
|
|
|
|
t.Run("owner with no scoped sources: configured contexts unchanged", func(t *testing.T) {
|
|
noSourceRepo := &repo_model.Repository{ID: consumer.ID, OwnerID: 99999}
|
|
got, err := EffectiveRequiredContexts(t.Context(), noSourceRepo, pbOn)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, []string{"configured/check"}, got)
|
|
})
|
|
|
|
t.Run("required workflow patterns appended", func(t *testing.T) {
|
|
require.NoError(t, db.Insert(t.Context(), &actions_model.ActionScopedWorkflowSource{
|
|
OwnerID: consumer.OwnerID,
|
|
SourceRepoID: 1,
|
|
WorkflowConfigs: map[string]*actions_model.ScopedWorkflowConfig{
|
|
"ci.yaml": {Required: true, Patterns: []string{"org/src: ci.yaml / build (pull_request)", "org/src: ci.yaml / lint (pull_request)"}},
|
|
"old.yaml": {Required: false, Patterns: []string{"org/src: old.yaml / *"}}, // kept as history, must NOT be enforced
|
|
},
|
|
}))
|
|
// No status is passed/needed: required patterns are enforced even though nothing has posted them yet (must-present).
|
|
got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOn)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{
|
|
"configured/check",
|
|
"org/src: ci.yaml / build (pull_request)",
|
|
"org/src: ci.yaml / lint (pull_request)",
|
|
}, got)
|
|
assert.NotContains(t, got, "org/src: old.yaml / *", "a non-required (history) config must not be enforced")
|
|
})
|
|
|
|
t.Run("status checks disabled, with required scoped: only the scoped patterns gate", func(t *testing.T) {
|
|
pbOff := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"configured/check"}}
|
|
got, err := EffectiveRequiredContexts(t.Context(), consumer, pbOff)
|
|
require.NoError(t, err)
|
|
// "configured/check" is dropped (the rule's own status check is off); only the required scoped patterns remain.
|
|
assert.ElementsMatch(t, []string{
|
|
"org/src: ci.yaml / build (pull_request)",
|
|
"org/src: ci.yaml / lint (pull_request)",
|
|
}, got)
|
|
})
|
|
|
|
t.Run("several rules union their enabled contexts, deduplicated; a disabled rule contributes none", func(t *testing.T) {
|
|
noSourceRepo := &repo_model.Repository{ID: consumer.ID, OwnerID: 99999} // no scoped sources, so only the rules' contexts gate
|
|
a := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"a/check", "shared/check"}}
|
|
b := &git_model.ProtectedBranch{EnableStatusCheck: true, StatusCheckContexts: []string{"b/check", "shared/check"}}
|
|
off := &git_model.ProtectedBranch{EnableStatusCheck: false, StatusCheckContexts: []string{"off/check"}}
|
|
got, err := EffectiveRequiredContexts(t.Context(), noSourceRepo, a, b, off)
|
|
require.NoError(t, err)
|
|
assert.ElementsMatch(t, []string{"a/check", "b/check", "shared/check"}, got)
|
|
})
|
|
}
|