Repository: google/go-github
Pull request #1449 (2020-03-01 16:30:18)

Implement support for actions workflow runs
Relates to: #1399.

gmlewis: Thank you, @ayzu . It looks like you need to merge master into your PR because it is modifying files it shouldn't which might mean that you based your work on another PR that got changed during a code review. ... If I'm making any sense at all.

I can be more detailed if this doesn't make sense... But that should fix the broken tests.


ayzu: > Thank you, @ayzu . It looks like you need to merge master into your PR because it is modifying files it shouldn't which might mean that you based your work on another PR that got changed during a code review. ... If I'm making any sense at all.
> 
> I can be more detailed if this doesn't make sense... But that should fix the broken tests.

Thank you, @gmlewis 

I didn't run `go-generate` :) By the way, what do you think of adding a Makefile like:

```
do:
	go generate ./...
	go test ./...
	go vet ./...
```
gmlewis: > I didn't run go-generate :) By the way, what do you think of adding a Makefile like:

:smile: Funny you should mention it. I've had this bash script in my directory for a long time:

```bash
$ cat testit.sh 
#!/bin/bash -ex
GO111MODULE=on
go generate ./...
go test ./...
go vet ./...
```

I could probably get rid of that environment variable now.  :smile:

Adding a `Makefile` to Go projects just brings back memories of waiting for C++ compilations, so I'd rather not.  :smile:

ayzu: > This is looking great, @ayzu!
> Just a few tweaks, please, and then I think we'll be ready for a second review before merging.

Thank you for the review, @gmlewis 

I updated the code and renamed the structure to `HeadCommit`. It is great that we can reuse an existing type. 

Patch:
diff --git a/github/actions_workflow_jobs.go b/github/actions_workflow_jobs.go
index d9adad7d..e3593b6a 100644
--- a/github/actions_workflow_jobs.go
+++ b/github/actions_workflow_jobs.go
@@ -96,7 +96,7 @@ func (s *ActionsService) GetWorkflowJobByID(ctx context.Context, owner, repo str
 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)
+	resp, err := s.getWorkflowLogsFromURL(ctx, u, followRedirects)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -108,7 +108,7 @@ func (s *ActionsService) GetWorkflowJobLogs(ctx context.Context, owner, repo str
 	return parsedURL, newResponse(resp), err
 }
 
-func (s *ActionsService) getWorkflowJobLogsFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
+func (s *ActionsService) getWorkflowLogsFromURL(ctx context.Context, u string, followRedirects bool) (*http.Response, error) {
 	req, err := s.client.NewRequest("GET", u, nil)
 	if err != nil {
 		return nil, err
@@ -130,7 +130,7 @@ func (s *ActionsService) getWorkflowJobLogsFromURL(ctx context.Context, u string
 	// 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)
+		resp, err = s.getWorkflowLogsFromURL(ctx, u, false)
 	}
 	return resp, err
 
diff --git a/github/actions_workflow_runs.go b/github/actions_workflow_runs.go
new file mode 100644
index 00000000..b365bb1c
--- /dev/null
+++ b/github/actions_workflow_runs.go
@@ -0,0 +1,181 @@
+// 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"
+)
+
+// WorkflowRun represents a repository action workflow run.
+type WorkflowRun struct {
+	ID             *int64         `json:"id,omitempty"`
+	NodeID         *string        `json:"node_id,omitempty"`
+	HeadBranch     *string        `json:"head_branch,omitempty"`
+	HeadSHA        *string        `json:"head_sha,omitempty"`
+	RunNumber      *int           `json:"run_number,omitempty"`
+	Event          *string        `json:"event,omitempty"`
+	Status         *string        `json:"status,omitempty"`
+	Conclusion     *string        `json:"conclusion,omitempty"`
+	URL            *string        `json:"url,omitempty"`
+	HTMLURL        *string        `json:"html_url,omitempty"`
+	PullRequests   []*PullRequest `json:"pull_requests,omitempty"`
+	CreatedAt      *Timestamp     `json:"created_at,omitempty"`
+	UpdatedAt      *Timestamp     `json:"updated_at,omitempty"`
+	JobsURL        *string        `json:"jobs_url,omitempty"`
+	LogsURL        *string        `json:"logs_url,omitempty"`
+	CheckSuiteURL  *string        `json:"check_suite_url,omitempty"`
+	ArtifactsURL   *string        `json:"artifacts_url,omitempty"`
+	CancelURL      *string        `json:"cancel_url,omitempty"`
+	RerunURL       *string        `json:"rerun_url,omitempty"`
+	HeadCommit     *HeadCommit    `json:"head_commit,omitempty"`
+	WorkflowURL    *string        `json:"workflow_url,omitempty"`
+	Repository     *Repository    `json:"repository,omitempty"`
+	HeadRepository *Repository    `json:"head_repository,omitempty"`
+}
+
+// WorkflowRuns represents a slice of repository action workflow run.
+type WorkflowRuns struct {
+	TotalCount   *int           `json:"total_count,omitempty"`
+	WorkflowRuns []*WorkflowRun `json:"workflow_runs,omitempty"`
+}
+
+// ListWorkflowRunsOptions specifies optional parameters to ListWorkflowRuns.
+type ListWorkflowRunsOptions struct {
+	Actor  string `url:"actor,omitempty"`
+	Branch string `url:"branch,omitempty"`
+	Event  string `url:"event,omitempty"`
+	Status string `url:"status,omitempty"`
+	ListOptions
+}
+
+func (s *ActionsService) listWorkflowRuns(ctx context.Context, endpoint string, opts *ListWorkflowRunsOptions) (*WorkflowRuns, *Response, error) {
+	u, err := addOptions(endpoint, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	runs := new(WorkflowRuns)
+	resp, err := s.client.Do(ctx, req, &runs)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return runs, resp, nil
+}
+
+// ListWorkflowRunsByID lists all workflow runs by workflow ID.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#list-workflow-runs
+func (s *ActionsService) ListWorkflowRunsByID(ctx context.Context, owner, repo string, workflowID int64, opts *ListWorkflowRunsOptions) (*WorkflowRuns, *Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/actions/workflows/%v/runs", owner, repo, workflowID)
+	return s.listWorkflowRuns(ctx, u, opts)
+}
+
+// ListWorkflowRunsByFileName lists all workflow runs by workflow file name.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#list-workflow-runs
+func (s *ActionsService) ListWorkflowRunsByFileName(ctx context.Context, owner, repo, workflowFileName string, opts *ListWorkflowRunsOptions) (*WorkflowRuns, *Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/actions/workflows/%v/runs", owner, repo, workflowFileName)
+	return s.listWorkflowRuns(ctx, u, opts)
+}
+
+// ListRepositoryWorkflowRuns lists all workflow runs for a repository.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#list-repository-workflow-runs
+func (s *ActionsService) ListRepositoryWorkflowRuns(ctx context.Context, owner, repo string, opts *ListOptions) (*WorkflowRuns, *Response, error) {
+	u := fmt.Sprintf("repos/%s/%s/actions/runs", owner, repo)
+	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
+	}
+
+	runs := new(WorkflowRuns)
+	resp, err := s.client.Do(ctx, req, &runs)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return runs, resp, nil
+}
+
+// GetWorkflowRunByID gets a specific workflow run by ID.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#get-a-workflow-run
+func (s *ActionsService) GetWorkflowRunByID(ctx context.Context, owner, repo string, runID int64) (*WorkflowRun, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v", owner, repo, runID)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	run := new(WorkflowRun)
+	resp, err := s.client.Do(ctx, req, run)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return run, resp, nil
+}
+
+// RerunWorkflow re-runs a workflow by ID.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#re-run-a-workflow
+func (s *ActionsService) RerunWorkflowByID(ctx context.Context, owner, repo string, runID int64) (*Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v/rerun", owner, repo, runID)
+
+	req, err := s.client.NewRequest("POST", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(ctx, req, nil)
+}
+
+// CancelWorkflowRunByID cancels a workflow run by ID.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#cancel-a-workflow-run
+func (s *ActionsService) CancelWorkflowRunByID(ctx context.Context, owner, repo string, runID int64) (*Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v/cancel", owner, repo, runID)
+
+	req, err := s.client.NewRequest("POST", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(ctx, req, nil)
+}
+
+// GetWorkflowRunLogs gets a redirect URL to download a plain text file of logs for a workflow run.
+//
+// GitHub API docs: https://developer.github.com/v3/actions/workflow_runs/#list-workflow-run-logs
+func (s *ActionsService) GetWorkflowRunLogs(ctx context.Context, owner, repo string, runID int64, followRedirects bool) (*url.URL, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v/logs", owner, repo, runID)
+
+	resp, err := s.getWorkflowLogsFromURL(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
+}
diff --git a/github/event_types.go b/github/event_types.go
index bf145de0..2c18de6c 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -649,13 +649,13 @@ type PullRequestReviewCommentEvent struct {
 //
 // GitHub API docs: https://developer.github.com/v3/activity/events/types/#pushevent
 type PushEvent struct {
-	PushID       *int64             `json:"push_id,omitempty"`
-	Head         *string            `json:"head,omitempty"`
-	Ref          *string            `json:"ref,omitempty"`
-	Size         *int               `json:"size,omitempty"`
-	Commits      []*PushEventCommit `json:"commits,omitempty"`
-	Before       *string            `json:"before,omitempty"`
-	DistinctSize *int               `json:"distinct_size,omitempty"`
+	PushID       *int64        `json:"push_id,omitempty"`
+	Head         *string       `json:"head,omitempty"`
+	Ref          *string       `json:"ref,omitempty"`
+	Size         *int          `json:"size,omitempty"`
+	Commits      []*HeadCommit `json:"commits,omitempty"`
+	Before       *string       `json:"before,omitempty"`
+	DistinctSize *int          `json:"distinct_size,omitempty"`
 
 	// The following fields are only populated by Webhook events.
 	After        *string              `json:"after,omitempty"`
@@ -665,7 +665,7 @@ type PushEvent struct {
 	BaseRef      *string              `json:"base_ref,omitempty"`
 	Compare      *string              `json:"compare,omitempty"`
 	Repo         *PushEventRepository `json:"repository,omitempty"`
-	HeadCommit   *PushEventCommit     `json:"head_commit,omitempty"`
+	HeadCommit   *HeadCommit          `json:"head_commit,omitempty"`
 	Pusher       *User                `json:"pusher,omitempty"`
 	Sender       *User                `json:"sender,omitempty"`
 	Installation *Installation        `json:"installation,omitempty"`
@@ -675,8 +675,8 @@ func (p PushEvent) String() string {
 	return Stringify(p)
 }
 
-// PushEventCommit represents a git commit in a GitHub PushEvent.
-type PushEventCommit struct {
+// HeadCommit represents a git commit in a GitHub PushEvent.
+type HeadCommit struct {
 	Message  *string       `json:"message,omitempty"`
 	Author   *CommitAuthor `json:"author,omitempty"`
 	URL      *string       `json:"url,omitempty"`
@@ -695,7 +695,7 @@ type PushEventCommit struct {
 	Modified  []string      `json:"modified,omitempty"`
 }
 
-func (p PushEventCommit) String() string {
+func (p HeadCommit) String() string {
 	return Stringify(p)
 }
 
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 2ad2af18..f58ab0f5 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -3628,6 +3628,78 @@ func (g *Grant) GetURL() string {
 	return *g.URL
 }
 
+// GetAuthor returns the Author field.
+func (h *HeadCommit) GetAuthor() *CommitAuthor {
+	if h == nil {
+		return nil
+	}
+	return h.Author
+}
+
+// GetCommitter returns the Committer field.
+func (h *HeadCommit) GetCommitter() *CommitAuthor {
+	if h == nil {
+		return nil
+	}
+	return h.Committer
+}
+
+// GetDistinct returns the Distinct field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetDistinct() bool {
+	if h == nil || h.Distinct == nil {
+		return false
+	}
+	return *h.Distinct
+}
+
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetID() string {
+	if h == nil || h.ID == nil {
+		return ""
+	}
+	return *h.ID
+}
+
+// GetMessage returns the Message field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetMessage() string {
+	if h == nil || h.Message == nil {
+		return ""
+	}
+	return *h.Message
+}
+
+// GetSHA returns the SHA field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetSHA() string {
+	if h == nil || h.SHA == nil {
+		return ""
+	}
+	return *h.SHA
+}
+
+// GetTimestamp returns the Timestamp field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetTimestamp() Timestamp {
+	if h == nil || h.Timestamp == nil {
+		return Timestamp{}
+	}
+	return *h.Timestamp
+}
+
+// GetTreeID returns the TreeID field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetTreeID() string {
+	if h == nil || h.TreeID == nil {
+		return ""
+	}
+	return *h.TreeID
+}
+
+// GetURL returns the URL field if it's non-nil, zero value otherwise.
+func (h *HeadCommit) GetURL() string {
+	if h == nil || h.URL == nil {
+		return ""
+	}
+	return *h.URL
+}
+
 // GetActive returns the Active field if it's non-nil, zero value otherwise.
 func (h *Hook) GetActive() bool {
 	if h == nil || h.Active == nil {
@@ -9197,7 +9269,7 @@ func (p *PushEvent) GetHead() string {
 }
 
 // GetHeadCommit returns the HeadCommit field.
-func (p *PushEvent) GetHeadCommit() *PushEventCommit {
+func (p *PushEvent) GetHeadCommit() *HeadCommit {
 	if p == nil {
 		return nil
 	}
@@ -9260,78 +9332,6 @@ func (p *PushEvent) GetSize() int {
 	return *p.Size
 }
 
-// GetAuthor returns the Author field.
-func (p *PushEventCommit) GetAuthor() *CommitAuthor {
-	if p == nil {
-		return nil
-	}
-	return p.Author
-}
-
-// GetCommitter returns the Committer field.
-func (p *PushEventCommit) GetCommitter() *CommitAuthor {
-	if p == nil {
-		return nil
-	}
-	return p.Committer
-}
-
-// GetDistinct returns the Distinct field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetDistinct() bool {
-	if p == nil || p.Distinct == nil {
-		return false
-	}
-	return *p.Distinct
-}
-
-// GetID returns the ID field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetID() string {
-	if p == nil || p.ID == nil {
-		return ""
-	}
-	return *p.ID
-}
-
-// GetMessage returns the Message field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetMessage() string {
-	if p == nil || p.Message == nil {
-		return ""
-	}
-	return *p.Message
-}
-
-// GetSHA returns the SHA field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetSHA() string {
-	if p == nil || p.SHA == nil {
-		return ""
-	}
-	return *p.SHA
-}
-
-// GetTimestamp returns the Timestamp field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetTimestamp() Timestamp {
-	if p == nil || p.Timestamp == nil {
-		return Timestamp{}
-	}
-	return *p.Timestamp
-}
-
-// GetTreeID returns the TreeID field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetTreeID() string {
-	if p == nil || p.TreeID == nil {
-		return ""
-	}
-	return *p.TreeID
-}
-
-// GetURL returns the URL field if it's non-nil, zero value otherwise.
-func (p *PushEventCommit) GetURL() string {
-	if p == nil || p.URL == nil {
-		return ""
-	}
-	return *p.URL
-}
-
 // GetEmail returns the Email field if it's non-nil, zero value otherwise.
 func (p *PushEventRepoOwner) GetEmail() string {
 	if p == nil || p.Email == nil {
@@ -14164,6 +14164,190 @@ func (w *WorkflowJob) GetURL() string {
 	return *w.URL
 }
 
+// GetArtifactsURL returns the ArtifactsURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetArtifactsURL() string {
+	if w == nil || w.ArtifactsURL == nil {
+		return ""
+	}
+	return *w.ArtifactsURL
+}
+
+// GetCancelURL returns the CancelURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetCancelURL() string {
+	if w == nil || w.CancelURL == nil {
+		return ""
+	}
+	return *w.CancelURL
+}
+
+// GetCheckSuiteURL returns the CheckSuiteURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetCheckSuiteURL() string {
+	if w == nil || w.CheckSuiteURL == nil {
+		return ""
+	}
+	return *w.CheckSuiteURL
+}
+
+// GetConclusion returns the Conclusion field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetConclusion() string {
+	if w == nil || w.Conclusion == nil {
+		return ""
+	}
+	return *w.Conclusion
+}
+
+// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetCreatedAt() Timestamp {
+	if w == nil || w.CreatedAt == nil {
+		return Timestamp{}
+	}
+	return *w.CreatedAt
+}
+
+// GetEvent returns the Event field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetEvent() string {
+	if w == nil || w.Event == nil {
+		return ""
+	}
+	return *w.Event
+}
+
+// GetHeadBranch returns the HeadBranch field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetHeadBranch() string {
+	if w == nil || w.HeadBranch == nil {
+		return ""
+	}
+	return *w.HeadBranch
+}
+
+// GetHeadCommit returns the HeadCommit field.
+func (w *WorkflowRun) GetHeadCommit() *HeadCommit {
+	if w == nil {
+		return nil
+	}
+	return w.HeadCommit
+}
+
+// GetHeadRepository returns the HeadRepository field.
+func (w *WorkflowRun) GetHeadRepository() *Repository {
+	if w == nil {
+		return nil
+	}
+	return w.HeadRepository
+}
+
+// GetHeadSHA returns the HeadSHA field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) 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 *WorkflowRun) 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 *WorkflowRun) GetID() int64 {
+	if w == nil || w.ID == nil {
+		return 0
+	}
+	return *w.ID
+}
+
+// GetJobsURL returns the JobsURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetJobsURL() string {
+	if w == nil || w.JobsURL == nil {
+		return ""
+	}
+	return *w.JobsURL
+}
+
+// GetLogsURL returns the LogsURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetLogsURL() string {
+	if w == nil || w.LogsURL == nil {
+		return ""
+	}
+	return *w.LogsURL
+}
+
+// GetNodeID returns the NodeID field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetNodeID() string {
+	if w == nil || w.NodeID == nil {
+		return ""
+	}
+	return *w.NodeID
+}
+
+// GetRepository returns the Repository field.
+func (w *WorkflowRun) GetRepository() *Repository {
+	if w == nil {
+		return nil
+	}
+	return w.Repository
+}
+
+// GetRerunURL returns the RerunURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetRerunURL() string {
+	if w == nil || w.RerunURL == nil {
+		return ""
+	}
+	return *w.RerunURL
+}
+
+// GetRunNumber returns the RunNumber field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetRunNumber() int {
+	if w == nil || w.RunNumber == nil {
+		return 0
+	}
+	return *w.RunNumber
+}
+
+// GetStatus returns the Status field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetStatus() string {
+	if w == nil || w.Status == nil {
+		return ""
+	}
+	return *w.Status
+}
+
+// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetUpdatedAt() Timestamp {
+	if w == nil || w.UpdatedAt == nil {
+		return Timestamp{}
+	}
+	return *w.UpdatedAt
+}
+
+// GetURL returns the URL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetURL() string {
+	if w == nil || w.URL == nil {
+		return ""
+	}
+	return *w.URL
+}
+
+// GetWorkflowURL returns the WorkflowURL field if it's non-nil, zero value otherwise.
+func (w *WorkflowRun) GetWorkflowURL() string {
+	if w == nil || w.WorkflowURL == nil {
+		return ""
+	}
+	return *w.WorkflowURL
+}
+
+// GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
+func (w *WorkflowRuns) GetTotalCount() int {
+	if w == nil || w.TotalCount == nil {
+		return 0
+	}
+	return *w.TotalCount
+}
+
 // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
 func (w *Workflows) GetTotalCount() int {
 	if w == nil || w.TotalCount == nil {
diff --git a/scrape/forms.go b/scrape/forms.go
index ee559395..6e01171e 100644
--- a/scrape/forms.go
+++ b/scrape/forms.go
@@ -13,6 +13,7 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
+	"strings"
 
 	"github.com/PuerkitoBio/goquery"
 	"golang.org/x/net/html"
@@ -29,8 +30,8 @@ type htmlForm struct {
 }
 
 // parseForms parses and returns all form elements beneath node.  Form values
-// include all nested input elements within the form (textarea is not currently
-// supported).
+// include all input and textarea elements within the form. The values of radio
+// and checkbox inputs are included only if they are checked.
 //
 // In the future, we might want to allow a custom selector to be passed in to
 // further restrict what forms will be returned.
@@ -47,10 +48,28 @@ func parseForms(node *html.Node) (forms []htmlForm) {
 
 		s.Find("input").Each(func(_ int, s *goquery.Selection) {
 			name, _ := s.Attr("name")
+			if name == "" {
+				return
+			}
+
+			typ, _ := s.Attr("type")
+			typ = strings.ToLower(typ)
+			_, checked := s.Attr("checked")
+			if (typ == "radio" || typ == "checkbox") && !checked {
+				return
+			}
+
 			value, _ := s.Attr("value")
-			if name != "" {
-				form.Values.Add(name, value)
+			form.Values.Add(name, value)
+		})
+		s.Find("textarea").Each(func(_ int, s *goquery.Selection) {
+			name, _ := s.Attr("name")
+			if name == "" {
+				return
 			}
+
+			value := s.Text()
+			form.Values.Add(name, value)
 		})
 		forms = append(forms, form)
 	})
@@ -87,7 +106,9 @@ func fetchAndSubmitForm(client *http.Client, urlStr string, setValues func(url.V
 	actionURL = resp.Request.URL.ResolveReference(actionURL)
 
 	// allow caller to fill out the form
-	setValues(form.Values)
+	if setValues != nil {
+		setValues(form.Values)
+	}
 
 	resp, err = client.PostForm(actionURL.String(), form.Values)
 	if err != nil {


Test patch:
diff --git a/github/actions_workflow_runs_test.go b/github/actions_workflow_runs_test.go
new file mode 100644
index 00000000..b31ca18d
--- /dev/null
+++ b/github/actions_workflow_runs_test.go
@@ -0,0 +1,202 @@
+// 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_ListWorkflowRunsByID(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/workflows/29679449/runs", 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,"workflow_runs":[{"id":399444496,"run_number":296,"created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"},{"id":399444497,"run_number":296,"created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}]}`)
+	})
+
+	opts := &ListWorkflowRunsOptions{ListOptions: ListOptions{Page: 2, PerPage: 2}}
+	runs, _, err := client.Actions.ListWorkflowRunsByID(context.Background(), "o", "r", 29679449, opts)
+	if err != nil {
+		t.Errorf("Actions.ListWorkFlowRunsByID returned error: %v", err)
+	}
+
+	want := &WorkflowRuns{
+		TotalCount: Int(4),
+		WorkflowRuns: []*WorkflowRun{
+			{ID: Int64(399444496), RunNumber: Int(296), CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}},
+			{ID: Int64(399444497), RunNumber: Int(296), CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}},
+		},
+	}
+	if !reflect.DeepEqual(runs, want) {
+		t.Errorf("Actions.ListWorkflowRunsByID returned %+v, want %+v", runs, want)
+	}
+}
+
+func TestActionsService_ListWorkflowRunsFileName(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/workflows/29679449/runs", 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,"workflow_runs":[{"id":399444496,"run_number":296,"created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"},{"id":399444497,"run_number":296,"created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}]}`)
+	})
+
+	opts := &ListWorkflowRunsOptions{ListOptions: ListOptions{Page: 2, PerPage: 2}}
+	runs, _, err := client.Actions.ListWorkflowRunsByFileName(context.Background(), "o", "r", "29679449", opts)
+	if err != nil {
+		t.Errorf("Actions.ListWorkFlowRunsByFileName returned error: %v", err)
+	}
+
+	want := &WorkflowRuns{
+		TotalCount: Int(4),
+		WorkflowRuns: []*WorkflowRun{
+			{ID: Int64(399444496), RunNumber: Int(296), CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}},
+			{ID: Int64(399444497), RunNumber: Int(296), CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)}, UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)}},
+		},
+	}
+	if !reflect.DeepEqual(runs, want) {
+		t.Errorf("Actions.ListWorkflowRunsByFileName returned %+v, want %+v", runs, want)
+	}
+}
+
+func TestActionsService_GetWorkflowRunByID(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/29679449", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"id":399444496,"run_number":296,"created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}}`)
+	})
+
+	runs, _, err := client.Actions.GetWorkflowRunByID(context.Background(), "o", "r", 29679449)
+	if err != nil {
+		t.Errorf("Actions.ListWorkFlowRunsByFileName returned error: %v", err)
+	}
+
+	want := &WorkflowRun{
+		ID:        Int64(399444496),
+		RunNumber: Int(296),
+		CreatedAt: &Timestamp{time.Date(2019, time.January, 02, 15, 04, 05, 0, time.UTC)},
+		UpdatedAt: &Timestamp{time.Date(2020, time.January, 02, 15, 04, 05, 0, time.UTC)},
+	}
+
+	if !reflect.DeepEqual(runs, want) {
+		t.Errorf("Actions.GetWorkflowRunByID returned %+v, want %+v", runs, want)
+	}
+}
+
+func TestActionsService_RerunWorkflowRunByID(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/3434/rerun", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		w.WriteHeader(http.StatusCreated)
+	})
+
+	resp, err := client.Actions.RerunWorkflowByID(context.Background(), "o", "r", 3434)
+	if err != nil {
+		t.Errorf("Actions.RerunWorkflowByID returned error: %v", err)
+	}
+	if resp.StatusCode != http.StatusCreated {
+		t.Errorf("Actions.RerunWorkflowRunByID returned status: %d, want %d", resp.StatusCode, http.StatusCreated)
+	}
+}
+
+func TestActionsService_CancelWorkflowRunByID(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/3434/cancel", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		w.WriteHeader(http.StatusAccepted)
+	})
+
+	resp, err := client.Actions.CancelWorkflowRunByID(context.Background(), "o", "r", 3434)
+	if _, ok := err.(*AcceptedError); !ok {
+		t.Errorf("Actions.CancelWorkflowRunByID returned error: %v (want AcceptedError)", err)
+	}
+	if resp.StatusCode != http.StatusAccepted {
+		t.Errorf("Actions.CancelWorkflowRunByID returned status: %d, want %d", resp.StatusCode, http.StatusAccepted)
+	}
+}
+
+func TestActionsService_GetWorkflowRunLogs(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/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.GetWorkflowRunLogs(context.Background(), "o", "r", 399444496, true)
+	if err != nil {
+		t.Errorf("Actions.GetWorkflowRunLogs returned error: %v", err)
+	}
+	if resp.StatusCode != http.StatusFound {
+		t.Errorf("Actions.GetWorkflowRunLogs returned status: %d, want %d", resp.StatusCode, http.StatusFound)
+	}
+	want := "http://github.com/a"
+	if url.String() != want {
+		t.Errorf("Actions.GetWorkflowRunLogs returned %+v, want %+v", url.String(), want)
+	}
+}
+
+func TestActionsService_GetWorkflowRunLogs_StatusMovedPermanently_dontFollowRedirects(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/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.GetWorkflowRunLogs(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_GetWorkflowRunLogs_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/runs/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.GetWorkflowRunLogs(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-stringify_test.go b/github/github-stringify_test.go
index a8050964..375f3b6a 100644
--- a/github/github-stringify_test.go
+++ b/github/github-stringify_test.go
@@ -484,6 +484,24 @@ func TestGrant_String(t *testing.T) {
 	}
 }
 
+func TestHeadCommit_String(t *testing.T) {
+	v := HeadCommit{
+		Message:   String(""),
+		Author:    &CommitAuthor{},
+		URL:       String(""),
+		Distinct:  Bool(false),
+		SHA:       String(""),
+		ID:        String(""),
+		TreeID:    String(""),
+		Timestamp: &Timestamp{},
+		Committer: &CommitAuthor{},
+	}
+	want := `github.HeadCommit{Message:"", Author:github.CommitAuthor{}, URL:"", Distinct:false, SHA:"", ID:"", TreeID:"", Timestamp:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Committer:github.CommitAuthor{}}`
+	if got := v.String(); got != want {
+		t.Errorf("HeadCommit.String = %v, want %v", got, want)
+	}
+}
+
 func TestHook_String(t *testing.T) {
 	v := Hook{
 		URL:    String(""),
@@ -1079,35 +1097,17 @@ func TestPushEvent_String(t *testing.T) {
 		BaseRef:      String(""),
 		Compare:      String(""),
 		Repo:         &PushEventRepository{},
-		HeadCommit:   &PushEventCommit{},
+		HeadCommit:   &HeadCommit{},
 		Pusher:       &User{},
 		Sender:       &User{},
 		Installation: &Installation{},
 	}
-	want := `github.PushEvent{PushID:0, Head:"", Ref:"", Size:0, Before:"", DistinctSize:0, After:"", Created:false, Deleted:false, Forced:false, BaseRef:"", Compare:"", Repo:github.PushEventRepository{}, HeadCommit:github.PushEventCommit{}, Pusher:github.User{}, Sender:github.User{}, Installation:github.Installation{}}`
+	want := `github.PushEvent{PushID:0, Head:"", Ref:"", Size:0, Before:"", DistinctSize:0, After:"", Created:false, Deleted:false, Forced:false, BaseRef:"", Compare:"", Repo:github.PushEventRepository{}, HeadCommit:github.HeadCommit{}, Pusher:github.User{}, Sender:github.User{}, Installation:github.Installation{}}`
 	if got := v.String(); got != want {
 		t.Errorf("PushEvent.String = %v, want %v", got, want)
 	}
 }
 
-func TestPushEventCommit_String(t *testing.T) {
-	v := PushEventCommit{
-		Message:   String(""),
-		Author:    &CommitAuthor{},
-		URL:       String(""),
-		Distinct:  Bool(false),
-		SHA:       String(""),
-		ID:        String(""),
-		TreeID:    String(""),
-		Timestamp: &Timestamp{},
-		Committer: &CommitAuthor{},
-	}
-	want := `github.PushEventCommit{Message:"", Author:github.CommitAuthor{}, URL:"", Distinct:false, SHA:"", ID:"", TreeID:"", Timestamp:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Committer:github.CommitAuthor{}}`
-	if got := v.String(); got != want {
-		t.Errorf("PushEventCommit.String = %v, want %v", got, want)
-	}
-}
-
 func TestRate_String(t *testing.T) {
 	v := Rate{
 		Limit:     0,
diff --git a/github/strings_test.go b/github/strings_test.go
index e16d4a54..f86feb75 100644
--- a/github/strings_test.go
+++ b/github/strings_test.go
@@ -113,7 +113,7 @@ func TestString(t *testing.T) {
 		{DraftReviewComment{Position: Int(1)}, `github.DraftReviewComment{Position:1}`},
 		{PullRequestReviewRequest{Body: String("r")}, `github.PullRequestReviewRequest{Body:"r"}`},
 		{PullRequestReviewDismissalRequest{Message: String("r")}, `github.PullRequestReviewDismissalRequest{Message:"r"}`},
-		{PushEventCommit{SHA: String("s")}, `github.PushEventCommit{SHA:"s"}`},
+		{HeadCommit{SHA: String("s")}, `github.HeadCommit{SHA:"s"}`},
 		{PushEvent{PushID: Int64(1)}, `github.PushEvent{PushID:1}`},
 		{Reference{Ref: String("r")}, `github.Reference{Ref:"r"}`},
 		{ReleaseAsset{ID: Int64(1)}, `github.ReleaseAsset{ID:1}`},
diff --git a/scrape/forms_test.go b/scrape/forms_test.go
index f45010cf..2c7b9fef 100644
--- a/scrape/forms_test.go
+++ b/scrape/forms_test.go
@@ -35,6 +35,38 @@ func Test_ParseForms(t *testing.T) {
 				{Action: "a2", Method: "m2", Values: url.Values{"n2": {"v2"}}},
 			},
 		},
+		{
+			"form with radio buttons (none checked)",
+			`<html><form>
+			   <input type="radio" name="n1" value="v1">
+			   <input type="radio" name="n1" value="v2">
+			   <input type="radio" name="n1" value="v3">
+			</form></html>`,
+			[]htmlForm{{Values: url.Values{}}},
+		},
+		{
+			"form with radio buttons",
+			`<html><form>
+			   <input type="radio" name="n1" value="v1">
+			   <input type="radio" name="n1" value="v2">
+			   <input type="radio" name="n1" value="v3" checked>
+			</form></html>`,
+			[]htmlForm{{Values: url.Values{"n1": {"v3"}}}},
+		},
+		{
+			"form with checkboxes",
+			`<html><form>
+			   <input type="checkbox" name="n1" value="v1" checked>
+			   <input type="checkbox" name="n2" value="v2">
+			   <input type="checkbox" name="n3" value="v3" checked>
+			</form></html>`,
+			[]htmlForm{{Values: url.Values{"n1": {"v1"}, "n3": {"v3"}}}},
+		},
+		{
+			"single form with textarea",
+			`<html><form><textarea name="n1">v1</textarea></form></html>`,
+			[]htmlForm{{Values: url.Values{"n1": {"v1"}}}},
+		},
 	}
 
 	for _, tt := range tests {