diff --git a/internal/engine/commit_evaluator.go b/internal/engine/commit_evaluator.go new file mode 100644 index 0000000000..4ff9bb288b --- /dev/null +++ b/internal/engine/commit_evaluator.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: Copyright 2026 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package engine + +// Commit contains commit-level data that can be evaluated by policies. +type Commit struct { + SHA string + Message string +} + +// CommitEvaluation captures the result of evaluating one policy. +type CommitEvaluation struct { + Policy string + Passed bool + Reason string +} + +// CommitResult captures all evaluations for a single commit. +type CommitResult struct { + SHA string + Evaluations []CommitEvaluation +} + +// CommitPolicy defines the minimal contract for demo-oriented commit-level policies. +// Concrete implementations should live outside internal/engine. +type CommitPolicy interface { + Name() string + Evaluate(commit Commit) CommitEvaluation +} + +// CommitEvaluator evaluates commits against a list of commit-level policies. +type CommitEvaluator struct { + policies []CommitPolicy +} + +// NewCommitEvaluator creates a commit evaluator with the provided policies. +func NewCommitEvaluator(policies ...CommitPolicy) *CommitEvaluator { + return &CommitEvaluator{policies: policies} +} + +// Evaluate evaluates the commit against all configured policies. +func (e *CommitEvaluator) Evaluate(commit Commit) []CommitEvaluation { + if len(e.policies) == 0 { + return []CommitEvaluation{} + } + + results := make([]CommitEvaluation, 0, len(e.policies)) + for _, policy := range e.policies { + res := policy.Evaluate(commit) + if res.Policy == "" { + res.Policy = policy.Name() + } + results = append(results, res) + } + + return results +} + +// EvaluateAll evaluates all commits in a PR, preserving commit order. +func (e *CommitEvaluator) EvaluateAll(commits []Commit) []CommitResult { + results := make([]CommitResult, 0, len(commits)) + + for _, commit := range commits { + results = append(results, CommitResult{ + SHA: commit.SHA, + Evaluations: e.Evaluate(commit), + }) + } + + return results +} + +// HasPolicyFailures returns true if any policy evaluation fails. +func HasPolicyFailures(results []CommitResult) bool { + for _, commitResult := range results { + for _, eval := range commitResult.Evaluations { + if !eval.Passed { + return true + } + } + } + + return false +} + +// HasFailures returns true if any policy evaluation fails. +// Deprecated: use HasPolicyFailures for clearer semantics. +func HasFailures(results []CommitResult) bool { + return HasPolicyFailures(results) +} diff --git a/internal/engine/commit_evaluator_test.go b/internal/engine/commit_evaluator_test.go new file mode 100644 index 0000000000..dc3dc37f34 --- /dev/null +++ b/internal/engine/commit_evaluator_test.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: Copyright 2026 The Minder Authors +// SPDX-License-Identifier: Apache-2.0 + +package engine + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type alwaysPassPolicy struct{} +type alwaysFailPolicy struct{} + +func (alwaysPassPolicy) Name() string { + return "always_pass" +} + +func (p alwaysPassPolicy) Evaluate(_ Commit) CommitEvaluation { + return CommitEvaluation{ + Policy: p.Name(), + Passed: true, + Reason: "policy passed", + } +} + +func (alwaysFailPolicy) Name() string { + return "always_fail" +} + +func (p alwaysFailPolicy) Evaluate(_ Commit) CommitEvaluation { + return CommitEvaluation{ + Policy: p.Name(), + Passed: false, + Reason: "policy failed", + } +} + +func TestCommitEvaluatorEvaluateWithoutPolicies(t *testing.T) { + t.Parallel() + + evaluator := NewCommitEvaluator() + + results := evaluator.Evaluate(Commit{Message: "feat: add evaluator"}) + require.NotNil(t, results) + require.Empty(t, results) +} + +func TestCommitEvaluatorEvaluateWithPolicy(t *testing.T) { + t.Parallel() + + evaluator := NewCommitEvaluator(alwaysPassPolicy{}) + + results := evaluator.Evaluate(Commit{Message: "anything"}) + require.Len(t, results, 1) + require.Equal(t, "always_pass", results[0].Policy) + require.True(t, results[0].Passed) + require.Equal(t, "policy passed", results[0].Reason) +} + +func TestCommitEvaluatorEvaluateAll(t *testing.T) { + t.Parallel() + + evaluator := NewCommitEvaluator(alwaysPassPolicy{}) + + results := evaluator.EvaluateAll([]Commit{ + {SHA: "sha1", Message: "feat: one"}, + {SHA: "sha2", Message: "fix: two"}, + }) + + require.Len(t, results, 2) + require.Equal(t, "sha1", results[0].SHA) + require.Equal(t, "sha2", results[1].SHA) + require.Len(t, results[0].Evaluations, 1) + require.True(t, results[0].Evaluations[0].Passed) +} + +func TestEvaluateAllEmpty(t *testing.T) { + t.Parallel() + + evaluator := NewCommitEvaluator(alwaysPassPolicy{}) + results := evaluator.EvaluateAll(nil) + require.Empty(t, results) +} + +func TestHasPolicyFailures(t *testing.T) { + t.Parallel() + + allPass := []CommitResult{ + {SHA: "sha1", Evaluations: []CommitEvaluation{{Policy: "p1", Passed: true}}}, + {SHA: "sha2", Evaluations: []CommitEvaluation{{Policy: "p1", Passed: true}}}, + } + require.False(t, HasPolicyFailures(allPass)) + require.False(t, HasFailures(allPass)) + + hasFail := []CommitResult{ + {SHA: "sha1", Evaluations: []CommitEvaluation{{Policy: "p1", Passed: true}}}, + {SHA: "sha2", Evaluations: []CommitEvaluation{{Policy: "p1", Passed: false}}}, + } + require.True(t, HasPolicyFailures(hasFail)) + require.True(t, HasFailures(hasFail)) + + evaluator := NewCommitEvaluator(alwaysPassPolicy{}, alwaysFailPolicy{}) + require.True(t, HasPolicyFailures(evaluator.EvaluateAll([]Commit{{SHA: "sha3", Message: "any"}}))) +}