Skip to content
Open
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
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ workflows:
- lint
- vulnerability-scan:
context: org-global-employees
filters:
branches:
ignore: /^pull\/.*/
- deploy-test
- docs:
requires:
Expand Down
138 changes: 138 additions & 0 deletions api/job/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package job

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"

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

type StepOutput struct {
Message string `json:"message"`
Time string `json:"time"`
Type string `json:"type"`
Truncated bool `json:"truncated"`
}

type StepAction struct {
Name string `json:"name"`
OutputURL string `json:"output_url"`
Status string `json:"status"`
Type string `json:"type"`
}

type Step struct {
Name string `json:"name"`
Actions []StepAction `json:"actions"`
}

type JobDetails struct {
BuildNum int `json:"build_num"`
Status string `json:"status"`
Steps []Step `json:"steps"`
}

type JobClient interface {
GetJobDetails(vcs string, org string, repo string, jobNumber int) (*JobDetails, error)
GetStepOutput(outputURL string) ([]StepOutput, error)
}

type client struct {
host string
token string
httpClient *http.Client
}

func NewClient(config settings.Config) *client {
httpClient := config.HTTPClient
if httpClient == nil {
httpClient = http.DefaultClient
}
return &client{
host: strings.TrimRight(config.Host, "/"),
token: config.Token,
httpClient: httpClient,
}
}

func (c *client) GetJobDetails(vcs string, org string, repo string, jobNumber int) (*JobDetails, error) {
requestURL := fmt.Sprintf("%s/api/v1.1/project/%s/%s/%s/%d", c.host, vcs, org, repo, jobNumber)
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
if c.token != "" {
req.Header.Set("Circle-Token", c.token)
}

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

if resp.StatusCode >= 400 {
var msg struct {
Message string `json:"message"`
}
if err := json.Unmarshal(body, &msg); err == nil && msg.Message != "" {
return nil, fmt.Errorf("failed to fetch job details: %s", msg.Message)
}
return nil, fmt.Errorf("failed to fetch job details: %s", strings.TrimSpace(string(body)))
}

var details JobDetails
if err := json.Unmarshal(body, &details); err != nil {
return nil, err
}
return &details, nil
}

func (c *client) GetStepOutput(outputURL string) ([]StepOutput, error) {
req, err := http.NewRequest(http.MethodGet, outputURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")

resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

if resp.StatusCode >= 400 {
return nil, fmt.Errorf("failed to fetch step output (%s): %s", sanitizeURL(outputURL), strings.TrimSpace(string(body)))
}

var outputs []StepOutput
if err := json.Unmarshal(body, &outputs); err != nil {
return nil, err
}
return outputs, nil
}

func sanitizeURL(raw string) string {
u, err := url.Parse(raw)
if err != nil {
return "<redacted>"
}
u.RawQuery = ""
u.Fragment = ""
return u.String()
}
37 changes: 37 additions & 0 deletions api/pipeline/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,48 @@ type PipelineRunOptions struct {
ConfigFilePath string
}

type Pipeline struct {
ID string `json:"id"`
ProjectSlug string `json:"project_slug"`
UpdatedAt string `json:"updated_at"`
Number int `json:"number"`
State string `json:"state"`
CreatedAt string `json:"created_at"`
}

type ListPipelinesResponse struct {
NextPageToken string `json:"next_page_token"`
Items []Pipeline `json:"items"`
}

type ListPipelinesOptions struct {
Branch string
PageToken string
}

type Workflow struct {
PipelineID string `json:"pipeline_id"`
ID string `json:"id"`
Name string `json:"name"`
ProjectSlug string `json:"project_slug"`
Status string `json:"status"`
PipelineNumber int `json:"pipeline_number"`
CreatedAt string `json:"created_at"`
StoppedAt string `json:"stopped_at"`
}

type ListWorkflowsResponse struct {
NextPageToken string `json:"next_page_token"`
Items []Workflow `json:"items"`
}

// PipelineClient is the interface to interact with pipeline and it's
// components.
type PipelineClient interface {
CreatePipeline(projectID string, name string, description string, repoID string, configRepoID string, filePath string) (*CreatePipelineInfo, error)
GetPipelineDefinition(options GetPipelineDefinitionOptions) (*PipelineDefinition, error)
ListPipelineDefinitions(projectID string) ([]*PipelineDefinitionInfo, error)
ListPipelinesForProject(projectSlug string, options ListPipelinesOptions) (*ListPipelinesResponse, error)
ListWorkflowsByPipelineId(pipelineID string) ([]Workflow, error)
PipelineRun(options PipelineRunOptions) (*PipelineRunResponse, error)
}
64 changes: 64 additions & 0 deletions api/pipeline/pipelines_rest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package pipeline

import (
"fmt"
"net/url"
)

func (c *pipelineRestClient) ListPipelinesForProject(projectSlug string, options ListPipelinesOptions) (*ListPipelinesResponse, error) {
path := fmt.Sprintf("project/%s/pipeline", projectSlug)

params := url.Values{}
if options.Branch != "" {
params.Set("branch", options.Branch)
}
if options.PageToken != "" {
params.Set("page-token", options.PageToken)
}

req, err := c.client.NewRequest("GET", &url.URL{Path: path, RawQuery: params.Encode()}, nil)
if err != nil {
return nil, err
}

var resp ListPipelinesResponse
_, err = c.client.DoRequest(req, &resp)
if err != nil {
return nil, err
}
return &resp, nil
}

func (c *pipelineRestClient) ListWorkflowsByPipelineId(pipelineID string) ([]Workflow, error) {
items := []Workflow{}
pageToken := ""

for {
path := fmt.Sprintf("pipeline/%s/workflow", pipelineID)

params := url.Values{}
if pageToken != "" {
params.Set("page-token", pageToken)
}

req, err := c.client.NewRequest("GET", &url.URL{Path: path, RawQuery: params.Encode()}, nil)
if err != nil {
return nil, err
}

var resp ListWorkflowsResponse
_, err = c.client.DoRequest(req, &resp)
if err != nil {
return nil, err
}

items = append(items, resp.Items...)

if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}

return items, nil
}
26 changes: 26 additions & 0 deletions api/workflow/workflow.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package workflow

type Job struct {
CanceledBy string `json:"canceled_by"`
Dependencies []string `json:"dependencies"`
JobNumber int `json:"job_number"`
ID string `json:"id"`
StartedAt string `json:"started_at"`
Name string `json:"name"`
ApprovedBy string `json:"approved_by"`
ProjectSlug string `json:"project_slug"`
Status string `json:"status"`
Type string `json:"type"`
Requires map[string]interface{} `json:"requires"`
StoppedAt string `json:"stopped_at"`
ApprovalRequestID string `json:"approval_request_id"`
}

type JobsResponse struct {
NextPageToken string `json:"next_page_token"`
Items []Job `json:"items"`
}

type WorkflowClient interface {
ListWorkflowJobs(workflowID string) ([]Job, error)
}
56 changes: 56 additions & 0 deletions api/workflow/workflow_rest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package workflow

import (
"fmt"
"net/url"

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

type workflowRestClient struct {
client *rest.Client
}

var _ WorkflowClient = &workflowRestClient{}

func NewWorkflowRestClient(config settings.Config) (*workflowRestClient, error) {
client := &workflowRestClient{
client: rest.NewFromConfig(config.Host, &config),
}
return client, nil
}

func (c *workflowRestClient) ListWorkflowJobs(workflowID string) ([]Job, error) {
items := []Job{}
pageToken := ""

for {
path := fmt.Sprintf("workflow/%s/job", workflowID)

params := url.Values{}
if pageToken != "" {
params.Set("page-token", pageToken)
}

req, err := c.client.NewRequest("GET", &url.URL{Path: path, RawQuery: params.Encode()}, nil)
if err != nil {
return nil, err
}

var resp JobsResponse
_, err = c.client.DoRequest(req, &resp)
if err != nil {
return nil, err
}

items = append(items, resp.Items...)

if resp.NextPageToken == "" {
break
}
pageToken = resp.NextPageToken
}

return items, nil
}
Loading