Repository: google/go-github
Pull request #2217 (2021-12-05 16:07:46)

Add support for External Groups
Fixes: #2213 .
https://docs.github.com/en/enterprise-cloud@latest/rest/reference/teams#external-groups

gmlewis: Also, please make sure to run unit tests before pushing (not force-pushing) your upcoming changes.
We squash and merge, so don't worry about the number of commits.
And finally, make sure to run `go generate ./...` and push those changes.
See our CONTRIBUTING.md file for more details.

Patch:
diff --git a/.gitignore b/.gitignore
index aea73858..c903cf6b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,6 @@ coverage.out
 .idea/
 vendor/
 .DS_Store
-.vscode
\ No newline at end of file
+.vscode
+# vim temp files
+*.swp
diff --git a/github/apps_hooks_deliveries.go b/github/apps_hooks_deliveries.go
new file mode 100644
index 00000000..0b631b80
--- /dev/null
+++ b/github/apps_hooks_deliveries.go
@@ -0,0 +1,72 @@
+// 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.
+
+package github
+
+import (
+	"context"
+	"fmt"
+)
+
+// ListHookDeliveries lists deliveries of an App webhook.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/apps#list-deliveries-for-an-app-webhook
+func (s *AppsService) ListHookDeliveries(ctx context.Context, opts *ListCursorOptions) ([]*HookDelivery, *Response, error) {
+	u, err := addOptions("app/hook/deliveries", opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	deliveries := []*HookDelivery{}
+	resp, err := s.client.Do(ctx, req, &deliveries)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return deliveries, resp, nil
+}
+
+// GetHookDelivery returns the App webhook delivery with the specified ID.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/apps#get-a-delivery-for-an-app-webhook
+func (s *AppsService) GetHookDelivery(ctx context.Context, deliveryID int64) (*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("app/hook/deliveries/%v", deliveryID)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	h := new(HookDelivery)
+	resp, err := s.client.Do(ctx, req, h)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return h, resp, nil
+}
+
+// RedeliverHookDelivery redelivers a delivery for an App webhook.
+//
+// GitHub API docs: https://docs.github.com/en/rest/reference/apps#redeliver-a-delivery-for-an-app-webhook
+func (s *AppsService) RedeliverHookDelivery(ctx context.Context, deliveryID int64) (*HookDelivery, *Response, error) {
+	u := fmt.Sprintf("app/hook/deliveries/%v/attempts", deliveryID)
+	req, err := s.client.NewRequest("POST", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	h := new(HookDelivery)
+	resp, err := s.client.Do(ctx, req, h)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return h, resp, nil
+}
diff --git a/github/doc.go b/github/doc.go
index d67e2869..3bf67d7d 100644
--- a/github/doc.go
+++ b/github/doc.go
@@ -125,12 +125,16 @@ from the most recent API call. If a recent enough response isn't
 available, you can use RateLimits to fetch the most up-to-date rate
 limit data for the client.
 
-To detect an API rate limit error, you can check if its type is *github.RateLimitError:
+To detect an API rate limit error, you can check if its type is *github.RateLimitError.
+For secondary rate limits, you can check if its type is *github.AbuseRateLimitError:
 
 	repos, _, err := client.Repositories.List(ctx, "", nil)
 	if _, ok := err.(*github.RateLimitError); ok {
 		log.Println("hit rate limit")
 	}
+	if _, ok := err.(*github.AbuseRateLimitError); ok {
+		log.Println("hit secondary rate limit")
+	}
 
 Learn more about GitHub rate limiting at
 https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#rate-limiting.
diff --git a/github/event_types.go b/github/event_types.go
index f63c8040..122a7da9 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -230,12 +230,13 @@ type GollumEvent struct {
 	Installation *Installation `json:"installation,omitempty"`
 }
 
-// EditChange represents the changes when an issue, pull request, or comment has
-// been edited.
+// EditChange represents the changes when an issue, pull request, comment,
+// or repository has been edited.
 type EditChange struct {
 	Title *EditTitle `json:"title,omitempty"`
 	Body  *EditBody  `json:"body,omitempty"`
 	Base  *EditBase  `json:"base,omitempty"`
+	Repo  *EditRepo  `json:"repository,omitempty"`
 }
 
 // EditTitle represents a pull-request title change.
@@ -259,6 +260,16 @@ type EditRef struct {
 	From *string `json:"from,omitempty"`
 }
 
+// EditRepo represents a change of repository name.
+type EditRepo struct {
+	Name *RepoName `json:"name,omitempty"`
+}
+
+// RepoName represents a change of repository name.
+type RepoName struct {
+	From *string `json:"from,omitempty"`
+}
+
 // EditSHA represents a sha change of a pull-request.
 type EditSHA struct {
 	From *string `json:"from,omitempty"`
@@ -938,6 +949,7 @@ type RepositoryEvent struct {
 	Repo   *Repository `json:"repository,omitempty"`
 
 	// The following fields are only populated by Webhook events.
+	Changes      *EditChange   `json:"changes,omitempty"`
 	Org          *Organization `json:"organization,omitempty"`
 	Sender       *User         `json:"sender,omitempty"`
 	Installation *Installation `json:"installation,omitempty"`
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 166e2ad5..69e3499e 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -4100,6 +4100,14 @@ func (e *EditChange) GetBody() *EditBody {
 	return e.Body
 }
 
+// GetRepo returns the Repo field.
+func (e *EditChange) GetRepo() *EditRepo {
+	if e == nil {
+		return nil
+	}
+	return e.Repo
+}
+
 // GetTitle returns the Title field.
 func (e *EditChange) GetTitle() *EditTitle {
 	if e == nil {
@@ -4116,6 +4124,14 @@ func (e *EditRef) GetFrom() string {
 	return *e.From
 }
 
+// GetName returns the Name field.
+func (e *EditRepo) GetName() *RepoName {
+	if e == nil {
+		return nil
+	}
+	return e.Name
+}
+
 // GetFrom returns the From field if it's non-nil, zero value otherwise.
 func (e *EditSHA) GetFrom() string {
 	if e == nil || e.From == nil {
@@ -4412,6 +4428,78 @@ func (e *Event) GetType() string {
 	return *e.Type
 }
 
+// GetGroupID returns the GroupID field if it's non-nil, zero value otherwise.
+func (e *ExternalGroup) GetGroupID() int64 {
+	if e == nil || e.GroupID == nil {
+		return 0
+	}
+	return *e.GroupID
+}
+
+// GetGroupName returns the GroupName field if it's non-nil, zero value otherwise.
+func (e *ExternalGroup) GetGroupName() string {
+	if e == nil || e.GroupName == nil {
+		return ""
+	}
+	return *e.GroupName
+}
+
+// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
+func (e *ExternalGroup) GetUpdatedAt() Timestamp {
+	if e == nil || e.UpdatedAt == nil {
+		return Timestamp{}
+	}
+	return *e.UpdatedAt
+}
+
+// GetMemberEmail returns the MemberEmail field if it's non-nil, zero value otherwise.
+func (e *ExternalGroupMember) GetMemberEmail() string {
+	if e == nil || e.MemberEmail == nil {
+		return ""
+	}
+	return *e.MemberEmail
+}
+
+// GetMemberID returns the MemberID field if it's non-nil, zero value otherwise.
+func (e *ExternalGroupMember) GetMemberID() int64 {
+	if e == nil || e.MemberID == nil {
+		return 0
+	}
+	return *e.MemberID
+}
+
+// GetMemberLogin returns the MemberLogin field if it's non-nil, zero value otherwise.
+func (e *ExternalGroupMember) GetMemberLogin() string {
+	if e == nil || e.MemberLogin == nil {
+		return ""
+	}
+	return *e.MemberLogin
+}
+
+// GetMemberName returns the MemberName field if it's non-nil, zero value otherwise.
+func (e *ExternalGroupMember) GetMemberName() string {
+	if e == nil || e.MemberName == nil {
+		return ""
+	}
+	return *e.MemberName
+}
+
+// GetTeamID returns the TeamID field if it's non-nil, zero value otherwise.
+func (e *ExternalGroupTeam) GetTeamID() int64 {
+	if e == nil || e.TeamID == nil {
+		return 0
+	}
+	return *e.TeamID
+}
+
+// GetTeamName returns the TeamName field if it's non-nil, zero value otherwise.
+func (e *ExternalGroupTeam) GetTeamName() string {
+	if e == nil || e.TeamName == nil {
+		return ""
+	}
+	return *e.TeamName
+}
+
 // GetHRef returns the HRef field if it's non-nil, zero value otherwise.
 func (f *FeedLink) GetHRef() string {
 	if f == nil || f.HRef == nil {
@@ -7572,6 +7660,14 @@ func (l *ListCollaboratorOptions) GetAffiliation() string {
 	return *l.Affiliation
 }
 
+// GetDisplayName returns the DisplayName field if it's non-nil, zero value otherwise.
+func (l *ListExternalGroupsOptions) GetDisplayName() string {
+	if l == nil || l.DisplayName == nil {
+		return ""
+	}
+	return *l.DisplayName
+}
+
 // GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise.
 func (l *ListRepositories) GetTotalCount() int {
 	if l == nil || l.TotalCount == nil {
@@ -13196,6 +13292,14 @@ func (r *RenameOrgResponse) GetURL() string {
 	return *r.URL
 }
 
+// GetFrom returns the From field if it's non-nil, zero value otherwise.
+func (r *RepoName) GetFrom() string {
+	if r == nil || r.From == nil {
+		return ""
+	}
+	return *r.From
+}
+
 // GetIncompleteResults returns the IncompleteResults field if it's non-nil, zero value otherwise.
 func (r *RepositoriesSearchResult) GetIncompleteResults() bool {
 	if r == nil || r.IncompleteResults == nil {
@@ -14332,6 +14436,14 @@ func (r *RepositoryEvent) GetAction() string {
 	return *r.Action
 }
 
+// GetChanges returns the Changes field.
+func (r *RepositoryEvent) GetChanges() *EditChange {
+	if r == nil {
+		return nil
+	}
+	return r.Changes
+}
+
 // GetInstallation returns the Installation field.
 func (r *RepositoryEvent) GetInstallation() *Installation {
 	if r == nil {
diff --git a/github/github.go b/github/github.go
index a7afd834..26bf78a9 100644
--- a/github/github.go
+++ b/github/github.go
@@ -544,7 +544,7 @@ func (r *Response) populatePageValues() {
 				continue
 			}
 
-			if since != "" {
+			if since != "" && page == "" {
 				page = since
 			}
 
@@ -884,7 +884,7 @@ func (ae *AcceptedError) Is(target error) bool {
 }
 
 // AbuseRateLimitError occurs when GitHub returns 403 Forbidden response with the
-// "documentation_url" field value equal to "https://docs.github.com/en/free-pro-team@latest/rest/reference/#abuse-rate-limits".
+// "documentation_url" field value equal to "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#secondary-rate-limits".
 type AbuseRateLimitError struct {
 	Response *http.Response // HTTP response that caused this error
 	Message  string         `json:"message"` // error message
@@ -1003,7 +1003,9 @@ func CheckResponse(r *http.Response) error {
 			Response: errorResponse.Response,
 			Message:  errorResponse.Message,
 		}
-	case r.StatusCode == http.StatusForbidden && strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits"):
+	case r.StatusCode == http.StatusForbidden &&
+		(strings.HasSuffix(errorResponse.DocumentationURL, "#abuse-rate-limits") ||
+			strings.HasSuffix(errorResponse.DocumentationURL, "#secondary-rate-limits")):
 		abuseRateLimitError := &AbuseRateLimitError{
 			Response: errorResponse.Response,
 			Message:  errorResponse.Message,
diff --git a/github/messages.go b/github/messages.go
index 4a03890c..bad2605b 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -255,14 +255,14 @@ func ValidateSignature(signature string, payload, secretToken []byte) error {
 
 // WebHookType returns the event type of webhook request r.
 //
-// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers
+// GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types
 func WebHookType(r *http.Request) string {
 	return r.Header.Get(EventTypeHeader)
 }
 
 // DeliveryID returns the unique delivery ID of webhook request r.
 //
-// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/hooks/#webhook-headers
+// GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types
 func DeliveryID(r *http.Request) string {
 	return r.Header.Get(DeliveryIDHeader)
 }
diff --git a/github/pulls.go b/github/pulls.go
index 6c866ea8..37fb7413 100644
--- a/github/pulls.go
+++ b/github/pulls.go
@@ -167,9 +167,10 @@ func (s *PullRequestsService) List(ctx context.Context, owner string, repo strin
 
 // ListPullRequestsWithCommit returns pull requests associated with a commit SHA.
 //
-// The results will include open and closed pull requests.
+// The results may include open and closed pull requests.
+// By default, the PullRequestListOptions State filters for "open".
 //
-// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/repos/#list-pull-requests-associated-with-a-commit
+// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/commits/#list-pull-requests-associated-with-a-commit
 func (s *PullRequestsService) ListPullRequestsWithCommit(ctx context.Context, owner, repo, sha string, opts *PullRequestListOptions) ([]*PullRequest, *Response, error) {
 	u := fmt.Sprintf("repos/%v/%v/commits/%v/pulls", owner, repo, sha)
 	u, err := addOptions(u, opts)
diff --git a/github/teams.go b/github/teams.go
index 7a8a5d8a..71cbbab6 100644
--- a/github/teams.go
+++ b/github/teams.go
@@ -854,3 +854,116 @@ func (s *TeamsService) CreateOrUpdateIDPGroupConnectionsBySlug(ctx context.Conte
 
 	return groups, resp, nil
 }
+
+// ExternalGroupMember represents a member of an external group.
+type ExternalGroupMember struct {
+	MemberID    *int64  `json:"member_id,omitempty"`
+	MemberLogin *string `json:"member_login,omitempty"`
+	MemberName  *string `json:"member_name,omitempty"`
+	MemberEmail *string `json:"member_email,omitempty"`
+}
+
+// ExternalGroupTeam represents a team connected to an external group.
+type ExternalGroupTeam struct {
+	TeamID   *int64  `json:"team_id,omitempty"`
+	TeamName *string `json:"team_name,omitempty"`
+}
+
+// ExternalGroup represents an external group.
+type ExternalGroup struct {
+	GroupID   *int64                 `json:"group_id,omitempty"`
+	GroupName *string                `json:"group_name,omitempty"`
+	UpdatedAt *Timestamp             `json:"updated_at,omitempty"`
+	Teams     []*ExternalGroupTeam   `json:"teams,omitempty"`
+	Members   []*ExternalGroupMember `json:"members,omitempty"`
+}
+
+// ExternalGroupList represents a list of external groups.
+type ExternalGroupList struct {
+	Groups []*ExternalGroup `json:"groups"`
+}
+
+// GetExternalGroup fetches an external group.
+//
+// GitHub API docs: https://docs.github.com/en/enterprise-cloud@latest/rest/reference/teams#get-an-external-group
+func (s *TeamsService) GetExternalGroup(ctx context.Context, org string, groupID int64) (*ExternalGroup, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/external-group/%v", org, groupID)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	externalGroup := new(ExternalGroup)
+	resp, err := s.client.Do(ctx, req, externalGroup)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return externalGroup, resp, nil
+}
+
+// ListExternalGroupsOptions specifies the optional parameters to the
+// TeamsService.ListExternalGroups method.
+type ListExternalGroupsOptions struct {
+	DisplayName *string `url:"display_name,omitempty"`
+
+	ListOptions
+}
+
+// ListExternalGroups lists external groups connected to a team on GitHub.
+//
+// GitHub API docs: https://docs.github.com/en/enterprise-cloud@latest/rest/reference/teams#list-external-groups-in-an-organization
+func (s *TeamsService) ListExternalGroups(ctx context.Context, org string, opts *ListExternalGroupsOptions) (*ExternalGroupList, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/external-groups", org)
+	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
+	}
+
+	externalGroups := new(ExternalGroupList)
+	resp, err := s.client.Do(ctx, req, externalGroups)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return externalGroups, resp, nil
+}
+
+// UpdateConnectedExternalGroup updates the connection between an external group and a team.
+//
+// GitHub API docs: https://docs.github.com/en/enterprise-cloud@latest/rest/reference/teams#update-the-connection-between-an-external-group-and-a-team
+func (s *TeamsService) UpdateConnectedExternalGroup(ctx context.Context, org, slug string, eg *ExternalGroup) (*ExternalGroup, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/teams/%v/external-groups", org, slug)
+
+	req, err := s.client.NewRequest("PATCH", u, eg)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	externalGroup := new(ExternalGroup)
+	resp, err := s.client.Do(ctx, req, externalGroup)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return externalGroup, resp, nil
+}
+
+// RemoveConnectedExternalGroup removes the connection between an external group and a team.
+//
+// GitHub API docs: https://docs.github.com/en/enterprise-cloud@latest/rest/reference/teams#remove-the-connection-between-an-external-group-and-a-team
+func (s *TeamsService) RemoveConnectedExternalGroup(ctx context.Context, org, slug string) (*Response, error) {
+	u := fmt.Sprintf("orgs/%v/teams/%v/external-groups", org, slug)
+
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(ctx, req, nil)
+}
diff --git a/go.mod b/go.mod
index fd327a51..238a7a61 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,6 @@
 module github.com/google/go-github/v41
 
 require (
-	github.com/golang/protobuf v1.3.2 // indirect
 	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,4 +8,11 @@ require (
 	google.golang.org/appengine v1.6.7
 )
 
-go 1.16
+require (
+	github.com/golang/protobuf v1.3.2 // 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
+)
+
+go 1.17


Test patch:
diff --git a/github/apps_hooks_deliveries_test.go b/github/apps_hooks_deliveries_test.go
new file mode 100644
index 00000000..38e25540
--- /dev/null
+++ b/github/apps_hooks_deliveries_test.go
@@ -0,0 +1,119 @@
+// 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.
+
+package github
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestAppsService_ListHookDeliveries(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/hook/deliveries", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{"cursor": "v1_12077215967"})
+		fmt.Fprint(w, `[{"id":1}, {"id":2}]`)
+	})
+
+	opts := &ListCursorOptions{Cursor: "v1_12077215967"}
+
+	ctx := context.Background()
+
+	deliveries, _, err := client.Apps.ListHookDeliveries(ctx, opts)
+	if err != nil {
+		t.Errorf("Apps.ListHookDeliveries returned error: %v", err)
+	}
+
+	want := []*HookDelivery{{ID: Int64(1)}, {ID: Int64(2)}}
+	if d := cmp.Diff(deliveries, want); d != "" {
+		t.Errorf("Apps.ListHooks want (-), got (+):\n%s", d)
+	}
+
+	const methodName = "ListHookDeliveries"
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Apps.ListHookDeliveries(ctx, opts)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestAppsService_GetHookDelivery(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/hook/deliveries/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	ctx := context.Background()
+	hook, _, err := client.Apps.GetHookDelivery(ctx, 1)
+	if err != nil {
+		t.Errorf("Apps.GetHookDelivery returned error: %v", err)
+	}
+
+	want := &HookDelivery{ID: Int64(1)}
+	if !cmp.Equal(hook, want) {
+		t.Errorf("Apps.GetHookDelivery returned %+v, want %+v", hook, want)
+	}
+
+	const methodName = "GetHookDelivery"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Apps.GetHookDelivery(ctx, -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Apps.GetHookDelivery(ctx, 1)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestAppsService_RedeliverHookDelivery(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/hook/deliveries/1/attempts", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	ctx := context.Background()
+	hook, _, err := client.Apps.RedeliverHookDelivery(ctx, 1)
+	if err != nil {
+		t.Errorf("Apps.RedeliverHookDelivery returned error: %v", err)
+	}
+
+	want := &HookDelivery{ID: Int64(1)}
+	if !cmp.Equal(hook, want) {
+		t.Errorf("Apps.RedeliverHookDelivery returned %+v, want %+v", hook, want)
+	}
+
+	const methodName = "RedeliverHookDelivery"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Apps.RedeliverHookDelivery(ctx, -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Apps.RedeliverHookDelivery(ctx, 1)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
diff --git a/github/event_types_test.go b/github/event_types_test.go
index d7113b55..bb5dd40d 100644
--- a/github/event_types_test.go
+++ b/github/event_types_test.go
@@ -82,6 +82,28 @@ func TestEditChange_Marshal_BaseChange(t *testing.T) {
 	testJSONMarshal(t, u, want)
 }
 
+func TestEditChange_Marshal_Repo(t *testing.T) {
+	testJSONMarshal(t, &EditChange{}, "{}")
+
+	u := &EditChange{
+		Repo: &EditRepo{
+			Name: &RepoName{
+				From: String("old-repo-name"),
+			},
+		},
+	}
+
+	want := `{
+		"repository": {
+			"name": {
+				"from": "old-repo-name"
+			}
+		}
+	}`
+
+	testJSONMarshal(t, u, want)
+}
+
 func TestProjectChange_Marshal_NameChange(t *testing.T) {
 	testJSONMarshal(t, &ProjectChange{}, "{}")
 
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index fa4dc56c..27e0ab14 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -4796,6 +4796,13 @@ func TestEditChange_GetBody(tt *testing.T) {
 	e.GetBody()
 }
 
+func TestEditChange_GetRepo(tt *testing.T) {
+	e := &EditChange{}
+	e.GetRepo()
+	e = nil
+	e.GetRepo()
+}
+
 func TestEditChange_GetTitle(tt *testing.T) {
 	e := &EditChange{}
 	e.GetTitle()
@@ -4813,6 +4820,13 @@ func TestEditRef_GetFrom(tt *testing.T) {
 	e.GetFrom()
 }
 
+func TestEditRepo_GetName(tt *testing.T) {
+	e := &EditRepo{}
+	e.GetName()
+	e = nil
+	e.GetName()
+}
+
 func TestEditSHA_GetFrom(tt *testing.T) {
 	var zeroValue string
 	e := &EditSHA{From: &zeroValue}
@@ -5168,6 +5182,96 @@ func TestEvent_GetType(tt *testing.T) {
 	e.GetType()
 }
 
+func TestExternalGroup_GetGroupID(tt *testing.T) {
+	var zeroValue int64
+	e := &ExternalGroup{GroupID: &zeroValue}
+	e.GetGroupID()
+	e = &ExternalGroup{}
+	e.GetGroupID()
+	e = nil
+	e.GetGroupID()
+}
+
+func TestExternalGroup_GetGroupName(tt *testing.T) {
+	var zeroValue string
+	e := &ExternalGroup{GroupName: &zeroValue}
+	e.GetGroupName()
+	e = &ExternalGroup{}
+	e.GetGroupName()
+	e = nil
+	e.GetGroupName()
+}
+
+func TestExternalGroup_GetUpdatedAt(tt *testing.T) {
+	var zeroValue Timestamp
+	e := &ExternalGroup{UpdatedAt: &zeroValue}
+	e.GetUpdatedAt()
+	e = &ExternalGroup{}
+	e.GetUpdatedAt()
+	e = nil
+	e.GetUpdatedAt()
+}
+
+func TestExternalGroupMember_GetMemberEmail(tt *testing.T) {
+	var zeroValue string
+	e := &ExternalGroupMember{MemberEmail: &zeroValue}
+	e.GetMemberEmail()
+	e = &ExternalGroupMember{}
+	e.GetMemberEmail()
+	e = nil
+	e.GetMemberEmail()
+}
+
+func TestExternalGroupMember_GetMemberID(tt *testing.T) {
+	var zeroValue int64
+	e := &ExternalGroupMember{MemberID: &zeroValue}
+	e.GetMemberID()
+	e = &ExternalGroupMember{}
+	e.GetMemberID()
+	e = nil
+	e.GetMemberID()
+}
+
+func TestExternalGroupMember_GetMemberLogin(tt *testing.T) {
+	var zeroValue string
+	e := &ExternalGroupMember{MemberLogin: &zeroValue}
+	e.GetMemberLogin()
+	e = &ExternalGroupMember{}
+	e.GetMemberLogin()
+	e = nil
+	e.GetMemberLogin()
+}
+
+func TestExternalGroupMember_GetMemberName(tt *testing.T) {
+	var zeroValue string
+	e := &ExternalGroupMember{MemberName: &zeroValue}
+	e.GetMemberName()
+	e = &ExternalGroupMember{}
+	e.GetMemberName()
+	e = nil
+	e.GetMemberName()
+}
+
+func TestExternalGroupTeam_GetTeamID(tt *testing.T) {
+	var zeroValue int64
+	e := &ExternalGroupTeam{TeamID: &zeroValue}
+	e.GetTeamID()
+	e = &ExternalGroupTeam{}
+	e.GetTeamID()
+	e = nil
+	e.GetTeamID()
+}
+
+func TestExternalGroupTeam_GetTeamName(tt *testing.T) {
+	var zeroValue string
+	e := &ExternalGroupTeam{TeamName: &zeroValue}
+	e.GetTeamName()
+	e = &ExternalGroupTeam{}
+	e.GetTeamName()
+	e = nil
+	e.GetTeamName()
+}
+
 func TestFeedLink_GetHRef(tt *testing.T) {
 	var zeroValue string
 	f := &FeedLink{HRef: &zeroValue}
@@ -8887,6 +8991,16 @@ func TestListCollaboratorOptions_GetAffiliation(tt *testing.T) {
 	l.GetAffiliation()
 }
 
+func TestListExternalGroupsOptions_GetDisplayName(tt *testing.T) {
+	var zeroValue string
+	l := &ListExternalGroupsOptions{DisplayName: &zeroValue}
+	l.GetDisplayName()
+	l = &ListExternalGroupsOptions{}
+	l.GetDisplayName()
+	l = nil
+	l.GetDisplayName()
+}
+
 func TestListRepositories_GetTotalCount(tt *testing.T) {
 	var zeroValue int
 	l := &ListRepositories{TotalCount: &zeroValue}
@@ -15374,6 +15488,16 @@ func TestRenameOrgResponse_GetURL(tt *testing.T) {
 	r.GetURL()
 }
 
+func TestRepoName_GetFrom(tt *testing.T) {
+	var zeroValue string
+	r := &RepoName{From: &zeroValue}
+	r.GetFrom()
+	r = &RepoName{}
+	r.GetFrom()
+	r = nil
+	r.GetFrom()
+}
+
 func TestRepositoriesSearchResult_GetIncompleteResults(tt *testing.T) {
 	var zeroValue bool
 	r := &RepositoriesSearchResult{IncompleteResults: &zeroValue}
@@ -16731,6 +16855,13 @@ func TestRepositoryEvent_GetAction(tt *testing.T) {
 	r.GetAction()
 }
 
+func TestRepositoryEvent_GetChanges(tt *testing.T) {
+	r := &RepositoryEvent{}
+	r.GetChanges()
+	r = nil
+	r.GetChanges()
+}
+
 func TestRepositoryEvent_GetInstallation(tt *testing.T) {
 	r := &RepositoryEvent{}
 	r.GetInstallation()
diff --git a/github/github_test.go b/github/github_test.go
index 3a0c2931..06059e6d 100644
--- a/github/github_test.go
+++ b/github/github_test.go
@@ -677,6 +677,35 @@ func TestResponse_populateSinceValues(t *testing.T) {
 	}
 }
 
+func TestResponse_SinceWithPage(t *testing.T) {
+	r := http.Response{
+		Header: http.Header{
+			"Link": {`<https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=1>; rel="first",` +
+				` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=2>; rel="prev",` +
+				` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=4>; rel="next",` +
+				` <https://api.github.com/?since=2021-12-04T10%3A43%3A42Z&page=5>; rel="last"`,
+			},
+		},
+	}
+
+	response := newResponse(&r)
+	if got, want := response.FirstPage, 1; got != want {
+		t.Errorf("response.FirstPage: %v, want %v", got, want)
+	}
+	if got, want := response.PrevPage, 2; want != got {
+		t.Errorf("response.PrevPage: %v, want %v", got, want)
+	}
+	if got, want := response.NextPage, 4; want != got {
+		t.Errorf("response.NextPage: %v, want %v", got, want)
+	}
+	if got, want := response.LastPage, 5; want != got {
+		t.Errorf("response.LastPage: %v, want %v", got, want)
+	}
+	if got, want := response.NextPageToken, ""; want != got {
+		t.Errorf("response.NextPageToken: %v, want %v", got, want)
+	}
+}
+
 func TestResponse_cursorPagination(t *testing.T) {
 	r := http.Response{
 		Header: http.Header{
diff --git a/github/teams_test.go b/github/teams_test.go
index 26cf2c03..83f559be 100644
--- a/github/teams_test.go
+++ b/github/teams_test.go
@@ -1745,3 +1745,356 @@ func TestIDPGroup_Marshal(t *testing.T) {
 
 	testJSONMarshal(t, u, want)
 }
+
+func TestTeamsService_GetExternalGroup(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/external-group/123", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{
+			"group_id": 123,
+			"group_name": "Octocat admins",
+			"updated_at": "2006-01-02T15:04:05Z",
+			"teams": [
+				{
+					"team_id": 1,
+					"team_name": "team-test"
+				},
+				{
+					"team_id": 2,
+					"team_name": "team-test2"
+				}
+			],
+			"members": [
+				{
+					"member_id": 1,
+					"member_login": "mona-lisa_eocsaxrs",
+					"member_name": "Mona Lisa",
+					"member_email": "mona_lisa@github.com"
+				},
+				{
+					"member_id": 2,
+					"member_login": "octo-lisa_eocsaxrs",
+					"member_name": "Octo Lisa",
+					"member_email": "octo_lisa@github.com"
+				}
+			]
+		}`)
+	})
+
+	ctx := context.Background()
+	externalGroup, _, err := client.Teams.GetExternalGroup(ctx, "o", 123)
+	if err != nil {
+		t.Errorf("Teams.GetExternalGroup returned error: %v", err)
+	}
+
+	want := &ExternalGroup{
+		GroupID:   Int64(123),
+		GroupName: String("Octocat admins"),
+		UpdatedAt: &Timestamp{Time: referenceTime},
+		Teams: []*ExternalGroupTeam{
+			{
+				TeamID:   Int64(1),
+				TeamName: String("team-test"),
+			},
+			{
+				TeamID:   Int64(2),
+				TeamName: String("team-test2"),
+			},
+		},
+		Members: []*ExternalGroupMember{
+			{
+				MemberID:    Int64(1),
+				MemberLogin: String("mona-lisa_eocsaxrs"),
+				MemberName:  String("Mona Lisa"),
+				MemberEmail: String("mona_lisa@github.com"),
+			},
+			{
+				MemberID:    Int64(2),
+				MemberLogin: String("octo-lisa_eocsaxrs"),
+				MemberName:  String("Octo Lisa"),
+				MemberEmail: String("octo_lisa@github.com"),
+			},
+		},
+	}
+	if !cmp.Equal(externalGroup, want) {
+		t.Errorf("Teams.GetExternalGroup returned %+v, want %+v", externalGroup, want)
+	}
+
+	const methodName = "GetExternalGroup"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Teams.GetExternalGroup(ctx, "\n", -1)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Teams.GetExternalGroup(ctx, "o", 123)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestTeamsService_GetExternalGroup_notFound(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/external-group/123", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		w.WriteHeader(http.StatusNotFound)
+	})
+
+	ctx := context.Background()
+	eg, resp, err := client.Teams.GetExternalGroup(ctx, "o", 123)
+	if err == nil {
+		t.Errorf("Expected HTTP 404 response")
+	}
+	if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want {
+		t.Errorf("Teams.GetExternalGroup returned status %d, want %d", got, want)
+	}
+	if eg != nil {
+		t.Errorf("Teams.GetExternalGroup returned %+v, want nil", eg)
+	}
+}
+
+func TestTeamsService_ListExternalGroups(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/external-groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{
+			"groups": [
+				{
+					"group_id": 123,
+					"group_name": "Octocat admins",
+					"updated_at": "2006-01-02T15:04:05Z"
+				}
+			]
+		}`)
+	})
+
+	ctx := context.Background()
+	opts := &ListExternalGroupsOptions{
+		DisplayName: String("Octocat"),
+	}
+	list, _, err := client.Teams.ListExternalGroups(ctx, "o", opts)
+	if err != nil {
+		t.Errorf("Teams.ListExternalGroups returned error: %v", err)
+	}
+
+	want := &ExternalGroupList{
+		Groups: []*ExternalGroup{
+			{
+				GroupID:   Int64(123),
+				GroupName: String("Octocat admins"),
+				UpdatedAt: &Timestamp{Time: referenceTime},
+			},
+		},
+	}
+	if !cmp.Equal(list, want) {
+		t.Errorf("Teams.ListExternalGroups returned %+v, want %+v", list, want)
+	}
+
+	const methodName = "ListExternalGroups"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Teams.ListExternalGroups(ctx, "\n", opts)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Teams.ListExternalGroups(ctx, "o", opts)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestTeamsService_ListExternalGroups_notFound(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/external-groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		w.WriteHeader(http.StatusNotFound)
+	})
+
+	ctx := context.Background()
+	eg, resp, err := client.Teams.ListExternalGroups(ctx, "o", nil)
+	if err == nil {
+		t.Errorf("Expected HTTP 404 response")
+	}
+	if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want {
+		t.Errorf("Teams.ListExternalGroups returned status %d, want %d", got, want)
+	}
+	if eg != nil {
+		t.Errorf("Teams.ListExternalGroups returned %+v, want nil", eg)
+	}
+}
+
+func TestTeamsService_UpdateConnectedExternalGroup(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/teams/t/external-groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PATCH")
+		fmt.Fprint(w, `{
+			"group_id": 123,
+			"group_name": "Octocat admins",
+			"updated_at": "2006-01-02T15:04:05Z",
+			"teams": [
+				{
+					"team_id": 1,
+					"team_name": "team-test"
+				},
+				{
+					"team_id": 2,
+					"team_name": "team-test2"
+				}
+			],
+			"members": [
+				{
+					"member_id": 1,
+					"member_login": "mona-lisa_eocsaxrs",
+					"member_name": "Mona Lisa",
+					"member_email": "mona_lisa@github.com"
+				},
+				{
+					"member_id": 2,
+					"member_login": "octo-lisa_eocsaxrs",
+					"member_name": "Octo Lisa",
+					"member_email": "octo_lisa@github.com"
+				}
+			]
+		}`)
+	})
+
+	ctx := context.Background()
+	body := &ExternalGroup{
+		GroupID: Int64(123),
+	}
+	externalGroup, _, err := client.Teams.UpdateConnectedExternalGroup(ctx, "o", "t", body)
+	if err != nil {
+		t.Errorf("Teams.UpdateConnectedExternalGroup returned error: %v", err)
+	}
+
+	want := &ExternalGroup{
+		GroupID:   Int64(123),
+		GroupName: String("Octocat admins"),
+		UpdatedAt: &Timestamp{Time: referenceTime},
+		Teams: []*ExternalGroupTeam{
+			{
+				TeamID:   Int64(1),
+				TeamName: String("team-test"),
+			},
+			{
+				TeamID:   Int64(2),
+				TeamName: String("team-test2"),
+			},
+		},
+		Members: []*ExternalGroupMember{
+			{
+				MemberID:    Int64(1),
+				MemberLogin: String("mona-lisa_eocsaxrs"),
+				MemberName:  String("Mona Lisa"),
+				MemberEmail: String("mona_lisa@github.com"),
+			},
+			{
+				MemberID:    Int64(2),
+				MemberLogin: String("octo-lisa_eocsaxrs"),
+				MemberName:  String("Octo Lisa"),
+				MemberEmail: String("octo_lisa@github.com"),
+			},
+		},
+	}
+	if !cmp.Equal(externalGroup, want) {
+		t.Errorf("Teams.GetExternalGroup returned %+v, want %+v", externalGroup, want)
+	}
+
+	const methodName = "UpdateConnectedExternalGroup"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Teams.UpdateConnectedExternalGroup(ctx, "\n", "\n", body)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Teams.UpdateConnectedExternalGroup(ctx, "o", "t", body)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestTeamsService_UpdateConnectedExternalGroup_notFound(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/teams/t/external-groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PATCH")
+		w.WriteHeader(http.StatusNotFound)
+	})
+
+	ctx := context.Background()
+	body := &ExternalGroup{
+		GroupID: Int64(123),
+	}
+	eg, resp, err := client.Teams.UpdateConnectedExternalGroup(ctx, "o", "t", body)
+	if err == nil {
+		t.Errorf("Expected HTTP 404 response")
+	}
+	if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want {
+		t.Errorf("Teams.UpdateConnectedExternalGroup returned status %d, want %d", got, want)
+	}
+	if eg != nil {
+		t.Errorf("Teams.UpdateConnectedExternalGroup returned %+v, want nil", eg)
+	}
+}
+
+func TestTeamsService_RemoveConnectedExternalGroup(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/teams/t/external-groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	ctx := context.Background()
+	_, err := client.Teams.RemoveConnectedExternalGroup(ctx, "o", "t")
+	if err != nil {
+		t.Errorf("Teams.RemoveConnectedExternalGroup returned error: %v", err)
+	}
+
+	const methodName = "RemoveConnectedExternalGroup"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Teams.RemoveConnectedExternalGroup(ctx, "\n", "\n")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		return client.Teams.RemoveConnectedExternalGroup(ctx, "o", "t")
+	})
+}
+
+func TestTeamsService_RemoveConnectedExternalGroup_notFound(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/teams/t/external-groups", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		w.WriteHeader(http.StatusNotFound)
+	})
+
+	ctx := context.Background()
+	resp, err := client.Teams.RemoveConnectedExternalGroup(ctx, "o", "t")
+	if err == nil {
+		t.Errorf("Expected HTTP 404 response")
+	}
+	if got, want := resp.Response.StatusCode, http.StatusNotFound; got != want {
+		t.Errorf("Teams.GetExternalGroup returned status %d, want %d", got, want)
+	}
+}