Go · 4080 bytes Raw Blame History
1 // SPDX-License-Identifier: AGPL-3.0-or-later
2
3 package checks
4
5 import (
6 "context"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "strings"
11 "time"
12
13 "github.com/jackc/pgx/v5"
14 "github.com/jackc/pgx/v5/pgtype"
15
16 checksdb "github.com/tenseleyFlow/shithub/internal/checks/sqlc"
17 )
18
19 // UpdateParams describes a check-run patch. Empty optional fields
20 // keep the existing value (rather than clearing); callers that want
21 // to clear a field set it to the zero string explicitly via the
22 // dedicated *Set boolean — keeps the JSON contract simple while
23 // avoiding pointer-everywhere fields.
24 //
25 // Status transitions are validated: queued/pending → in_progress →
26 // completed; completed requires a conclusion. Re-applying the same
27 // status is a no-op, which makes external-system retries safe.
28 type UpdateParams struct {
29 RunID int64
30 Status string
31 Conclusion string
32 StartedAt time.Time
33 CompletedAt time.Time
34 DetailsURL string
35 Output Output
36
37 HasStatus bool
38 HasConclusion bool
39 HasStartedAt bool
40 HasCompletedAt bool
41 HasDetailsURL bool
42 HasOutput bool
43 }
44
45 // Update patches a check run + recomputes its suite's rollup. Fields
46 // that aren't marked Has* keep their current value.
47 func Update(ctx context.Context, deps Deps, p UpdateParams) (checksdb.CheckRun, error) {
48 q := checksdb.New()
49 cur, err := q.GetCheckRun(ctx, deps.Pool, p.RunID)
50 if err != nil {
51 if errors.Is(err, pgx.ErrNoRows) {
52 return checksdb.CheckRun{}, ErrCheckRunNotFound
53 }
54 return checksdb.CheckRun{}, err
55 }
56
57 // Resolve effective values.
58 status := string(cur.Status)
59 if p.HasStatus {
60 s := strings.TrimSpace(p.Status)
61 if !validStatus(s) {
62 return checksdb.CheckRun{}, ErrInvalidStatus
63 }
64 status = s
65 }
66 conclusion := ""
67 if cur.Conclusion.Valid {
68 conclusion = string(cur.Conclusion.CheckConclusion)
69 }
70 if p.HasConclusion {
71 c := strings.TrimSpace(p.Conclusion)
72 if c != "" && !validConclusion(c) {
73 return checksdb.CheckRun{}, ErrInvalidConclusion
74 }
75 conclusion = c
76 }
77 if status == "completed" && conclusion == "" {
78 return checksdb.CheckRun{}, ErrCompletedNeedsConclusion
79 }
80 startedAt := cur.StartedAt
81 if p.HasStartedAt {
82 if p.StartedAt.IsZero() {
83 startedAt = pgtype.Timestamptz{}
84 } else {
85 startedAt = pgtype.Timestamptz{Time: p.StartedAt, Valid: true}
86 }
87 }
88 completedAt := cur.CompletedAt
89 if p.HasCompletedAt {
90 if p.CompletedAt.IsZero() {
91 completedAt = pgtype.Timestamptz{}
92 } else {
93 completedAt = pgtype.Timestamptz{Time: p.CompletedAt, Valid: true}
94 }
95 }
96 detailsURL := cur.DetailsUrl
97 if p.HasDetailsURL {
98 detailsURL = p.DetailsURL
99 }
100 outputBytes := cur.Output
101 if p.HasOutput {
102 if len(p.Output.Text) > MaxOutputTextBytes {
103 return checksdb.CheckRun{}, ErrOutputTextTooLarge
104 }
105 if len(p.Output.Summary) > MaxOutputSummaryBytes {
106 return checksdb.CheckRun{}, ErrOutputSummaryTooLarge
107 }
108 outputBytes, err = json.Marshal(p.Output)
109 if err != nil {
110 return checksdb.CheckRun{}, fmt.Errorf("output marshal: %w", err)
111 }
112 }
113
114 tx, err := deps.Pool.Begin(ctx)
115 if err != nil {
116 return checksdb.CheckRun{}, err
117 }
118 committed := false
119 defer func() {
120 if !committed {
121 _ = tx.Rollback(ctx)
122 }
123 }()
124
125 conclusionParam := checksdb.NullCheckConclusion{}
126 if conclusion != "" {
127 conclusionParam = checksdb.NullCheckConclusion{
128 CheckConclusion: checksdb.CheckConclusion(conclusion),
129 Valid: true,
130 }
131 }
132
133 if err := q.UpdateCheckRun(ctx, tx, checksdb.UpdateCheckRunParams{
134 ID: p.RunID,
135 Status: checksdb.CheckStatus(status),
136 Conclusion: conclusionParam,
137 StartedAt: startedAt,
138 CompletedAt: completedAt,
139 DetailsUrl: detailsURL,
140 Output: outputBytes,
141 }); err != nil {
142 return checksdb.CheckRun{}, fmt.Errorf("update run: %w", err)
143 }
144 if err := rollupSuiteInTx(ctx, tx, cur.SuiteID); err != nil {
145 return checksdb.CheckRun{}, err
146 }
147 if err := tx.Commit(ctx); err != nil {
148 return checksdb.CheckRun{}, err
149 }
150 committed = true
151
152 updated, err := q.GetCheckRun(ctx, deps.Pool, p.RunID)
153 if err != nil {
154 return checksdb.CheckRun{}, err
155 }
156 return updated, nil
157 }
158