diff --git a/cmd/kosli/pullrequest.go b/cmd/kosli/pullrequest.go index 4ab82966c..e8e8368c3 100644 --- a/cmd/kosli/pullrequest.go +++ b/cmd/kosli/pullrequest.go @@ -129,11 +129,7 @@ func (o *attestPROptions) run(args []string) error { o.payload.GitProvider, label = getGitProviderAndLabel(o.retriever) var pullRequestsEvidence []*types.PREvidence - if o.payload.GitProvider == "azure" { - pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV1(o.payload.Commit.Sha1) - } else { - pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV2(o.payload.Commit.Sha1) - } + pullRequestsEvidence, err = o.getRetriever().PREvidenceForCommitV2(o.payload.Commit.Sha1) if err != nil { return err } diff --git a/internal/azure/azure.go b/internal/azure/azure.go index f7f2b6393..b0592abb7 100644 --- a/internal/azure/azure.go +++ b/internal/azure/azure.go @@ -2,12 +2,12 @@ package azure import ( "context" + "fmt" "net/url" "strconv" "strings" "github.com/kosli-dev/cli/internal/types" - "github.com/kosli-dev/cli/internal/utils" "github.com/microsoft/azure-devops-go-api/azuredevops" "github.com/microsoft/azure-devops-go-api/azuredevops/git" ) @@ -74,7 +74,19 @@ func (c *AzureConfig) PREvidenceForCommitV1(commit string) ([]*types.PREvidence, // This is the new implementation, it will be used for Azure func (c *AzureConfig) PREvidenceForCommitV2(commit string) ([]*types.PREvidence, error) { - return []*types.PREvidence{}, nil + pullRequestsEvidence := []*types.PREvidence{} + prs, err := c.PullRequestsForCommit(commit) + if err != nil { + return pullRequestsEvidence, err + } + for _, pr := range prs { + evidence, err := c.newPRAzureEvidenceV2(pr) + if err != nil { + return pullRequestsEvidence, err + } + pullRequestsEvidence = append(pullRequestsEvidence, evidence) + } + return pullRequestsEvidence, nil } func (c *AzureConfig) newPRAzureEvidence(pr git.GitPullRequest) (*types.PREvidence, error) { @@ -88,14 +100,79 @@ func (c *AzureConfig) newPRAzureEvidence(pr git.GitPullRequest) (*types.PREviden MergeCommit: *(pr.LastMergeCommit.CommitId), State: string(*pr.Status), } - approvers, err := c.GetPullRequestApprovers(*pr.PullRequestId) + evidence.Approvers, err = c.GetPullRequestApprovers(*pr.PullRequestId, 1) if err != nil { return evidence, err } - evidence.Approvers = utils.ConvertStringListToInterfaceList(approvers) return evidence, nil } +// newPRAzureEvidenceV2 creates a new PREvidence for a given pull request in V2 format +func (c *AzureConfig) newPRAzureEvidenceV2(pr git.GitPullRequest) (*types.PREvidence, error) { + prID := strconv.Itoa(*pr.PullRequestId) + prURL, err := url.JoinPath(c.OrgURL, c.Project, "_git", c.Repository, "pullrequest", prID) + if err != nil { + return nil, err + } + + evidence := &types.PREvidence{ + URL: prURL, + MergeCommit: *(pr.LastMergeCommit.CommitId), + State: string(*pr.Status), + Author: fmt.Sprintf("%s (%s)", *pr.CreatedBy.DisplayName, *pr.CreatedBy.UniqueName), + CreatedAt: pr.CreationDate.Time.Unix(), + Title: *pr.Title, + HeadRef: *pr.SourceRefName, + } + if pr.Status != nil && pr.ClosedDate != nil && *pr.Status == git.PullRequestStatusValues.Completed { + evidence.MergedAt = pr.ClosedDate.Time.Unix() + } + commits, err := c.GetPullRequestCommits(pr) + if err != nil { + return evidence, err + } + evidence.Commits = commits + evidence.Approvers, err = c.GetPullRequestApprovers(*pr.PullRequestId, 2) + if err != nil { + return evidence, err + } + return evidence, nil +} + +// GetPullRequestCommits returns a list of commits for a given pull request +func (c *AzureConfig) GetPullRequestCommits(pr git.GitPullRequest) ([]types.Commit, error) { + commits := []types.Commit{} + + ctx := context.Background() + client, err := NewAzureClientFromToken(ctx, c.Token, c.OrgURL) + if err != nil { + return commits, err + } + + prCommitsResponse, err := client.GetPullRequestCommits(ctx, git.GetPullRequestCommitsArgs{ + RepositoryId: &c.Repository, + PullRequestId: pr.PullRequestId, + Project: &c.Project, + }) + if err != nil { + return commits, err + } + + for _, commit := range prCommitsResponse.Value { + commits = append(commits, types.Commit{ + SHA: *commit.CommitId, + Message: *commit.Comment, + Committer: *commit.Author.Name, + Timestamp: commit.Committer.Date.Time.Unix(), + URL: *commit.Url, + Branch: *pr.SourceRefName, + CommitterUsername: *commit.Committer.Name, + }) + } + + return commits, nil +} + // PullRequestsForCommit returns a list of pull requests for a specific commit func (c *AzureConfig) PullRequestsForCommit(commit string) ([]git.GitPullRequest, error) { ctx := context.Background() @@ -131,8 +208,8 @@ func (c *AzureConfig) PullRequestsForCommit(commit string) ([]git.GitPullRequest } // GetPullRequestApprovers returns a list of approvers for a given pull request -func (c *AzureConfig) GetPullRequestApprovers(number int) ([]string, error) { - approvers := []string{} +func (c *AzureConfig) GetPullRequestApprovers(prNumber, version int) ([]any, error) { + var approvers []any ctx := context.Background() client, err := NewAzureClientFromToken(ctx, c.Token, c.OrgURL) if err != nil { @@ -141,7 +218,7 @@ func (c *AzureConfig) GetPullRequestApprovers(number int) ([]string, error) { reviewers, err := client.GetPullRequestReviewers(ctx, git.GetPullRequestReviewersArgs{ RepositoryId: &c.Repository, - PullRequestId: &number, + PullRequestId: &prNumber, Project: &c.Project, }) if err != nil { @@ -150,7 +227,15 @@ func (c *AzureConfig) GetPullRequestApprovers(number int) ([]string, error) { for _, r := range *reviewers { if *r.Vote == 10 { - approvers = append(approvers, *r.DisplayName) + approverName := fmt.Sprintf("%s (%s)", *r.DisplayName, *r.UniqueName) + if version == 1 { + approvers = append(approvers, approverName) + } else { + approvers = append(approvers, types.PRApprovals{ + Username: approverName, + State: "APPROVED", + }) + } } } return approvers, nil diff --git a/internal/azure/azure_test.go b/internal/azure/azure_test.go index 65ffdd593..5a69b8963 100644 --- a/internal/azure/azure_test.go +++ b/internal/azure/azure_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/kosli-dev/cli/internal/testHelpers" + "github.com/kosli-dev/cli/internal/types" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" ) @@ -123,7 +124,7 @@ func (suite *AzureTestSuite) TestPullRequestsForCommit() { } } -func (suite *AzureTestSuite) TestGetPullRequestApprovers() { +func (suite *AzureTestSuite) TestGetPullRequestApproversV1() { type result struct { wantError bool approvers []string @@ -153,7 +154,7 @@ func (suite *AzureTestSuite) TestGetPullRequestApprovers() { project: "kosli-azure", number: 1, result: result{ - approvers: []string{"Ewelina"}, + approvers: []string{"Ewelina (ewelina@merkely.onmicrosoft.com)"}, }, }, { @@ -176,7 +177,78 @@ func (suite *AzureTestSuite) TestGetPullRequestApprovers() { Repository: t.repository, Project: t.project, } - approvers, err := c.GetPullRequestApprovers(t.number) + approvers, err := c.GetPullRequestApprovers(t.number, 1) + if t.result.wantError { + require.Errorf(suite.T(), err, "expected an error but got: %s", err) + } else { + require.NoErrorf(suite.T(), err, "was NOT expecting error but got: %s", err) + require.ElementsMatchf(suite.T(), t.result.approvers, approvers, "want approvers: %v, got approvers: %v", + t.result.approvers, approvers) + } + }) + } +} + +func (suite *AzureTestSuite) TestGetPullRequestApproversV2() { + type result struct { + wantError bool + approvers []types.PRApprovals + } + for _, t := range []struct { + name string + azOrgURL string + repository string + project string + number int + result result + }{ + { + name: "get an empty list for a PR without approvers", + azOrgURL: "https://dev.azure.com/kosli", + repository: "cli", + project: "kosli-azure", + number: 2, + result: result{ + approvers: []types.PRApprovals{}, + }, + }, + { + name: "get the list of approvers for an approved PR", + azOrgURL: "https://dev.azure.com/kosli", + repository: "cli", + project: "kosli-azure", + number: 1, + result: result{ + approvers: []types.PRApprovals{ + { + Username: "Ewelina (ewelina@merkely.onmicrosoft.com)", + State: "APPROVED", + Timestamp: 0, + }, + }, + }, + }, + { + name: "non-existing PR causes an error", + azOrgURL: "https://dev.azure.com/kosli", + repository: "cli", + project: "kosli-azure", + number: 666, + result: result{ + wantError: true, + }, + }, + } { + suite.Run(t.name, func() { + testHelpers.SkipIfEnvVarUnset(suite.T(), []string{"KOSLI_AZURE_TOKEN"}) + token := os.Getenv("KOSLI_AZURE_TOKEN") + c := &AzureConfig{ + Token: token, + OrgURL: t.azOrgURL, + Repository: t.repository, + Project: t.project, + } + approvers, err := c.GetPullRequestApprovers(t.number, 2) if t.result.wantError { require.Errorf(suite.T(), err, "expected an error but got: %s", err) } else {