Repository: google/go-github
Pull request #2240 (2021-12-24 08:23:14)

adding examples for app auth
@gmlewis opened a seperate PR from master, have a headache from trying to revert fetches I had pulled. I'd have to cherry pick commits and would rather start fresh.

To recap, this is a simple change to README.md and an example for GitHub app authentication flow so folks can understand how to leverage installations and access tokens.

patalwell: @gmlewis good to go!

Patch:
diff --git a/.gitignore b/.gitignore
index c903cf6b..704f4ef5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,6 @@ vendor/
 .vscode
 # vim temp files
 *.swp
+
+# goenv local version. See https://github.com/syndbg/goenv/blob/master/COMMANDS.md#goenv-local for more info.
+.go-version
diff --git a/README.md b/README.md
index 1eb9054c..184fc104 100644
--- a/README.md
+++ b/README.md
@@ -121,7 +121,10 @@ GitHub Apps authentication can be provided by the [ghinstallation](https://githu
 package.
 
 ```go
-import "github.com/bradleyfalzon/ghinstallation"
+import (
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	"github.com/google/go-github/v41/github"
+)
 
 func main() {
 	// Wrap the shared transport for use with the integration ID 1 authenticating with installation ID 99.
@@ -137,6 +140,9 @@ func main() {
 }
 ```
 
+*Note*: In order to interact with certain APIs, for example writing a file to a repo, one must generate an installation token
+using the installation ID of the GitHub app and authenticate with the OAuth method mentioned above. See the examples.
+
 ### Rate Limiting ###
 
 GitHub imposes a rate limit on all API clients. Unauthenticated clients are
diff --git a/example/newfilewithappauth/main.go b/example/newfilewithappauth/main.go
new file mode 100644
index 00000000..ef124c55
--- /dev/null
+++ b/example/newfilewithappauth/main.go
@@ -0,0 +1,95 @@
+// Copyright 2021 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.
+
+// newfilewithappauth demonstrates the functionality of GitHub's app authentication
+// methods by fetching an installation access token and reauthenticating to GitHub
+// with OAuth configurations.
+package main
+
+import (
+	"context"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"time"
+
+	"github.com/bradleyfalzon/ghinstallation/v2"
+	"github.com/google/go-github/v41/github"
+	"golang.org/x/oauth2"
+)
+
+func main() {
+	const gitHost = "https://git.api.com"
+
+	privatePem, err := ioutil.ReadFile("path/to/pem")
+	if err != nil {
+		log.Fatalf("failed to read pem: %v", err)
+	}
+
+	itr, err := ghinstallation.NewAppsTransport(http.DefaultTransport, 10, privatePem)
+	if err != nil {
+		log.Fatalf("faild to create app transport: %v\n", err)
+	}
+	itr.BaseURL = gitHost
+
+	//create git client with app transport
+	client, err := github.NewEnterpriseClient(
+		gitHost,
+		gitHost,
+		&http.Client{
+			Transport: itr,
+			Timeout:   time.Second * 30,
+		})
+	if err != nil {
+		log.Fatalf("faild to create git client for app: %v\n", err)
+	}
+
+	installations, _, err := client.Apps.ListInstallations(context.Background(), &github.ListOptions{})
+	if err != nil {
+		log.Fatalf("failed to list installations: %v\n", err)
+	}
+
+	//capture our installationId for our app
+	//we need this for the access token
+	var installID int64
+	for _, val := range installations {
+		installID = val.GetID()
+	}
+
+	token, _, err := client.Apps.CreateInstallationToken(
+		context.Background(),
+		installID,
+		&github.InstallationTokenOptions{})
+	if err != nil {
+		log.Fatalf("failed to create installation token: %v\n", err)
+	}
+
+	ts := oauth2.StaticTokenSource(
+		&oauth2.Token{AccessToken: token.GetToken()},
+	)
+	oAuthClient := oauth2.NewClient(context.Background(), ts)
+
+	//create new git hub client with accessToken
+	apiClient, err := github.NewEnterpriseClient(gitHost, gitHost, oAuthClient)
+	if err != nil {
+		log.Fatalf("failed to create new git client with token: %v\n", err)
+	}
+
+	_, resp, err := apiClient.Repositories.CreateFile(
+		context.Background(),
+		"repoOwner",
+		"sample-repo",
+		"example/foo.txt",
+		&github.RepositoryContentFileOptions{
+			Content: []byte("foo"),
+			Message: github.String("sample commit"),
+			SHA:     nil,
+		})
+	if err != nil {
+		log.Fatalf("failed to create new file: %v\n", err)
+	}
+
+	log.Printf("file written status code: %v", resp.StatusCode)
+}
diff --git a/github/checks.go b/github/checks.go
index 07afb7c4..253d3518 100644
--- a/github/checks.go
+++ b/github/checks.go
@@ -234,6 +234,7 @@ type ListCheckRunsOptions struct {
 	CheckName *string `url:"check_name,omitempty"` // Returns check runs with the specified name.
 	Status    *string `url:"status,omitempty"`     // Returns check runs with the specified status. Can be one of "queued", "in_progress", or "completed".
 	Filter    *string `url:"filter,omitempty"`     // Filters check runs by their completed_at timestamp. Can be one of "latest" (returning the most recent check runs) or "all". Default: "latest"
+	AppID     *int64  `url:"app_id,omitempty"`     // Filters check runs by GitHub App ID.
 
 	ListOptions
 }
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 7ed9610d..7ee63226 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -5453,9 +5453,9 @@ func (h *HookDelivery) GetID() int64 {
 }
 
 // GetInstallationID returns the InstallationID field if it's non-nil, zero value otherwise.
-func (h *HookDelivery) GetInstallationID() string {
+func (h *HookDelivery) GetInstallationID() int64 {
 	if h == nil || h.InstallationID == nil {
-		return ""
+		return 0
 	}
 	return *h.InstallationID
 }
@@ -7596,6 +7596,14 @@ func (l *License) GetURL() string {
 	return *l.URL
 }
 
+// GetAppID returns the AppID field if it's non-nil, zero value otherwise.
+func (l *ListCheckRunsOptions) GetAppID() int64 {
+	if l == nil || l.AppID == nil {
+		return 0
+	}
+	return *l.AppID
+}
+
 // GetCheckName returns the CheckName field if it's non-nil, zero value otherwise.
 func (l *ListCheckRunsOptions) GetCheckName() string {
 	if l == nil || l.CheckName == nil {
@@ -16156,6 +16164,14 @@ func (t *Team) GetDescription() string {
 	return *t.Description
 }
 
+// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise.
+func (t *Team) GetHTMLURL() string {
+	if t == nil || t.HTMLURL == nil {
+		return ""
+	}
+	return *t.HTMLURL
+}
+
 // GetID returns the ID field if it's non-nil, zero value otherwise.
 func (t *Team) GetID() int64 {
 	if t == nil || t.ID == nil {
diff --git a/github/repos_hooks_deliveries.go b/github/repos_hooks_deliveries.go
index 12267446..17132695 100644
--- a/github/repos_hooks_deliveries.go
+++ b/github/repos_hooks_deliveries.go
@@ -17,17 +17,17 @@ import (
 // - https://docs.github.com/en/rest/reference/repos#list-deliveries-for-a-repository-webhook
 // - https://docs.github.com/en/rest/reference/repos#get-a-delivery-for-a-repository-webhook
 type HookDelivery struct {
-	ID             *int64     `json:"id"`
-	GUID           *string    `json:"guid"`
-	DeliveredAt    *Timestamp `json:"delivered_at"`
-	Redelivery     *bool      `json:"redelivery"`
-	Duration       *float64   `json:"duration"`
-	Status         *string    `json:"status"`
-	StatusCode     *int       `json:"status_code"`
-	Event          *string    `json:"event"`
-	Action         *string    `json:"action"`
-	InstallationID *string    `json:"installation_id"`
-	RepositoryID   *int64     `json:"repository_id"`
+	ID             *int64     `json:"id,omitempty"`
+	GUID           *string    `json:"guid,omitempty"`
+	DeliveredAt    *Timestamp `json:"delivered_at,omitempty"`
+	Redelivery     *bool      `json:"redelivery,omitempty"`
+	Duration       *float64   `json:"duration,omitempty"`
+	Status         *string    `json:"status,omitempty"`
+	StatusCode     *int       `json:"status_code,omitempty"`
+	Event          *string    `json:"event,omitempty"`
+	Action         *string    `json:"action,omitempty"`
+	InstallationID *int64     `json:"installation_id,omitempty"`
+	RepositoryID   *int64     `json:"repository_id,omitempty"`
 
 	// Request is populated by GetHookDelivery.
 	Request *HookRequest `json:"request,omitempty"`
diff --git a/github/teams.go b/github/teams.go
index 71cbbab6..82d4093b 100644
--- a/github/teams.go
+++ b/github/teams.go
@@ -46,6 +46,7 @@ type Team struct {
 	MembersCount    *int          `json:"members_count,omitempty"`
 	ReposCount      *int          `json:"repos_count,omitempty"`
 	Organization    *Organization `json:"organization,omitempty"`
+	HTMLURL         *string       `json:"html_url,omitempty"`
 	MembersURL      *string       `json:"members_url,omitempty"`
 	RepositoriesURL *string       `json:"repositories_url,omitempty"`
 	Parent          *Team         `json:"parent,omitempty"`
diff --git a/go.mod b/go.mod
index 238a7a61..fba98e39 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,7 @@
 module github.com/google/go-github/v41
 
 require (
+	github.com/bradleyfalzon/ghinstallation/v2 v2.0.3
 	github.com/google/go-cmp v0.5.6
 	github.com/google/go-querystring v1.1.0
 	golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
@@ -9,7 +10,9 @@ require (
 )
 
 require (
+	github.com/golang-jwt/jwt/v4 v4.0.0 // indirect
 	github.com/golang/protobuf v1.3.2 // indirect
+	github.com/google/go-github/v39 v39.0.0 // indirect
 	golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
 	golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect
 	golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
diff --git a/go.sum b/go.sum
index 7e85c44d..95ca91e2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,15 @@
+github.com/bradleyfalzon/ghinstallation/v2 v2.0.3 h1:ywF/8q+GVpvlsEuvRb1SGSDQDUxntW1d4kFu/9q/YAE=
+github.com/bradleyfalzon/ghinstallation/v2 v2.0.3/go.mod h1:tlgi+JWCXnKFx/Y4WtnDbZEINo31N5bcvnCoqieefmk=
+github.com/golang-jwt/jwt/v4 v4.0.0 h1:RAqyYixv1p7uEnocuy8P1nru5wprCh/MH2BIlW5z5/o=
+github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
 github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
 github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github/v39 v39.0.0 h1:pygGA5ySwxEez1N39GnDauD0PaWWuGgayudyZAc941s=
+github.com/google/go-github/v39 v39.0.0/go.mod h1:C1s8C5aCC9L+JXIYpJM5GYytdX52vC1bLvHEF1IhBrE=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=


Test patch:
diff --git a/github/checks_test.go b/github/checks_test.go
index 26da9abf..1e07d314 100644
--- a/github/checks_test.go
+++ b/github/checks_test.go
@@ -319,6 +319,7 @@ func TestChecksService_ListCheckRunsForRef(t *testing.T) {
 			"page":       "1",
 			"status":     "completed",
 			"filter":     "all",
+			"app_id":     "1",
 		})
 		fmt.Fprint(w, `{"total_count":1,
                                 "check_runs": [{
@@ -327,7 +328,9 @@ func TestChecksService_ListCheckRunsForRef(t *testing.T) {
                                     "status": "completed",
                                     "conclusion": "neutral",
                                     "started_at": "2018-05-04T01:14:52Z",
-                                    "completed_at": "2018-05-04T01:14:52Z"}]}`,
+                                    "completed_at": "2018-05-04T01:14:52Z",
+                                    "app": {
+                                      "id": 1}}]}`,
 		)
 	})
 
@@ -335,6 +338,7 @@ func TestChecksService_ListCheckRunsForRef(t *testing.T) {
 		CheckName:   String("testing"),
 		Status:      String("completed"),
 		Filter:      String("all"),
+		AppID:       Int64(1),
 		ListOptions: ListOptions{Page: 1},
 	}
 	ctx := context.Background()
@@ -352,6 +356,7 @@ func TestChecksService_ListCheckRunsForRef(t *testing.T) {
 			CompletedAt: &Timestamp{startedAt},
 			Conclusion:  String("neutral"),
 			HeadSHA:     String("deadbeef"),
+			App:         &App{ID: Int64(1)},
 		}},
 	}
 
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index 2dfed628..c9c27bb4 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -6388,7 +6388,7 @@ func TestHookDelivery_GetID(tt *testing.T) {
 }
 
 func TestHookDelivery_GetInstallationID(tt *testing.T) {
-	var zeroValue string
+	var zeroValue int64
 	h := &HookDelivery{InstallationID: &zeroValue}
 	h.GetInstallationID()
 	h = &HookDelivery{}
@@ -8911,6 +8911,16 @@ func TestLicense_GetURL(tt *testing.T) {
 	l.GetURL()
 }
 
+func TestListCheckRunsOptions_GetAppID(tt *testing.T) {
+	var zeroValue int64
+	l := &ListCheckRunsOptions{AppID: &zeroValue}
+	l.GetAppID()
+	l = &ListCheckRunsOptions{}
+	l.GetAppID()
+	l = nil
+	l.GetAppID()
+}
+
 func TestListCheckRunsOptions_GetCheckName(tt *testing.T) {
 	var zeroValue string
 	l := &ListCheckRunsOptions{CheckName: &zeroValue}
@@ -18900,6 +18910,16 @@ func TestTeam_GetDescription(tt *testing.T) {
 	t.GetDescription()
 }
 
+func TestTeam_GetHTMLURL(tt *testing.T) {
+	var zeroValue string
+	t := &Team{HTMLURL: &zeroValue}
+	t.GetHTMLURL()
+	t = &Team{}
+	t.GetHTMLURL()
+	t = nil
+	t.GetHTMLURL()
+}
+
 func TestTeam_GetID(tt *testing.T) {
 	var zeroValue int64
 	t := &Team{ID: &zeroValue}
diff --git a/github/github-stringify_test.go b/github/github-stringify_test.go
index 41228378..2d961348 100644
--- a/github/github-stringify_test.go
+++ b/github/github-stringify_test.go
@@ -568,12 +568,12 @@ func TestHookDelivery_String(t *testing.T) {
 		StatusCode:     Int(0),
 		Event:          String(""),
 		Action:         String(""),
-		InstallationID: String(""),
+		InstallationID: Int64(0),
 		RepositoryID:   Int64(0),
 		Request:        &HookRequest{},
 		Response:       &HookResponse{},
 	}
-	want := `github.HookDelivery{ID:0, GUID:"", DeliveredAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Redelivery:false, Duration:0, Status:"", StatusCode:0, Event:"", Action:"", InstallationID:"", RepositoryID:0, Request:github.HookRequest{}, Response:github.HookResponse{}}`
+	want := `github.HookDelivery{ID:0, GUID:"", DeliveredAt:github.Timestamp{0001-01-01 00:00:00 +0000 UTC}, Redelivery:false, Duration:0, Status:"", StatusCode:0, Event:"", Action:"", InstallationID:0, RepositoryID:0, Request:github.HookRequest{}, Response:github.HookResponse{}}`
 	if got := v.String(); got != want {
 		t.Errorf("HookDelivery.String = %v, want %v", got, want)
 	}
@@ -1672,12 +1672,13 @@ func TestTeam_String(t *testing.T) {
 		MembersCount:    Int(0),
 		ReposCount:      Int(0),
 		Organization:    &Organization{},
+		HTMLURL:         String(""),
 		MembersURL:      String(""),
 		RepositoriesURL: String(""),
 		Parent:          &Team{},
 		LDAPDN:          String(""),
 	}
-	want := `github.Team{ID:0, NodeID:"", Name:"", Description:"", URL:"", Slug:"", Permission:"", Privacy:"", MembersCount:0, ReposCount:0, Organization:github.Organization{}, MembersURL:"", RepositoriesURL:"", Parent:github.Team{}, LDAPDN:""}`
+	want := `github.Team{ID:0, NodeID:"", Name:"", Description:"", URL:"", Slug:"", Permission:"", Privacy:"", MembersCount:0, ReposCount:0, Organization:github.Organization{}, HTMLURL:"", MembersURL:"", RepositoriesURL:"", Parent:github.Team{}, LDAPDN:""}`
 	if got := v.String(); got != want {
 		t.Errorf("Team.String = %v, want %v", got, want)
 	}
diff --git a/github/repos_hooks_deliveries_test.go b/github/repos_hooks_deliveries_test.go
index 8724e863..a7788831 100644
--- a/github/repos_hooks_deliveries_test.go
+++ b/github/repos_hooks_deliveries_test.go
@@ -288,7 +288,7 @@ func TestHookDelivery_Marshal(t *testing.T) {
 		StatusCode:     Int(1),
 		Event:          String("guid"),
 		Action:         String("guid"),
-		InstallationID: String("guid"),
+		InstallationID: Int64(1),
 		RepositoryID:   Int64(1),
 		Request: &HookRequest{
 			Headers:    header,
@@ -310,7 +310,7 @@ func TestHookDelivery_Marshal(t *testing.T) {
 		"status_code": 1,
 		"event": "guid",
 		"action": "guid",
-		"installation_id": "guid",
+		"installation_id": 1,
 		"repository_id": 1,
 		"request": {
 			"headers": {