Files
kandev/apps/backend/internal/github/service_pr_feedback_sync.go
T
2026-09-10 23:39:56 +01:00

67 lines
2.8 KiB
Go

package github
import (
"context"
"go.uber.org/zap"
)
// persistPRFeedbackState reconciles the workspace's github_task_prs rows for a
// PR from the live data a feedback fetch just retrieved.
//
// The feedback read is the same server-side GitHub fetch the status sync makes
// — GetPR + ListPRReviews + ListCheckRuns, plus comments — so the authoritative
// PR state is already in hand here. It used to be shipped to the browser and
// re-derived there (useSyncLivePRState in pr-detail-panel.tsx), which patched
// only the Zustand copy: opening a PR's detail panel turned that one tab purple
// while the kanban card, the boot payload and every other surface kept reading
// state=open from the stale row, and a reload snapped it back. Writing through
// SyncTaskPR here makes the backend the single writer and lets the resulting
// github.task_pr.updated event converge every surface at once.
//
// Best-effort by contract: this hangs off a read endpoint, so every failure is
// logged at Debug and swallowed. A dead store must never turn a working PR
// panel into an error.
//
// Scoping: rows are looked up inside workspaceID only, matching the credential
// scope the caller already passed ensureRepositoryInWorkspaceScope with. The
// legacy workspace-less GetPRFeedback entry point passes "" and is skipped
// rather than being allowed to write across workspaces.
func (s *Service) persistPRFeedbackState(ctx context.Context, workspaceID string, feedback *PRFeedback) {
if s == nil || s.store == nil || workspaceID == "" {
return
}
if feedback == nil || feedback.PR == nil || feedback.PR.Number == 0 {
return
}
pr := feedback.PR
rows, err := s.store.ListTaskPRsByPRNumber(ctx, workspaceID, pr.RepoOwner, pr.RepoName, pr.Number)
if err != nil {
s.logger.Debug("list task PRs for feedback persist failed",
zap.String("workspace_id", workspaceID), zap.String("owner", pr.RepoOwner),
zap.String("repo", pr.RepoName), zap.Int("pr_number", pr.Number), zap.Error(err))
return
}
if len(rows) == 0 {
return
}
status := newPRStatusWithWorkflow(pr, feedback.Reviews, feedback.Checks, feedback.WorkflowAttention)
// Several tasks in a workspace can link the same PR; each owns its own row.
// SyncTaskPR re-narrows by (task, owner, repo, number) internally.
seen := make(map[string]struct{}, len(rows))
for _, tp := range rows {
if tp == nil || tp.TaskID == "" {
continue
}
effectiveTaskID := s.reconcileTaskPROwnership(ctx, "", tp.TaskID, tp.RepositoryID, pr.Number)
if _, dup := seen[effectiveTaskID]; dup {
continue
}
seen[effectiveTaskID] = struct{}{}
if syncErr := s.SyncTaskPR(ctx, effectiveTaskID, status); syncErr != nil {
s.logger.Debug("task PR sync from PR feedback failed",
zap.String("task_id", effectiveTaskID), zap.Int("pr_number", pr.Number), zap.Error(syncErr))
}
}
}