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  }