Go · 2917 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package checks
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9
10 "github.com/jackc/pgx/v5"
11 "github.com/jackc/pgx/v5/pgxpool"
12
13 checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc"
14 )
15
16 // GateInputs is the small struct the merge gate cares about.
17 type GateInputs struct {
18 RepoID int64
19 HeadSHA string
20 RequiredNames []string // from branch_protection_rules.status_checks_required
21 }
22
23 // GateResult mirrors the spec's blocked-with-reason vocabulary.
24 type GateResult struct {
25 Satisfied bool
26 Reason string // "" when Satisfied; otherwise human-readable cause
27 // Missing is the subset of RequiredNames that haven't yet succeeded
28 // or returned neutral on HeadSHA. Populated for UI tooltips.
29 Missing []string
30 }
31
32 // EvaluateRequiredChecks returns Satisfied=true when every required
33 // check name has a `success` or `neutral` conclusion on HeadSHA. A
34 // missing run, an in-progress run, or any other conclusion (failure /
35 // stale / cancelled / etc.) is unsatisfied.
36 //
37 // The evaluator is the same one consulted by `pulls.Mergeability` and
38 // `pulls.Merge` — composes with `review.Evaluate` at the call site.
39 func EvaluateRequiredChecks(ctx context.Context, pool *pgxpool.Pool, in GateInputs) (GateResult, error) {
40 if len(in.RequiredNames) == 0 {
41 return GateResult{Satisfied: true}, nil
42 }
43 if in.HeadSHA == "" {
44 // No head snapshot yet — block, since we can't verify any check
45 // passed against an unknown commit.
46 return GateResult{
47 Reason: "head SHA not yet recorded for this PR",
48 Missing: append([]string(nil), in.RequiredNames...),
49 }, nil
50 }
51 q := checksdb.New()
52 missing := []string{}
53 for _, name := range in.RequiredNames {
54 run, err := q.GetLatestCheckRunByName(ctx, pool, checksdb.GetLatestCheckRunByNameParams{
55 RepoID: in.RepoID,
56 HeadSha: in.HeadSHA,
57 Name: name,
58 })
59 if err != nil {
60 if errors.Is(err, pgx.ErrNoRows) {
61 missing = append(missing, name)
62 continue
63 }
64 return GateResult{}, fmt.Errorf("required-check lookup %q: %w", name, err)
65 }
66 if !runSatisfies(run) {
67 missing = append(missing, name)
68 }
69 }
70 if len(missing) == 0 {
71 return GateResult{Satisfied: true}, nil
72 }
73 reason := "Required check missing: " + missing[0]
74 if len(missing) > 1 {
75 reason = fmt.Sprintf("Required checks missing: %v", missing)
76 }
77 return GateResult{Reason: reason, Missing: missing}, nil
78 }
79
80 // runSatisfies reports whether a single run "passes" for the
81 // required-check gate. Matches the spec: `success` and `neutral` on
82 // the head SHA satisfy; anything else (including in_progress) does
83 // not.
84 func runSatisfies(r checksdb.CheckRun) bool {
85 if r.Status != checksdb.CheckStatusCompleted {
86 return false
87 }
88 if !r.Conclusion.Valid {
89 return false
90 }
91 switch r.Conclusion.CheckConclusion {
92 case checksdb.CheckConclusionSuccess, checksdb.CheckConclusionNeutral:
93 return true
94 }
95 return false
96 }
97