Skip to content

Commit

Permalink
Build resolver for user's UUID (#295)
Browse files Browse the repository at this point in the history
* create a build resolver based on user id

* various changes based on PR feedback

* fix fn call to pass branch name instead of build options

* rename resolver filename for userid

* rename fn name for resolver for user id

* rename fn name for resolver for user id
  • Loading branch information
lizrabuya authored Jun 20, 2024
1 parent 6b3ed3a commit cb797ad
Show file tree
Hide file tree
Showing 6 changed files with 322 additions and 182 deletions.
24 changes: 24 additions & 0 deletions internal/build/resolver/current_user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package resolver

import (
"context"

"github.com/buildkite/cli/v3/internal/build"
pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/go-buildkite/v3/buildkite"
)

// ResolveBuildForCurrentUser Finds the most recent build for the current user and branch
func ResolveBuildForCurrentUser(branch string, pipelineResolver pipelineResolver.PipelineResolverFn, f *factory.Factory) BuildResolverFn {
return func(ctx context.Context) (*build.Build, error) {
var user *buildkite.User

user, _, err := f.RestAPIClient.User.Get()
if err != nil {
return nil, err
}

return ResolveBuildForUser(ctx, *user.Email, branch, pipelineResolver, f)
}
}
145 changes: 145 additions & 0 deletions internal/build/resolver/current_user_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package resolver_test

import (
"bytes"
"context"
"io"
"net/http"
"os"
"strings"
"testing"

"github.com/buildkite/cli/v3/internal/build/resolver"
"github.com/buildkite/cli/v3/internal/config"
"github.com/buildkite/cli/v3/internal/pipeline"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/go-buildkite/v3/buildkite"
"github.com/spf13/afero"
)

func TestResolveBuildForCurrentUser(t *testing.T) {
t.Parallel()

pipelineResolver := func(context.Context) (*pipeline.Pipeline, error) {
return &pipeline.Pipeline{
Name: "testing",
Org: "test org",
}, nil
}

t.Run("Errors if user cannot be found", func(t *testing.T) {
t.Parallel()

// mock a failed repsonse
transport := roundTripperFunc(func(r *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusNotFound,
}, nil
})
client := &http.Client{Transport: transport}
f := &factory.Factory{
RestAPIClient: buildkite.NewClient(client),
}

r := resolver.ResolveBuildForCurrentUser("main", pipelineResolver, f)
_, err := r(context.Background())

if err == nil {
t.Fatal("Resolver should return error if user not found")
}
})

t.Run("Returns first build found", func(t *testing.T) {
t.Parallel()

in, _ := os.ReadFile("../../../fixtures/build.json")
callIndex := 0
responses := []http.Response{
{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{
"id": "abc123-4567-8910-...",
"graphql_id": "VXNlci0tLWU1N2ZiYTBmLWFiMTQtNGNjMC1iYjViLTY5NTc3NGZmYmZiZQ==",
"name": "John Smith",
"email": "[email protected]",
"avatar_url": "https://www.gravatar.com/avatar/abc123...",
"created_at": "2012-03-04T06:07:08.910Z"
}
`)),
},
{
StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(in)),
},
}
// mock a failed repsonse
transport := roundTripperFunc(func(r *http.Request) (*http.Response, error) {
resp := responses[callIndex]
callIndex++
return &resp, nil
})
client := &http.Client{Transport: transport}
fs := afero.NewMemMapFs()
f := &factory.Factory{
RestAPIClient: buildkite.NewClient(client),
Config: config.New(fs, nil),
}

r := resolver.ResolveBuildForCurrentUser("main", pipelineResolver, f)
build, err := r(context.Background())
if err != nil {
t.Fatal(err)
}

if build.BuildNumber != 584 {
t.Fatalf("Expected build 584, got %d", build.BuildNumber)
}
})

t.Run("Errors if no matching builds found", func(t *testing.T) {
t.Parallel()

callIndex := 0
responses := []http.Response{
{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{
"id": "abc123-4567-8910-...",
"graphql_id": "VXNlci0tLWU1N2ZiYTBmLWFiMTQtNGNjMC1iYjViLTY5NTc3NGZmYmZiZQ==",
"name": "John Smith",
"email": "[email protected]",
"avatar_url": "https://www.gravatar.com/avatar/abc123...",
"created_at": "2012-03-04T06:07:08.910Z"
}
`)),
},
{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader("[]")),
},
}
// mock a failed repsonse
transport := roundTripperFunc(func(r *http.Request) (*http.Response, error) {
resp := responses[callIndex]
callIndex++
return &resp, nil
})
client := &http.Client{Transport: transport}
fs := afero.NewMemMapFs()
f := &factory.Factory{
RestAPIClient: buildkite.NewClient(client),
Config: config.New(fs, nil),
}

r := resolver.ResolveBuildForCurrentUser("main", pipelineResolver, f)
build, err := r(context.Background())

if err == nil {
t.Fatal("Should return an error when no build is found")
}

if build != nil {
t.Fatal("Expected no build to be found")
}
})
}
85 changes: 34 additions & 51 deletions internal/build/resolver/user_builds.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,47 @@ import (
"fmt"

"github.com/buildkite/cli/v3/internal/build"
"github.com/buildkite/cli/v3/internal/pipeline"
pipelineResolver "github.com/buildkite/cli/v3/internal/pipeline/resolver"
"github.com/buildkite/cli/v3/pkg/cmd/factory"
"github.com/buildkite/go-buildkite/v3/buildkite"
"golang.org/x/sync/errgroup"
)

// ResolveBuildFromCurrentBranch Finds the most recent build for the branch in the current working directory
func ResolveBuildForCurrentUser(branch string, pipelineResolver pipelineResolver.PipelineResolverFn, f *factory.Factory) BuildResolverFn {
return func(ctx context.Context) (*build.Build, error) {
var pipeline *pipeline.Pipeline
var user *buildkite.User
// ResolveBuildForUser Finds the most recent build for the user and branch
func ResolveBuildForUser(ctx context.Context, userInfo string, branch string, pipelineResolver pipelineResolver.PipelineResolverFn, f *factory.Factory) (*build.Build, error) {

// use an errgroup so a few API calls can be done in parallel
// and then we check for any errors that occurred
g, _ := errgroup.WithContext(ctx)
g.Go(func() error {
p, e := pipelineResolver(ctx)
if p != nil {
pipeline = p
}
return e
})
g.Go(func() error {
u, _, e := f.RestAPIClient.User.Get()
if u != nil {
user = u
}
return e
})
err := g.Wait()
if err != nil {
return nil, err
}
if pipeline == nil {
return nil, fmt.Errorf("failed to resolve a pipeline to query builds on.")
}
pipeline, err := pipelineResolver(ctx)
if err != nil {
return nil, err
}
if pipeline == nil {
return nil, fmt.Errorf("failed to resolve a pipeline to query builds on")
}

opt := &buildkite.BuildsListOptions{
Creator: userInfo,
ListOptions: buildkite.ListOptions{
PerPage: 1,
},
}

builds, _, err := f.RestAPIClient.Builds.ListByPipeline(f.Config.OrganizationSlug(), pipeline.Name, &buildkite.BuildsListOptions{
Creator: *user.Email,
Branch: []string{branch},
ListOptions: buildkite.ListOptions{
PerPage: 1,
},
})
if err != nil {
return nil, err
}
if len(builds) == 0 {
// we error here because this resolver is explicitly used so any case where it doesn't resolve a build is a
// problem
return nil, fmt.Errorf("failed to find a build for current user (email: %s)", *user.Email)
}
if len(branch) > 0 {
opt.Branch = []string{branch}
}

builds, _, err := f.RestAPIClient.Builds.ListByPipeline(f.Config.OrganizationSlug(), pipeline.Name, opt)

return &build.Build{
Organization: f.Config.OrganizationSlug(),
Pipeline: pipeline.Name,
BuildNumber: *builds[0].Number,
}, nil
if err != nil {
return nil, err
}
if len(builds) == 0 {
// we error here because this resolver is explicitly used so any case where it doesn't resolve a build is a
// problem
return nil, fmt.Errorf("failed to find a build for current user (%s)", userInfo)
}

return &build.Build{
Organization: f.Config.OrganizationSlug(),
Pipeline: pipeline.Name,
BuildNumber: *builds[0].Number,
}, nil
}
Loading

0 comments on commit cb797ad

Please sign in to comment.