Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ implement more of the Azure DevOps client #4456

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,11 @@ ifndef GITLAB_AUTH_TOKEN
$(error GITLAB_AUTH_TOKEN is undefined)
endif

check-env-azure-devops:
ifndef AZURE_DEVOPS_AUTH_TOKEN
$(error AZURE_DEVOPS_AUTH_TOKEN is undefined)
endif

e2e-pat: ## Runs e2e tests. Requires GITHUB_AUTH_TOKEN env var to be set to GitHub personal access token
e2e-pat: build-scorecard check-env | $(GINKGO)
# Run e2e tests. GITHUB_AUTH_TOKEN with personal access token must be exported to run this
Expand All @@ -357,8 +362,8 @@ e2e-gitlab: build-scorecard | $(GINKGO)
TEST_GITLAB_EXTERNAL=1 TOKEN_TYPE="PAT" $(GINKGO) --race -p -vv -coverprofile=e2e-coverage.out --keep-separate-coverprofiles --focus ".*GitLab" ./...

e2e-azure-devops-token: ## Runs e2e tests that require a AZURE_DEVOPS_AUTH_TOKEN
e2e-azure-devops-token: build-scorecard check-env-gitlab | $(GINKGO)
TEST_AZURE_DEVOPS_EXTERNAL=1 TOKEN_TYPE="GITLAB_PAT" $(GINKGO) --race -p -vv -coverprofile=e2e-coverage.out --keep-separate-coverprofiles --focus '.*Azure DevOps' ./...
e2e-azure-devops-token: build-scorecard check-env-azure-devops | $(GINKGO)
TEST_AZURE_DEVOPS_EXTERNAL=1 $(GINKGO) --race -p -vv -coverprofile=e2e-coverage.out --keep-separate-coverprofiles --focus "Azure DevOps" ./...

e2e-attestor: ## Runs e2e tests for scorecard-attestor
cd attestor/e2e; go test -covermode=atomic -coverprofile=e2e-coverage.out; cd ../..
Expand Down
98 changes: 70 additions & 28 deletions clients/azuredevopsrepo/branches.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,57 +19,99 @@
"fmt"
"sync"

"github.com/google/uuid"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"

"github.com/ossf/scorecard/v5/clients"
)

type branchesHandler struct {
gitClient git.Client
ctx context.Context
once *sync.Once
errSetup error
repourl *Repo
defaultBranchRef *clients.BranchRef
queryBranch fnQueryBranch
gitClient git.Client
ctx context.Context
once *sync.Once
errSetup error
repourl *Repo
defaultBranchRef *clients.BranchRef
queryBranch fnQueryBranch
getPolicyConfigurations fnGetPolicyConfigurations
}

func (handler *branchesHandler) init(ctx context.Context, repourl *Repo) {
handler.ctx = ctx
handler.repourl = repourl
handler.errSetup = nil
handler.once = new(sync.Once)
handler.queryBranch = handler.gitClient.GetBranch
func (b *branchesHandler) init(ctx context.Context, repourl *Repo) {
b.ctx = ctx
b.repourl = repourl
b.errSetup = nil
b.once = new(sync.Once)
b.queryBranch = b.gitClient.GetBranch
b.getPolicyConfigurations = b.gitClient.GetPolicyConfigurations

Check warning on line 45 in clients/azuredevopsrepo/branches.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/branches.go#L39-L45

Added lines #L39 - L45 were not covered by tests
}

type (
fnQueryBranch func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error)
fnQueryBranch func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error)
fnGetPolicyConfigurations func(
ctx context.Context,
args git.GetPolicyConfigurationsArgs,
) (*git.GitPolicyConfigurationResponse, error)
)

func (handler *branchesHandler) setup() error {
handler.once.Do(func() {
branch, err := handler.queryBranch(handler.ctx, git.GetBranchArgs{
RepositoryId: &handler.repourl.id,
Name: &handler.repourl.defaultBranch,
})
func (b *branchesHandler) setup() error {
b.once.Do(func() {
args := git.GetBranchArgs{
RepositoryId: &b.repourl.id,
Name: &b.repourl.defaultBranch,
}
branch, err := b.queryBranch(b.ctx, args)
if err != nil {
handler.errSetup = fmt.Errorf("request for default branch failed with error %w", err)
b.errSetup = fmt.Errorf("request for default branch failed with error %w", err)
return
}
handler.defaultBranchRef = &clients.BranchRef{
b.defaultBranchRef = &clients.BranchRef{
Name: branch.Name,
}

handler.errSetup = nil
b.errSetup = nil
})
return handler.errSetup
return b.errSetup
}

func (handler *branchesHandler) getDefaultBranch() (*clients.BranchRef, error) {
err := handler.setup()
if err != nil {
func (b *branchesHandler) getDefaultBranch() (*clients.BranchRef, error) {
if err := b.setup(); err != nil {
return nil, fmt.Errorf("error during branchesHandler.setup: %w", err)
}

return handler.defaultBranchRef, nil
return b.defaultBranchRef, nil
}

func (b *branchesHandler) getBranch(branchName string) (*clients.BranchRef, error) {
branch, err := b.queryBranch(b.ctx, git.GetBranchArgs{
RepositoryId: &b.repourl.id,
Name: &branchName,
})
if err != nil {
return nil, fmt.Errorf("request for branch %s failed with error %w", branchName, err)
}

refName := fmt.Sprintf("refs/heads/%s", *branch.Name)
repositoryID, err := uuid.Parse(b.repourl.id)
if err != nil {
return nil, fmt.Errorf("error parsing repository ID %s: %w", b.repourl.id, err)
}

Check warning on line 97 in clients/azuredevopsrepo/branches.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/branches.go#L96-L97

Added lines #L96 - L97 were not covered by tests
args := git.GetPolicyConfigurationsArgs{
RepositoryId: &repositoryID,
RefName: &refName,
}
response, err := b.getPolicyConfigurations(b.ctx, args)
if err != nil {
return nil, fmt.Errorf("request for policy configurations failed with error %w", err)
}

isBranchProtected := false
if len(*response.PolicyConfigurations) > 0 {
isBranchProtected = true
}

Check warning on line 110 in clients/azuredevopsrepo/branches.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/branches.go#L109-L110

Added lines #L109 - L110 were not covered by tests

// TODO: map Azure DevOps branch protection to Scorecard branch protection
return &clients.BranchRef{
Name: branch.Name,
Protected: &isBranchProtected,
}, nil
}
100 changes: 89 additions & 11 deletions clients/azuredevopsrepo/branches_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,34 @@ import (
"sync"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/uuid"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/git"
"github.com/microsoft/azure-devops-go-api/azuredevops/v7/policy"

"github.com/ossf/scorecard/v5/clients"
)

func TestGetDefaultBranch(t *testing.T) {
func Test_getDefaultBranch(t *testing.T) {
t.Parallel()
tests := []struct {
setupMock func() fnQueryBranch
queryBranch fnQueryBranch
name string
expectedName string
expectedError bool
}{
{
name: "successful branch retrieval",
setupMock: func() fnQueryBranch {
return func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return &git.GitBranchStats{Name: args.Name}, nil
}
queryBranch: func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return &git.GitBranchStats{Name: args.Name}, nil
},
expectedError: false,
expectedName: "main",
},
{
name: "error during branch retrieval",
setupMock: func() fnQueryBranch {
return func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return nil, fmt.Errorf("error")
}
queryBranch: func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return nil, fmt.Errorf("error")
},
expectedError: true,
},
Expand All @@ -56,7 +57,7 @@ func TestGetDefaultBranch(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
handler := &branchesHandler{
queryBranch: tt.setupMock(),
queryBranch: tt.queryBranch,
once: new(sync.Once),
repourl: &Repo{
id: "repo-id",
Expand All @@ -74,3 +75,80 @@ func TestGetDefaultBranch(t *testing.T) {
})
}
}

func Test_getBranch(t *testing.T) {
t.Parallel()
tests := []struct {
getBranch fnQueryBranch
getPolicyConfigurations fnGetPolicyConfigurations
expected *clients.BranchRef
name string
branchName string
expectedError bool
}{
{
name: "successful branch retrieval",
branchName: "main",
getBranch: func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return &git.GitBranchStats{Name: args.Name}, nil
},
getPolicyConfigurations: func(ctx context.Context, args git.GetPolicyConfigurationsArgs) (*git.GitPolicyConfigurationResponse, error) {
return &git.GitPolicyConfigurationResponse{
PolicyConfigurations: &[]policy.PolicyConfiguration{},
}, nil
},
expected: &clients.BranchRef{
Name: toPtr("main"),
Protected: toPtr(false),
},
expectedError: false,
},
{
name: "error during branch retrieval",
branchName: "main",
getBranch: func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return nil, fmt.Errorf("error")
},
getPolicyConfigurations: func(ctx context.Context, args git.GetPolicyConfigurationsArgs) (*git.GitPolicyConfigurationResponse, error) {
return &git.GitPolicyConfigurationResponse{}, nil
},
expected: nil,
expectedError: true,
},
{
name: "error during policy configuration retrieval",
branchName: "main",
getBranch: func(ctx context.Context, args git.GetBranchArgs) (*git.GitBranchStats, error) {
return &git.GitBranchStats{Name: args.Name}, nil
},
getPolicyConfigurations: func(ctx context.Context, args git.GetPolicyConfigurationsArgs) (*git.GitPolicyConfigurationResponse, error) {
return nil, fmt.Errorf("error")
},
expected: nil,
expectedError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
handler := &branchesHandler{
queryBranch: tt.getBranch,
getPolicyConfigurations: tt.getPolicyConfigurations,
once: new(sync.Once),
repourl: &Repo{
id: uuid.Nil.String(),
defaultBranch: "main",
},
}

branch, err := handler.getBranch(tt.branchName)
if (err != nil) != tt.expectedError {
t.Errorf("expected error: %v, got: %v", tt.expectedError, err)
}
if diff := cmp.Diff(branch, tt.expected); diff != "" {
t.Errorf("mismatch in branch ref (-want +got):\n%s", diff)
}
})
}
}
105 changes: 105 additions & 0 deletions clients/azuredevopsrepo/builds.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2024 OpenSSF Scorecard Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package azuredevopsrepo

import (
"context"

"github.com/microsoft/azure-devops-go-api/azuredevops/v7/build"

"github.com/ossf/scorecard/v5/clients"
)

type buildsHandler struct {
ctx context.Context
repourl *Repo
buildClient build.Client
getBuildDefinitions fnListBuildDefinitions
getBuilds fnGetBuilds
}

type (
fnListBuildDefinitions func(
ctx context.Context,
args build.GetDefinitionsArgs,
) (*build.GetDefinitionsResponseValue, error)
fnGetBuilds func(
ctx context.Context,
args build.GetBuildsArgs,
) (*build.GetBuildsResponseValue, error)
)

func (b *buildsHandler) init(ctx context.Context, repourl *Repo) {
b.ctx = ctx
b.repourl = repourl
b.getBuildDefinitions = b.buildClient.GetDefinitions
b.getBuilds = b.buildClient.GetBuilds

Check warning on line 48 in clients/azuredevopsrepo/builds.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/builds.go#L44-L48

Added lines #L44 - L48 were not covered by tests
}

func (b *buildsHandler) listSuccessfulBuilds(filename string) ([]clients.WorkflowRun, error) {
buildDefinitions := make([]build.BuildDefinitionReference, 0)

includeAllProperties := true
repositoryType := "TfsGit"
continuationToken := ""
for {
args := build.GetDefinitionsArgs{
Project: &b.repourl.project,
RepositoryId: &b.repourl.id,
RepositoryType: &repositoryType,
IncludeAllProperties: &includeAllProperties,
YamlFilename: &filename,
ContinuationToken: &continuationToken,
}

response, err := b.getBuildDefinitions(b.ctx, args)
if err != nil {
return nil, err
}

buildDefinitions = append(buildDefinitions, response.Value...)

if response.ContinuationToken == "" {
break
}
continuationToken = response.ContinuationToken

Check warning on line 77 in clients/azuredevopsrepo/builds.go

View check run for this annotation

Codecov / codecov/patch

clients/azuredevopsrepo/builds.go#L77

Added line #L77 was not covered by tests
}

buildIds := make([]int, 0, len(buildDefinitions))
for i := range buildDefinitions {
buildIds = append(buildIds, *buildDefinitions[i].Id)
}

args := build.GetBuildsArgs{
Project: &b.repourl.project,
Definitions: &buildIds,
ResultFilter: &build.BuildResultValues.Succeeded,
}
builds, err := b.getBuilds(b.ctx, args)
if err != nil {
return nil, err
}

workflowRuns := make([]clients.WorkflowRun, 0, len(builds.Value))
for i := range builds.Value {
currentBuild := builds.Value[i]
workflowRuns = append(workflowRuns, clients.WorkflowRun{
URL: *currentBuild.Url,
HeadSHA: currentBuild.SourceVersion,
})
}

return workflowRuns, nil
}
Loading
Loading