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 2, 2020
1 parent 75976b1 commit a5daeef
Show file tree
Hide file tree
Showing 15 changed files with 279 additions and 31 deletions.
74 changes: 74 additions & 0 deletions api/pipeline/pipeline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package pipeline

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

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

type pipeline struct {
rc *rest.Client
}

func New(rc *rest.Client) *pipeline {
return &pipeline{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 *pipeline) Get(remote git.Remote) ([]Pipeline, error) {
req, err := p.rc.NewRequest("GET", pipelineSlug(remote), nil)
if err != nil {
return nil, err
}

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 *pipeline) Trigger(remote git.Remote, params *TriggerParameters) (*Pipeline, error) {
req, err := p.rc.NewRequest("POST", pipelineSlug(remote), params)
if err != nil {
return nil, err
}

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))}
}
8 changes: 3 additions & 5 deletions api/rest/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"net/url"
"strings"
"time"

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

type Client struct {
Expand Down Expand Up @@ -52,7 +54,7 @@ func (c *Client) NewRequest(method string, u *url.URL, payload interface{}) (req

req.Header.Set("Circle-Token", c.circleToken)
req.Header.Set("Accept-Type", "application/json")
req.Header.Set("User-Agent", "circleci-cli")
req.Header.Set("User-Agent", version.UserAgent())
if payload != nil {
req.Header.Set("Content-Type", "application/json")
}
Expand All @@ -79,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
8 changes: 5 additions & 3 deletions api/rest/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

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

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

func TestClient_DoRequest(t *testing.T) {
Expand Down Expand Up @@ -47,7 +49,7 @@ func TestClient_DoRequest(t *testing.T) {
"Circle-Token": {"fake-token"},
"Content-Length": {"20"},
"Content-Type": {"application/json"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), `{"A":"aaa","B":123}`+"\n"))
})
Expand Down Expand Up @@ -76,7 +78,7 @@ func TestClient_DoRequest(t *testing.T) {
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ""))
})
Expand Down Expand Up @@ -108,7 +110,7 @@ func TestClient_DoRequest(t *testing.T) {
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ""))
})
Expand Down
13 changes: 7 additions & 6 deletions api/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"gotest.tools/v3/assert/cmp"

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

func TestRunner_CreateResourceClass(t *testing.T) {
Expand Down Expand Up @@ -48,7 +49,7 @@ func TestRunner_CreateResourceClass(t *testing.T) {
"Circle-Token": {"fake-token"},
"Content-Length": {"86"},
"Content-Type": {"application/json"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), `{"resource_class":"the-namespace/the-resource-class","description":"the-description"}`+"\n"))
})
Expand Down Expand Up @@ -92,7 +93,7 @@ func TestRunner_GetResourceClassesByNamespace(t *testing.T) {
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ``))
})
Expand All @@ -115,7 +116,7 @@ func TestRunner_DeleteResourceClass(t *testing.T) {
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ``))
})
Expand Down Expand Up @@ -155,7 +156,7 @@ func TestRunner_CreateToken(t *testing.T) {
"Circle-Token": {"fake-token"},
"Content-Length": {"80"},
"Content-Type": {"application/json"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), `{"resource_class":"the-namespace/the-resource-class","nickname":"the-nickname"}`+"\n"))
})
Expand Down Expand Up @@ -223,7 +224,7 @@ func TestRunner_GetRunnerTokensByResourceClass(t *testing.T) {
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ""))
})
Expand All @@ -246,7 +247,7 @@ func TestRunner_DeleteToken(t *testing.T) {
"Accept-Encoding": {"gzip"},
"Accept-Type": {"application/json"},
"Circle-Token": {"fake-token"},
"User-Agent": {"circleci-cli"},
"User-Agent": {version.UserAgent()},
}))
assert.Check(t, cmp.Equal(fix.Body(), ``))
})
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))
}

func newOpenCommand() *cobra.Command {
Expand Down
107 changes: 107 additions & 0 deletions cmd/pipeline/pipeline.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package pipeline

import (
"fmt"
"os"
"strconv"
"time"

"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/spf13/cobra"

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

func NewCommand(config *settings.Config, preRunE validator) *cobra.Command {
p := pipeline.New(rest.New(config.Host, config.RestEndpoint, config.Token))
cmd := &cobra.Command{
Use: "pipeline",
Short: "Operate on pipelines",
PreRunE: preRunE,
}

cmd.AddCommand(&cobra.Command{
Use: "trigger",
Short: "Trigger a pipeline for the current project",
PreRunE: preRunE,
RunE: func(cmd *cobra.Command, _ []string) error {
remote, err := git.InferProjectFromGitRemotes()
if err != nil {
return errors.Wrap(err, "this command must be run from inside a git repository")
}

fmt.Printf("Triggering pipeline for: VCS=%q organization=%q project=%q\n", remote.VcsType, remote.Organization, remote.Project)

pipe, err := p.Trigger(*remote, &pipeline.TriggerParameters{})
if err != nil {
return err
}

table := newPipelineTable()
defer table.Render()
appendPipeline(table, *pipe)

return nil
},
})

cmd.AddCommand(&cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List all pipelines for the current project",
PreRunE: preRunE,
RunE: func(cmd *cobra.Command, _ []string) error {
remote, err := git.InferProjectFromGitRemotes()
if err != nil {
return errors.Wrap(err, "this command must be run from inside a git repository")
}

pipes, err := p.Get(*remote)
if err != nil {
return err
}

table := newPipelineTable()
defer table.Render()
for _, pipe := range pipes {
appendPipeline(table, pipe)
}

return nil
},
})

return cmd
}

func newPipelineTable() *tablewriter.Table {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{
"ID",
"Number",
"Created At",
"Updated At",
"State",
"Trigger Type",
"Actor Login",
})
return table
}

func appendPipeline(table *tablewriter.Table, pipe pipeline.Pipeline) {
table.Append([]string{
pipe.ID,
strconv.Itoa(pipe.Number),
pipe.CreatedAt.Format(time.RFC3339),
pipe.UpdatedAt.Format(time.RFC3339),
pipe.State,
pipe.Trigger.Type,
pipe.Trigger.Actor.Login,
})
}

type validator func(cmd *cobra.Command, args []string) error
8 changes: 8 additions & 0 deletions cmd/pipeline/testdata/pipeline-expected-usage.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Usage:
pipeline [command]

Available Commands:
list List all pipelines for the current project
trigger Trigger a pipeline for the current project

Use "pipeline [command] --help" for more information about a command.
5 changes: 5 additions & 0 deletions cmd/pipeline/testdata/pipeline/list-expected-usage.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Usage:
pipeline list

Aliases:
list, ls
2 changes: 2 additions & 0 deletions cmd/pipeline/testdata/pipeline/trigger-expected-usage.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Usage:
pipeline trigger
28 changes: 28 additions & 0 deletions cmd/pipeline/usage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package pipeline

import (
"fmt"
"testing"

"gotest.tools/v3/golden"

"github.com/spf13/cobra"

"github.com/CircleCI-Public/circleci-cli/settings"
)

func TestUsage(t *testing.T) {
preRunE := func(cmd *cobra.Command, args []string) error { return nil }
cmd := NewCommand(&settings.Config{}, preRunE)
testSubCommandUsage(t, cmd.Name(), cmd)
}

func testSubCommandUsage(t *testing.T, prefix string, parent *cobra.Command) {
t.Helper()
t.Run(parent.Name(), func(t *testing.T) {
golden.Assert(t, parent.UsageString(), fmt.Sprintf("%s-expected-usage.txt", prefix))
for _, cmd := range parent.Commands() {
testSubCommandUsage(t, fmt.Sprintf("%s/%s", prefix, cmd.Name()), cmd)
}
})
}
Loading

0 comments on commit a5daeef

Please sign in to comment.