| 1 | // SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | |
| 3 | package checks |
| 4 | |
| 5 | import ( |
| 6 | "context" |
| 7 | |
| 8 | "github.com/jackc/pgx/v5" |
| 9 | |
| 10 | checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc" |
| 11 | ) |
| 12 | |
| 13 | // rollupSuiteInTx recomputes the suite's status + conclusion from its |
| 14 | // runs and persists the result. Called from Create and Update inside |
| 15 | // the same tx so the API response reflects the latest derived state. |
| 16 | // |
| 17 | // Per spec design table: |
| 18 | // |
| 19 | // status = 'completed' iff every run is completed |
| 20 | // conclusion priority (when status='completed'): |
| 21 | // failure > timed_out > cancelled > action_required > |
| 22 | // success > neutral > skipped > stale |
| 23 | // |
| 24 | // The "first failure-class wins" ordering matches GitHub's roll-up. |
| 25 | // Any non-completed run forces status to 'in_progress' (or keeps the |
| 26 | // existing 'queued' if every run is queued — we treat queued+completed |
| 27 | // as 'in_progress' too, since *something* moved). |
| 28 | func rollupSuiteInTx(ctx context.Context, tx pgx.Tx, suiteID int64) error { |
| 29 | q := checksdb.New() |
| 30 | runs, err := q.ListCheckRunsBySuite(ctx, tx, suiteID) |
| 31 | if err != nil { |
| 32 | return err |
| 33 | } |
| 34 | status, conclusion := DeriveSuiteRollup(runs) |
| 35 | conclusionParam := checksdb.NullCheckConclusion{} |
| 36 | if conclusion != "" { |
| 37 | conclusionParam = checksdb.NullCheckConclusion{ |
| 38 | CheckConclusion: checksdb.CheckConclusion(conclusion), |
| 39 | Valid: true, |
| 40 | } |
| 41 | } |
| 42 | return q.UpdateCheckSuiteRollup(ctx, tx, checksdb.UpdateCheckSuiteRollupParams{ |
| 43 | ID: suiteID, |
| 44 | Status: checksdb.CheckStatus(status), |
| 45 | Conclusion: conclusionParam, |
| 46 | }) |
| 47 | } |
| 48 | |
| 49 | // DeriveSuiteRollup is the pure-function form of the rollup so tests |
| 50 | // can exercise the priority order without a DB. Public so the API |
| 51 | // layer can preview the rollup if needed. |
| 52 | func DeriveSuiteRollup(runs []checksdb.CheckRun) (status, conclusion string) { |
| 53 | if len(runs) == 0 { |
| 54 | return "queued", "" |
| 55 | } |
| 56 | allCompleted := true |
| 57 | anyMoved := false |
| 58 | for _, r := range runs { |
| 59 | if r.Status != checksdb.CheckStatusCompleted { |
| 60 | allCompleted = false |
| 61 | } |
| 62 | if r.Status != checksdb.CheckStatusQueued { |
| 63 | anyMoved = true |
| 64 | } |
| 65 | } |
| 66 | if !allCompleted { |
| 67 | if anyMoved { |
| 68 | return "in_progress", "" |
| 69 | } |
| 70 | return "queued", "" |
| 71 | } |
| 72 | // Every run completed → derive aggregate conclusion. |
| 73 | priority := []string{ |
| 74 | "failure", "timed_out", "cancelled", "action_required", |
| 75 | "success", "neutral", "skipped", "stale", |
| 76 | } |
| 77 | rank := map[string]int{} |
| 78 | for i, p := range priority { |
| 79 | rank[p] = i |
| 80 | } |
| 81 | bestIdx := -1 |
| 82 | for _, r := range runs { |
| 83 | if !r.Conclusion.Valid { |
| 84 | continue |
| 85 | } |
| 86 | c := string(r.Conclusion.CheckConclusion) |
| 87 | idx, ok := rank[c] |
| 88 | if !ok { |
| 89 | continue |
| 90 | } |
| 91 | if bestIdx < 0 || idx < bestIdx { |
| 92 | bestIdx = idx |
| 93 | } |
| 94 | } |
| 95 | if bestIdx < 0 { |
| 96 | return "completed", "neutral" |
| 97 | } |
| 98 | return "completed", priority[bestIdx] |
| 99 | } |
| 100 |