Skip to content

Commit

Permalink
Make a new command to trigger a pipeline
Browse files Browse the repository at this point in the history
  • Loading branch information
marcomorain authored and pete-woods committed Oct 5, 2020
1 parent 79ecb9b commit d39fbfd
Show file tree
Hide file tree
Showing 14 changed files with 497 additions and 22 deletions.
74 changes: 74 additions & 0 deletions api/pipelines/pipelines.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package pipelines

import (
"fmt"
"net/url"
"strings"
"time"

"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/git"
)

type Pipelines struct {
rc *rest.Client
}

func New(rc *rest.Client) *Pipelines {
return &Pipelines{rc: rc}
}

type Pipeline struct {
ID string `json:"id"`
Number int `json:"number"`
State string `json:"state"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Trigger Trigger `json:"trigger"`
}

type Trigger struct {
Type string `json:"type"`
ReceivedAt time.Time `json:"received_at"`
Actor Actor `json:"actor"`
}

type Actor struct {
Login string `json:"login"`
AvatarURL string `json:"avatar_url"`
}

func (p *Pipelines) Get(remote git.Remote) ([]Pipeline, error) {
req, err := p.rc.NewRequest("GET", pipelineSlug(remote), nil)
if err != nil {
return nil, err
}

Check warning on line 45 in api/pipelines/pipelines.go

View check run for this annotation

Codecov / codecov/patch

api/pipelines/pipelines.go#L44-L45

Added lines #L44 - L45 were not covered by tests

resp := struct {
Items []Pipeline `json:"items"`
}{}
_, err = p.rc.DoRequest(req, &resp)
return resp.Items, err
}

type TriggerParameters struct {
Branch string `json:"branch,omitempty"`
}

func (p *Pipelines) Trigger(remote git.Remote, params *TriggerParameters) (*Pipeline, error) {
req, err := p.rc.NewRequest("POST", pipelineSlug(remote), params)
if err != nil {
return nil, err
}

Check warning on line 62 in api/pipelines/pipelines.go

View check run for this annotation

Codecov / codecov/patch

api/pipelines/pipelines.go#L61-L62

Added lines #L61 - L62 were not covered by tests

resp := &Pipeline{}
_, err = p.rc.DoRequest(req, resp)
return resp, err
}

func pipelineSlug(remote git.Remote) *url.URL {
return &url.URL{Path: fmt.Sprintf("project/%s/%s/%s/pipeline",
strings.ToLower(string(remote.VcsType)),
url.QueryEscape(remote.Organization),
url.QueryEscape(remote.Project))}
}
234 changes: 234 additions & 0 deletions api/pipelines/pipelines_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
package pipelines

import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"net/url"
"sync"
"testing"
"time"

"github.com/CircleCI-Public/circleci-cli/git"

"gotest.tools/v3/assert"
"gotest.tools/v3/assert/cmp"

"github.com/CircleCI-Public/circleci-cli/api/rest"
"github.com/CircleCI-Public/circleci-cli/version"
)

func TestPipelines_Trigger(t *testing.T) {
fix := fixture{}
p, cleanup := fix.Run(
http.StatusOK,
`
{
"id": "2bc0df8e-d258-4ae8-9c2b-3793f004725f",
"number": 123,
"state": "created",
"created_at": "2020-03-01T09:30:00Z",
"updated_at": "2020-03-01T09:32:00Z",
"trigger": {
"type": "api",
"received_at": "2020-03-01T09:30:00Z",
"actor": {
"login": "the-actor-login",
"avatar_url": "the-actor-avatar"
}
}
}`)
defer cleanup()

t.Run("Check resource-class is created", func(t *testing.T) {
pipe, err := p.Trigger(
git.Remote{
VcsType: "github",
Organization: "the-organization",
Project: "the-project",
},
&TriggerParameters{
Branch: "the-branch",
},
)
assert.NilError(t, err)
assert.Check(t, cmp.DeepEqual(pipe, &Pipeline{
ID: "2bc0df8e-d258-4ae8-9c2b-3793f004725f",
Number: 123,
State: "created",
CreatedAt: time.Date(2020, 3, 1, 9, 30, 0, 0, time.UTC),
UpdatedAt: time.Date(2020, 3, 1, 9, 32, 0, 0, time.UTC),
Trigger: Trigger{
Type: "api",
ReceivedAt: time.Date(2020, 3, 1, 9, 30, 0, 0, time.UTC),
Actor: Actor{
Login: "the-actor-login",
AvatarURL: "the-actor-avatar",
},
},
}))
})

t.Run("Check request", func(t *testing.T) {
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/project/github/the-organization/the-project/pipeline"}))
assert.Check(t, cmp.Equal(fix.method, "POST"))
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"Content-Length": {"24"},
"Content-Type": {"application/json"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), `{"branch":"the-branch"}`+"\n"))
})
}

func TestPipelines_Get(t *testing.T) {
fix := fixture{}
p, cleanup := fix.Run(
http.StatusOK,
`
{
"items": [
{
"id": "673b09d4-bb6f-41e0-8923-61c486376bff",
"number": 123,
"state": "created",
"created_at": "2020-03-01T09:30:00Z",
"updated_at": "2020-03-01T09:32:00Z",
"trigger": {
"type": "api",
"received_at": "2020-03-01T09:30:00Z",
"actor": {
"login": "the-actor-login",
"avatar_url": "the-actor-avatar"
}
}
},
{
"id": "ba7fea2b-47a4-4213-8425-dfa37d900a62",
"number": 234,
"state": "created",
"created_at": "2020-04-01T09:30:00Z",
"updated_at": "2020-04-01T09:32:00Z",
"trigger": {
"type": "webhook",
"received_at": "2020-04-01T09:30:00Z",
"actor": {
"login": "the-actor-login",
"avatar_url": "the-actor-avatar"
}
}
}
]
}`,
)
defer cleanup()

t.Run("Check pipeline list results", func(t *testing.T) {
pipes, err := p.Get(git.Remote{
VcsType: "github",
Organization: "the-organization",
Project: "the-project",
})
assert.NilError(t, err)
assert.Check(t, cmp.DeepEqual(pipes, []Pipeline{
{
ID: "673b09d4-bb6f-41e0-8923-61c486376bff",
Number: 123,
State: "created",
CreatedAt: time.Date(2020, 3, 1, 9, 30, 0, 0, time.UTC),
UpdatedAt: time.Date(2020, 3, 1, 9, 32, 0, 0, time.UTC),
Trigger: Trigger{
Type: "api",
ReceivedAt: time.Date(2020, 3, 1, 9, 30, 0, 0, time.UTC),
Actor: Actor{
Login: "the-actor-login",
AvatarURL: "the-actor-avatar",
},
},
},
{
ID: "ba7fea2b-47a4-4213-8425-dfa37d900a62",
Number: 234,
State: "created",
CreatedAt: time.Date(2020, 4, 1, 9, 30, 0, 0, time.UTC),
UpdatedAt: time.Date(2020, 4, 1, 9, 32, 0, 0, time.UTC),
Trigger: Trigger{
Type: "webhook",
ReceivedAt: time.Date(2020, 4, 1, 9, 30, 0, 0, time.UTC),
Actor: Actor{
Login: "the-actor-login",
AvatarURL: "the-actor-avatar",
},
},
},
}))
})

t.Run("Check request", func(t *testing.T) {
assert.Check(t, cmp.Equal(fix.URL(), url.URL{Path: "/api/v2/project/github/the-organization/the-project/pipeline"}))
assert.Check(t, cmp.Equal(fix.method, "GET"))
assert.Check(t, cmp.DeepEqual(fix.Header(), http.Header{
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ``))
})
}

type fixture struct {
mu sync.Mutex
url url.URL
method string
header http.Header
body bytes.Buffer
}

func (f *fixture) URL() url.URL {
f.mu.Lock()
defer f.mu.Unlock()
return f.url
}

func (f *fixture) Method() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.method
}

func (f *fixture) Header() http.Header {
f.mu.Lock()
defer f.mu.Unlock()
return f.header
}

func (f *fixture) Body() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.body.String()
}

func (f *fixture) Run(statusCode int, respBody string) (p *Pipelines, cleanup func()) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
f.mu.Lock()
defer f.mu.Unlock()

defer r.Body.Close()
_, _ = io.Copy(&f.body, r.Body)
f.url = *r.URL
f.header = r.Header
f.method = r.Method

w.WriteHeader(statusCode)
_, _ = io.WriteString(w, respBody)
})
server := httptest.NewServer(mux)

return New(rest.New(server.URL, "api/v2", "fake-token")), server.Close
}
4 changes: 0 additions & 4 deletions api/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,6 @@ func (c *Client) DoRequest(req *http.Request, resp interface{}) (statusCode int,
}

if resp != nil {
if !strings.Contains(httpResp.Header.Get("Content-Type"), "application/json") {
return httpResp.StatusCode, errors.New("wrong content type received")
}

err = json.NewDecoder(httpResp.Body).Decode(resp)
if err != nil {
return httpResp.StatusCode, err
Expand Down
14 changes: 2 additions & 12 deletions cmd/open.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,13 @@
package cmd

import (
"fmt"
"net/url"
"strings"

"github.com/CircleCI-Public/circleci-cli/git"
"github.com/CircleCI-Public/circleci-cli/paths"
"github.com/pkg/browser"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)

func projectUrl(remote *git.Remote) string {
return fmt.Sprintf("https://app.circleci.com/pipelines/%s/%s/%s",
url.PathEscape(strings.ToLower(string(remote.VcsType))),
url.PathEscape(remote.Organization),
url.PathEscape(remote.Project))
}

var errorMessage = `
Unable detect which URL should be opened. This command is intended to be run from
a git repository with a remote named 'origin' that is hosted on Github or Bitbucket
Expand All @@ -31,7 +21,7 @@ func openProjectInBrowser() error {
return errors.Wrap(err, errorMessage)
}

return browser.OpenURL(projectUrl(remote))
return browser.OpenURL(paths.ProjectUrl(remote))

Check warning on line 24 in cmd/open.go

View check run for this annotation

Codecov / codecov/patch

cmd/open.go#L24

Added line #L24 was not covered by tests
}

func newOpenCommand() *cobra.Command {
Expand Down
Loading

0 comments on commit d39fbfd

Please sign in to comment.