Repository: google/go-github
Pull request #2273 (2022-01-26 11:10:49)

Don't update the ratelimits if we got a response from a cache
The recommended cache, https://github.com/gregjones/httpcache, returns
an X-From-Cache: 1 header whenever it serves a response from the cache.

Cached responses might include (possibly stale) ratelimit headers, which
we would've already processed in the past.

codecov[bot]: # [Codecov](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) Report
> Merging [#2273](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (155f27f) into [master](https://codecov.io/gh/google/go-github/commit/51df45c7be1462614aed9884f2090a81f8d27154?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) (51df45c) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/google/go-github/pull/2273/graphs/tree.svg?width=650&height=150&src=pr&token=EgYjGjZlI0&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)

```diff
@@           Coverage Diff           @@
##           master    #2273   +/-   ##
=======================================
  Coverage   97.81%   97.81%           
=======================================
  Files         114      114           
  Lines       10266    10269    +3     
=======================================
+ Hits        10042    10045    +3     
  Misses        156      156           
  Partials       68       68           
```


| [Impacted Files](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google) | Coverage Δ | |
|---|---|---|
| [github/github.go](https://codecov.io/gh/google/go-github/pull/2273/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google#diff-Z2l0aHViL2dpdGh1Yi5nbw==) | `97.64% <100.00%> (+0.01%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Last update [51df45c...155f27f](https://codecov.io/gh/google/go-github/pull/2273?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=google).

Jille: Thanks for the quick review :)

Added tests, rebased to master, signed the CLA.

Patch:
diff --git a/github/actions_workflow_runs.go b/github/actions_workflow_runs.go
index 1e1ff61a..273d2cc4 100644
--- a/github/actions_workflow_runs.go
+++ b/github/actions_workflow_runs.go
@@ -88,6 +88,11 @@ type WorkflowRunJobRun struct {
 	DurationMS *int64 `json:"duration_ms,omitempty"`
 }
 
+// WorkflowRunAttemptOptions specifies optional parameters to GetWorkflowRunAttempt.
+type WorkflowRunAttemptOptions struct {
+	ExcludePullRequests *bool `url:"exclude_pull_requests,omitempty"`
+}
+
 func (s *ActionsService) listWorkflowRuns(ctx context.Context, endpoint string, opts *ListWorkflowRunsOptions) (*WorkflowRuns, *Response, error) {
 	u, err := addOptions(endpoint, opts)
 	if err != nil {
@@ -168,6 +173,30 @@ func (s *ActionsService) GetWorkflowRunByID(ctx context.Context, owner, repo str
 	return run, resp, nil
 }
 
+// GetWorkflowRunAttempt gets a specific workflow run attempt.
+//
+// GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/actions/#get-a-workflow-run-attempt
+func (s *ActionsService) GetWorkflowRunAttempt(ctx context.Context, owner, repo string, runID int64, attemptNumber int, opts *WorkflowRunAttemptOptions) (*WorkflowRun, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/actions/runs/%v/attempts/%v", owner, repo, runID, attemptNumber)
+	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
+	}
+
+	run := new(WorkflowRun)
+	resp, err := s.client.Do(ctx, req, run)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return run, resp, nil
+}
+
 // RerunWorkflowByID re-runs a workflow by ID.
 //
 // GitHub API docs: https://docs.github.com/en/free-pro-team@latest/rest/reference/actions/#re-run-a-workflow
diff --git a/github/billing.go b/github/billing.go
index 12a79fa6..8921833b 100644
--- a/github/billing.go
+++ b/github/billing.go
@@ -19,7 +19,7 @@ type BillingService service
 // ActionBilling represents a GitHub Action billing.
 type ActionBilling struct {
 	TotalMinutesUsed     int                  `json:"total_minutes_used"`
-	TotalPaidMinutesUsed int                  `json:"total_paid_minutes_used"`
+	TotalPaidMinutesUsed float64              `json:"total_paid_minutes_used"`
 	IncludedMinutes      int                  `json:"included_minutes"`
 	MinutesUsedBreakdown MinutesUsedBreakdown `json:"minutes_used_breakdown"`
 }
diff --git a/github/event.go b/github/event.go
index 136abb27..9a39e2e9 100644
--- a/github/event.go
+++ b/github/event.go
@@ -114,6 +114,8 @@ func (e *Event) ParsePayload() (payload interface{}, err error) {
 		payload = &RepositoryDispatchEvent{}
 	case "RepositoryVulnerabilityAlertEvent":
 		payload = &RepositoryVulnerabilityAlertEvent{}
+	case "SecretScanningAlertEvent":
+		payload = &SecretScanningAlertEvent{}
 	case "StarEvent":
 		payload = &StarEvent{}
 	case "StatusEvent":
diff --git a/github/event_types.go b/github/event_types.go
index c2382883..c940ec8f 100644
--- a/github/event_types.go
+++ b/github/event_types.go
@@ -1065,6 +1065,26 @@ type RepositoryVulnerabilityAlert struct {
 	DismissedAt              *Timestamp `json:"dismissed_at,omitempty"`
 }
 
+// SecretScanningAlertEvent is triggered when a secret scanning alert occurs in a repository.
+// The Webhook name is secret_scanning_alert.
+//
+// GitHub API docs: https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#secret_scanning_alert
+type SecretScanningAlertEvent struct {
+	// Action is the action that was performed. Possible values are: "created", "resolved", or "reopened".
+	Action *string `json:"action,omitempty"`
+
+	// Alert is the secret scanning alert involved in the event.
+	Alert *SecretScanningAlert `json:"alert,omitempty"`
+
+	// Only populated by the "resolved" and "reopen" actions
+	Sender *User `json:"sender,omitempty"`
+	// The following fields are only populated by Webhook events.
+	Repo         *Repository   `json:"repository,omitempty"`
+	Organization *Organization `json:"organization,omitempty"`
+	Enterprise   *Enterprise   `json:"enterprise,omitempty"`
+	Installation *Installation `json:"installation,omitempty"`
+}
+
 // StarEvent is triggered when a star is added or removed from a repository.
 // The Webhook event name is "star".
 //
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 90b92334..6c13f545 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -316,6 +316,14 @@ func (a *AnalysesListOptions) GetSarifID() string {
 	return *a.SarifID
 }
 
+// GetSSHKeyFingerprints returns the SSHKeyFingerprints map if it's non-nil, an empty map otherwise.
+func (a *APIMeta) GetSSHKeyFingerprints() map[string]string {
+	if a == nil || a.SSHKeyFingerprints == nil {
+		return map[string]string{}
+	}
+	return a.SSHKeyFingerprints
+}
+
 // GetVerifiablePasswordAuthentication returns the VerifiablePasswordAuthentication field if it's non-nil, zero value otherwise.
 func (a *APIMeta) GetVerifiablePasswordAuthentication() bool {
 	if a == nil || a.VerifiablePasswordAuthentication == nil {
@@ -12636,6 +12644,14 @@ func (p *PullRequestReviewsEnforcementUpdate) GetDismissStaleReviews() bool {
 	return *p.DismissStaleReviews
 }
 
+// GetRequireCodeOwnerReviews returns the RequireCodeOwnerReviews field if it's non-nil, zero value otherwise.
+func (p *PullRequestReviewsEnforcementUpdate) GetRequireCodeOwnerReviews() bool {
+	if p == nil || p.RequireCodeOwnerReviews == nil {
+		return false
+	}
+	return *p.RequireCodeOwnerReviews
+}
+
 // GetAction returns the Action field if it's non-nil, zero value otherwise.
 func (p *PullRequestTargetEvent) GetAction() string {
 	if p == nil || p.Action == nil {
@@ -16052,6 +16068,62 @@ func (s *SecretScanningAlert) GetURL() string {
 	return *s.URL
 }
 
+// GetAction returns the Action field if it's non-nil, zero value otherwise.
+func (s *SecretScanningAlertEvent) GetAction() string {
+	if s == nil || s.Action == nil {
+		return ""
+	}
+	return *s.Action
+}
+
+// GetAlert returns the Alert field.
+func (s *SecretScanningAlertEvent) GetAlert() *SecretScanningAlert {
+	if s == nil {
+		return nil
+	}
+	return s.Alert
+}
+
+// GetEnterprise returns the Enterprise field.
+func (s *SecretScanningAlertEvent) GetEnterprise() *Enterprise {
+	if s == nil {
+		return nil
+	}
+	return s.Enterprise
+}
+
+// GetInstallation returns the Installation field.
+func (s *SecretScanningAlertEvent) GetInstallation() *Installation {
+	if s == nil {
+		return nil
+	}
+	return s.Installation
+}
+
+// GetOrganization returns the Organization field.
+func (s *SecretScanningAlertEvent) GetOrganization() *Organization {
+	if s == nil {
+		return nil
+	}
+	return s.Organization
+}
+
+// GetRepo returns the Repo field.
+func (s *SecretScanningAlertEvent) GetRepo() *Repository {
+	if s == nil {
+		return nil
+	}
+	return s.Repo
+}
+
+// GetSender returns the Sender field.
+func (s *SecretScanningAlertEvent) GetSender() *User {
+	if s == nil {
+		return nil
+	}
+	return s.Sender
+}
+
 // GetDetails returns the Details field.
 func (s *SecretScanningAlertLocation) GetDetails() *SecretScanningAlertLocationDetails {
 	if s == nil {
@@ -19500,6 +19572,14 @@ func (w *WorkflowRun) GetWorkflowURL() string {
 	return *w.WorkflowURL
 }
 
+// GetExcludePullRequests returns the ExcludePullRequests field if it's non-nil, zero value otherwise.
+func (w *WorkflowRunAttemptOptions) GetExcludePullRequests() bool {
+	if w == nil || w.ExcludePullRequests == nil {
+		return false
+	}
+	return *w.ExcludePullRequests
+}
+
 // GetJobs returns the Jobs field if it's non-nil, zero value otherwise.
 func (w *WorkflowRunBill) GetJobs() int {
 	if w == nil || w.Jobs == nil {
diff --git a/github/github.go b/github/github.go
index e5d59400..19109629 100644
--- a/github/github.go
+++ b/github/github.go
@@ -655,9 +655,13 @@ func (c *Client) BareDo(ctx context.Context, req *http.Request) (*Response, erro
 
 	response := newResponse(resp)
 
-	c.rateMu.Lock()
-	c.rateLimits[rateLimitCategory] = response.Rate
-	c.rateMu.Unlock()
+	// Don't update the rate limits if this was a cached response.
+	// X-From-Cache is set by https://github.com/gregjones/httpcache
+	if response.Header.Get("X-From-Cache") == "" {
+		c.rateMu.Lock()
+		c.rateLimits[rateLimitCategory] = response.Rate
+		c.rateMu.Unlock()
+	}
 
 	err = CheckResponse(resp)
 	if err != nil {
diff --git a/github/messages.go b/github/messages.go
index 2e69c523..c1e8161b 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -87,6 +87,7 @@ var (
 		"repository_dispatch":            "RepositoryDispatchEvent",
 		"repository_vulnerability_alert": "RepositoryVulnerabilityAlertEvent",
 		"release":                        "ReleaseEvent",
+		"secret_scanning_alert":          "SecretScanningAlertEvent",
 		"star":                           "StarEvent",
 		"status":                         "StatusEvent",
 		"team":                           "TeamEvent",
diff --git a/github/misc.go b/github/misc.go
index 1fa6c4e1..7672e08a 100644
--- a/github/misc.go
+++ b/github/misc.go
@@ -170,6 +170,12 @@ type APIMeta struct {
 	// An array of IP addresses in CIDR format specifying the IP addresses
 	// Dependabot will originate from.
 	Dependabot []string `json:"dependabot,omitempty"`
+
+	// A map of algorithms to SSH key fingerprints.
+	SSHKeyFingerprints map[string]string `json:"ssh_key_fingerprints,omitempty"`
+
+	// An array of SSH keys.
+	SSHKeys []string `json:"ssh_keys,omitempty"`
 }
 
 // APIMeta returns information about GitHub.com, the service. Or, if you access
diff --git a/github/repos.go b/github/repos.go
index 42cd1636..2b1139b6 100644
--- a/github/repos.go
+++ b/github/repos.go
@@ -944,10 +944,10 @@ type PullRequestReviewsEnforcementUpdate struct {
 	DismissalRestrictionsRequest *DismissalRestrictionsRequest `json:"dismissal_restrictions,omitempty"`
 	// Specifies if approved reviews can be dismissed automatically, when a new commit is pushed. Can be omitted.
 	DismissStaleReviews *bool `json:"dismiss_stale_reviews,omitempty"`
-	// RequireCodeOwnerReviews specifies if an approved review is required in pull requests including files with a designated code owner.
-	RequireCodeOwnerReviews bool `json:"require_code_owner_reviews,omitempty"`
+	// RequireCodeOwnerReviews specifies if merging pull requests is blocked until code owners have reviewed.
+	RequireCodeOwnerReviews *bool `json:"require_code_owner_reviews,omitempty"`
 	// RequiredApprovingReviewCount specifies the number of approvals required before the pull request can be merged.
-	// Valid values are 1 - 6.
+	// Valid values are 1 - 6 or 0 to not require reviewers.
 	RequiredApprovingReviewCount int `json:"required_approving_review_count"`
 }
 


Test patch:
diff --git a/github/actions_workflow_runs_test.go b/github/actions_workflow_runs_test.go
index 7ebdbf58..0b38ae38 100644
--- a/github/actions_workflow_runs_test.go
+++ b/github/actions_workflow_runs_test.go
@@ -143,6 +143,50 @@ func TestActionsService_GetWorkflowRunByID(t *testing.T) {
 	})
 }
 
+func TestActionsService_GetWorkflowRunAttempt(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/actions/runs/29679449/attempts/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{"exclude_pull_requests": "true"})
+		fmt.Fprint(w, `{"id":399444496,"run_number":296,"run_attempt":3,"created_at":"2019-01-02T15:04:05Z","updated_at":"2020-01-02T15:04:05Z"}}`)
+	})
+
+	opts := &WorkflowRunAttemptOptions{ExcludePullRequests: Bool(true)}
+	ctx := context.Background()
+	runs, _, err := client.Actions.GetWorkflowRunAttempt(ctx, "o", "r", 29679449, 3, opts)
+	if err != nil {
+		t.Errorf("Actions.GetWorkflowRunAttempt returned error: %v", err)
+	}
+
+	want := &WorkflowRun{
+		ID:         Int64(399444496),
+		RunNumber:  Int(296),
+		RunAttempt: Int(3),
+		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 !cmp.Equal(runs, want) {
+		t.Errorf("Actions.GetWorkflowRunAttempt returned %+v, want %+v", runs, want)
+	}
+
+	const methodName = "GetWorkflowRunAttempt"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Actions.GetWorkflowRunAttempt(ctx, "\n", "\n", 29679449, 3, opts)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Actions.GetWorkflowRunAttempt(ctx, "o", "r", 29679449, 3, opts)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
 func TestActionsService_RerunWorkflowRunByID(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()
diff --git a/github/billing_test.go b/github/billing_test.go
index 11acd623..2df9a0e9 100644
--- a/github/billing_test.go
+++ b/github/billing_test.go
@@ -22,7 +22,7 @@ func TestBillingService_GetActionsBillingOrg(t *testing.T) {
 		testMethod(t, r, "GET")
 		fmt.Fprint(w, `{
 				"total_minutes_used": 305,
-				"total_paid_minutes_used": 0,
+				"total_paid_minutes_used": 0.0,
 				"included_minutes": 3000,
 				"minutes_used_breakdown": {
 					"UBUNTU": 205,
diff --git a/github/event_types_test.go b/github/event_types_test.go
index 67cc1171..c4a79b77 100644
--- a/github/event_types_test.go
+++ b/github/event_types_test.go
@@ -13484,3 +13484,333 @@ func TestRepositoryVulnerabilityAlertEvent_Marshal(t *testing.T) {
 
 	testJSONMarshal(t, u, want)
 }
+
+func TestSecretScanningAlertEvent_Marshal(t *testing.T) {
+	testJSONMarshal(t, &SecretScanningAlertEvent{}, "{}")
+
+	u := &SecretScanningAlertEvent{
+		Action: String("a"),
+		Alert: &SecretScanningAlert{
+			Number:     Int(1),
+			SecretType: String("t"),
+			Resolution: String("r"),
+			ResolvedBy: &User{
+				Login:     String("l"),
+				ID:        Int64(1),
+				NodeID:    String("n"),
+				URL:       String("u"),
+				ReposURL:  String("r"),
+				EventsURL: String("e"),
+				AvatarURL: String("a"),
+			},
+			ResolvedAt: &Timestamp{referenceTime},
+		},
+		Repo: &Repository{
+			ID:   Int64(1),
+			URL:  String("s"),
+			Name: String("n"),
+		},
+		Organization: &Organization{
+			BillingEmail:                         String("be"),
+			Blog:                                 String("b"),
+			Company:                              String("c"),
+			Email:                                String("e"),
+			TwitterUsername:                      String("tu"),
+			Location:                             String("loc"),
+			Name:                                 String("n"),
+			Description:                          String("d"),
+			IsVerified:                           Bool(true),
+			HasOrganizationProjects:              Bool(true),
+			HasRepositoryProjects:                Bool(true),
+			DefaultRepoPermission:                String("drp"),
+			MembersCanCreateRepos:                Bool(true),
+			MembersCanCreateInternalRepos:        Bool(true),
+			MembersCanCreatePrivateRepos:         Bool(true),
+			MembersCanCreatePublicRepos:          Bool(false),
+			MembersAllowedRepositoryCreationType: String("marct"),
+			MembersCanCreatePages:                Bool(true),
+			MembersCanCreatePublicPages:          Bool(false),
+			MembersCanCreatePrivatePages:         Bool(true),
+		},
+		Enterprise: &Enterprise{
+			ID:          Int(1),
+			Slug:        String("s"),
+			Name:        String("n"),
+			NodeID:      String("nid"),
+			AvatarURL:   String("au"),
+			Description: String("d"),
+			WebsiteURL:  String("wu"),
+			HTMLURL:     String("hu"),
+			CreatedAt:   &Timestamp{referenceTime},
+			UpdatedAt:   &Timestamp{referenceTime},
+		},
+		Sender: &User{
+			Login:     String("l"),
+			ID:        Int64(1),
+			NodeID:    String("n"),
+			URL:       String("u"),
+			ReposURL:  String("r"),
+			EventsURL: String("e"),
+			AvatarURL: String("a"),
+		},
+		Installation: &Installation{
+			ID:       Int64(1),
+			NodeID:   String("nid"),
+			AppID:    Int64(1),
+			AppSlug:  String("as"),
+			TargetID: Int64(1),
+			Account: &User{
+				Login:           String("l"),
+				ID:              Int64(1),
+				URL:             String("u"),
+				AvatarURL:       String("a"),
+				GravatarID:      String("g"),
+				Name:            String("n"),
+				Company:         String("c"),
+				Blog:            String("b"),
+				Location:        String("l"),
+				Email:           String("e"),
+				Hireable:        Bool(true),
+				Bio:             String("b"),
+				TwitterUsername: String("t"),
+				PublicRepos:     Int(1),
+				Followers:       Int(1),
+				Following:       Int(1),
+				CreatedAt:       &Timestamp{referenceTime},
+				SuspendedAt:     &Timestamp{referenceTime},
+			},
+			AccessTokensURL:     String("atu"),
+			RepositoriesURL:     String("ru"),
+			HTMLURL:             String("hu"),
+			TargetType:          String("tt"),
+			SingleFileName:      String("sfn"),
+			RepositorySelection: String("rs"),
+			Events:              []string{"e"},
+			SingleFilePaths:     []string{"s"},
+			Permissions: &InstallationPermissions{
+				Actions:                       String("a"),
+				Administration:                String("ad"),
+				Checks:                        String("c"),
+				Contents:                      String("co"),
+				ContentReferences:             String("cr"),
+				Deployments:                   String("d"),
+				Environments:                  String("e"),
+				Issues:                        String("i"),
+				Metadata:                      String("md"),
+				Members:                       String("m"),
+				OrganizationAdministration:    String("oa"),
+				OrganizationHooks:             String("oh"),
+				OrganizationPlan:              String("op"),
+				OrganizationPreReceiveHooks:   String("opr"),
+				OrganizationProjects:          String("op"),
+				OrganizationSecrets:           String("os"),
+				OrganizationSelfHostedRunners: String("osh"),
+				OrganizationUserBlocking:      String("oub"),
+				Packages:                      String("pkg"),
+				Pages:                         String("pg"),
+				PullRequests:                  String("pr"),
+				RepositoryHooks:               String("rh"),
+				RepositoryProjects:            String("rp"),
+				RepositoryPreReceiveHooks:     String("rprh"),
+				Secrets:                       String("s"),
+				SecretScanningAlerts:          String("ssa"),
+				SecurityEvents:                String("se"),
+				SingleFile:                    String("sf"),
+				Statuses:                      String("s"),
+				TeamDiscussions:               String("td"),
+				VulnerabilityAlerts:           String("va"),
+				Workflows:                     String("w"),
+			},
+			CreatedAt:              &Timestamp{referenceTime},
+			UpdatedAt:              &Timestamp{referenceTime},
+			HasMultipleSingleFiles: Bool(false),
+			SuspendedBy: &User{
+				Login:           String("l"),
+				ID:              Int64(1),
+				URL:             String("u"),
+				AvatarURL:       String("a"),
+				GravatarID:      String("g"),
+				Name:            String("n"),
+				Company:         String("c"),
+				Blog:            String("b"),
+				Location:        String("l"),
+				Email:           String("e"),
+				Hireable:        Bool(true),
+				Bio:             String("b"),
+				TwitterUsername: String("t"),
+				PublicRepos:     Int(1),
+				Followers:       Int(1),
+				Following:       Int(1),
+				CreatedAt:       &Timestamp{referenceTime},
+				SuspendedAt:     &Timestamp{referenceTime},
+			},
+			SuspendedAt: &Timestamp{referenceTime},
+		},
+	}
+
+	want := `{
+		"action": "a",
+		"alert": {
+			"number": 1,
+			"secret_type": "t",
+			"resolution": "r",
+			"resolved_by": {
+				"login": "l",
+				"id": 1,
+				"node_id": "n",
+				"avatar_url": "a",
+				"url": "u",
+				"events_url": "e",
+				"repos_url": "r"
+			},
+			"resolved_at": ` + referenceTimeStr + `
+		},
+		"repository": {
+			"id": 1,
+			"name": "n",
+			"url": "s"
+		},
+        "organization": {
+			"name": "n",
+			"company": "c",
+			"blog": "b",
+			"location": "loc",
+			"email": "e",
+			"twitter_username": "tu",
+			"description": "d",
+			"billing_email": "be",
+			"is_verified": true,
+			"has_organization_projects": true,
+			"has_repository_projects": true,
+			"default_repository_permission": "drp",
+			"members_can_create_repositories": true,
+			"members_can_create_public_repositories": false,
+			"members_can_create_private_repositories": true,
+			"members_can_create_internal_repositories": true,
+			"members_allowed_repository_creation_type": "marct",
+			"members_can_create_pages": true,
+			"members_can_create_public_pages": false,
+			"members_can_create_private_pages": true
+		},
+        "enterprise": {
+			"id": 1,
+			"slug": "s",
+			"name": "n",
+			"node_id": "nid",
+			"avatar_url": "au",
+			"description": "d",
+			"website_url": "wu",
+			"html_url": "hu",
+			"created_at": ` + referenceTimeStr + `,
+			"updated_at": ` + referenceTimeStr + `
+		},
+		"sender": {
+			"login": "l",
+			"id": 1,
+			"node_id": "n",
+			"avatar_url": "a",
+			"url": "u",
+			"events_url": "e",
+			"repos_url": "r"
+		},
+        "installation": {
+			"id": 1,
+			"node_id": "nid",
+			"app_id": 1,
+			"app_slug": "as",
+			"target_id": 1,
+			"account": {
+				"login": "l",
+				"id": 1,
+				"avatar_url": "a",
+				"gravatar_id": "g",
+				"name": "n",
+				"company": "c",
+				"blog": "b",
+				"location": "l",
+				"email": "e",
+				"hireable": true,
+				"bio": "b",
+				"twitter_username": "t",
+				"public_repos": 1,
+				"followers": 1,
+				"following": 1,
+				"created_at": ` + referenceTimeStr + `,
+				"suspended_at": ` + referenceTimeStr + `,
+				"url": "u"
+			},
+			"access_tokens_url": "atu",
+			"repositories_url": "ru",
+			"html_url": "hu",
+			"target_type": "tt",
+			"single_file_name": "sfn",
+			"repository_selection": "rs",
+			"events": [
+				"e"
+			],
+			"single_file_paths": [
+				"s"
+			],
+			"permissions": {
+				"actions": "a",
+				"administration": "ad",
+				"checks": "c",
+				"contents": "co",
+				"content_references": "cr",
+				"deployments": "d",
+				"environments": "e",
+				"issues": "i",
+				"metadata": "md",
+				"members": "m",
+				"organization_administration": "oa",
+				"organization_hooks": "oh",
+				"organization_plan": "op",
+				"organization_pre_receive_hooks": "opr",
+				"organization_projects": "op",
+				"organization_secrets": "os",
+				"organization_self_hosted_runners": "osh",
+				"organization_user_blocking": "oub",
+				"packages": "pkg",
+				"pages": "pg",
+				"pull_requests": "pr",
+				"repository_hooks": "rh",
+				"repository_projects": "rp",
+				"repository_pre_receive_hooks": "rprh",
+				"secrets": "s",
+				"secret_scanning_alerts": "ssa",
+				"security_events": "se",
+				"single_file": "sf",
+				"statuses": "s",
+				"team_discussions": "td",
+				"vulnerability_alerts": "va",
+				"workflows": "w"
+			},
+			"created_at": ` + referenceTimeStr + `,
+			"updated_at": ` + referenceTimeStr + `,
+			"has_multiple_single_files": false,
+			"suspended_by": {
+				"login": "l",
+				"id": 1,
+				"avatar_url": "a",
+				"gravatar_id": "g",
+				"name": "n",
+				"company": "c",
+				"blog": "b",
+				"location": "l",
+				"email": "e",
+				"hireable": true,
+				"bio": "b",
+				"twitter_username": "t",
+				"public_repos": 1,
+				"followers": 1,
+				"following": 1,
+				"created_at": ` + referenceTimeStr + `,
+				"suspended_at": ` + referenceTimeStr + `,
+				"url": "u"
+			},
+			"suspended_at": ` + referenceTimeStr + `
+		}
+	}`
+
+	testJSONMarshal(t, u, want)
+}
diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go
index df8754f5..d06e6356 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -348,6 +348,16 @@ func TestAnalysesListOptions_GetSarifID(tt *testing.T) {
 	a.GetSarifID()
 }
 
+func TestAPIMeta_GetSSHKeyFingerprints(tt *testing.T) {
+	zeroValue := map[string]string{}
+	a := &APIMeta{SSHKeyFingerprints: zeroValue}
+	a.GetSSHKeyFingerprints()
+	a = &APIMeta{}
+	a.GetSSHKeyFingerprints()
+	a = nil
+	a.GetSSHKeyFingerprints()
+}
+
 func TestAPIMeta_GetVerifiablePasswordAuthentication(tt *testing.T) {
 	var zeroValue bool
 	a := &APIMeta{VerifiablePasswordAuthentication: &zeroValue}
@@ -14716,6 +14726,16 @@ func TestPullRequestReviewsEnforcementUpdate_GetDismissStaleReviews(tt *testing.
 	p.GetDismissStaleReviews()
 }
 
+func TestPullRequestReviewsEnforcementUpdate_GetRequireCodeOwnerReviews(tt *testing.T) {
+	var zeroValue bool
+	p := &PullRequestReviewsEnforcementUpdate{RequireCodeOwnerReviews: &zeroValue}
+	p.GetRequireCodeOwnerReviews()
+	p = &PullRequestReviewsEnforcementUpdate{}
+	p.GetRequireCodeOwnerReviews()
+	p = nil
+	p.GetRequireCodeOwnerReviews()
+}
+
 func TestPullRequestTargetEvent_GetAction(tt *testing.T) {
 	var zeroValue string
 	p := &PullRequestTargetEvent{Action: &zeroValue}
@@ -18788,6 +18808,58 @@ func TestSecretScanningAlert_GetURL(tt *testing.T) {
 	s.GetURL()
 }
 
+func TestSecretScanningAlertEvent_GetAction(tt *testing.T) {
+	var zeroValue string
+	s := &SecretScanningAlertEvent{Action: &zeroValue}
+	s.GetAction()
+	s = &SecretScanningAlertEvent{}
+	s.GetAction()
+	s = nil
+	s.GetAction()
+}
+
+func TestSecretScanningAlertEvent_GetAlert(tt *testing.T) {
+	s := &SecretScanningAlertEvent{}
+	s.GetAlert()
+	s = nil
+	s.GetAlert()
+}
+
+func TestSecretScanningAlertEvent_GetEnterprise(tt *testing.T) {
+	s := &SecretScanningAlertEvent{}
+	s.GetEnterprise()
+	s = nil
+	s.GetEnterprise()
+}
+
+func TestSecretScanningAlertEvent_GetInstallation(tt *testing.T) {
+	s := &SecretScanningAlertEvent{}
+	s.GetInstallation()
+	s = nil
+	s.GetInstallation()
+}
+
+func TestSecretScanningAlertEvent_GetOrganization(tt *testing.T) {
+	s := &SecretScanningAlertEvent{}
+	s.GetOrganization()
+	s = nil
+	s.GetOrganization()
+}
+
+func TestSecretScanningAlertEvent_GetRepo(tt *testing.T) {
+	s := &SecretScanningAlertEvent{}
+	s.GetRepo()
+	s = nil
+	s.GetRepo()
+}
+
+func TestSecretScanningAlertEvent_GetSender(tt *testing.T) {
+	s := &SecretScanningAlertEvent{}
+	s.GetSender()
+	s = nil
+	s.GetSender()
+}
+
 func TestSecretScanningAlertLocation_GetDetails(tt *testing.T) {
 	s := &SecretScanningAlertLocation{}
 	s.GetDetails()
@@ -22840,6 +22912,16 @@ func TestWorkflowRun_GetWorkflowURL(tt *testing.T) {
 	w.GetWorkflowURL()
 }
 
+func TestWorkflowRunAttemptOptions_GetExcludePullRequests(tt *testing.T) {
+	var zeroValue bool
+	w := &WorkflowRunAttemptOptions{ExcludePullRequests: &zeroValue}
+	w.GetExcludePullRequests()
+	w = &WorkflowRunAttemptOptions{}
+	w.GetExcludePullRequests()
+	w = nil
+	w.GetExcludePullRequests()
+}
+
 func TestWorkflowRunBill_GetJobs(tt *testing.T) {
 	var zeroValue int
 	w := &WorkflowRunBill{Jobs: &zeroValue}
diff --git a/github/github_test.go b/github/github_test.go
index 06059e6d..aa030d4f 100644
--- a/github/github_test.go
+++ b/github/github_test.go
@@ -1116,6 +1116,49 @@ func TestDo_rateLimit_noNetworkCall(t *testing.T) {
 	}
 }
 
+// Ignore rate limit headers if the response was served from cache.
+func TestDo_rateLimit_ignoredFromCache(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	reset := time.Now().UTC().Add(time.Minute).Round(time.Second) // Rate reset is a minute from now, with 1 second precision.
+
+	// By adding the X-From-Cache header we pretend this is served from a cache.
+	mux.HandleFunc("/first", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("X-From-Cache", "1")
+		w.Header().Set(headerRateLimit, "60")
+		w.Header().Set(headerRateRemaining, "0")
+		w.Header().Set(headerRateReset, fmt.Sprint(reset.Unix()))
+		w.Header().Set("Content-Type", "application/json; charset=utf-8")
+		w.WriteHeader(http.StatusForbidden)
+		fmt.Fprintln(w, `{
+   "message": "API rate limit exceeded for xxx.xxx.xxx.xxx. (But here's the good news: Authenticated requests get a higher rate limit. Check out the documentation for more details.)",
+   "documentation_url": "https://docs.github.com/en/free-pro-team@latest/rest/overview/resources-in-the-rest-api#abuse-rate-limits"
+}`)
+	})
+
+	madeNetworkCall := false
+	mux.HandleFunc("/second", func(w http.ResponseWriter, r *http.Request) {
+		madeNetworkCall = true
+	})
+
+	// First request is made so afterwards we can check the returned rate limit headers were ignored.
+	req, _ := client.NewRequest("GET", "first", nil)
+	ctx := context.Background()
+	client.Do(ctx, req, nil)
+
+	// Second request should not by hindered by rate limits.
+	req, _ = client.NewRequest("GET", "second", nil)
+	_, err := client.Do(ctx, req, nil)
+
+	if err != nil {
+		t.Fatalf("Second request failed, even though the rate limits from the cache should've been ignored: %v", err)
+	}
+	if !madeNetworkCall {
+		t.Fatal("Network call was not made, even though the rate limits from the cache should've been ignored")
+	}
+}
+
 // Ensure *AbuseRateLimitError is returned when the response indicates that
 // the client has triggered an abuse detection mechanism.
 func TestDo_rateLimit_abuseRateLimitError(t *testing.T) {
diff --git a/github/messages_test.go b/github/messages_test.go
index b50ad119..a0cf83f2 100644
--- a/github/messages_test.go
+++ b/github/messages_test.go
@@ -425,6 +425,10 @@ func TestParseWebHook(t *testing.T) {
 			payload:     &RepositoryVulnerabilityAlertEvent{},
 			messageType: "repository_vulnerability_alert",
 		},
+		{
+			payload:     &SecretScanningAlertEvent{},
+			messageType: "secret_scanning_alert",
+		},
 		{
 			payload:     &StarEvent{},
 			messageType: "star",
diff --git a/github/misc_test.go b/github/misc_test.go
index 8b15fd46..fc91d712 100644
--- a/github/misc_test.go
+++ b/github/misc_test.go
@@ -188,6 +188,8 @@ func TestAPIMeta_Marshal(t *testing.T) {
 		Importer:                         []string{"i"},
 		Actions:                          []string{"a"},
 		Dependabot:                       []string{"d"},
+		SSHKeyFingerprints:               map[string]string{"a": "f"},
+		SSHKeys:                          []string{"k"},
 	}
 	want := `{
 		"hooks":["h"],
@@ -196,7 +198,9 @@ func TestAPIMeta_Marshal(t *testing.T) {
 		"pages":["p"],
 		"importer":["i"],
 		"actions":["a"],
-		"dependabot":["d"]
+		"dependabot":["d"],
+		"ssh_key_fingerprints":{"a":"f"},
+		"ssh_keys":["k"]
 	}`
 
 	testJSONMarshal(t, a, want)
diff --git a/github/repos_hooks_deliveries_test.go b/github/repos_hooks_deliveries_test.go
index 9e6e6960..bdcaabec 100644
--- a/github/repos_hooks_deliveries_test.go
+++ b/github/repos_hooks_deliveries_test.go
@@ -149,6 +149,7 @@ var hookDeliveryPayloadTypeToStruct = map[string]interface{}{
 	"repository":                     &RepositoryEvent{},
 	"repository_dispatch":            &RepositoryDispatchEvent{},
 	"repository_vulnerability_alert": &RepositoryVulnerabilityAlertEvent{},
+	"secret_scanning_alert":          &SecretScanningAlertEvent{},
 	"star":                           &StarEvent{},
 	"status":                         &StatusEvent{},
 	"team":                           &TeamEvent{},