github.com/diggerhq/digger/libs@v0.0.0-20240604170430-9d61cdf01cc5/orchestrator/github/github.go (about) 1 package github 2 3 import ( 4 "context" 5 "fmt" 6 "log" 7 "os" 8 "strings" 9 10 "github.com/diggerhq/digger/libs/digger_config" 11 orchestrator "github.com/diggerhq/digger/libs/orchestrator" 12 "github.com/dominikbraun/graph" 13 14 "github.com/google/go-github/v61/github" 15 ) 16 17 func NewGitHubService(ghToken string, repoName string, owner string) GithubService { 18 client := github.NewClient(nil) 19 if ghToken != "" { 20 client = client.WithAuthToken(ghToken) 21 } 22 23 return GithubService{ 24 Client: client, 25 RepoName: repoName, 26 Owner: owner, 27 } 28 } 29 30 type GithubService struct { 31 Client *github.Client 32 RepoName string 33 Owner string 34 } 35 36 func (svc GithubService) GetUserTeams(organisation string, user string) ([]string, error) { 37 teamsResponse, _, err := svc.Client.Teams.ListTeams(context.Background(), organisation, nil) 38 if err != nil { 39 return nil, fmt.Errorf("failed to list github teams: %v", err) 40 } 41 var teams []string 42 for _, team := range teamsResponse { 43 teamMembers, _, _ := svc.Client.Teams.ListTeamMembersBySlug(context.Background(), organisation, *team.Slug, nil) 44 for _, member := range teamMembers { 45 if *member.Login == user { 46 teams = append(teams, *team.Name) 47 break 48 } 49 } 50 } 51 52 return teams, nil 53 } 54 55 func (svc GithubService) GetChangedFiles(prNumber int) ([]string, error) { 56 var fileNames []string 57 opts := github.ListOptions{PerPage: 100} 58 for { 59 files, resp, err := svc.Client.PullRequests.ListFiles(context.Background(), svc.Owner, svc.RepoName, prNumber, &opts) 60 if err != nil { 61 log.Fatalf("error getting pull request files: %v", err) 62 } 63 64 for _, file := range files { 65 fileNames = append(fileNames, *file.Filename) 66 if file.PreviousFilename != nil { 67 fileNames = append(fileNames, *file.PreviousFilename) 68 } 69 } 70 if resp.NextPage == 0 { 71 break 72 } 73 opts.Page = resp.NextPage 74 } 75 return fileNames, nil 76 } 77 78 func (svc GithubService) GetChangedFilesForCommit(owner string, repo string, commitID string) ([]string, error) { 79 var fileNames []string 80 opts := github.ListOptions{PerPage: 100} 81 82 for { 83 commit, resp, err := svc.Client.Repositories.GetCommit(context.Background(), owner, repo, commitID, &opts) 84 if err != nil { 85 log.Fatalf("error getting commitfiles: %v", err) 86 } 87 for _, file := range commit.Files { 88 fileNames = append(fileNames, *file.Filename) 89 if file.PreviousFilename != nil { 90 fileNames = append(fileNames, *file.PreviousFilename) 91 } 92 } 93 94 if resp.NextPage == 0 { 95 break 96 } 97 opts.Page = resp.NextPage 98 } 99 return fileNames, nil 100 } 101 102 func (svc GithubService) ListIssues() ([]*orchestrator.Issue, error) { 103 allIssues := make([]*orchestrator.Issue, 0) 104 opts := &github.IssueListByRepoOptions{ 105 State: "open", 106 ListOptions: github.ListOptions{PerPage: 100}, 107 } 108 for { 109 issues, resp, err := svc.Client.Issues.ListByRepo(context.Background(), svc.Owner, svc.RepoName, opts) 110 if err != nil { 111 log.Fatalf("error getting pull request files: %v", err) 112 } 113 for _, issue := range issues { 114 if issue.PullRequestLinks != nil { 115 // this is an pull request, skip 116 continue 117 } 118 119 allIssues = append(allIssues, &orchestrator.Issue{ID: int64(*issue.Number), Title: *issue.Title, Body: *issue.Body}) 120 } 121 if resp.NextPage == 0 { 122 break 123 } 124 opts.Page = resp.NextPage 125 } 126 return allIssues, nil 127 } 128 129 func (svc GithubService) PublishIssue(title string, body string) (int64, error) { 130 githubissue, _, err := svc.Client.Issues.Create(context.Background(), svc.Owner, svc.RepoName, &github.IssueRequest{Title: &title, Body: &body}) 131 if err != nil { 132 return 0, fmt.Errorf("could not publish issue: %v", err) 133 } 134 return *githubissue.ID, err 135 } 136 137 func (svc GithubService) PublishComment(prNumber int, comment string) (*orchestrator.Comment, error) { 138 githubComment, _, err := svc.Client.Issues.CreateComment(context.Background(), svc.Owner, svc.RepoName, prNumber, &github.IssueComment{Body: &comment}) 139 if err != nil { 140 return nil, fmt.Errorf("could not publish comment to PR %v, %v", prNumber, err) 141 } 142 return &orchestrator.Comment{ 143 Id: *githubComment.ID, 144 Body: githubComment.Body, 145 Url: *githubComment.HTMLURL, 146 }, err 147 } 148 149 func (svc GithubService) GetComments(prNumber int) ([]orchestrator.Comment, error) { 150 comments, _, err := svc.Client.Issues.ListComments(context.Background(), svc.Owner, svc.RepoName, prNumber, &github.IssueListCommentsOptions{ListOptions: github.ListOptions{PerPage: 100}}) 151 commentBodies := make([]orchestrator.Comment, len(comments)) 152 for i, comment := range comments { 153 commentBodies[i] = orchestrator.Comment{ 154 Id: *comment.ID, 155 Body: comment.Body, 156 Url: *comment.HTMLURL, 157 } 158 } 159 return commentBodies, err 160 } 161 162 func (svc GithubService) GetApprovals(prNumber int) ([]string, error) { 163 reviews, _, err := svc.Client.PullRequests.ListReviews(context.Background(), svc.Owner, svc.RepoName, prNumber, &github.ListOptions{}) 164 approvals := make([]string, 0) 165 for _, review := range reviews { 166 if *review.State == "APPROVED" { 167 approvals = append(approvals, *review.User.Login) 168 } 169 } 170 return approvals, err 171 } 172 173 func (svc GithubService) EditComment(prNumber int, id interface{}, comment string) error { 174 commentId := id.(int64) 175 _, _, err := svc.Client.Issues.EditComment(context.Background(), svc.Owner, svc.RepoName, commentId, &github.IssueComment{Body: &comment}) 176 return err 177 } 178 179 type GithubCommentReaction string 180 181 const GithubCommentPlusOneReaction GithubCommentReaction = "+1" 182 const GithubCommentMinusOneReaction GithubCommentReaction = "-1" 183 const GithubCommentLaughReaction GithubCommentReaction = "laugh" 184 const GithubCommentConfusedReaction GithubCommentReaction = "confused" 185 const GithubCommentHeartReaction GithubCommentReaction = "heart" 186 const GithubCommentHoorayReaction GithubCommentReaction = "hooray" 187 const GithubCommentRocketReaction GithubCommentReaction = "rocket" 188 const GithubCommentEyesReaction GithubCommentReaction = "eyes" 189 190 func (svc GithubService) CreateCommentReaction(id interface{}, reaction string) error { 191 _, _, err := svc.Client.Reactions.CreateIssueCommentReaction(context.Background(), svc.Owner, svc.RepoName, id.(int64), reaction) 192 if err != nil { 193 log.Printf("could not addd reaction to comment: %v", err) 194 return fmt.Errorf("could not addd reaction to comment: %v", err) 195 } 196 return nil 197 } 198 199 func (svc GithubService) SetStatus(prNumber int, status string, statusContext string) error { 200 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 201 if err != nil { 202 log.Fatalf("error getting pull request: %v", err) 203 } 204 205 _, _, err = svc.Client.Repositories.CreateStatus(context.Background(), svc.Owner, svc.RepoName, *pr.Head.SHA, &github.RepoStatus{ 206 State: &status, 207 Context: &statusContext, 208 Description: &statusContext, 209 }) 210 return err 211 } 212 213 func (svc GithubService) GetCombinedPullRequestStatus(prNumber int) (string, error) { 214 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 215 if err != nil { 216 log.Fatalf("error getting pull request: %v", err) 217 } 218 219 statuses, _, err := svc.Client.Repositories.GetCombinedStatus(context.Background(), svc.Owner, svc.RepoName, pr.Head.GetSHA(), nil) 220 if err != nil { 221 log.Fatalf("error getting combined status: %v", err) 222 } 223 224 return *statuses.State, nil 225 } 226 227 func (svc GithubService) MergePullRequest(prNumber int) error { 228 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 229 if err != nil { 230 log.Fatalf("error getting pull request: %v", err) 231 } 232 233 _, _, err = svc.Client.PullRequests.Merge(context.Background(), svc.Owner, svc.RepoName, prNumber, "auto-merge", &github.PullRequestOptions{ 234 MergeMethod: "squash", 235 SHA: pr.Head.GetSHA(), 236 }) 237 return err 238 } 239 240 func isMergeableState(mergeableState string) bool { 241 // https://docs.github.com/en/github-ae@latest/graphql/reference/enums#mergestatestatus 242 mergeableStates := map[string]int{ 243 "clean": 0, 244 "unstable": 0, 245 "has_hooks": 1, 246 } 247 _, exists := mergeableStates[strings.ToLower(mergeableState)] 248 if !exists { 249 log.Printf("pr.GetMergeableState() returned: %v", mergeableState) 250 } 251 252 return exists 253 } 254 255 func (svc GithubService) IsMergeable(prNumber int) (bool, error) { 256 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 257 if err != nil { 258 log.Fatalf("error getting pull request: %v", err) 259 return false, err 260 } 261 return pr.GetMergeable() && isMergeableState(pr.GetMergeableState()), nil 262 } 263 264 func (svc GithubService) IsMerged(prNumber int) (bool, error) { 265 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 266 if err != nil { 267 log.Fatalf("error getting pull request: %v", err) 268 return false, err 269 } 270 return *pr.Merged, nil 271 } 272 273 func (svc GithubService) IsClosed(prNumber int) (bool, error) { 274 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 275 if err != nil { 276 log.Fatalf("error getting pull request: %v", err) 277 return false, err 278 } 279 280 return pr.GetState() == "closed", nil 281 } 282 283 func (svc GithubService) SetOutput(prNumber int, key string, value string) error { 284 gout := os.Getenv("GITHUB_ENV") 285 if gout == "" { 286 return fmt.Errorf("GITHUB_ENV not set, could not set the output in digger step") 287 } 288 f, err := os.OpenFile(gout, os.O_APPEND|os.O_WRONLY, 0644) 289 if err != nil { 290 return fmt.Errorf("could not open file for writing during digger step") 291 } 292 _, err = f.WriteString(fmt.Sprintf("%v=%v", key, value)) 293 if err != nil { 294 return fmt.Errorf("could not write digger file step") 295 } 296 f.Close() 297 return nil 298 } 299 300 func (svc GithubService) GetBranchName(prNumber int) (string, string, error) { 301 pr, _, err := svc.Client.PullRequests.Get(context.Background(), svc.Owner, svc.RepoName, prNumber) 302 if err != nil { 303 log.Fatalf("error getting pull request: %v", err) 304 return "", "", err 305 } 306 307 return pr.Head.GetRef(), pr.Head.GetSHA(), nil 308 } 309 310 func ConvertGithubPullRequestEventToJobs(payload *github.PullRequestEvent, impactedProjects []digger_config.Project, requestedProject *digger_config.Project, config digger_config.DiggerConfig) ([]orchestrator.Job, bool, error) { 311 workflows := config.Workflows 312 jobs := make([]orchestrator.Job, 0) 313 314 defaultBranch := *payload.Repo.DefaultBranch 315 prBranch := payload.PullRequest.Head.GetRef() 316 317 for _, project := range impactedProjects { 318 workflow, ok := workflows[project.Workflow] 319 if !ok { 320 return nil, false, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name) 321 } 322 323 runEnvVars := GetRunEnvVars(defaultBranch, prBranch, project.Name, project.Dir) 324 325 stateEnvVars, commandEnvVars := digger_config.CollectTerraformEnvConfig(workflow.EnvVars) 326 pullRequestNumber := payload.PullRequest.Number 327 328 StateEnvProvider, CommandEnvProvider := orchestrator.GetStateAndCommandProviders(project) 329 if *payload.Action == "closed" && *payload.PullRequest.Merged && *(payload.PullRequest.Base).Ref == *(payload.Repo).DefaultBranch { 330 jobs = append(jobs, orchestrator.Job{ 331 ProjectName: project.Name, 332 ProjectDir: project.Dir, 333 ProjectWorkspace: project.Workspace, 334 ProjectWorkflow: project.Workflow, 335 Terragrunt: project.Terragrunt, 336 Commands: workflow.Configuration.OnCommitToDefault, 337 ApplyStage: orchestrator.ToConfigStage(workflow.Apply), 338 PlanStage: orchestrator.ToConfigStage(workflow.Plan), 339 RunEnvVars: runEnvVars, 340 CommandEnvVars: commandEnvVars, 341 StateEnvVars: stateEnvVars, 342 PullRequestNumber: pullRequestNumber, 343 EventName: "pull_request", 344 Namespace: *payload.Repo.FullName, 345 RequestedBy: *payload.Sender.Login, 346 CommandEnvProvider: CommandEnvProvider, 347 StateEnvProvider: StateEnvProvider, 348 }) 349 } else if *payload.Action == "opened" || *payload.Action == "reopened" || *payload.Action == "synchronize" { 350 jobs = append(jobs, orchestrator.Job{ 351 ProjectName: project.Name, 352 ProjectDir: project.Dir, 353 ProjectWorkspace: project.Workspace, 354 ProjectWorkflow: project.Workflow, 355 Terragrunt: project.Terragrunt, 356 OpenTofu: project.OpenTofu, 357 Commands: workflow.Configuration.OnPullRequestPushed, 358 ApplyStage: orchestrator.ToConfigStage(workflow.Apply), 359 PlanStage: orchestrator.ToConfigStage(workflow.Plan), 360 RunEnvVars: runEnvVars, 361 CommandEnvVars: commandEnvVars, 362 StateEnvVars: stateEnvVars, 363 PullRequestNumber: pullRequestNumber, 364 EventName: "pull_request", 365 Namespace: *payload.Repo.FullName, 366 RequestedBy: *payload.Sender.Login, 367 CommandEnvProvider: CommandEnvProvider, 368 StateEnvProvider: StateEnvProvider, 369 }) 370 } else if *payload.Action == "closed" { 371 jobs = append(jobs, orchestrator.Job{ 372 ProjectName: project.Name, 373 ProjectDir: project.Dir, 374 ProjectWorkspace: project.Workspace, 375 ProjectWorkflow: project.Workflow, 376 Terragrunt: project.Terragrunt, 377 OpenTofu: project.OpenTofu, 378 Commands: workflow.Configuration.OnPullRequestClosed, 379 ApplyStage: orchestrator.ToConfigStage(workflow.Apply), 380 PlanStage: orchestrator.ToConfigStage(workflow.Plan), 381 RunEnvVars: runEnvVars, 382 CommandEnvVars: commandEnvVars, 383 StateEnvVars: stateEnvVars, 384 PullRequestNumber: pullRequestNumber, 385 EventName: "pull_request", 386 Namespace: *payload.Repo.FullName, 387 RequestedBy: *payload.Sender.Login, 388 CommandEnvProvider: CommandEnvProvider, 389 StateEnvProvider: StateEnvProvider, 390 }) 391 } else if *payload.Action == "converted_to_draft" { 392 var commands []string 393 if config.AllowDraftPRs == false && len(workflow.Configuration.OnPullRequestConvertedToDraft) == 0 { 394 commands = []string{"digger unlock"} 395 } else { 396 commands = workflow.Configuration.OnPullRequestConvertedToDraft 397 } 398 399 jobs = append(jobs, orchestrator.Job{ 400 ProjectName: project.Name, 401 ProjectDir: project.Dir, 402 ProjectWorkspace: project.Workspace, 403 ProjectWorkflow: project.Workflow, 404 Terragrunt: project.Terragrunt, 405 OpenTofu: project.OpenTofu, 406 Commands: commands, 407 ApplyStage: orchestrator.ToConfigStage(workflow.Apply), 408 PlanStage: orchestrator.ToConfigStage(workflow.Plan), 409 RunEnvVars: runEnvVars, 410 CommandEnvVars: commandEnvVars, 411 StateEnvVars: stateEnvVars, 412 PullRequestNumber: pullRequestNumber, 413 EventName: "pull_request_converted_to_draft", 414 Namespace: *payload.Repo.FullName, 415 RequestedBy: *payload.Sender.Login, 416 CommandEnvProvider: CommandEnvProvider, 417 StateEnvProvider: StateEnvProvider, 418 }) 419 } 420 421 } 422 return jobs, true, nil 423 } 424 425 func GetRunEnvVars(defaultBranch string, prBranch string, projectName string, projectDir string) map[string]string { 426 return map[string]string{ 427 "DEFAULT_BRANCH": defaultBranch, 428 "PR_BRANCH": prBranch, 429 "PROJECT_NAME": projectName, 430 "PROJECT_DIR": projectDir, 431 } 432 } 433 434 func ConvertGithubIssueCommentEventToJobs(payload *github.IssueCommentEvent, impactedProjects []digger_config.Project, requestedProject *digger_config.Project, workflows map[string]digger_config.Workflow, prBranchName string) ([]orchestrator.Job, bool, error) { 435 jobs := make([]orchestrator.Job, 0) 436 repoFullName := *payload.Repo.FullName 437 requestedBy := *payload.Sender.Login 438 issueNumber := *payload.Issue.Number 439 440 defaultBranch := *payload.Repo.DefaultBranch 441 prBranch := prBranchName 442 443 supportedCommands := []string{"digger plan", "digger apply", "digger unlock", "digger lock"} 444 445 coversAllImpactedProjects := true 446 447 runForProjects := impactedProjects 448 449 if requestedProject != nil { 450 if len(impactedProjects) > 1 { 451 coversAllImpactedProjects = false 452 runForProjects = []digger_config.Project{*requestedProject} 453 } else if len(impactedProjects) == 1 && impactedProjects[0].Name != requestedProject.Name { 454 return jobs, false, fmt.Errorf("requested project %v is not impacted by this PR", requestedProject.Name) 455 } 456 } 457 diggerCommand := strings.ToLower(*payload.Comment.Body) 458 diggerCommand = strings.TrimSpace(diggerCommand) 459 var commandToRun string 460 isSupportedCommand := false 461 for _, command := range supportedCommands { 462 if strings.HasPrefix(diggerCommand, command) { 463 isSupportedCommand = true 464 commandToRun = command 465 } 466 } 467 if !isSupportedCommand { 468 return nil, false, fmt.Errorf("command is not supported: %v", diggerCommand) 469 } 470 471 jobs, err := CreateJobsForProjects(runForProjects, commandToRun, "issue_comment", repoFullName, requestedBy, workflows, &issueNumber, nil, defaultBranch, prBranch) 472 if err != nil { 473 return nil, false, err 474 } 475 476 return jobs, coversAllImpactedProjects, nil 477 478 } 479 480 func CreateJobsForProjects(projects []digger_config.Project, command string, event string, repoFullName string, requestedBy string, workflows map[string]digger_config.Workflow, issueNumber *int, commitSha *string, defaultBranch string, prBranch string) ([]orchestrator.Job, error) { 481 jobs := make([]orchestrator.Job, 0) 482 483 for _, project := range projects { 484 workflow, ok := workflows[project.Workflow] 485 if !ok { 486 return nil, fmt.Errorf("failed to find workflow config '%s' for project '%s'", project.Workflow, project.Name) 487 } 488 489 runEnvVars := GetRunEnvVars(defaultBranch, prBranch, project.Name, project.Dir) 490 stateEnvVars, commandEnvVars := digger_config.CollectTerraformEnvConfig(workflow.EnvVars) 491 StateEnvProvider, CommandEnvProvider := orchestrator.GetStateAndCommandProviders(project) 492 workspace := project.Workspace 493 jobs = append(jobs, orchestrator.Job{ 494 ProjectName: project.Name, 495 ProjectDir: project.Dir, 496 ProjectWorkspace: workspace, 497 ProjectWorkflow: project.Workflow, 498 Terragrunt: project.Terragrunt, 499 OpenTofu: project.OpenTofu, 500 Commands: []string{command}, 501 ApplyStage: orchestrator.ToConfigStage(workflow.Apply), 502 PlanStage: orchestrator.ToConfigStage(workflow.Plan), 503 RunEnvVars: runEnvVars, 504 CommandEnvVars: commandEnvVars, 505 StateEnvVars: stateEnvVars, 506 PullRequestNumber: issueNumber, 507 EventName: event, //"issue_comment", 508 Namespace: repoFullName, 509 RequestedBy: requestedBy, 510 StateEnvProvider: StateEnvProvider, 511 CommandEnvProvider: CommandEnvProvider, 512 }) 513 } 514 return jobs, nil 515 } 516 517 func ProcessGitHubEvent(ghEvent interface{}, diggerConfig *digger_config.DiggerConfig, ciService orchestrator.PullRequestService) ([]digger_config.Project, *digger_config.Project, int, error) { 518 var impactedProjects []digger_config.Project 519 var prNumber int 520 521 switch event := ghEvent.(type) { 522 case github.PullRequestEvent: 523 prNumber = *event.GetPullRequest().Number 524 changedFiles, err := ciService.GetChangedFiles(prNumber) 525 526 if err != nil { 527 return nil, nil, 0, fmt.Errorf("could not get changed files") 528 } 529 530 impactedProjects, _ = diggerConfig.GetModifiedProjects(changedFiles) 531 case github.IssueCommentEvent: 532 prNumber = *event.GetIssue().Number 533 changedFiles, err := ciService.GetChangedFiles(prNumber) 534 535 if err != nil { 536 return nil, nil, 0, fmt.Errorf("could not get changed files") 537 } 538 539 impactedProjects, _ = diggerConfig.GetModifiedProjects(changedFiles) 540 requestedProject := orchestrator.ParseProjectName(*event.Comment.Body) 541 542 if requestedProject == "" { 543 return impactedProjects, nil, prNumber, nil 544 } 545 546 for _, project := range impactedProjects { 547 if project.Name == requestedProject { 548 return impactedProjects, &project, prNumber, nil 549 } 550 } 551 return nil, nil, 0, fmt.Errorf("requested project not found in modified projects") 552 case github.MergeGroupEvent: 553 return nil, nil, 0, UnhandledMergeGroupEventError 554 default: 555 return nil, nil, 0, fmt.Errorf("unsupported event type") 556 } 557 return impactedProjects, nil, prNumber, nil 558 } 559 560 func ProcessGitHubPullRequestEvent(payload *github.PullRequestEvent, diggerConfig *digger_config.DiggerConfig, dependencyGraph graph.Graph[string, digger_config.Project], ciService orchestrator.PullRequestService) ([]digger_config.Project, map[string]digger_config.ProjectToSourceMapping, int, error) { 561 var impactedProjects []digger_config.Project 562 var prNumber int 563 prNumber = *payload.PullRequest.Number 564 changedFiles, err := ciService.GetChangedFiles(prNumber) 565 566 if err != nil { 567 return nil, nil, prNumber, fmt.Errorf("could not get changed files") 568 } 569 impactedProjects, impactedProjectsSourceLocations := diggerConfig.GetModifiedProjects(changedFiles) 570 571 if diggerConfig.DependencyConfiguration.Mode == digger_config.DependencyConfigurationHard { 572 impactedProjects, err = FindAllProjectsDependantOnImpactedProjects(impactedProjects, dependencyGraph) 573 if err != nil { 574 return nil, nil, prNumber, fmt.Errorf("failed to find all projects dependant on impacted projects") 575 } 576 } 577 578 return impactedProjects, impactedProjectsSourceLocations, prNumber, nil 579 } 580 581 func FindAllProjectsDependantOnImpactedProjects(impactedProjects []digger_config.Project, dependencyGraph graph.Graph[string, digger_config.Project]) ([]digger_config.Project, error) { 582 impactedProjectsMap := make(map[string]digger_config.Project) 583 for _, project := range impactedProjects { 584 impactedProjectsMap[project.Name] = project 585 } 586 visited := make(map[string]bool) 587 predecessorMap, err := dependencyGraph.PredecessorMap() 588 if err != nil { 589 return nil, fmt.Errorf("failed to get predecessor map") 590 } 591 impactedProjectsWithDependantProjects := make([]digger_config.Project, 0) 592 for currentNode := range predecessorMap { 593 // find all roots of the graph 594 if len(predecessorMap[currentNode]) == 0 { 595 err := graph.BFS(dependencyGraph, currentNode, func(node string) bool { 596 currentProject, err := dependencyGraph.Vertex(node) 597 if err != nil { 598 return true 599 } 600 if _, ok := visited[node]; ok { 601 return true 602 } 603 // add a project if it was impacted 604 if _, ok := impactedProjectsMap[node]; ok { 605 impactedProjectsWithDependantProjects = append(impactedProjectsWithDependantProjects, currentProject) 606 visited[node] = true 607 return false 608 } else { 609 // if a project was not impacted, check if it has a parent that was impacted and add it to the map of impacted projects 610 for parent := range predecessorMap[node] { 611 if _, ok := impactedProjectsMap[parent]; ok { 612 impactedProjectsWithDependantProjects = append(impactedProjectsWithDependantProjects, currentProject) 613 impactedProjectsMap[node] = currentProject 614 visited[node] = true 615 return false 616 } 617 } 618 } 619 return true 620 }) 621 if err != nil { 622 return nil, err 623 } 624 } 625 } 626 return impactedProjectsWithDependantProjects, nil 627 } 628 629 func ProcessGitHubPushEvent(payload *github.PushEvent, diggerConfig *digger_config.DiggerConfig, dependencyGraph graph.Graph[string, digger_config.Project], ciService orchestrator.PullRequestService) ([]digger_config.Project, map[string]digger_config.ProjectToSourceMapping, *digger_config.Project, int, error) { 630 var impactedProjects []digger_config.Project 631 var prNumber int 632 633 commitId := *payload.After 634 owner := *payload.Repo.Owner.Login 635 repo := *payload.Repo.Name 636 637 // TODO: Refactor to make generic interface 638 changedFiles, err := ciService.(*GithubService).GetChangedFilesForCommit(owner, repo, commitId) 639 if err != nil { 640 return nil, nil, nil, 0, fmt.Errorf("could not get changed files") 641 } 642 643 impactedProjects, impactedProjectsSourceMapping := diggerConfig.GetModifiedProjects(changedFiles) 644 return impactedProjects, impactedProjectsSourceMapping, nil, prNumber, nil 645 } 646 647 func ProcessGitHubIssueCommentEvent(payload *github.IssueCommentEvent, diggerConfig *digger_config.DiggerConfig, dependencyGraph graph.Graph[string, digger_config.Project], ciService orchestrator.PullRequestService) ([]digger_config.Project, map[string]digger_config.ProjectToSourceMapping, *digger_config.Project, int, error) { 648 var impactedProjects []digger_config.Project 649 var prNumber int 650 651 prNumber = *payload.Issue.Number 652 changedFiles, err := ciService.GetChangedFiles(prNumber) 653 654 if err != nil { 655 return nil, nil, nil, 0, fmt.Errorf("could not get changed files") 656 } 657 658 impactedProjects, impactedProjectsSourceMapping := diggerConfig.GetModifiedProjects(changedFiles) 659 660 if diggerConfig.DependencyConfiguration.Mode == digger_config.DependencyConfigurationHard { 661 impactedProjects, err = FindAllProjectsDependantOnImpactedProjects(impactedProjects, dependencyGraph) 662 if err != nil { 663 return nil, nil, nil, prNumber, fmt.Errorf("failed to find all projects dependant on impacted projects") 664 } 665 } 666 667 requestedProject := orchestrator.ParseProjectName(*payload.Comment.Body) 668 669 if requestedProject == "" { 670 return impactedProjects, impactedProjectsSourceMapping, nil, prNumber, nil 671 } 672 673 for _, project := range impactedProjects { 674 if project.Name == requestedProject { 675 return impactedProjects, impactedProjectsSourceMapping, &project, prNumber, nil 676 } 677 } 678 return nil, nil, nil, 0, fmt.Errorf("requested project not found in modified projects") 679 } 680 681 func issueCommentEventContainsComment(event interface{}, comment string) bool { 682 switch event.(type) { 683 case github.IssueCommentEvent: 684 event := event.(github.IssueCommentEvent) 685 if strings.Contains(*event.Comment.Body, comment) { 686 return true 687 } 688 } 689 return false 690 } 691 692 func CheckIfHelpComment(event interface{}) bool { 693 return issueCommentEventContainsComment(event, "digger help") 694 } 695 696 func CheckIfShowProjectsComment(event interface{}) bool { 697 return issueCommentEventContainsComment(event, "digger show-projects") 698 }