Files
kandev/apps/backend/internal/github/service_comparison_target.go

131 lines
4.7 KiB
Go

package github
import (
"context"
"fmt"
"strings"
"go.uber.org/zap"
taskmodels "github.com/kandev/kandev/internal/task/models"
)
// ComparisonTargetObserver is the narrow provider-to-task boundary for
// comparison reconciliation. GitHub owns authoritative PR identity; the task
// service owns attachment matching and durable metadata.
type ComparisonTargetObserver interface {
ReconcileComparisonTarget(ctx context.Context, taskID string, candidate taskmodels.ComparisonTargetCandidate) (*taskmodels.ComparisonTargetReconciliation, error)
ReconcileComparisonTargetFromSync(ctx context.Context, taskID string, candidate taskmodels.ComparisonTargetCandidate) (*taskmodels.ComparisonTargetReconciliation, error)
RemoveComparisonTargetForChange(ctx context.Context, taskID, repositoryID, provider, kind string, number int) error
}
// SetComparisonTargetObserver wires the task-service reconciler after both
// services are constructed, avoiding a package import cycle.
func (s *Service) SetComparisonTargetObserver(observer ComparisonTargetObserver) {
s.comparisonTargetObserver = observer
}
func (s *Service) reconcileComparisonTarget(ctx context.Context, taskID string, pr *PR) {
s.reconcileComparisonTargetMode(ctx, taskID, pr, true)
}
func (s *Service) reconcileComparisonTargetFromSync(ctx context.Context, taskID string, pr *PR) {
s.reconcileComparisonTargetMode(ctx, taskID, pr, false)
}
func (s *Service) reconcileComparisonTargetMode(ctx context.Context, taskID string, pr *PR, allowReplacement bool) {
if s.comparisonTargetObserver == nil || strings.TrimSpace(taskID) == "" {
return
}
candidate, ok := githubComparisonTargetCandidate(pr)
if !ok {
if s.logger != nil {
s.logger.Debug("comparison target reconciliation skipped: PR identity incomplete",
zap.String("task_id", taskID),
zap.Int("pr_number", prNumber(pr)))
}
return
}
var result *taskmodels.ComparisonTargetReconciliation
var err error
if allowReplacement {
result, err = s.comparisonTargetObserver.ReconcileComparisonTarget(ctx, taskID, candidate)
} else {
result, err = s.comparisonTargetObserver.ReconcileComparisonTargetFromSync(ctx, taskID, candidate)
}
if err != nil {
if s.logger != nil {
s.logger.Warn("comparison target reconciliation failed",
zap.String("task_id", taskID),
zap.Int("pr_number", candidate.Number),
zap.Error(err))
}
return
}
if s.logger != nil && result != nil {
s.logger.Info("comparison target reconciliation completed",
zap.String("task_id", taskID),
zap.Int("pr_number", candidate.Number),
zap.String("outcome", string(result.Status)),
zap.String("target", displayComparisonTarget(candidate)))
}
}
func githubComparisonTargetCandidate(pr *PR) (taskmodels.ComparisonTargetCandidate, bool) {
if pr == nil || pr.Number <= 0 {
return taskmodels.ComparisonTargetCandidate{}, false
}
headOwner := strings.TrimSpace(pr.HeadRepoOwner)
headName := strings.TrimSpace(pr.HeadRepoName)
targetOwner := strings.TrimSpace(pr.BaseRepoOwner)
targetName := strings.TrimSpace(pr.BaseRepoName)
if targetOwner == "" {
targetOwner = strings.TrimSpace(pr.RepoOwner)
}
if targetName == "" {
targetName = strings.TrimSpace(pr.RepoName)
}
if !validGitHubPathPart(headOwner) || !validGitHubPathPart(headName) ||
!validGitHubPathPart(targetOwner) || !validGitHubPathPart(targetName) {
return taskmodels.ComparisonTargetCandidate{}, false
}
headBranch, err := taskmodels.NormalizeComparisonBranch(strings.TrimSpace(pr.HeadBranch))
if err != nil {
return taskmodels.ComparisonTargetCandidate{}, false
}
targetBranch, err := taskmodels.NormalizeComparisonBranch(strings.TrimSpace(pr.BaseBranch))
if err != nil {
return taskmodels.ComparisonTargetCandidate{}, false
}
return taskmodels.ComparisonTargetCandidate{
Provider: taskmodels.ComparisonTargetProviderGitHub,
Kind: taskmodels.ComparisonTargetKindPullRequest,
Number: pr.Number,
HeadBranch: headBranch,
TargetBranch: targetBranch,
HeadRepository: taskmodels.ComparisonTargetRepository{
Host: "github.com",
Path: headOwner + "/" + headName,
ProviderID: githubHeadProviderID(pr),
RemoteURL: fmt.Sprintf("https://github.com/%s/%s.git", headOwner, headName),
},
TargetRepository: taskmodels.ComparisonTargetRepository{
Host: "github.com",
Path: targetOwner + "/" + targetName,
ProviderID: positiveID(pr.BaseRepoID),
RemoteURL: fmt.Sprintf("https://github.com/%s/%s.git", targetOwner, targetName),
},
}, true
}
func displayComparisonTarget(candidate taskmodels.ComparisonTargetCandidate) string {
return candidate.TargetRepository.Path + ":" + candidate.TargetBranch
}
func prNumber(pr *PR) int {
if pr == nil {
return 0
}
return pr.Number
}