Repository: google/go-github
Pull request #1527 (2020-05-18 18:17:20)

Add support for workflow usage and workflow run usage API endpoints
Fixes: #1526 

Add methods and tests for :
- [x] Workflow usage
- [x] Workflow run usage 

joshuabezaleel: I still have some hesitancy on how should I name the structs since I am not that familiar with GitHub actions workflow and workflow run so I am sorry if I got some wrong. Looking forward to hearing your review @gmlewis ! :slightly_smiling_face: 
codecov[bot]: # [Codecov](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=h1) Report
> Merging [#1527](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=desc) into [master](https://codecov.io/gh/google/go-github/commit/45c502145579f884f5a107c227333fd25090b7e3&el=desc) will **increase** coverage by `5.12%`.
> The diff coverage is `68.00%`.

[![Impacted file tree graph](https://codecov.io/gh/google/go-github/pull/1527/graphs/tree.svg?width=650&height=150&src=pr&token=EgYjGjZlI0)](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #1527      +/-   ##
==========================================
+ Coverage   68.03%   73.16%   +5.12%     
==========================================
  Files          94       94              
  Lines        8528     6890    -1638     
==========================================
- Hits         5802     5041     -761     
+ Misses       1844      963     -881     
- Partials      882      886       +4     
```


| [Impacted Files](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [github/actions\_workflow\_runs.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL2FjdGlvbnNfd29ya2Zsb3dfcnVucy5nbw==) | `64.10% <60.00%> (+5.96%)` | :arrow_up: |
| [github/actions\_workflows.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL2FjdGlvbnNfd29ya2Zsb3dzLmdv) | `67.44% <73.33%> (+10.29%)` | :arrow_up: |
| [github/github.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL2dpdGh1Yi5nbw==) | `89.62% <0.00%> (-0.03%)` | :arrow_down: |
| [github/admin.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL2FkbWluLmdv) | `100.00% <0.00%> (ø)` | |
| [github/strings.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL3N0cmluZ3MuZ28=) | `100.00% <0.00%> (ø)` | |
| [github/activity.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL2FjdGl2aXR5Lmdv) | `100.00% <0.00%> (ø)` | |
| [github/timestamp.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL3RpbWVzdGFtcC5nbw==) | `100.00% <0.00%> (ø)` | |
| [github/admin\_stats.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL2FkbWluX3N0YXRzLmdv) | `100.00% <0.00%> (ø)` | |
| [github/repos\_traffic.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL3JlcG9zX3RyYWZmaWMuZ28=) | `100.00% <0.00%> (ø)` | |
| [github/pulls\_reviewers.go](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree#diff-Z2l0aHViL3B1bGxzX3Jldmlld2Vycy5nbw==) | `100.00% <0.00%> (ø)` | |
| ... and [83 more](https://codecov.io/gh/google/go-github/pull/1527/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=footer). Last update [45c5021...cfee03f](https://codecov.io/gh/google/go-github/pull/1527?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/github/actions_workflow_runs.go b/github/actions_workflow_runs.go
index e118cedc..14d6cd34 100644
--- a/github/actions_workflow_runs.go
+++ b/github/actions_workflow_runs.go
@@ -54,6 +54,25 @@ type ListWorkflowRunsOptions struct {
 	ListOptions
 }
 
+// WorkflowRunUsage represents a usage of a specific workflow run.
+type WorkflowRunUsage struct {
+	Billable      *WorkflowRunEnvironment `json:"billable,omitempty"`
+	RunDurationMS *int64                  `json:"run_duration_ms,omitempty"`
+}
+
+// WorkflowRunEnvironment represents different runner environments available for a workflow run.
+type WorkflowRunEnvironment struct {
+	Ubuntu  *WorkflowRunBill `json:"UBUNTU,omitempty"`
+	MacOS   *WorkflowRunBill `json:"MACOS,omitempty"`
+	Windows *WorkflowRunBill `json:"WINDOWS,omitempty"`
+}
+
+// WorkflowRunBill specifies billable time for a specific environment in a workflow run.
+type WorkflowRunBill struct {
+	TotalMS *int64 `json:"total_ms,omitempty"`
+	Jobs    *int   `json:"jobs,omitempty"`
+}
+
 func (s *ActionsService) listWorkflowRuns(ctx context.Context, endpoint string, opts *ListWorkflowRunsOptions) (*WorkflowRuns, *Response, error) {
 	u, err := addOptions(endpoint, opts)
 	if err != nil {
@@ -193,3 +212,23 @@ func (s *ActionsService) DeleteWorkflowRunLogs(ctx context.Context, owner, repo
 
 	return s.client.Do(ctx, req, nil)
 }
+
+// GetWorkflowRunUsageByID gets a specific workflow usage run by run ID in the unit of billable milliseconds.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow-runs/#get-workflow-run-usage
+func (s *ActionsService) GetWorkflowRunUsageByID(ctx context.Context, owner, repo string, runID int64) (*WorkflowRunUsage, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v/timing", owner, repo, runID)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	workflowRunUsage := new(WorkflowRunUsage)
+	resp, err := s.client.Do(ctx, req, workflowRunUsage)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return workflowRunUsage, resp, nil
+}
diff --git a/github/actions_workflows.go b/github/actions_workflows.go
index 9743f507..6dd02feb 100644
--- a/github/actions_workflows.go
+++ b/github/actions_workflows.go
@@ -30,6 +30,23 @@ type Workflows struct {
 	Workflows  []*Workflow `json:"workflows,omitempty"`
 }
 
+// WorkflowUsage represents a usage of a specific workflow.
+type WorkflowUsage struct {
+	Billable *WorkflowEnvironment `json:"billable,omitempty"`
+}
+
+// WorkflowEnvironment represents different runner environments available for a workflow.
+type WorkflowEnvironment struct {
+	Ubuntu  *WorkflowBill `json:"UBUNTU,omitempty"`
+	MacOS   *WorkflowBill `json:"MACOS,omitempty"`
+	Windows *WorkflowBill `json:"WINDOWS,omitempty"`
+}
+
+// WorkflowBill specifies billable time for a specific environment in a workflow.
+type WorkflowBill struct {
+	TotalMS *int64 `json:"total_ms,omitempty"`
+}
+
 // ListWorkflows lists all workflows in a repository.
 //
 // GitHub API docs: https://developer.github.com/v3/actions/workflows/#list-repository-workflows
@@ -86,3 +103,36 @@ func (s *ActionsService) getWorkflow(ctx context.Context, url string) (*Workflow
 
 	return workflow, resp, nil
 }
+
+// GetWorkflowUsageByID gets a specific workflow usage by ID in the unit of billable milliseconds.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflows/#get-workflow-usage
+func (s *ActionsService) GetWorkflowUsageByID(ctx context.Context, owner, repo string, workflowID int64) (*WorkflowUsage, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/workflows/%v/timing", owner, repo, workflowID)
+
+	return s.getWorkflowUsage(ctx, u)
+}
+
+// GetWorkflowUsageByFileName gets a specific workflow usage by file name in the unit of billable milliseconds.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflows/#get-workflow-usage
+func (s *ActionsService) GetWorkflowUsageByFileName(ctx context.Context, owner, repo, workflowFileName string) (*WorkflowUsage, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/workflows/%v/timing", owner, repo, workflowFileName)
+
+	return s.getWorkflowUsage(ctx, u)
+}
+
+func (s *ActionsService) getWorkflowUsage(ctx context.Context, url string) (*WorkflowUsage, *Response, error) {
+	req, err := s.client.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	workflowUsage := new(WorkflowUsage)
+	resp, err := s.client.Do(ctx, req, workflowUsage)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return workflowUsage, resp, nil
+}
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 884f1acc..a72cfa02 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -14244,6 +14244,38 @@ func (w *Workflow) GetURL() string {
 	return *w.URL
 }
 
+// GetTotalMS returns the TotalMS field if it's non-nil, zero value otherwise.
+func (w *WorkflowBill) GetTotalMS() int64 {
+	if w == nil || w.TotalMS == nil {
+		return 0
+	}
+	return *w.TotalMS
+}
+
+// GetMacOS returns the MacOS field.
+func (w *WorkflowEnvironment) GetMacOS() *WorkflowBill {
+	if w == nil {
+		return nil
+	}
+	return w.MacOS
+}
+
+// GetUbuntu returns the Ubuntu field.
+func (w *WorkflowEnvironment) GetUbuntu() *WorkflowBill {
+	if w == nil {
+		return nil
+	}
+	return w.Ubuntu
+}
+
+// GetWindows returns the Windows field.
+func (w *WorkflowEnvironment) GetWindows() *WorkflowBill {
+	if w == nil {
+		return nil
+	}
+	return w.Windows
+}
+
 // GetCheckRunURL returns the CheckRunURL field if it's non-nil, zero value otherwise.
 func (w *WorkflowJob) GetCheckRunURL() string {
 	if w == nil || w.CheckRunURL == nil {
@@ -14524,6 +14556,46 @@ func (w *WorkflowRun) GetWorkflowURL() string {
 	return *w.WorkflowURL
 }
 
+// GetJobs returns the Jobs field if it's non-nil, zero value otherwise.
+func (w *WorkflowRunBill) GetJobs() int {
+	if w == nil || w.Jobs == nil {
+		return 0
+	}
+	return *w.Jobs
+}
+
+// GetTotalMS returns the TotalMS field if it's non-nil, zero value otherwise.
+func (w *WorkflowRunBill) GetTotalMS() int64 {
+	if w == nil || w.TotalMS == nil {
+		return 0
+	}
+	return *w.TotalMS
+}
+
+// GetMacOS returns the MacOS field.
+func (w *WorkflowRunEnvironment) GetMacOS() *WorkflowRunBill {
+	if w == nil {
+		return nil
+	}
+	return w.MacOS
+}
+
+// GetUbuntu returns the Ubuntu field.
+func (w *WorkflowRunEnvironment) GetUbuntu() *WorkflowRunBill {
+	if w == nil {
+		return nil
+	}
+	return w.Ubuntu
+}
+
+// GetWindows returns the Windows field.
+func (w *WorkflowRunEnvironment) GetWindows() *WorkflowRunBill {
+	if w == nil {
+		return nil
+	}
+	return w.Windows
+}
+
 // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
 func (w *WorkflowRuns) GetTotalCount() int {
 	if w == nil || w.TotalCount == nil {
@@ -14532,6 +14604,22 @@ func (w *WorkflowRuns) GetTotalCount() int {
 	return *w.TotalCount
 }
 
+// GetBillable returns the Billable field.
+func (w *WorkflowRunUsage) GetBillable() *WorkflowRunEnvironment {
+	if w == nil {
+		return nil
+	}
+	return w.Billable
+}
+
+// GetRunDurationMS returns the RunDurationMS field if it's non-nil, zero value otherwise.
+func (w *WorkflowRunUsage) GetRunDurationMS() int64 {
+	if w == nil || w.RunDurationMS == nil {
+		return 0
+	}
+	return *w.RunDurationMS
+}
+
 // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
 func (w *Workflows) GetTotalCount() int {
 	if w == nil || w.TotalCount == nil {
@@ -14539,3 +14627,11 @@ func (w *Workflows) GetTotalCount() int {
 	}
 	return *w.TotalCount
 }
+
+// GetBillable returns the Billable field.
+func (w *WorkflowUsage) GetBillable() *WorkflowEnvironment {
+	if w == nil {
+		return nil
+	}
+	return w.Billable
+}
diff --git a/github/repos.go b/github/repos.go
index 1014d91f..b8c8166b 100644
--- a/github/repos.go
+++ b/github/repos.go
@@ -864,9 +864,9 @@ type BranchRestrictions struct {
 // different from the response structure.
 type BranchRestrictionsRequest struct {
 	// The list of user logins with push access. (Required; use []string{} instead of nil for empty list.)
-	Users []string `json:"users"`
+	Users []string `json:"users,omitempty"`
 	// The list of team slugs with push access. (Required; use []string{} instead of nil for empty list.)
-	Teams []string `json:"teams"`
+	Teams []string `json:"teams,omitempty"`
 	// The list of app slugs with push access.
 	Apps []string `json:"apps,omitempty"`
 }


Test patch:
diff --git a/github/actions_workflow_runs_test.go b/github/actions_workflow_runs_test.go
index d233819b..984babd1 100644
--- a/github/actions_workflow_runs_test.go
+++ b/github/actions_workflow_runs_test.go
@@ -82,7 +82,7 @@ func TestActionsService_GetWorkflowRunByID(t *testing.T) {
 
 	runs, _, err := client.Actions.GetWorkflowRunByID(context.Background(), "o", "r", 29679449)
 	if err != nil {
-		t.Errorf("Actions.ListWorkFlowRunsByFileName returned error: %v", err)
+		t.Errorf("Actions.GetWorkflowRunByID returned error: %v", err)
 	}
 
 	want := &WorkflowRun{
@@ -250,3 +250,40 @@ func TestActionService_DeleteWorkflowRunLogs(t *testing.T) {
 		t.Errorf("DeleteWorkflowRunLogs returned error: %v", err)
 	}
 }
+
+func TestActionsService_GetWorkflowRunUsageByID(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/29679449/timing", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"billable":{"UBUNTU":{"total_ms":180000,"jobs":1},"MACOS":{"total_ms":240000,"jobs":4},"WINDOWS":{"total_ms":300000,"jobs":2}},"run_duration_ms":500000}`)
+	})
+
+	workflowRunUsage, _, err := client.Actions.GetWorkflowRunUsageByID(context.Background(), "o", "r", 29679449)
+	if err != nil {
+		t.Errorf("Actions.GetWorkflowRunUsageByID returned error: %v", err)
+	}
+
+	want := &WorkflowRunUsage{
+		Billable: &WorkflowRunEnvironment{
+			Ubuntu: &WorkflowRunBill{
+				TotalMS: Int64(180000),
+				Jobs:    Int(1),
+			},
+			MacOS: &WorkflowRunBill{
+				TotalMS: Int64(240000),
+				Jobs:    Int(4),
+			},
+			Windows: &WorkflowRunBill{
+				TotalMS: Int64(300000),
+				Jobs:    Int(2),
+			},
+		},
+		RunDurationMS: Int64(500000),
+	}
+
+	if !reflect.DeepEqual(workflowRunUsage, want) {
+		t.Errorf("Actions.GetWorkflowRunUsageByID returned %+v, want %+v", workflowRunUsage, want)
+	}
+}
diff --git a/github/actions_workflows_test.go b/github/actions_workflows_test.go
index e9448645..39b71491 100644
--- a/github/actions_workflows_test.go
+++ b/github/actions_workflows_test.go
@@ -89,3 +89,67 @@ func TestActionsService_GetWorkflowByFileName(t *testing.T) {
 		t.Errorf("Actions.GetWorkflowByFileName returned %+v, want %+v", workflow, want)
 	}
 }
+
+func TestActionsService_GetWorkflowUsageByID(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/workflows/72844/timing", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"billable":{"UBUNTU":{"total_ms":180000},"MACOS":{"total_ms":240000},"WINDOWS":{"total_ms":300000}}}`)
+	})
+
+	workflowUsage, _, err := client.Actions.GetWorkflowUsageByID(context.Background(), "o", "r", 72844)
+	if err != nil {
+		t.Errorf("Actions.GetWorkflowUsageByID returned error: %v", err)
+	}
+
+	want := &WorkflowUsage{
+		Billable: &WorkflowEnvironment{
+			Ubuntu: &WorkflowBill{
+				TotalMS: Int64(180000),
+			},
+			MacOS: &WorkflowBill{
+				TotalMS: Int64(240000),
+			},
+			Windows: &WorkflowBill{
+				TotalMS: Int64(300000),
+			},
+		},
+	}
+	if !reflect.DeepEqual(workflowUsage, want) {
+		t.Errorf("Actions.GetWorkflowUsageByID returned %+v, want %+v", workflowUsage, want)
+	}
+}
+
+func TestActionsService_GetWorkflowUsageByFileName(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/workflows/main.yml/timing", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"billable":{"UBUNTU":{"total_ms":180000},"MACOS":{"total_ms":240000},"WINDOWS":{"total_ms":300000}}}`)
+	})
+
+	workflowUsage, _, err := client.Actions.GetWorkflowUsageByFileName(context.Background(), "o", "r", "main.yml")
+	if err != nil {
+		t.Errorf("Actions.GetWorkflowUsageByFileName returned error: %v", err)
+	}
+
+	want := &WorkflowUsage{
+		Billable: &WorkflowEnvironment{
+			Ubuntu: &WorkflowBill{
+				TotalMS: Int64(180000),
+			},
+			MacOS: &WorkflowBill{
+				TotalMS: Int64(240000),
+			},
+			Windows: &WorkflowBill{
+				TotalMS: Int64(300000),
+			},
+		},
+	}
+	if !reflect.DeepEqual(workflowUsage, want) {
+		t.Errorf("Actions.GetWorkflowUsageByFileName returned %+v, want %+v", workflowUsage, want)
+	}
+}