diff --git a/github/actions_workflow_jobs.go b/github/actions_workflow_jobs.go new file mode 100644 index 00000000000..d9adad7de4d --- /dev/null +++ b/github/actions_workflow_jobs.go @@ -0,0 +1,137 @@ +// Copyright 2020 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +// TaskStep represents a single task step from a sequence of tasks of a job. +type TaskStep struct { + Name *string `json:"name,omitempty"` + Status *string `json:"status,omitempty"` + Conclusion *string `json:"conclusion,omitempty"` + Number *int64 `json:"number,omitempty"` + StartedAt *Timestamp `json:"started_at,omitempty"` + CompletedAt *Timestamp `json:"completed_at,omitempty"` +} + +// WorkflowJob represents a repository action workflow job. +type WorkflowJob struct { + ID *int64 `json:"id,omitempty"` + RunID *int64 `json:"run_id,omitempty"` + RunURL *string `json:"run_url,omitempty"` + NodeID *string `json:"node_id,omitempty"` + HeadSHA *string `json:"head_sha,omitempty"` + URL *string `json:"url,omitempty"` + HTMLURL *string `json:"html_url,omitempty"` + Status *string `json:"status,omitempty"` + Conclusion *string `json:"conclusion,omitempty"` + StartedAt *Timestamp `json:"started_at,omitempty"` + CompletedAt *Timestamp `json:"completed_at,omitempty"` + Name *string `json:"name,omitempty"` + Steps []*TaskStep `json:"steps,omitempty"` + CheckRunURL *string `json:"check_run_url,omitempty"` +} + +// Jobs represents a slice of repository action workflow job. +type Jobs struct { + TotalCount *int `json:"total_count,omitempty"` + Jobs []*WorkflowJob `json:"jobs,omitempty"` +} + +// ListWorkflowJobs lists all jobs for a workflow run. +// +// GitHub API docs: https://developer.github.com/v3/actions/workflow_jobs/#list-jobs-for-a-workflow-run +func (s *ActionsService) ListWorkflowJobs(ctx context.Context, owner, repo string, runID int64, opts *ListOptions) (*Jobs, *Response, error) { + u := fmt.Sprintf("repos/%s/%s/actions/runs/%v/jobs", owner, repo, runID) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + jobs := new(Jobs) + resp, err := s.client.Do(ctx, req, &jobs) + if err != nil { + return nil, resp, err + } + + return jobs, resp, nil +} + +// GetWorkflowJobByID gets a specific job in a workflow run by ID. +// +// GitHub API docs: https://developer.github.com/v3/actions/workflow_jobs/#list-jobs-for-a-workflow-run +func (s *ActionsService) GetWorkflowJobByID(ctx context.Context, owner, repo string, jobID int64) (*WorkflowJob, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/actions/jobs/%v", owner, repo, jobID) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + job := new(WorkflowJob) + resp, err := s.client.Do(ctx, req, job) + if err != nil { + return nil, resp, err + } + + return job, resp, nil +} + +// GetWorkflowJobLogs gets a redirect URL to download a plain text file of logs for a workflow job. +// +// GitHub API docs: https://developer.github.com/v3/actions/workflow_jobs/#list-workflow-job-logs +func (s *ActionsService) GetWorkflowJobLogs(ctx context.Context, owner, repo string, jobID int64, followRedirects bool) (*url.URL, *Response, error) { + u := fmt.Sprintf("repos/%v/%v/actions/jobs/%v/logs", owner, repo, jobID) + + resp, err := s.getWorkflowJobLogsFromURL(ctx, u, followRedirects) + if err != nil { + return nil, nil, err + } + + if resp.StatusCode != http.StatusFound { + return nil, newResponse(resp), fmt.Errorf("unexpected status code: %s", resp.Status) + } + parsedURL, err := url.Parse(resp.Header.Get("Location")) + return parsedURL, newResponse(resp), err +} + +func (s *ActionsService) getWorkflowJobLogsFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) { + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, err + } + + var resp *http.Response + // Use http.DefaultTransport if no custom Transport is configured + req = withContext(ctx, req) + if s.client.client.Transport == nil { + resp, err = http.DefaultTransport.RoundTrip(req) + } else { + resp, err = s.client.client.Transport.RoundTrip(req) + } + if err != nil { + return nil, err + } + resp.Body.Close() + + // If redirect response is returned, follow it + if followRedirects && resp.StatusCode == http.StatusMovedPermanently { + u = resp.Header.Get("Location") + resp, err = s.getWorkflowJobLogsFromURL(ctx, u, false) + } + return resp, err + +} diff --git a/github/actions_workflow_jobs_test.go b/github/actions_workflow_jobs_test.go new file mode 100644 index 00000000000..36cf7a82ea4 --- /dev/null +++ b/github/actions_workflow_jobs_test.go @@ -0,0 +1,136 @@ +// Copyright 2020 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "fmt" + "net/http" + "net/url" + "reflect" + "testing" + "time" +) + +func TestActionsService_ListWorkflowJobs(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/runs/29679449/jobs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, `{"total_count":4,"jobs":[{"id":399444496,"run_id":29679449,"started_at":"2019-01-02T15:04:05Z","completed_at":"2020-01-02T15:04:05Z"},{"id":399444497,"run_id":29679449,"started_at":"2019-01-02T15:04:05Z","completed_at":"2020-01-02T15:04:05Z"}]}`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + jobs, _, err := client.Actions.ListWorkflowJobs(context.Background(), "o", "r", 29679449, opts) + if err != nil { + t.Errorf("Actions.ListWorkflowJobs returned error: %v", err) + } + + want := &Jobs{ + TotalCount: Int(4), + Jobs: []*WorkflowJob{ + {ID: Int64(399444496), RunID: Int64(29679449), StartedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, CompletedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + {ID: Int64(399444497), RunID: Int64(29679449), StartedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, CompletedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}}, + }, + } + if !reflect.DeepEqual(jobs, want) { + t.Errorf("Actions.ListWorkflowJobs returned %+v, want %+v", jobs, want) + } +} + +func TestActionsService_GetWorkflowJobByID(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/jobs/399444496", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"id":399444496,"started_at":"2019-01-02T15:04:05Z","completed_at":"2020-01-02T15:04:05Z"}`) + }) + + job, _, err := client.Actions.GetWorkflowJobByID(context.Background(), "o", "r", 399444496) + if err != nil { + t.Errorf("Actions.GetWorkflowJobByID returned error: %v", err) + } + + want := &WorkflowJob{ + ID: Int64(399444496), + StartedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, + CompletedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}, + } + if !reflect.DeepEqual(job, want) { + t.Errorf("Actions.GetWorkflowJobByID returned %+v, want %+v", job, want) + } +} + +func TestActionsService_GetWorkflowJobLogs(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/jobs/399444496/logs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + http.Redirect(w, r, "http://github.com/a", http.StatusFound) + }) + + url, resp, err := client.Actions.GetWorkflowJobLogs(context.Background(), "o", "r", 399444496, true) + if err != nil { + t.Errorf("Actions.GetWorkflowJobLogs returned error: %v", err) + } + if resp.StatusCode != http.StatusFound { + t.Errorf("Actions.GetWorkflowJobLogs returned status: %d, want %d", resp.StatusCode, http.StatusFound) + } + want := "http://github.com/a" + if url.String() != want { + t.Errorf("Actions.GetWorkflowJobLogs returned %+v, want %+v", url.String(), want) + } +} + +func TestActionsService_GetWorkflowJobLogs_StatusMovedPermanently_dontFollowRedirects(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/repos/o/r/actions/jobs/399444496/logs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + http.Redirect(w, r, "http://github.com/a", http.StatusMovedPermanently) + }) + + _, resp, _ := client.Actions.GetWorkflowJobLogs(context.Background(), "o", "r", 399444496, false) + if resp.StatusCode != http.StatusMovedPermanently { + t.Errorf("Actions.GetWorkflowJobLogs returned status: %d, want %d", resp.StatusCode, http.StatusMovedPermanently) + } +} + +func TestActionsService_GetWorkflowJobLogs_StatusMovedPermanently_followRedirects(t *testing.T) { + client, mux, serverURL, teardown := setup() + defer teardown() + + // Mock a redirect link, which leads to an archive link + mux.HandleFunc("/repos/o/r/actions/jobs/399444496/logs", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + redirectURL, _ := url.Parse(serverURL + baseURLPath + "/redirect") + http.Redirect(w, r, redirectURL.String(), http.StatusMovedPermanently) + }) + + mux.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + http.Redirect(w, r, "http://github.com/a", http.StatusFound) + }) + + url, resp, err := client.Actions.GetWorkflowJobLogs(context.Background(), "o", "r", 399444496, true) + if err != nil { + t.Errorf("Actions.GetWorkflowJobLogs returned error: %v", err) + } + + if resp.StatusCode != http.StatusFound { + t.Errorf("Actions.GetWorkflowJobLogs returned status: %d, want %d", resp.StatusCode, http.StatusFound) + } + + want := "http://github.com/a" + if url.String() != want { + t.Errorf("Actions.GetWorkflowJobLogs returned %+v, want %+v", url.String(), want) + } +} diff --git a/github/github-accessors.go b/github/github-accessors.go index a1f3e0c19a3..ecd6a762535 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -4972,6 +4972,14 @@ func (i *IssueStats) GetTotalIssues() int { return *i.TotalIssues } +// GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. +func (j *Jobs) GetTotalCount() int { + if j == nil || j.TotalCount == nil { + return 0 + } + return *j.TotalCount +} + // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. func (k *Key) GetCreatedAt() Timestamp { if k == nil || k.CreatedAt == nil { @@ -11916,6 +11924,54 @@ func (t *Tag) GetVerification() *SignatureVerification { return t.Verification } +// GetCompletedAt returns the CompletedAt field if it's non-nil, zero value otherwise. +func (t *TaskStep) GetCompletedAt() Timestamp { + if t == nil || t.CompletedAt == nil { + return Timestamp{} + } + return *t.CompletedAt +} + +// GetConclusion returns the Conclusion field if it's non-nil, zero value otherwise. +func (t *TaskStep) GetConclusion() string { + if t == nil || t.Conclusion == nil { + return "" + } + return *t.Conclusion +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (t *TaskStep) GetName() string { + if t == nil || t.Name == nil { + return "" + } + return *t.Name +} + +// GetNumber returns the Number field if it's non-nil, zero value otherwise. +func (t *TaskStep) GetNumber() int64 { + if t == nil || t.Number == nil { + return 0 + } + return *t.Number +} + +// GetStartedAt returns the StartedAt field if it's non-nil, zero value otherwise. +func (t *TaskStep) GetStartedAt() Timestamp { + if t == nil || t.StartedAt == nil { + return Timestamp{} + } + return *t.StartedAt +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (t *TaskStep) GetStatus() string { + if t == nil || t.Status == nil { + return "" + } + return *t.Status +} + // GetDescription returns the Description field if it's non-nil, zero value otherwise. func (t *Team) GetDescription() string { if t == nil || t.Description == nil { @@ -13932,6 +13988,110 @@ func (w *Workflow) GetURL() string { return *w.URL } +// GetCheckRunURL returns the CheckRunURL field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetCheckRunURL() string { + if w == nil || w.CheckRunURL == nil { + return "" + } + return *w.CheckRunURL +} + +// GetCompletedAt returns the CompletedAt field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetCompletedAt() Timestamp { + if w == nil || w.CompletedAt == nil { + return Timestamp{} + } + return *w.CompletedAt +} + +// GetConclusion returns the Conclusion field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetConclusion() string { + if w == nil || w.Conclusion == nil { + return "" + } + return *w.Conclusion +} + +// GetHeadSHA returns the HeadSHA field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetHeadSHA() string { + if w == nil || w.HeadSHA == nil { + return "" + } + return *w.HeadSHA +} + +// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetHTMLURL() string { + if w == nil || w.HTMLURL == nil { + return "" + } + return *w.HTMLURL +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetID() int64 { + if w == nil || w.ID == nil { + return 0 + } + return *w.ID +} + +// GetName returns the Name field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetName() string { + if w == nil || w.Name == nil { + return "" + } + return *w.Name +} + +// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetNodeID() string { + if w == nil || w.NodeID == nil { + return "" + } + return *w.NodeID +} + +// GetRunID returns the RunID field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetRunID() int64 { + if w == nil || w.RunID == nil { + return 0 + } + return *w.RunID +} + +// GetRunURL returns the RunURL field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetRunURL() string { + if w == nil || w.RunURL == nil { + return "" + } + return *w.RunURL +} + +// GetStartedAt returns the StartedAt field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetStartedAt() Timestamp { + if w == nil || w.StartedAt == nil { + return Timestamp{} + } + return *w.StartedAt +} + +// GetStatus returns the Status field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetStatus() string { + if w == nil || w.Status == nil { + return "" + } + return *w.Status +} + +// GetURL returns the URL field if it's non-nil, zero value otherwise. +func (w *WorkflowJob) GetURL() string { + if w == nil || w.URL == nil { + return "" + } + return *w.URL +} + // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. func (w *Workflows) GetTotalCount() int { if w == nil || w.TotalCount == nil {