Repository: google/go-github
Pull request #1306 (2019-10-10 22:26:52)

Add support for repository dispatches
Add repository dispatch to trigger workflow on GitHub actions.

fleskesvor: Thanks for the feedback, @gmlewis. I've adressed the requested changes and pushed a new commit. I've also read the contributors doc, and I ran `go generate` on the first commit yesterday and the new one today, but the build is still failing. There must be something I'm missing, since I'm pretty new to Go, and the command seems to have no effect at all.
codecov[bot]: # [Codecov](https://codecov.io/gh/google/go-github/pull/1306?src=pr&el=h1) Report
> Merging [#1306](https://codecov.io/gh/google/go-github/pull/1306?src=pr&el=desc) into [master](https://codecov.io/gh/google/go-github/commit/740806ae9dd43b6f68f872c299eedee17bcfa717?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `63.63%`.

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

```diff
@@            Coverage Diff            @@
##           master   #1306      +/-   ##
=========================================
- Coverage   73.42%   73.4%   -0.02%     
=========================================
  Files          86      86              
  Lines        6047    6058      +11     
=========================================
+ Hits         4440    4447       +7     
- Misses        837     839       +2     
- Partials      770     772       +2
```


| [Impacted Files](https://codecov.io/gh/google/go-github/pull/1306?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [github/github.go](https://codecov.io/gh/google/go-github/pull/1306/diff?src=pr&el=tree#diff-Z2l0aHViL2dpdGh1Yi5nbw==) | `89.44% <ø> (ø)` | :arrow_up: |
| [github/repos.go](https://codecov.io/gh/google/go-github/pull/1306/diff?src=pr&el=tree#diff-Z2l0aHViL3JlcG9zLmdv) | `74.89% <63.63%> (-0.28%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/go-github/pull/1306?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/1306?src=pr&el=footer). Last update [740806a...9997098](https://codecov.io/gh/google/go-github/pull/1306?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

gmlewis: Thanks, @fleskesvor.  Just FYI... the `CONTRIBUTING.md` doc should also talk about the use of `gofmt`... you should never have to hunt down and find an extra space... always run your code through `gofmt` and it will never be an issue.

I personally make sure that every editor I use runs `goimports` on saving the file which takes care of all imports and formatting for me.  The Go tooling is awesome.
fleskesvor: Thank you, @gmlewis. :slightly_smiling_face: `goimports` work in my IDE, but I'll try to find out why `gofmt` isn't working as it should. Yes, the tooling is great, and the language has a ton of nice features.
fleskesvor: @gmlewis Yes, you are right. The endpoint now accepts an additional field, `client_payload`, that was added after I opened my PR. It makes the endpoint a lot more useful, since until now I've had to stuff separate data into the `event_type` field separated with a dash, and parse it with `IFS=- read` in my workflows.
yeya24: Any progress for this PR?
abatilo: Looking to follow up here. Anything I can do to help drive this to completion?
fleskesvor: Sorry for taking so long. I've tried to address the missing web hook and recently added client payload to my PR now.
fleskesvor: OK, I've re-addressed the naming issues, and I've also added some tests for scenarios with different client payloads. I don't have enough experience with Go to tell if they are good or terrible though, and couldn't find anything similar to base them on in the existing code base.
fleskesvor: Yes, I've tested it on a GitHub Actions workflow and verified that
- I can filter the action on dispatches:
```
on:
  repository_dispatch:
    types: [name_of_action]
```
- action is set in the context variable `github.event.action`
- that client payload is set in context variables, eg. `github.event.client_payload.foo` or `github.event.client_payload.bar`
- That the full payload, action/event_type and client payload is set in the environment variable `$GITHUB_EVENT_PATH`

I wish I could share an example repo, but I can't for the life of me figure out a clean way to use my branch as a dependency in a project, other than removing `go.mod` and pushing to a separate, throwaway repo, or perhaps merging on master in my own fork. 
gmlewis: Sounds great!  When we get a second LGTM we can merge.
gmlewis: Thank you, @wesleimp!
Merging.

Patch:
diff --git a/github/event.go b/github/event.go
index de20f584..848fa6da 100644
--- a/github/event.go
+++ b/github/event.go
@@ -100,6 +100,8 @@ func (e *Event) ParsePayload() (payload interface{}, err error) {
 		payload = &ReleaseEvent{}
 	case "RepositoryEvent":
 		payload = &RepositoryEvent{}
+	case "RepositoryDispatchEvent":
+		payload = &RepositoryDispatchEvent{}
 	case "RepositoryVulnerabilityAlertEvent":
 		payload = &RepositoryVulnerabilityAlertEvent{}
 	case "StarEvent":
diff --git a/github/event_types.go b/github/event_types.go
index a04c7f62..df12821c 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -7,6 +7,8 @@
 
 package github
 
+import "encoding/json"
+
 // RequestedAction is included in a CheckRunEvent when a user has invoked an action,
 // i.e. when the CheckRunEvent's Action field is "requested_action".
 type RequestedAction struct {
@@ -776,6 +778,22 @@ type RepositoryEvent struct {
 	Installation *Installation `json:"installation,omitempty"`
 }
 
+// RepositoryDispatchEvent is triggered when a client sends a POST request to the repository dispatch event endpoint.
+//
+// GitHub API docs: https://developer.github.com/v3/activity/events/types/#repositorydispatchevent
+type RepositoryDispatchEvent struct {
+	// Action is the event_type that submitted with the repository dispatch payload. Value can be any string.
+	Action        *string         `json:"action,omitempty"`
+	Branch        *string         `json:"branch,omitempty"`
+	ClientPayload json.RawMessage `json:"client_payload,omitempty"`
+	Repo          *Repository     `json:"repository,omitempty"`
+
+	// The following fields are only populated by Webhook events.
+	Org          *Organization `json:"organization,omitempty"`
+	Sender       *User         `json:"sender,omitempty"`
+	Installation *Installation `json:"installation,omitempty"`
+}
+
 // RepositoryVulnerabilityAlertEvent is triggered when a security alert is created, dismissed, or resolved.
 //
 // GitHub API docs: https://developer.github.com/v3/activity/events/types/#repositoryvulnerabilityalertevent
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 9e8a3993..4ff4a3f5 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -2764,6 +2764,14 @@ func (d *DismissedReview) GetState() string {
 	return *d.State
 }
 
+// GetClientPayload returns the ClientPayload field if it's non-nil, zero value otherwise.
+func (d *DispatchRequestOptions) GetClientPayload() json.RawMessage {
+	if d == nil || d.ClientPayload == nil {
+		return json.RawMessage{}
+	}
+	return *d.ClientPayload
+}
+
 // GetBody returns the Body field if it's non-nil, zero value otherwise.
 func (d *DraftReviewComment) GetBody() string {
 	if d == nil || d.Body == nil {
@@ -10756,6 +10764,54 @@ func (r *RepositoryContentResponse) GetContent() *RepositoryContent {
 	return r.Content
 }
 
+// GetAction returns the Action field if it's non-nil, zero value otherwise.
+func (r *RepositoryDispatchEvent) GetAction() string {
+	if r == nil || r.Action == nil {
+		return ""
+	}
+	return *r.Action
+}
+
+// GetBranch returns the Branch field if it's non-nil, zero value otherwise.
+func (r *RepositoryDispatchEvent) GetBranch() string {
+	if r == nil || r.Branch == nil {
+		return ""
+	}
+	return *r.Branch
+}
+
+// GetInstallation returns the Installation field.
+func (r *RepositoryDispatchEvent) GetInstallation() *Installation {
+	if r == nil {
+		return nil
+	}
+	return r.Installation
+}
+
+// GetOrg returns the Org field.
+func (r *RepositoryDispatchEvent) GetOrg() *Organization {
+	if r == nil {
+		return nil
+	}
+	return r.Org
+}
+
+// GetRepo returns the Repo field.
+func (r *RepositoryDispatchEvent) GetRepo() *Repository {
+	if r == nil {
+		return nil
+	}
+	return r.Repo
+}
+
+// GetSender returns the Sender field.
+func (r *RepositoryDispatchEvent) GetSender() *User {
+	if r == nil {
+		return nil
+	}
+	return r.Sender
+}
+
 // GetAction returns the Action field if it's non-nil, zero value otherwise.
 func (r *RepositoryEvent) GetAction() string {
 	if r == nil || r.Action == nil {
diff --git a/github/github.go b/github/github.go
index b9a2488a..9c5040ec 100644
--- a/github/github.go
+++ b/github/github.go
@@ -135,6 +135,9 @@ const (
 
 	// https://developer.github.com/changes/2019-10-03-multi-line-comments/
 	mediaTypeMultiLineCommentsPreview = "application/vnd.github.comfort-fade-preview+json"
+
+	// https://developer.github.com/v3/repos/#create-a-repository-dispatch-event
+	mediaTypeRepositoryDispatchPreview = "application/vnd.github.everest-preview+json"
 )
 
 // A Client manages communication with the GitHub API.
diff --git a/github/messages.go b/github/messages.go
index a87daa3e..759f9e81 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -74,6 +74,7 @@ var (
 		"pull_request":                   "PullRequestEvent",
 		"push":                           "PushEvent",
 		"repository":                     "RepositoryEvent",
+		"repository_dispatch":            "RepositoryDispatchEvent",
 		"repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent",
 		"release":                        "ReleaseEvent",
 		"star":                           "StarEvent",
diff --git a/github/repos.go b/github/repos.go
index fe85108a..c695d8c0 100644
--- a/github/repos.go
+++ b/github/repos.go
@@ -7,6 +7,7 @@ package github
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"strings"
 )
@@ -1427,3 +1428,35 @@ func (s *RepositoriesService) Transfer(ctx context.Context, owner, repo string,
 
 	return r, resp, nil
 }
+
+// DispatchRequestOptions represents a request to trigger a repository_dispatch event.
+type DispatchRequestOptions struct {
+	// EventType is a custom webhook event name. (Required.)
+	EventType string `json:"event_type"`
+	// ClientPayload is a custom JSON payload with extra information about the webhook event.
+	// Defaults to an empty JSON object.
+	ClientPayload *json.RawMessage `json:"client_payload,omitempty"`
+}
+
+// Dispatch triggers a repository_dispatch event in a GitHub Actions workflow.
+//
+// GitHub API docs: https://developer.github.com/v3/repos/#create-a-repository-dispatch-event
+func (s *RepositoriesService) Dispatch(ctx context.Context, owner, repo string, opts DispatchRequestOptions) (*Repository, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/dispatches", owner, repo)
+
+	req, err := s.client.NewRequest("POST", u, &opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeRepositoryDispatchPreview)
+
+	r := new(Repository)
+	resp, err := s.client.Do(ctx, req, r)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return r, resp, nil
+}


Test patch:
diff --git a/github/messages_test.go b/github/messages_test.go
index 30f7d95f..2dad2473 100644
--- a/github/messages_test.go
+++ b/github/messages_test.go
@@ -347,6 +347,10 @@ func TestParseWebHook(t *testing.T) {
 			payload:     &WatchEvent{},
 			messageType: "watch",
 		},
+		{
+			payload:     &RepositoryDispatchEvent{},
+			messageType: "repository_dispatch",
+		},
 	}
 
 	for _, test := range tests {
diff --git a/github/repos_test.go b/github/repos_test.go
index 1d454466..1e48b7e2 100644
--- a/github/repos_test.go
+++ b/github/repos_test.go
@@ -1796,3 +1796,95 @@ func TestRepositoriesService_Transfer(t *testing.T) {
 		t.Errorf("Repositories.Transfer returned %+v, want %+v", got, want)
 	}
 }
+
+func TestRepositoriesService_Dispatch(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	var input DispatchRequestOptions
+
+	mux.HandleFunc("/repos/o/r/dispatches", func(w http.ResponseWriter, r *http.Request) {
+		var v DispatchRequestOptions
+		json.NewDecoder(r.Body).Decode(&v)
+
+		testMethod(t, r, "POST")
+		testHeader(t, r, "Accept", mediaTypeRepositoryDispatchPreview)
+		if !reflect.DeepEqual(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+
+		fmt.Fprint(w, `{"owner":{"login":"a"}}`)
+	})
+
+	ctx := context.Background()
+
+	testCases := []interface{}{
+		nil,
+		struct {
+			Foo string
+		}{
+			Foo: "test",
+		},
+		struct {
+			Bar int
+		}{
+			Bar: 42,
+		},
+		struct {
+			Foo string
+			Bar int
+			Baz bool
+		}{
+			Foo: "test",
+			Bar: 42,
+			Baz: false,
+		},
+	}
+	for _, tc := range testCases {
+
+		if tc == nil {
+			input = DispatchRequestOptions{EventType: "go"}
+		} else {
+			bytes, _ := json.Marshal(tc)
+			payload := json.RawMessage(bytes)
+			input = DispatchRequestOptions{EventType: "go", ClientPayload: &payload}
+		}
+
+		got, _, err := client.Repositories.Dispatch(ctx, "o", "r", input)
+		if err != nil {
+			t.Errorf("Repositories.Dispatch returned error: %v", err)
+		}
+
+		want := &Repository{Owner: &User{Login: String("a")}}
+		if !reflect.DeepEqual(got, want) {
+			t.Errorf("Repositories.Dispatch returned %+v, want %+v", got, want)
+		}
+	}
+
+	// Test s.client.NewRequest failure
+	client.BaseURL.Path = ""
+	got, resp, err := client.Repositories.Dispatch(ctx, "o", "r", input)
+	if got != nil {
+		t.Errorf("client.BaseURL.Path='' Dispatch = %#v, want nil", got)
+	}
+	if resp != nil {
+		t.Errorf("client.BaseURL.Path='' Dispatch resp = %#v, want nil", resp)
+	}
+	if err == nil {
+		t.Error("client.BaseURL.Path='' Dispatch err = nil, want error")
+	}
+
+	// Test s.client.Do failure
+	client.BaseURL.Path = "/api-v3/"
+	client.rateLimits[0].Reset.Time = time.Now().Add(10 * time.Minute)
+	got, resp, err = client.Repositories.Dispatch(ctx, "o", "r", input)
+	if got != nil {
+		t.Errorf("rate.Reset.Time > now Dispatch = %#v, want nil", got)
+	}
+	if want := http.StatusForbidden; resp == nil || resp.Response.StatusCode != want {
+		t.Errorf("rate.Reset.Time > now Dispatch resp = %#v, want StatusCode=%v", resp.Response, want)
+	}
+	if err == nil {
+		t.Error("rate.Reset.Time > now Dispatch err = nil, want error")
+	}
+}