-
Notifications
You must be signed in to change notification settings - Fork 5.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(appset): add Pull Request Generator for Azure DevOps Repos (#13367)
Signed-off-by: Robin Lieb <robin.j.lieb@gmail.com> Signed-off-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com> Co-authored-by: Michael Crenshaw <350466+crenshaw-dev@users.noreply.github.com>
- Loading branch information
1 parent
6424cde
commit 46d4609
Showing
15 changed files
with
2,094 additions
and
710 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
package pull_request | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strings" | ||
|
||
"github.com/microsoft/azure-devops-go-api/azuredevops" | ||
core "github.com/microsoft/azure-devops-go-api/azuredevops/core" | ||
git "github.com/microsoft/azure-devops-go-api/azuredevops/git" | ||
) | ||
|
||
const AZURE_DEVOPS_DEFAULT_URL = "https://dev.azure.com" | ||
|
||
type AzureDevOpsClientFactory interface { | ||
// Returns an Azure Devops Client interface. | ||
GetClient(ctx context.Context) (git.Client, error) | ||
} | ||
|
||
type devopsFactoryImpl struct { | ||
connection *azuredevops.Connection | ||
} | ||
|
||
func (factory *devopsFactoryImpl) GetClient(ctx context.Context) (git.Client, error) { | ||
gitClient, err := git.NewClient(ctx, factory.connection) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get new Azure DevOps git client for pull request generator: %w", err) | ||
} | ||
return gitClient, nil | ||
} | ||
|
||
type AzureDevOpsService struct { | ||
clientFactory AzureDevOpsClientFactory | ||
project string | ||
repo string | ||
labels []string | ||
} | ||
|
||
var _ PullRequestService = (*AzureDevOpsService)(nil) | ||
var _ AzureDevOpsClientFactory = &devopsFactoryImpl{} | ||
|
||
func NewAzureDevOpsService(ctx context.Context, token, url, organization, project, repo string, labels []string) (PullRequestService, error) { | ||
organizationUrl := buildURL(url, organization) | ||
|
||
var connection *azuredevops.Connection | ||
if token == "" { | ||
connection = azuredevops.NewAnonymousConnection(organizationUrl) | ||
} else { | ||
connection = azuredevops.NewPatConnection(organizationUrl, token) | ||
} | ||
|
||
return &AzureDevOpsService{ | ||
clientFactory: &devopsFactoryImpl{connection: connection}, | ||
project: project, | ||
repo: repo, | ||
labels: labels, | ||
}, nil | ||
} | ||
|
||
func (a *AzureDevOpsService) List(ctx context.Context) ([]*PullRequest, error) { | ||
client, err := a.clientFactory.GetClient(ctx) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get Azure DevOps client: %w", err) | ||
} | ||
|
||
args := git.GetPullRequestsByProjectArgs{ | ||
Project: &a.project, | ||
SearchCriteria: &git.GitPullRequestSearchCriteria{}, | ||
} | ||
|
||
azurePullRequests, err := client.GetPullRequestsByProject(ctx, args) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get pull requests by project: %w", err) | ||
} | ||
|
||
pullRequests := []*PullRequest{} | ||
|
||
for _, pr := range *azurePullRequests { | ||
if pr.Repository == nil || | ||
pr.Repository.Name == nil || | ||
pr.PullRequestId == nil || | ||
pr.SourceRefName == nil || | ||
pr.LastMergeSourceCommit == nil || | ||
pr.LastMergeSourceCommit.CommitId == nil { | ||
continue | ||
} | ||
|
||
azureDevOpsLabels := convertLabels(pr.Labels) | ||
if !containAzureDevOpsLabels(a.labels, azureDevOpsLabels) { | ||
continue | ||
} | ||
|
||
if *pr.Repository.Name == a.repo { | ||
pullRequests = append(pullRequests, &PullRequest{ | ||
Number: *pr.PullRequestId, | ||
Branch: strings.Replace(*pr.SourceRefName, "refs/heads/", "", 1), | ||
HeadSHA: *pr.LastMergeSourceCommit.CommitId, | ||
Labels: azureDevOpsLabels, | ||
}) | ||
} | ||
} | ||
|
||
return pullRequests, nil | ||
} | ||
|
||
// convertLabels converts WebApiTagDefinitions to strings | ||
func convertLabels(tags *[]core.WebApiTagDefinition) []string { | ||
if tags == nil { | ||
return []string{} | ||
} | ||
labelStrings := make([]string, len(*tags)) | ||
for i, label := range *tags { | ||
labelStrings[i] = *label.Name | ||
} | ||
return labelStrings | ||
} | ||
|
||
// containAzureDevOpsLabels returns true if gotLabels contains expectedLabels | ||
func containAzureDevOpsLabels(expectedLabels []string, gotLabels []string) bool { | ||
for _, expected := range expectedLabels { | ||
found := false | ||
for _, got := range gotLabels { | ||
if expected == got { | ||
found = true | ||
break | ||
} | ||
} | ||
if !found { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
|
||
func buildURL(url, organization string) string { | ||
if url == "" { | ||
url = AZURE_DEVOPS_DEFAULT_URL | ||
} | ||
separator := "" | ||
if !strings.HasSuffix(url, "/") { | ||
separator = "/" | ||
} | ||
devOpsURL := fmt.Sprintf("%s%s%s", url, separator, organization) | ||
return devOpsURL | ||
} |
221 changes: 221 additions & 0 deletions
221
applicationset/services/pull_request/azure_devops_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,221 @@ | ||
package pull_request | ||
|
||
import ( | ||
"context" | ||
"testing" | ||
|
||
"github.com/microsoft/azure-devops-go-api/azuredevops/core" | ||
git "github.com/microsoft/azure-devops-go-api/azuredevops/git" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/mock" | ||
|
||
azureMock "github.com/argoproj/argo-cd/v2/applicationset/services/scm_provider/azure_devops/git/mocks" | ||
) | ||
|
||
func createBoolPtr(x bool) *bool { | ||
return &x | ||
} | ||
|
||
func createStringPtr(x string) *string { | ||
return &x | ||
} | ||
|
||
func createIntPtr(x int) *int { | ||
return &x | ||
} | ||
|
||
func createLabelsPtr(x []core.WebApiTagDefinition) *[]core.WebApiTagDefinition { | ||
return &x | ||
} | ||
|
||
type AzureClientFactoryMock struct { | ||
mock *mock.Mock | ||
} | ||
|
||
func (m *AzureClientFactoryMock) GetClient(ctx context.Context) (git.Client, error) { | ||
args := m.mock.Called(ctx) | ||
|
||
var client git.Client | ||
c := args.Get(0) | ||
if c != nil { | ||
client = c.(git.Client) | ||
} | ||
|
||
var err error | ||
if len(args) > 1 { | ||
if e, ok := args.Get(1).(error); ok { | ||
err = e | ||
} | ||
} | ||
|
||
return client, err | ||
} | ||
|
||
func TestListPullRequest(t *testing.T) { | ||
teamProject := "myorg_project" | ||
repoName := "myorg_project_repo" | ||
pr_id := 123 | ||
pr_head_sha := "cd4973d9d14a08ffe6b641a89a68891d6aac8056" | ||
ctx := context.Background() | ||
|
||
pullRequestMock := []git.GitPullRequest{ | ||
{ | ||
PullRequestId: createIntPtr(pr_id), | ||
SourceRefName: createStringPtr("refs/heads/feature-branch"), | ||
LastMergeSourceCommit: &git.GitCommitRef{ | ||
CommitId: createStringPtr(pr_head_sha), | ||
}, | ||
Labels: &[]core.WebApiTagDefinition{}, | ||
Repository: &git.GitRepository{ | ||
Name: createStringPtr(repoName), | ||
}, | ||
}, | ||
} | ||
|
||
args := git.GetPullRequestsByProjectArgs{ | ||
Project: &teamProject, | ||
SearchCriteria: &git.GitPullRequestSearchCriteria{}, | ||
} | ||
|
||
gitClientMock := azureMock.Client{} | ||
clientFactoryMock := &AzureClientFactoryMock{mock: &mock.Mock{}} | ||
clientFactoryMock.mock.On("GetClient", mock.Anything).Return(&gitClientMock, nil) | ||
gitClientMock.On("GetPullRequestsByProject", ctx, args).Return(&pullRequestMock, nil) | ||
|
||
provider := AzureDevOpsService{ | ||
clientFactory: clientFactoryMock, | ||
project: teamProject, | ||
repo: repoName, | ||
labels: nil, | ||
} | ||
|
||
list, err := provider.List(ctx) | ||
assert.NoError(t, err) | ||
assert.Equal(t, 1, len(list)) | ||
assert.Equal(t, "feature-branch", list[0].Branch) | ||
assert.Equal(t, pr_head_sha, list[0].HeadSHA) | ||
assert.Equal(t, pr_id, list[0].Number) | ||
} | ||
|
||
func TestConvertLabes(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
gotLabels *[]core.WebApiTagDefinition | ||
expectedLabels []string | ||
}{ | ||
{ | ||
name: "empty labels", | ||
gotLabels: createLabelsPtr([]core.WebApiTagDefinition{}), | ||
expectedLabels: []string{}, | ||
}, | ||
{ | ||
name: "nil labels", | ||
gotLabels: createLabelsPtr(nil), | ||
expectedLabels: []string{}, | ||
}, | ||
{ | ||
name: "one label", | ||
gotLabels: createLabelsPtr([]core.WebApiTagDefinition{ | ||
{Name: createStringPtr("label1"), Active: createBoolPtr(true)}, | ||
}), | ||
expectedLabels: []string{"label1"}, | ||
}, | ||
{ | ||
name: "two label", | ||
gotLabels: createLabelsPtr([]core.WebApiTagDefinition{ | ||
{Name: createStringPtr("label1"), Active: createBoolPtr(true)}, | ||
{Name: createStringPtr("label2"), Active: createBoolPtr(true)}, | ||
}), | ||
expectedLabels: []string{"label1", "label2"}, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
got := convertLabels(tc.gotLabels) | ||
assert.Equal(t, tc.expectedLabels, got) | ||
}) | ||
} | ||
} | ||
|
||
func TestContainAzureDevOpsLabels(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
expectedLabels []string | ||
gotLabels []string | ||
expectedResult bool | ||
}{ | ||
{ | ||
name: "empty labels", | ||
expectedLabels: []string{}, | ||
gotLabels: []string{}, | ||
expectedResult: true, | ||
}, | ||
{ | ||
name: "no matching labels", | ||
expectedLabels: []string{"label1", "label2"}, | ||
gotLabels: []string{"label3", "label4"}, | ||
expectedResult: false, | ||
}, | ||
{ | ||
name: "some matching labels", | ||
expectedLabels: []string{"label1", "label2"}, | ||
gotLabels: []string{"label1", "label3"}, | ||
expectedResult: false, | ||
}, | ||
{ | ||
name: "all matching labels", | ||
expectedLabels: []string{"label1", "label2"}, | ||
gotLabels: []string{"label1", "label2"}, | ||
expectedResult: true, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
got := containAzureDevOpsLabels(tc.expectedLabels, tc.gotLabels) | ||
assert.Equal(t, tc.expectedResult, got) | ||
}) | ||
} | ||
} | ||
|
||
func TestBuildURL(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
url string | ||
organization string | ||
expected string | ||
}{ | ||
{ | ||
name: "Provided default URL and organization", | ||
url: "https://dev.azure.com/", | ||
organization: "myorganization", | ||
expected: "https://dev.azure.com/myorganization", | ||
}, | ||
{ | ||
name: "Provided default URL and organization without trailing slash", | ||
url: "https://dev.azure.com", | ||
organization: "myorganization", | ||
expected: "https://dev.azure.com/myorganization", | ||
}, | ||
{ | ||
name: "Provided no URL and organization", | ||
url: "", | ||
organization: "myorganization", | ||
expected: "https://dev.azure.com/myorganization", | ||
}, | ||
{ | ||
name: "Provided custom URL and organization", | ||
url: "https://azuredevops.mycompany.com/", | ||
organization: "myorganization", | ||
expected: "https://azuredevops.mycompany.com/myorganization", | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
result := buildURL(tc.url, tc.organization) | ||
assert.Equal(t, result, tc.expected) | ||
}) | ||
} | ||
} |
Oops, something went wrong.