Repository: google/go-github (go)

Issue:
Support "secret_scanning_alert" webhooks and event payloads
Secret scanning has been GA since April 2021 (https://github.blog/changelog/2021-04-01-secret-scanning-for-private-repositories-is-generally-available/) for Advanced Security and since 3.1 in Enterprise.

The webhook payload is documented here
https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#secret_scanning_alert

Pull request:
Add SecretScanningAlertEvent Webhook
Fixes #2277


Patch:
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..c80a2c2c 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -12636,6 +12636,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 +16060,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 {
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/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/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..c8df83ad 100644
--- a/github/github-accessors_test.go
+++ b/github/github-accessors_test.go
@@ -14716,6 +14716,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 +18798,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()
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/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{},