tenseleyflow/shithub / ad6dfd8

Browse files

M: audit-log uniformity + renderBodyHTML helper for issues/PRs/reviews

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
ad6dfd840e0c28f3553a8e8d8b0a45e004cb296b
Parents
edda2d6
Tree
8197f23

7 changed files

StatusFile+-
M internal/auth/audit/audit.go 9 2
M internal/issues/issues.go 39 2
M internal/pulls/pulls.go 24 18
M internal/pulls/review/comment.go 2 3
M internal/pulls/review/review.go 16 0
M internal/pulls/review/submit.go 1 2
M internal/pulls/state.go 10 1
internal/auth/audit/audit.gomodified
@@ -60,14 +60,21 @@ const (
60
 	ActionRepoTransferDeclined   Action = "repo_transfer_declined"
60
 	ActionRepoTransferDeclined   Action = "repo_transfer_declined"
61
 	ActionRepoTransferCanceled   Action = "repo_transfer_canceled"
61
 	ActionRepoTransferCanceled   Action = "repo_transfer_canceled"
62
 	ActionRepoTransferExpired    Action = "repo_transfer_expired"
62
 	ActionRepoTransferExpired    Action = "repo_transfer_expired"
63
+	ActionIssueStateChanged      Action = "issue_state_changed"
64
+	ActionIssueLockChanged       Action = "issue_lock_changed"
65
+	ActionIssueCommentCreated    Action = "issue_comment_created"
66
+	ActionPullStateChanged       Action = "pull_state_changed"
67
+	ActionPullMerged             Action = "pull_merged"
63
 )
68
 )
64
 
69
 
65
 // Target is a typed target-type constant.
70
 // Target is a typed target-type constant.
66
 type Target string
71
 type Target string
67
 
72
 
68
 const (
73
 const (
69
-	TargetUser Target = "user"
74
+	TargetUser  Target = "user"
70
-	TargetRepo Target = "repo"
75
+	TargetRepo  Target = "repo"
76
+	TargetIssue Target = "issue"
77
+	TargetPull  Target = "pull"
71
 )
78
 )
72
 
79
 
73
 // Recorder writes audit rows. Bound to the sqlc queries handle.
80
 // Recorder writes audit rows. Bound to the sqlc queries handle.
internal/issues/issues.gomodified
@@ -21,6 +21,7 @@ import (
21
 	"github.com/jackc/pgx/v5/pgtype"
21
 	"github.com/jackc/pgx/v5/pgtype"
22
 	"github.com/jackc/pgx/v5/pgxpool"
22
 	"github.com/jackc/pgx/v5/pgxpool"
23
 
23
 
24
+	"github.com/tenseleyFlow/shithub/internal/auth/audit"
24
 	"github.com/tenseleyFlow/shithub/internal/auth/throttle"
25
 	"github.com/tenseleyFlow/shithub/internal/auth/throttle"
25
 	issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
26
 	issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
26
 	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
27
 	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
@@ -33,6 +34,12 @@ type Deps struct {
33
 	Pool    *pgxpool.Pool
34
 	Pool    *pgxpool.Pool
34
 	Limiter *throttle.Limiter
35
 	Limiter *throttle.Limiter
35
 	Logger  *slog.Logger
36
 	Logger  *slog.Logger
37
+	// Audit is optional; when non-nil, state-changing orchestrator
38
+	// calls (SetState, SetLock, AddComment) record an audit row. The
39
+	// repo lifecycle package writes audit rows directly via deps.Audit;
40
+	// this field ensures issues/PR mutations are equally traceable
41
+	// (S00-S25 audit, M).
42
+	Audit *audit.Recorder
36
 }
43
 }
37
 
44
 
38
 // Errors returned by the orchestrator. Handlers map these to status
45
 // Errors returned by the orchestrator. Handlers map these to status
@@ -117,7 +124,7 @@ func Create(ctx context.Context, deps Deps, p CreateParams) (issuesdb.Issue, err
117
 	}
124
 	}
118
 
125
 
119
 	// Render markdown for the cached body html.
126
 	// Render markdown for the cached body html.
120
-	html, _ := mdrender.RenderHTML([]byte(p.Body))
127
+	html := renderBodyHTML(ctx, deps, p.Body)
121
 	row.BodyHtmlCached = pgtype.Text{String: html, Valid: html != ""}
128
 	row.BodyHtmlCached = pgtype.Text{String: html, Valid: html != ""}
122
 
129
 
123
 	if err := q.UpdateIssueTitleBody(ctx, tx, issuesdb.UpdateIssueTitleBodyParams{
130
 	if err := q.UpdateIssueTitleBody(ctx, tx, issuesdb.UpdateIssueTitleBodyParams{
@@ -178,7 +185,7 @@ func AddComment(ctx context.Context, deps Deps, p CommentCreateParams) (issuesdb
178
 		return issuesdb.IssueComment{}, ErrIssueLocked
185
 		return issuesdb.IssueComment{}, ErrIssueLocked
179
 	}
186
 	}
180
 
187
 
181
-	html, _ := mdrender.RenderHTML([]byte(body))
188
+	html := renderBodyHTML(ctx, deps, body)
182
 
189
 
183
 	tx, err := deps.Pool.Begin(ctx)
190
 	tx, err := deps.Pool.Begin(ctx)
184
 	if err != nil {
191
 	if err != nil {
@@ -209,6 +216,11 @@ func AddComment(ctx context.Context, deps Deps, p CommentCreateParams) (issuesdb
209
 		return issuesdb.IssueComment{}, err
216
 		return issuesdb.IssueComment{}, err
210
 	}
217
 	}
211
 	committed = true
218
 	committed = true
219
+	if deps.Audit != nil {
220
+		_ = deps.Audit.Record(ctx, deps.Pool, p.AuthorUserID,
221
+			audit.ActionIssueCommentCreated, audit.TargetIssue, p.IssueID,
222
+			map[string]any{"comment_id": c.ID})
223
+	}
212
 	return c, nil
224
 	return c, nil
213
 }
225
 }
214
 
226
 
@@ -262,6 +274,11 @@ func SetState(ctx context.Context, deps Deps, actorUserID, issueID int64, newSta
262
 		return err
274
 		return err
263
 	}
275
 	}
264
 	committed = true
276
 	committed = true
277
+	if deps.Audit != nil {
278
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
279
+			audit.ActionIssueStateChanged, audit.TargetIssue, issueID,
280
+			map[string]any{"state": newState, "reason": reason})
281
+	}
265
 	return nil
282
 	return nil
266
 }
283
 }
267
 
284
 
@@ -308,5 +325,25 @@ func SetLock(ctx context.Context, deps Deps, actorUserID, issueID int64, locked
308
 		return err
325
 		return err
309
 	}
326
 	}
310
 	committed = true
327
 	committed = true
328
+	if deps.Audit != nil {
329
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
330
+			audit.ActionIssueLockChanged, audit.TargetIssue, issueID,
331
+			map[string]any{"locked": locked, "reason": reason})
332
+	}
311
 	return nil
333
 	return nil
312
 }
334
 }
335
+
336
+// renderBodyHTML wraps markdown.RenderHTML with a logger-aware error
337
+// path. Body length is bounded upstream (orchestrator validation +
338
+// DB CHECK at 65535), so ErrInputTooLarge is structurally impossible
339
+// here — but if it ever fires, log loudly: it means a precondition
340
+// somewhere upstream regressed. The audit (S00-S25, M) flagged the
341
+// `_`-discard pattern as the kind of slop where a real bug could hide.
342
+func renderBodyHTML(ctx context.Context, deps Deps, body string) string {
343
+	html, err := mdrender.RenderHTML([]byte(body))
344
+	if err != nil && deps.Logger != nil {
345
+		deps.Logger.WarnContext(ctx, "issues: markdown render failed",
346
+			"error", err, "body_bytes", len(body))
347
+	}
348
+	return html
349
+}
internal/pulls/pulls.gomodified
@@ -21,20 +21,21 @@ import (
21
 	"errors"
21
 	"errors"
22
 	"fmt"
22
 	"fmt"
23
 	"log/slog"
23
 	"log/slog"
24
-	"path/filepath"
25
 	"strings"
24
 	"strings"
26
 
25
 
27
 	"github.com/jackc/pgx/v5"
26
 	"github.com/jackc/pgx/v5"
28
 	"github.com/jackc/pgx/v5/pgtype"
27
 	"github.com/jackc/pgx/v5/pgtype"
29
 	"github.com/jackc/pgx/v5/pgxpool"
28
 	"github.com/jackc/pgx/v5/pgxpool"
30
 
29
 
30
+	"github.com/tenseleyFlow/shithub/internal/auth/audit"
31
 	"github.com/tenseleyFlow/shithub/internal/checks"
31
 	"github.com/tenseleyFlow/shithub/internal/checks"
32
 	"github.com/tenseleyFlow/shithub/internal/issues"
32
 	"github.com/tenseleyFlow/shithub/internal/issues"
33
 	issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
33
 	issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
34
+	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
34
 	"github.com/tenseleyFlow/shithub/internal/pulls/review"
35
 	"github.com/tenseleyFlow/shithub/internal/pulls/review"
35
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
36
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
36
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
37
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
37
-	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
38
+	"github.com/tenseleyFlow/shithub/internal/repos/protection"
38
 	reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
39
 	reposdb "github.com/tenseleyFlow/shithub/internal/repos/sqlc"
39
 )
40
 )
40
 
41
 
@@ -42,6 +43,10 @@ import (
42
 type Deps struct {
43
 type Deps struct {
43
 	Pool   *pgxpool.Pool
44
 	Pool   *pgxpool.Pool
44
 	Logger *slog.Logger
45
 	Logger *slog.Logger
46
+	// Audit is optional; when non-nil, Merge and SetState write audit
47
+	// rows. Mirrors the issues orchestrator's contract so PR-side
48
+	// state changes are equally traceable (S00-S25 audit, M).
49
+	Audit *audit.Recorder
45
 }
50
 }
46
 
51
 
47
 // Errors surfaced to handlers.
52
 // Errors surfaced to handlers.
@@ -372,23 +377,11 @@ func loadRequiredCheckNames(ctx context.Context, pool *pgxpool.Pool, repoID int6
372
 	if err != nil {
377
 	if err != nil {
373
 		return nil, err
378
 		return nil, err
374
 	}
379
 	}
375
-	var best reposdb.BranchProtectionRule
380
+	rule, ok := protection.MatchLongestRule(rules, baseRef)
376
-	bestLen := -1
381
+	if !ok {
377
-	for _, r := range rules {
378
-		ok, _ := filepath.Match(r.Pattern, baseRef)
379
-		if !ok {
380
-			continue
381
-		}
382
-		if len(r.Pattern) > bestLen ||
383
-			(len(r.Pattern) == bestLen && r.Pattern < best.Pattern) {
384
-			best = r
385
-			bestLen = len(r.Pattern)
386
-		}
387
-	}
388
-	if bestLen < 0 {
389
 		return []string{}, nil
382
 		return []string{}, nil
390
 	}
383
 	}
391
-	return best.StatusChecksRequired, nil
384
+	return rule.StatusChecksRequired, nil
392
 }
385
 }
393
 
386
 
394
 // int64FromPg unwraps a pgtype.Int8; returns 0 when invalid.
387
 // int64FromPg unwraps a pgtype.Int8; returns 0 when invalid.
@@ -412,7 +405,7 @@ func EditPR(ctx context.Context, deps Deps, prID int64, title, body string) erro
412
 	if len(body) > 65535 {
405
 	if len(body) > 65535 {
413
 		return issues.ErrBodyTooLong
406
 		return issues.ErrBodyTooLong
414
 	}
407
 	}
415
-	html, _ := mdrender.RenderHTML([]byte(body))
408
+	html := renderBodyHTML(ctx, deps, body)
416
 	q := issuesdb.New()
409
 	q := issuesdb.New()
417
 	return q.UpdateIssueTitleBody(ctx, deps.Pool, issuesdb.UpdateIssueTitleBodyParams{
410
 	return q.UpdateIssueTitleBody(ctx, deps.Pool, issuesdb.UpdateIssueTitleBodyParams{
418
 		ID:             prID,
411
 		ID:             prID,
@@ -468,3 +461,16 @@ func AllowedMethod(repo reposdb.Repo, method string) bool {
468
 	}
461
 	}
469
 	return false
462
 	return false
470
 }
463
 }
464
+
465
+// renderBodyHTML wraps markdown.RenderHTML with a logger-aware error
466
+// path. PR body length is bounded upstream at 65535 chars by the
467
+// orchestrator; markdown caps at 1 MiB. ErrInputTooLarge here means
468
+// a precondition regressed — log loudly. (S00-S25 audit, M.)
469
+func renderBodyHTML(ctx context.Context, deps Deps, body string) string {
470
+	html, err := mdrender.RenderHTML([]byte(body))
471
+	if err != nil && deps.Logger != nil {
472
+		deps.Logger.WarnContext(ctx, "pulls: markdown render failed",
473
+			"error", err, "body_bytes", len(body))
474
+	}
475
+	return html
476
+}
internal/pulls/review/comment.gomodified
@@ -12,7 +12,6 @@ import (
12
 	"github.com/jackc/pgx/v5/pgtype"
12
 	"github.com/jackc/pgx/v5/pgtype"
13
 
13
 
14
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
14
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
15
-	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
16
 )
15
 )
17
 
16
 
18
 // CommentParams describes a single inline comment on a PR. When
17
 // CommentParams describes a single inline comment on a PR. When
@@ -76,7 +75,7 @@ func AddComment(ctx context.Context, deps Deps, p CommentParams) (pullsdb.PrRevi
76
 		}
75
 		}
77
 	}
76
 	}
78
 
77
 
79
-	html, _ := mdrender.RenderHTML([]byte(body))
78
+	html := renderBodyHTML(ctx, deps, body)
80
 
79
 
81
 	cur := pgtype.Int4{}
80
 	cur := pgtype.Int4{}
82
 	if p.CurrentPosition >= 0 {
81
 	if p.CurrentPosition >= 0 {
@@ -115,7 +114,7 @@ func EditComment(ctx context.Context, deps Deps, commentID int64, body string) e
115
 	if len(body) > 65535 {
114
 	if len(body) > 65535 {
116
 		return ErrBodyTooLong
115
 		return ErrBodyTooLong
117
 	}
116
 	}
118
-	html, _ := mdrender.RenderHTML([]byte(body))
117
+	html := renderBodyHTML(ctx, deps, body)
119
 	return pullsdb.New().UpdatePRReviewCommentBody(ctx, deps.Pool, pullsdb.UpdatePRReviewCommentBodyParams{
118
 	return pullsdb.New().UpdatePRReviewCommentBody(ctx, deps.Pool, pullsdb.UpdatePRReviewCommentBodyParams{
120
 		ID: commentID, Body: body, BodyHtmlCached: pgtype.Text{String: html, Valid: html != ""},
119
 		ID: commentID, Body: body, BodyHtmlCached: pgtype.Text{String: html, Valid: html != ""},
121
 	})
120
 	})
internal/pulls/review/review.gomodified
@@ -15,10 +15,13 @@
15
 package review
15
 package review
16
 
16
 
17
 import (
17
 import (
18
+	"context"
18
 	"errors"
19
 	"errors"
19
 	"log/slog"
20
 	"log/slog"
20
 
21
 
21
 	"github.com/jackc/pgx/v5/pgxpool"
22
 	"github.com/jackc/pgx/v5/pgxpool"
23
+
24
+	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
22
 )
25
 )
23
 
26
 
24
 // Deps wires this package into the runtime.
27
 // Deps wires this package into the runtime.
@@ -44,3 +47,16 @@ var (
44
 // MaxReviewersPerPR caps active review requests per PR. Matches the
47
 // MaxReviewersPerPR caps active review requests per PR. Matches the
45
 // spec pitfall section.
48
 // spec pitfall section.
46
 const MaxReviewersPerPR = 20
49
 const MaxReviewersPerPR = 20
50
+
51
+// renderBodyHTML wraps markdown.RenderHTML with a logger-aware error
52
+// path. Review/comment body length is bounded upstream at 65535 chars
53
+// by the orchestrator. ErrInputTooLarge here means a precondition
54
+// regressed — log loudly. (S00-S25 audit, M.)
55
+func renderBodyHTML(ctx context.Context, deps Deps, body string) string {
56
+	html, err := mdrender.RenderHTML([]byte(body))
57
+	if err != nil && deps.Logger != nil {
58
+		deps.Logger.WarnContext(ctx, "review: markdown render failed",
59
+			"error", err, "body_bytes", len(body))
60
+	}
61
+	return html
62
+}
internal/pulls/review/submit.gomodified
@@ -13,7 +13,6 @@ import (
13
 
13
 
14
 	issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
14
 	issuesdb "github.com/tenseleyFlow/shithub/internal/issues/sqlc"
15
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
15
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
16
-	mdrender "github.com/tenseleyFlow/shithub/internal/markdown"
17
 )
16
 )
18
 
17
 
19
 // SubmitParams describes the submit-a-review action.
18
 // SubmitParams describes the submit-a-review action.
@@ -44,7 +43,7 @@ func Submit(ctx context.Context, deps Deps, p SubmitParams) (pullsdb.PrReview, e
44
 	if len(body) > 65535 {
43
 	if len(body) > 65535 {
45
 		return pullsdb.PrReview{}, ErrBodyTooLong
44
 		return pullsdb.PrReview{}, ErrBodyTooLong
46
 	}
45
 	}
47
-	html, _ := mdrender.RenderHTML([]byte(body))
46
+	html := renderBodyHTML(ctx, deps, body)
48
 
47
 
49
 	tx, err := deps.Pool.Begin(ctx)
48
 	tx, err := deps.Pool.Begin(ctx)
50
 	if err != nil {
49
 	if err != nil {
internal/pulls/state.gomodified
@@ -6,6 +6,7 @@ import (
6
 	"context"
6
 	"context"
7
 	"errors"
7
 	"errors"
8
 
8
 
9
+	"github.com/tenseleyFlow/shithub/internal/auth/audit"
9
 	"github.com/tenseleyFlow/shithub/internal/issues"
10
 	"github.com/tenseleyFlow/shithub/internal/issues"
10
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
11
 	pullsdb "github.com/tenseleyFlow/shithub/internal/pulls/sqlc"
11
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
12
 	repogit "github.com/tenseleyFlow/shithub/internal/repos/git"
@@ -43,5 +44,13 @@ func SetState(ctx context.Context, deps Deps, gitDir string, actorUserID, prID i
43
 			return err
44
 			return err
44
 		}
45
 		}
45
 	}
46
 	}
46
-	return issues.SetState(ctx, issues.Deps{Pool: deps.Pool, Logger: deps.Logger}, actorUserID, prID, newState, "")
47
+	if err := issues.SetState(ctx, issues.Deps{Pool: deps.Pool, Logger: deps.Logger}, actorUserID, prID, newState, ""); err != nil {
48
+		return err
49
+	}
50
+	if deps.Audit != nil {
51
+		_ = deps.Audit.Record(ctx, deps.Pool, actorUserID,
52
+			audit.ActionPullStateChanged, audit.TargetPull, prID,
53
+			map[string]any{"state": newState})
54
+	}
55
+	return nil
47
 }
56
 }