Conversation
|
🤖 Finished Review · ✅ Success · Started 11:25 AM UTC · Completed 11:38 AM UTC |
PR Summary by QodoAdd Phase 2 cron poller to dispatch GitLab events into child pipelines
AI Description
Diagram
High-Level Assessment
Files changed (16)
|
Code Review by Qodo
1.
|
ReviewVerdict: Approve — two prior low-severity findings resolved; three remaining findings are low severity. This is a re-review (prior SHA:
No assertions were weakened — FindingsLow
Reviewed dimensions: correctness (opus), security (opus), intent & coherence, docs currency. Findings adjudicated against prior review provenance: app-verified. Delta: 1 file changed (convert_test.go), 2 test functions added/updated. Labels: PR implements GitLab cron-polling feature under internal/poll/ and internal/dispatch/ Previous runReviewVerdict: Approve — both medium-severity findings from the prior review are resolved; remaining findings are low severity. This is a re-review (prior SHA:
The implementation is well-structured: watermark-based polling with at-least-once delivery, label state diffing, deny-by-default fork detection, and fail-closed authorization ( FindingsLow
Reviewed dimensions: correctness (opus), security (opus), intent & coherence, style & conventions, docs currency. Findings adjudicated by challenger pass. Prior review provenance: app-verified. Labels: PR adds GitLab polling infrastructure under Labels: PR implements a new feature (GitLab cron polling); type/feature label aligns with feat() PR title prefix Previous run (2)ReviewVerdict: Comment — two medium-severity API contract findings worth addressing; none blocking. This is a re-review (prior SHA:
The implementation is well-structured: watermark-based polling with at-least-once delivery, label state diffing, deny-by-default fork detection, and fail-closed authorization ( FindingsMedium
Low
Reviewed dimensions: correctness (opus), security (opus), intent & coherence, style & conventions, docs currency. Findings adjudicated by challenger pass. Prior review provenance: app-verified. Labels: PR adds GitLab polling infrastructure under Previous run (3)ReviewVerdict: Approve — all prior findings resolved; remaining findings are low severity. This is a re-review (prior SHA:
The implementation is well-structured: watermark-based polling with at-least-once delivery, label state diffing, deny-by-default fork detection via FindingsLow
Reviewed dimensions: correctness (opus), security (opus), intent & coherence, style & conventions, docs currency. Findings adjudicated by challenger pass. Prior review provenance: app-verified. Labels: PR adds GitLab polling infrastructure under Previous run (4)ReviewVerdict: Approve — all prior findings resolved; remaining findings are low severity. This is a re-review (prior SHA:
The implementation is well-structured: watermark-based polling with at-least-once delivery, label state diffing, deny-by-default fork detection via FindingsLow
Reviewed dimensions: correctness (opus), security (opus), intent & coherence, style & conventions, docs currency. Findings adjudicated by challenger pass. Prior review provenance: app-verified. Labels: PR adds GitLab polling infrastructure under Previous run (5)ReviewVerdict: Comment — medium-severity findings worth noting; none blocking. This PR implements the Phase 2 cron poller for GitLab event dispatch per ADR 0067. The design is sound: watermark-based polling with at-least-once delivery, label state diffing, deny-by-default fork detection, and fail-closed authorization. The test suite is thorough (83 tests, 91.8% coverage). Three medium-severity findings are worth addressing. FindingsMedium
Low
Reviewed dimensions: correctness (opus), security (opus), intent & coherence, style & conventions, docs currency. Findings adjudicated by challenger pass. Labels: PR adds GitLab polling infrastructure under internal/poll/ and dispatch event types under internal/dispatch/ |
|
🤖 Review · ❌ Terminated · Started 3:43 PM UTC · Ended 3:58 PM UTC |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
|
🤖 Finished Review · ✅ Success · Started 3:43 PM UTC · Completed 3:58 PM UTC |
|
🤖 Finished Review · ✅ Success · Started 7:51 PM UTC · Completed 8:05 PM UTC |
4b59124 to
3af2476
Compare
|
🤖 Finished Review · ✅ Success · Started 11:54 PM UTC · Completed 12:09 AM UTC |
Implements the cron-based poller (ADR 0067 Phase 2) that discovers GitLab events via API polling, converts them to NormalizedEvents, routes through the dispatch core, and triggers child pipelines. New packages and files: - internal/dispatch/event.go: NormalizedEvent type and EventRouter interface - internal/poll/: complete poller with event discovery, bot filtering, deduplication, label state diffing, watermark management, and child pipeline YAML generation - internal/cli/poll.go: `fullsend poll` CLI command Test coverage: 89.8% on internal/poll/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: Greg Allen <gallen@redhat.com>
|
🤖 Finished Review · ✅ Success · Started 12:16 AM UTC · Completed 12:24 AM UTC |
waynesun09
left a comment
There was a problem hiding this comment.
Review squad pass (3 agents: Claude, Claude, Gemini) cross-referencing this PR against ADR-0067. Findings not already covered by the 31 existing review comments — posted inline (1 CRITICAL, 3 MEDIUM). YAML-injection mitigation and prompt-injection surface were independently verified as correctly implemented per the ADR and are not flagged.
| if !minSkippedAt.IsZero() && minSkippedAt.Before(maxUpdatedAt) { | ||
| maxUpdatedAt = minSkippedAt | ||
| } | ||
| newWatermark := maxUpdatedAt.Add(-30 * time.Second) |
There was a problem hiding this comment.
[CRITICAL] Watermark has a stationary fixed point that causes perpetual re-dispatch of the same event every poll cycle
Run() sets the new watermark as maxUpdatedAt.Add(-30 * time.Second), where maxUpdatedAt is the timestamp of the most recently processed event. On the next poll cycle, event discovery includes anything with CreatedAt/UpdatedAt not before the watermark (see events.go — note.CreatedAt.Before(since) and the MR-merge equivalent mr.MergedAt.After(since)). Because the persisted watermark is always exactly 30s before the last event's own timestamp, that same event's timestamp T always satisfies T >= T-30s, so it is re-discovered, re-converted, re-routed, and re-dispatched on every subsequent poll cycle — not just once. If no new activity occurs on the project after the last event, the watermark stays frozen at T-30s forever and the same slash command / MR-merge / MR-note event keeps re-triggering its agent stage on every single poll cycle (every 5–60 min per the ADR schedule) until a genuinely newer event more than 30s later arrives. deduplicate() only dedupes within a single Run() invocation (in-memory map) — there is no persisted set of already-processed event keys to prevent cross-cycle re-dispatch.
Suggestion: Persist a small set of recently-dispatched event keys (similar to the label-state CI variable) and skip re-dispatch for keys already recorded, independent of watermark position. Alternatively/additionally, advance the watermark using a value that isn't self-referential when idle (e.g., only subtract the 30s overlap from event timestamps strictly newer than the previous watermark, and otherwise leave the watermark at now() - overlap when no events were found). Add a test that calls Run() twice in succession with no new activity between calls and asserts the second call produces zero dispatches.
| if event.Type == "mr_note" || event.Type == "mr_event" { | ||
| cpState, err := p.buildChangeProposalState(ctx, event) | ||
| if err != nil { | ||
| log.Printf("WARNING: buildChangeProposalState for %s IID %d: %v", event.Type, event.IID, err) | ||
| } else { | ||
| ne.State.ChangeProposal = cpState | ||
| } | ||
| } |
There was a problem hiding this comment.
[MEDIUM] Fork-MR protection for unknown project IDs relies entirely on downstream (not-in-this-PR) behavior, with no explicit deny-by-default check or test here
The ADR states fork protection is "deny-by-default when unknown," and specifically calls out the fast-poll path as not fetching MRSource/MRTarget. When these are 0 (fast-poll mr_note events), buildChangeProposalState calls GetProjectPath(ctx, 0), which errors, leaving ne.State.ChangeProposal as nil (only a warning is logged) while the NormalizedEvent is still emitted and passed to router.Route(). Whether this actually blocks the fix/code stage depends entirely on the (not-in-this-PR) harness CEL trigger correctly treating a missing state.change_proposal as non-matching rather than erroring/matching by default. A prior review round flagged that no explicit fork-check (isForkMR) existed in the poll package itself; that helper has since been removed from the diff entirely rather than wired in, and there's no test in this PR exercising buildChangeProposalState with MRSource == 0 to assert the nil-ChangeProposal fail path.
Suggestion: Either add an explicit, testable deny-by-default check in the poll package (reinstate isForkMR and wire it into the stage-filtering path), or add a unit test asserting a zero-value MRSource/MRTarget event produces a nil ChangeProposal, plus a link to the specific CEL trigger test (in the dispatch-core PR) verifying a missing change_proposal is treated as non-match for fix/code stages — so the "deny-by-default" claim is verifiable rather than assumed.
| // EventRouter routes a NormalizedEvent to zero or more stage names. | ||
| type EventRouter interface { | ||
| Route(event *NormalizedEvent) ([]string, error) | ||
| } |
There was a problem hiding this comment.
[MEDIUM] premature-decision — "shared dispatch core, not duplicated" is structurally correct but currently unverifiable — no implementation exists anywhere in the repo yet
Poller correctly depends on the dispatch.EventRouter interface and calls p.router.Route(&normalizedEvent) rather than hardcoding any event→stage mapping itself (verified: no stage-name string literals appear anywhere in internal/poll/*.go). However, a repo-wide search confirms fullsend dispatch does not exist as a CLI command, and no implementation of ADR-0054 authorization or ADR-0061 CEL trigger evaluation exists in the codebase at all (pre- or post- this PR). internal/cli/poll.go:62 wires poll.New(nil, nil, ...) unconditionally — there is no existing dispatch path for this PR to reuse, so the ADR's claim that the dispatch core "is shared... not duplicated" can't actually be verified by this PR; it's an assumption about a component that doesn't exist yet. The only thing pinning the expected EventRouter contract is a hand-written stub in poll_test.go.
Suggestion: Not a blocker for this PR (correctly scoped as poll-driver-only), but track explicitly as a follow-up gate: when the real dispatch core lands, validate it against dispatch.EventRouter as defined here, including a contract/integration test that exercises Poller.Run() against the real router (not just fixtures), to confirm the "not duplicated" claim actually holds once both sides exist.
| IsBot: isProjectAccessTokenBot(evt.Author.Username), | ||
| Labels: []string{}, | ||
| }) | ||
| } | ||
| return events, nil | ||
| } | ||
|
|
||
| // isProjectAccessTokenBot detects GitLab project access token bot users | ||
| // by username pattern. | ||
| func isProjectAccessTokenBot(username string) bool { |
There was a problem hiding this comment.
[MEDIUM] premature-decision — bot detection via username heuristic instead of the API's authoritative Bot field
isProjectAccessTokenBot (used by the fast-poll path via the GitLab Events API) classifies an author as a bot by pattern-matching the username (strings.HasPrefix(username, "project_") && strings.Contains(username, "_bot_")), rather than using the Bot bool field already present on the same UserRef struct. Every other event-discovery path in this PR (discoverAllEvents, using the Notes API) uses the authoritative Bot field directly. There's no comment explaining why the Events API path can't do the same, which suggests this is an untested assumption about GitLab's Events API response shape. If the Events API does populate bot reliably, this heuristic is unnecessary and can misclassify a human user whose username happens to match the pattern (silently dropping their event), or fail to recognize a differently-named bot account.
Suggestion: Use the Bot field for consistency with the other paths, unless there's a verified/documented reason the Events API omits or misreports it for project access token users — in which case add a comment citing that GitLab behavior and a test asserting the fallback triggers only when Bot is false/absent.
Summary
internal/poll/package with event discovery, bot filtering, deduplication, label state diffing, watermark management, and child pipeline YAML generationinternal/dispatch/event.gowith NormalizedEvent type and EventRouter interfacefullsend pollCLI command (internal/cli/poll.go)Test plan
go test ./internal/poll/ -count=1)internal/poll/(target >85%)go build ./...cleango vetclean🤖 Generated with Claude Code