github.com/argoproj/argo-cd/v3@v3.2.1/applicationset/services/pull_request/azure_devops.go (about) 1 package pull_request 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "github.com/microsoft/azure-devops-go-api/azuredevops/v7" 9 "github.com/microsoft/azure-devops-go-api/azuredevops/v7/core" 10 "github.com/microsoft/azure-devops-go-api/azuredevops/v7/git" 11 ) 12 13 const ( 14 AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com" 15 AZURE_DEVOPS_PROJECT_NOT_FOUND_ERROR = "The following project does not exist" 16 ) 17 18 type AzureDevOpsClientFactory interface { 19 // Returns an Azure Devops Client interface. 20 GetClient(ctx context.Context) (git.Client, error) 21 } 22 23 type devopsFactoryImpl struct { 24 connection *azuredevops.Connection 25 } 26 27 func (factory *devopsFactoryImpl) GetClient(ctx context.Context) (git.Client, error) { 28 gitClient, err := git.NewClient(ctx, factory.connection) 29 if err != nil { 30 return nil, fmt.Errorf("failed to get new Azure DevOps git client for pull request generator: %w", err) 31 } 32 return gitClient, nil 33 } 34 35 type AzureDevOpsService struct { 36 clientFactory AzureDevOpsClientFactory 37 project string 38 repo string 39 labels []string 40 } 41 42 var ( 43 _ PullRequestService = (*AzureDevOpsService)(nil) 44 _ AzureDevOpsClientFactory = &devopsFactoryImpl{} 45 ) 46 47 func NewAzureDevOpsService(token, url, organization, project, repo string, labels []string) (PullRequestService, error) { 48 organizationURL := buildURL(url, organization) 49 50 var connection *azuredevops.Connection 51 if token == "" { 52 connection = azuredevops.NewAnonymousConnection(organizationURL) 53 } else { 54 connection = azuredevops.NewPatConnection(organizationURL, token) 55 } 56 57 return &AzureDevOpsService{ 58 clientFactory: &devopsFactoryImpl{connection: connection}, 59 project: project, 60 repo: repo, 61 labels: labels, 62 }, nil 63 } 64 65 func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) { 66 client, err := a.clientFactory.GetClient(ctx) 67 if err != nil { 68 return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err) 69 } 70 71 args := git.GetPullRequestsByProjectArgs{ 72 Project: &a.project, 73 SearchCriteria: &git.GitPullRequestSearchCriteria{}, 74 } 75 76 pullRequests := []*PullRequest{} 77 78 azurePullRequests, err := client.GetPullRequestsByProject(ctx, args) 79 if err != nil { 80 // A standard Http 404 error is not returned for Azure DevOps, 81 // so checking the error message for a specific pattern. 82 // NOTE: Since the repos are filtered later, only existence of the project 83 // is relevant for AzureDevOps 84 if strings.Contains(err.Error(), AZURE_DEVOPS_PROJECT_NOT_FOUND_ERROR) { 85 // return a custom error indicating that the repository is not found, 86 // but also return the empty result since the decision to continue or not in this case is made by the caller 87 return pullRequests, NewRepositoryNotFoundError(err) 88 } 89 return nil, fmt.Errorf("failed to get pull requests by project: %w", err) 90 } 91 92 for _, pr := range *azurePullRequests { 93 if pr.Repository == nil || 94 pr.Repository.Name == nil || 95 pr.PullRequestId == nil || 96 pr.SourceRefName == nil || 97 pr.TargetRefName == nil || 98 pr.LastMergeSourceCommit == nil || 99 pr.LastMergeSourceCommit.CommitId == nil { 100 continue 101 } 102 103 azureDevOpsLabels := convertLabels(pr.Labels) 104 if !containAzureDevOpsLabels(a.labels, azureDevOpsLabels) { 105 continue 106 } 107 108 if *pr.Repository.Name == a.repo { 109 pullRequests = append(pullRequests, &PullRequest{ 110 Number: *pr.PullRequestId, 111 Title: *pr.Title, 112 Branch: strings.Replace(*pr.SourceRefName, "refs/heads/", "", 1), 113 TargetBranch: strings.Replace(*pr.TargetRefName, "refs/heads/", "", 1), 114 HeadSHA: *pr.LastMergeSourceCommit.CommitId, 115 Labels: azureDevOpsLabels, 116 Author: strings.Split(*pr.CreatedBy.UniqueName, "@")[0], // Get the part before the @ in the email-address 117 }) 118 } 119 } 120 121 return pullRequests, nil 122 } 123 124 // convertLabels converts WebApiTagDefinitions to strings 125 func convertLabels(tags *[]core.WebApiTagDefinition) []string { 126 if tags == nil { 127 return []string{} 128 } 129 labelStrings := make([]string, len(*tags)) 130 for i, label := range *tags { 131 labelStrings[i] = *label.Name 132 } 133 return labelStrings 134 } 135 136 // containAzureDevOpsLabels returns true if gotLabels contains expectedLabels 137 func containAzureDevOpsLabels(expectedLabels []string, gotLabels []string) bool { 138 for _, expected := range expectedLabels { 139 found := false 140 for _, got := range gotLabels { 141 if expected == got { 142 found = true 143 break 144 } 145 } 146 if !found { 147 return false 148 } 149 } 150 return true 151 } 152 153 func buildURL(url, organization string) string { 154 if url == "" { 155 url = AZURE_DEVOPS_DEFAULT_URL 156 } 157 separator := "" 158 if !strings.HasSuffix(url, "/") { 159 separator = "/" 160 } 161 devOpsURL := fmt.Sprintf("%s%s%s", url, separator, organization) 162 return devOpsURL 163 }