@@ -16,11 +16,14 @@ import ( |
| 16 | 16 | "github.com/jackc/pgx/v5/pgtype" |
| 17 | 17 | "github.com/jackc/pgx/v5/pgxpool" |
| 18 | 18 | |
| 19 | + "github.com/tenseleyFlow/shithub/internal/checks" |
| 19 | 20 | "github.com/tenseleyFlow/shithub/internal/infra/storage" |
| 20 | 21 | pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc" |
| 21 | 22 | reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc" |
| 22 | 23 | "github.com/tenseleyFlow/shithub/internal/worker" |
| 23 | 24 | workerdb "github.com/tenseleyFlow/shithub/internal/worker/sqlc" |
| 25 | + |
| 26 | + "path/filepath" |
| 24 | 27 | ) |
| 25 | 28 | |
| 26 | 29 | // PushProcessDeps wires the data this handler needs. |
@@ -146,6 +149,31 @@ func PushProcess(deps PushProcessDeps) worker.Handler { |
| 146 | 149 | } |
| 147 | 150 | } |
| 148 | 151 | |
| 152 | + // 4c: stale-on-push for required checks (S24). When a head ref |
| 153 | + // moves and the matching protection rule has |
| 154 | + // `dismiss_stale_status_checks_on_push = true`, mark every |
| 155 | + // not-yet-completed check suite on the previous SHA as |
| 156 | + // (completed, conclusion='stale'). Best-effort. |
| 157 | + if strings.HasPrefix(event.Ref, refPrefix) && !isZeroSHA(event.BeforeSha) { |
| 158 | + branch := event.Ref[len(refPrefix):] |
| 159 | + rules, err := rq.ListBranchProtectionRules(ctx, deps.Pool, repo.ID) |
| 160 | + if err == nil { |
| 161 | + rule, hasRule := longestMatchingRule(rules, branch) |
| 162 | + if hasRule && rule.DismissStaleStatusChecksOnPush { |
| 163 | + n, err := checks.MarkStaleForPreviousHead(ctx, |
| 164 | + checks.Deps{Pool: deps.Pool, Logger: deps.Logger}, |
| 165 | + repo.ID, event.BeforeSha) |
| 166 | + if err != nil { |
| 167 | + deps.Logger.WarnContext(ctx, "push:process: mark stale checks", |
| 168 | + "push_event_id", event.ID, "error", err) |
| 169 | + } else if n > 0 { |
| 170 | + deps.Logger.InfoContext(ctx, "push:process: marked check suites stale", |
| 171 | + "push_event_id", event.ID, "count", n) |
| 172 | + } |
| 173 | + } |
| 174 | + } |
| 175 | + } |
| 176 | + |
| 149 | 177 | // 5: mark processed last so a partial failure earlier triggers a |
| 150 | 178 | // retry that retries the whole pipeline. Idempotency is via the |
| 151 | 179 | // processed_at guard at the top. |
@@ -159,6 +187,26 @@ func PushProcess(deps PushProcessDeps) worker.Handler { |
| 159 | 187 | } |
| 160 | 188 | } |
| 161 | 189 | |
| 190 | +// longestMatchingRule duplicates the longest-pattern-wins matcher |
| 191 | +// from internal/repos/protection so this file doesn't take a circular |
| 192 | +// import. Same semantics: alphabetical tiebreaker, no rule = not found. |
| 193 | +func longestMatchingRule(rules []reposdb.BranchProtectionRule, branch string) (reposdb.BranchProtectionRule, bool) { |
| 194 | + var best reposdb.BranchProtectionRule |
| 195 | + bestLen := -1 |
| 196 | + for _, r := range rules { |
| 197 | + ok, _ := filepath.Match(r.Pattern, branch) |
| 198 | + if !ok { |
| 199 | + continue |
| 200 | + } |
| 201 | + if len(r.Pattern) > bestLen || |
| 202 | + (len(r.Pattern) == bestLen && r.Pattern < best.Pattern) { |
| 203 | + best = r |
| 204 | + bestLen = len(r.Pattern) |
| 205 | + } |
| 206 | + } |
| 207 | + return best, bestLen >= 0 |
| 208 | +} |
| 209 | + |
| 162 | 210 | func isZeroSHA(s string) bool { |
| 163 | 211 | for _, c := range s { |
| 164 | 212 | if c != '0' { |