Repository: google/go-github (go)

Issue:
Feature: Add support for Repo/Org Push rulesets
GitHub launched [push rules](https://github.blog/changelog/2024-09-10-push-rules-are-now-generally-available-and-updates-to-custom-properties/) for rulesets to GA recently where you can allow only a certain team or group to push particular paths or files. The API has documentation now to support `push` rule targets for both [Repository](https://docs.github.com/en/rest/repos/rules?apiVersion=2022-11-28#create-a-repository-ruleset--parameters) and [Organisation](https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-ruleset--parameters). 

It would be great to be able to support creating and managing these programatically.

Pull request description:
feat: Implement missing push rules
Adds support for the missing [Push Rules](https://github.blog/changelog/2024-09-10-push-rules-are-now-generally-available-and-updates-to-custom-properties/) - Restrict file path length, Restrict file extensions and Restrict file size with tests.

Closes #3274

Patch:
diff --git a/github/orgs_custom_repository_roles.go b/github/orgs_custom_repository_roles.go
new file mode 100644
index 00000000..2295e1b8
--- /dev/null
+++ b/github/orgs_custom_repository_roles.go
@@ -0,0 +1,131 @@
+// Copyright 2024 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"
+)
+
+// OrganizationCustomRepoRoles represents custom repository roles available in specified organization.
+type OrganizationCustomRepoRoles struct {
+	TotalCount      *int               `json:"total_count,omitempty"`
+	CustomRepoRoles []*CustomRepoRoles `json:"custom_roles,omitempty"`
+}
+
+// CustomRepoRoles represents custom repository roles for an organization.
+// See https://docs.github.com/enterprise-cloud@latest/organizations/managing-peoples-access-to-your-organization-with-roles/managing-custom-repository-roles-for-an-organization
+// for more information.
+type CustomRepoRoles struct {
+	ID          *int64        `json:"id,omitempty"`
+	Name        *string       `json:"name,omitempty"`
+	Description *string       `json:"description,omitempty"`
+	BaseRole    *string       `json:"base_role,omitempty"`
+	Permissions []string      `json:"permissions,omitempty"`
+	Org         *Organization `json:"organization,omitempty"`
+	CreatedAt   *Timestamp    `json:"created_at,omitempty"`
+	UpdatedAt   *Timestamp    `json:"updated_at,omitempty"`
+}
+
+// CreateOrUpdateCustomRepoRoleOptions represents options required to create or update a custom repository role.
+type CreateOrUpdateCustomRepoRoleOptions struct {
+	Name        *string  `json:"name,omitempty"`
+	Description *string  `json:"description,omitempty"`
+	BaseRole    *string  `json:"base_role,omitempty"`
+	Permissions []string `json:"permissions"`
+}
+
+// ListCustomRepoRoles lists the custom repository roles available in this organization.
+// In order to see custom repository roles in an organization, the authenticated user must be an organization owner.
+//
+// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#list-custom-repository-roles-in-an-organization
+//
+//meta:operation GET /orgs/{org}/custom-repository-roles
+func (s *OrganizationsService) ListCustomRepoRoles(ctx context.Context, org string) (*OrganizationCustomRepoRoles, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/custom-repository-roles", org)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	customRepoRoles := new(OrganizationCustomRepoRoles)
+	resp, err := s.client.Do(ctx, req, customRepoRoles)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return customRepoRoles, resp, nil
+}
+
+// CreateCustomRepoRole creates a custom repository role in this organization.
+// In order to create custom repository roles in an organization, the authenticated user must be an organization owner.
+//
+// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#create-a-custom-repository-role
+//
+//meta:operation POST /orgs/{org}/custom-repository-roles
+func (s *OrganizationsService) CreateCustomRepoRole(ctx context.Context, org string, opts *CreateOrUpdateCustomRepoRoleOptions) (*CustomRepoRoles, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/custom-repository-roles", org)
+
+	req, err := s.client.NewRequest("POST", u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	resultingRole := new(CustomRepoRoles)
+	resp, err := s.client.Do(ctx, req, resultingRole)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return resultingRole, resp, err
+}
+
+// UpdateCustomRepoRole updates a custom repository role in this organization.
+// In order to update custom repository roles in an organization, the authenticated user must be an organization owner.
+//
+// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#update-a-custom-repository-role
+//
+//meta:operation PATCH /orgs/{org}/custom-repository-roles/{role_id}
+func (s *OrganizationsService) UpdateCustomRepoRole(ctx context.Context, org string, roleID int64, opts *CreateOrUpdateCustomRepoRoleOptions) (*CustomRepoRoles, *Response, error) {
+	u := fmt.Sprintf("orgs/%v/custom-repository-roles/%v", org, roleID)
+
+	req, err := s.client.NewRequest("PATCH", u, opts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	resultingRole := new(CustomRepoRoles)
+	resp, err := s.client.Do(ctx, req, resultingRole)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return resultingRole, resp, err
+}
+
+// DeleteCustomRepoRole deletes an existing custom repository role in this organization.
+// In order to delete custom repository roles in an organization, the authenticated user must be an organization owner.
+//
+// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#delete-a-custom-repository-role
+//
+//meta:operation DELETE /orgs/{org}/custom-repository-roles/{role_id}
+func (s *OrganizationsService) DeleteCustomRepoRole(ctx context.Context, org string, roleID int64) (*Response, error) {
+	u := fmt.Sprintf("orgs/%v/custom-repository-roles/%v", org, roleID)
+
+	req, err := s.client.NewRequest("DELETE", u, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	resultingRole := new(CustomRepoRoles)
+	resp, err := s.client.Do(ctx, req, resultingRole)
+	if err != nil {
+		return resp, err
+	}
+
+	return resp, nil
+}
diff --git a/github/orgs_custom_roles.go b/github/orgs_organization_roles.go
similarity index 68%
rename from github/orgs_custom_roles.go
rename to github/orgs_organization_roles.go
index f48fc697..695c4dad 100644
--- a/github/orgs_custom_roles.go
+++ b/github/orgs_organization_roles.go
@@ -29,26 +29,6 @@ type CustomOrgRoles struct {
 	BaseRole    *string       `json:"base_role,omitempty"`
 }
 
-// OrganizationCustomRepoRoles represents custom repository roles available in specified organization.
-type OrganizationCustomRepoRoles struct {
-	TotalCount      *int               `json:"total_count,omitempty"`
-	CustomRepoRoles []*CustomRepoRoles `json:"custom_roles,omitempty"`
-}
-
-// CustomRepoRoles represents custom repository roles for an organization.
-// See https://docs.github.com/enterprise-cloud@latest/organizations/managing-peoples-access-to-your-organization-with-roles/managing-custom-repository-roles-for-an-organization
-// for more information.
-type CustomRepoRoles struct {
-	ID          *int64        `json:"id,omitempty"`
-	Name        *string       `json:"name,omitempty"`
-	Description *string       `json:"description,omitempty"`
-	BaseRole    *string       `json:"base_role,omitempty"`
-	Permissions []string      `json:"permissions,omitempty"`
-	Org         *Organization `json:"organization,omitempty"`
-	CreatedAt   *Timestamp    `json:"created_at,omitempty"`
-	UpdatedAt   *Timestamp    `json:"updated_at,omitempty"`
-}
-
 // CreateOrUpdateOrgRoleOptions represents options required to create or update a custom organization role.
 type CreateOrUpdateOrgRoleOptions struct {
 	Name        *string  `json:"name,omitempty"`
@@ -57,14 +37,6 @@ type CreateOrUpdateOrgRoleOptions struct {
 	BaseRole    *string  `json:"base_role,omitempty"`
 }
 
-// CreateOrUpdateCustomRepoRoleOptions represents options required to create or update a custom repository role.
-type CreateOrUpdateCustomRepoRoleOptions struct {
-	Name        *string  `json:"name,omitempty"`
-	Description *string  `json:"description,omitempty"`
-	BaseRole    *string  `json:"base_role,omitempty"`
-	Permissions []string `json:"permissions"`
-}
-
 // ListRoles lists the custom roles available in this organization.
 // In order to see custom roles in an organization, the authenticated user must be an organization owner.
 //
@@ -268,98 +240,6 @@ func (s *OrganizationsService) RemoveOrgRoleFromUser(ctx context.Context, org, u
 	return resp, nil
 }
 
-// ListCustomRepoRoles lists the custom repository roles available in this organization.
-// In order to see custom repository roles in an organization, the authenticated user must be an organization owner.
-//
-// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#list-custom-repository-roles-in-an-organization
-//
-//meta:operation GET /orgs/{org}/custom-repository-roles
-func (s *OrganizationsService) ListCustomRepoRoles(ctx context.Context, org string) (*OrganizationCustomRepoRoles, *Response, error) {
-	u := fmt.Sprintf("orgs/%v/custom-repository-roles", org)
-
-	req, err := s.client.NewRequest("GET", u, nil)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	customRepoRoles := new(OrganizationCustomRepoRoles)
-	resp, err := s.client.Do(ctx, req, customRepoRoles)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return customRepoRoles, resp, nil
-}
-
-// CreateCustomRepoRole creates a custom repository role in this organization.
-// In order to create custom repository roles in an organization, the authenticated user must be an organization owner.
-//
-// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#create-a-custom-repository-role
-//
-//meta:operation POST /orgs/{org}/custom-repository-roles
-func (s *OrganizationsService) CreateCustomRepoRole(ctx context.Context, org string, opts *CreateOrUpdateCustomRepoRoleOptions) (*CustomRepoRoles, *Response, error) {
-	u := fmt.Sprintf("orgs/%v/custom-repository-roles", org)
-
-	req, err := s.client.NewRequest("POST", u, opts)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	resultingRole := new(CustomRepoRoles)
-	resp, err := s.client.Do(ctx, req, resultingRole)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return resultingRole, resp, err
-}
-
-// UpdateCustomRepoRole updates a custom repository role in this organization.
-// In order to update custom repository roles in an organization, the authenticated user must be an organization owner.
-//
-// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#update-a-custom-repository-role
-//
-//meta:operation PATCH /orgs/{org}/custom-repository-roles/{role_id}
-func (s *OrganizationsService) UpdateCustomRepoRole(ctx context.Context, org string, roleID int64, opts *CreateOrUpdateCustomRepoRoleOptions) (*CustomRepoRoles, *Response, error) {
-	u := fmt.Sprintf("orgs/%v/custom-repository-roles/%v", org, roleID)
-
-	req, err := s.client.NewRequest("PATCH", u, opts)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	resultingRole := new(CustomRepoRoles)
-	resp, err := s.client.Do(ctx, req, resultingRole)
-	if err != nil {
-		return nil, resp, err
-	}
-
-	return resultingRole, resp, err
-}
-
-// DeleteCustomRepoRole deletes an existing custom repository role in this organization.
-// In order to delete custom repository roles in an organization, the authenticated user must be an organization owner.
-//
-// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/orgs/custom-roles#delete-a-custom-repository-role
-//
-//meta:operation DELETE /orgs/{org}/custom-repository-roles/{role_id}
-func (s *OrganizationsService) DeleteCustomRepoRole(ctx context.Context, org string, roleID int64) (*Response, error) {
-	u := fmt.Sprintf("orgs/%v/custom-repository-roles/%v", org, roleID)
-
-	req, err := s.client.NewRequest("DELETE", u, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	resultingRole := new(CustomRepoRoles)
-	resp, err := s.client.Do(ctx, req, resultingRole)
-	if err != nil {
-		return resp, err
-	}
-
-	return resp, nil
-}
-
 // ListTeamsAssignedToOrgRole returns all teams assigned to a specific organization role.
 // In order to list teams assigned to an organization role, the authenticated user must be an organization owner.
 //
diff --git a/github/repos_rules.go b/github/repos_rules.go
index 50e75cdb..d09bb71d 100644
--- a/github/repos_rules.go
+++ b/github/repos_rules.go
@@ -85,6 +85,21 @@ type RuleFileParameters struct {
 	RestrictedFilePaths *[]string `json:"restricted_file_paths"`
 }
 
+// RuleMaxFilePathLengthParameters represents the max_file_path_length rule parameters.
+type RuleMaxFilePathLengthParameters struct {
+	MaxFilePathLength int `json:"max_file_path_length"`
+}
+
+// RuleFileExtensionRestrictionParameters represents the file_extension_restriction rule parameters.
+type RuleFileExtensionRestrictionParameters struct {
+	RestrictedFileExtensions []string `json:"restricted_file_extensions"`
+}
+
+// RuleMaxFileSizeParameters represents the max_file_size rule parameters.
+type RuleMaxFileSizeParameters struct {
+	MaxFileSize int64 `json:"max_file_size"`
+}
+
 // UpdateAllowsFetchAndMergeRuleParameters represents the update rule parameters.
 type UpdateAllowsFetchAndMergeRuleParameters struct {
 	UpdateAllowsFetchAndMerge bool `json:"update_allows_fetch_and_merge"`
@@ -255,6 +270,33 @@ func (r *RepositoryRule) UnmarshalJSON(data []byte) error {
 		bytes, _ := json.Marshal(params)
 		rawParams := json.RawMessage(bytes)
 
+		r.Parameters = &rawParams
+	case "max_file_path_length":
+		params := RuleMaxFilePathLengthParameters{}
+		if err := json.Unmarshal(*RepositoryRule.Parameters, &params); err != nil {
+			return err
+		}
+		bytes, _ := json.Marshal(params)
+		rawParams := json.RawMessage(bytes)
+
+		r.Parameters = &rawParams
+	case "file_extension_restriction":
+		params := RuleFileExtensionRestrictionParameters{}
+		if err := json.Unmarshal(*RepositoryRule.Parameters, &params); err != nil {
+			return err
+		}
+		bytes, _ := json.Marshal(params)
+		rawParams := json.RawMessage(bytes)
+
+		r.Parameters = &rawParams
+	case "max_file_size":
+		params := RuleMaxFileSizeParameters{}
+		if err := json.Unmarshal(*RepositoryRule.Parameters, &params); err != nil {
+			return err
+		}
+		bytes, _ := json.Marshal(params)
+		rawParams := json.RawMessage(bytes)
+
 		r.Parameters = &rawParams
 	default:
 		r.Type = ""
@@ -454,6 +496,42 @@ func NewFilePathRestrictionRule(params *RuleFileParameters) (rule *RepositoryRul
 	}
 }
 
+// NewMaxFilePathLengthRule creates a rule to restrict file paths longer than the limit from being pushed.
+func NewMaxFilePathLengthRule(params *RuleMaxFilePathLengthParameters) (rule *RepositoryRule) {
+	bytes, _ := json.Marshal(params)
+
+	rawParams := json.RawMessage(bytes)
+
+	return &RepositoryRule{
+		Type:       "max_file_path_length",
+		Parameters: &rawParams,
+	}
+}
+
+// NewFileExtensionRestrictionRule creates a rule to restrict file extensions from being pushed to a commit.
+func NewFileExtensionRestrictionRule(params *RuleFileExtensionRestrictionParameters) (rule *RepositoryRule) {
+	bytes, _ := json.Marshal(params)
+
+	rawParams := json.RawMessage(bytes)
+
+	return &RepositoryRule{
+		Type:       "file_extension_restriction",
+		Parameters: &rawParams,
+	}
+}
+
+// NewMaxFileSizeRule creates a rule to restrict file sizes from being pushed to a commit.
+func NewMaxFileSizeRule(params *RuleMaxFileSizeParameters) (rule *RepositoryRule) {
+	bytes, _ := json.Marshal(params)
+
+	rawParams := json.RawMessage(bytes)
+
+	return &RepositoryRule{
+		Type:       "max_file_size",
+		Parameters: &rawParams,
+	}
+}
+
 // Ruleset represents a GitHub ruleset object.
 type Ruleset struct {
 	ID   *int64 `json:"id,omitempty"`


Test patch:
diff --git a/github/github_test.go b/github/github_test.go
index 8c6c38a8..5f5aba27 100644
--- a/github/github_test.go
+++ b/github/github_test.go
@@ -15,7 +15,7 @@ import (
 	"net/http/httptest"
 	"net/url"
 	"os"
-	"path"
+	"path/filepath"
 	"reflect"
 	"strconv"
 	"strings"
@@ -68,29 +68,23 @@ func setup() (client *Client, mux *http.ServeMux, serverURL string, teardown fun
 
 // openTestFile creates a new file with the given name and content for testing.
 // In order to ensure the exact file name, this function will create a new temp
-// directory, and create the file in that directory. It is the caller's
-// responsibility to remove the directory and its contents when no longer needed.
-func openTestFile(name, content string) (file *os.File, dir string, err error) {
-	dir, err = os.MkdirTemp("", "go-github")
+// directory, and create the file in that directory. The file is automatically
+// cleaned up after the test.
+func openTestFile(t *testing.T, name, content string) *os.File {
+	t.Helper()
+	fname := filepath.Join(t.TempDir(), name)
+	err := os.WriteFile(fname, []byte(content), 0600)
 	if err != nil {
-		return nil, dir, err
+		t.Fatal(err)
 	}
-
-	file, err = os.OpenFile(path.Join(dir, name), os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
+	file, err := os.Open(fname)
 	if err != nil {
-		return nil, dir, err
+		t.Fatal(err)
 	}
 
-	fmt.Fprint(file, content)
-
-	// close and re-open the file to keep file.Stat() happy
-	file.Close()
-	file, err = os.Open(file.Name())
-	if err != nil {
-		return nil, dir, err
-	}
+	t.Cleanup(func() { file.Close() })
 
-	return file, dir, err
+	return file
 }
 
 func testMethod(t *testing.T, r *http.Request, want string) {
diff --git a/github/orgs_custom_repository_roles_test.go b/github/orgs_custom_repository_roles_test.go
new file mode 100644
index 00000000..58a7c97c
--- /dev/null
+++ b/github/orgs_custom_repository_roles_test.go
@@ -0,0 +1,208 @@
+// Copyright 2024 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"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+)
+
+func TestOrganizationsService_ListCustomRepoRoles(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/custom-repository-roles", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		fmt.Fprint(w, `{"total_count": 1, "custom_roles": [
+			{
+				"id": 1,
+				"name": "Developer",
+				"base_role": "write",
+				"permissions": ["delete_alerts_code_scanning"],
+				"organization": {
+					"login": "l",
+					"id": 1,
+					"node_id": "n",
+					"avatar_url": "a",
+					"html_url": "h",
+					"name": "n",
+					"company": "c",
+					"blog": "b",
+					"location": "l",
+					"email": "e"
+				},
+				"created_at": "2024-07-21T19:33:08Z",
+				"updated_at": "2024-07-21T19:33:08Z"
+			}
+		  ]
+		}`)
+	})
+
+	ctx := context.Background()
+	apps, _, err := client.Organizations.ListCustomRepoRoles(ctx, "o")
+	if err != nil {
+		t.Errorf("Organizations.ListCustomRepoRoles returned error: %v", err)
+	}
+
+	want := &OrganizationCustomRepoRoles{
+		TotalCount: Int(1),
+		CustomRepoRoles: []*CustomRepoRoles{
+			{
+				ID:          Int64(1),
+				Name:        String("Developer"),
+				BaseRole:    String("write"),
+				Permissions: []string{"delete_alerts_code_scanning"},
+				Org: &Organization{
+					Login:     String("l"),
+					ID:        Int64(1),
+					NodeID:    String("n"),
+					AvatarURL: String("a"),
+					HTMLURL:   String("h"),
+					Name:      String("n"),
+					Company:   String("c"),
+					Blog:      String("b"),
+					Location:  String("l"),
+					Email:     String("e"),
+				},
+				CreatedAt: &Timestamp{time.Date(2024, time.July, 21, 19, 33, 8, 0, time.UTC)},
+				UpdatedAt: &Timestamp{time.Date(2024, time.July, 21, 19, 33, 8, 0, time.UTC)},
+			},
+		},
+	}
+	if !cmp.Equal(apps, want) {
+		t.Errorf("Organizations.ListCustomRepoRoles returned %+v, want %+v", apps, want)
+	}
+
+	const methodName = "ListCustomRepoRoles"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.ListCustomRepoRoles(ctx, "\no")
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.ListCustomRepoRoles(ctx, "o")
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_CreateCustomRepoRole(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/custom-repository-roles", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{"id":8030,"name":"Labeler","description":"A role for issue and PR labelers","base_role":"read","permissions":["add_label"]}`)
+	})
+
+	ctx := context.Background()
+
+	opts := &CreateOrUpdateCustomRepoRoleOptions{
+		Name:        String("Labeler"),
+		Description: String("A role for issue and PR labelers"),
+		BaseRole:    String("read"),
+		Permissions: []string{"add_label"},
+	}
+	apps, _, err := client.Organizations.CreateCustomRepoRole(ctx, "o", opts)
+	if err != nil {
+		t.Errorf("Organizations.CreateCustomRepoRole returned error: %v", err)
+	}
+
+	want := &CustomRepoRoles{ID: Int64(8030), Name: String("Labeler"), BaseRole: String("read"), Permissions: []string{"add_label"}, Description: String("A role for issue and PR labelers")}
+
+	if !cmp.Equal(apps, want) {
+		t.Errorf("Organizations.CreateCustomRepoRole returned %+v, want %+v", apps, want)
+	}
+
+	const methodName = "CreateCustomRepoRole"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.CreateCustomRepoRole(ctx, "\no", nil)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.CreateCustomRepoRole(ctx, "o", nil)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_UpdateCustomRepoRole(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/custom-repository-roles/8030", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "PATCH")
+		fmt.Fprint(w, `{"id":8030,"name":"Updated Name","description":"Updated Description","base_role":"read","permissions":["add_label"]}`)
+	})
+
+	ctx := context.Background()
+
+	opts := &CreateOrUpdateCustomRepoRoleOptions{
+		Name:        String("Updated Name"),
+		Description: String("Updated Description"),
+	}
+	apps, _, err := client.Organizations.UpdateCustomRepoRole(ctx, "o", 8030, opts)
+	if err != nil {
+		t.Errorf("Organizations.UpdateCustomRepoRole returned error: %v", err)
+	}
+
+	want := &CustomRepoRoles{ID: Int64(8030), Name: String("Updated Name"), BaseRole: String("read"), Permissions: []string{"add_label"}, Description: String("Updated Description")}
+
+	if !cmp.Equal(apps, want) {
+		t.Errorf("Organizations.UpdateCustomRepoRole returned %+v, want %+v", apps, want)
+	}
+
+	const methodName = "UpdateCustomRepoRole"
+	testBadOptions(t, methodName, func() (err error) {
+		_, _, err = client.Organizations.UpdateCustomRepoRole(ctx, "\no", 8030, nil)
+		return err
+	})
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Organizations.UpdateCustomRepoRole(ctx, "o", 8030, nil)
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
+func TestOrganizationsService_DeleteCustomRepoRole(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/orgs/o/custom-repository-roles/8030", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "DELETE")
+		w.WriteHeader(http.StatusNoContent)
+	})
+
+	ctx := context.Background()
+
+	resp, err := client.Organizations.DeleteCustomRepoRole(ctx, "o", 8030)
+	if err != nil {
+		t.Errorf("Organizations.DeleteCustomRepoRole returned error: %v", err)
+	}
+
+	if !cmp.Equal(resp.StatusCode, 204) {
+		t.Errorf("Organizations.DeleteCustomRepoRole returned  status code %+v, want %+v", resp.StatusCode, "204")
+	}
+
+	const methodName = "DeleteCustomRepoRole"
+	testBadOptions(t, methodName, func() (err error) {
+		_, err = client.Organizations.DeleteCustomRepoRole(ctx, "\no", 8030)
+		return err
+	})
+}
diff --git a/github/orgs_custom_roles_test.go b/github/orgs_organization_roles_test.go
similarity index 72%
rename from github/orgs_custom_roles_test.go
rename to github/orgs_organization_roles_test.go
index dad11594..3e2998fd 100644
--- a/github/orgs_custom_roles_test.go
+++ b/github/orgs_organization_roles_test.go
@@ -427,198 +427,6 @@ func TestOrganizationsService_RemoveOrgRoleFromUser(t *testing.T) {
 	})
 }
 
-func TestOrganizationsService_ListCustomRepoRoles(t *testing.T) {
-	client, mux, _, teardown := setup()
-	defer teardown()
-
-	mux.HandleFunc("/orgs/o/custom-repository-roles", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, "GET")
-		fmt.Fprint(w, `{"total_count": 1, "custom_roles": [
-			{
-				"id": 1,
-				"name": "Developer",
-				"base_role": "write",
-				"permissions": ["delete_alerts_code_scanning"],
-				"organization": {
-					"login": "l",
-					"id": 1,
-					"node_id": "n",
-					"avatar_url": "a",
-					"html_url": "h",
-					"name": "n",
-					"company": "c",
-					"blog": "b",
-					"location": "l",
-					"email": "e"
-				},
-				"created_at": "2024-07-21T19:33:08Z",
-				"updated_at": "2024-07-21T19:33:08Z"
-			}
-		  ]
-		}`)
-	})
-
-	ctx := context.Background()
-	apps, _, err := client.Organizations.ListCustomRepoRoles(ctx, "o")
-	if err != nil {
-		t.Errorf("Organizations.ListCustomRepoRoles returned error: %v", err)
-	}
-
-	want := &OrganizationCustomRepoRoles{
-		TotalCount: Int(1),
-		CustomRepoRoles: []*CustomRepoRoles{
-			{
-				ID:          Int64(1),
-				Name:        String("Developer"),
-				BaseRole:    String("write"),
-				Permissions: []string{"delete_alerts_code_scanning"},
-				Org: &Organization{
-					Login:     String("l"),
-					ID:        Int64(1),
-					NodeID:    String("n"),
-					AvatarURL: String("a"),
-					HTMLURL:   String("h"),
-					Name:      String("n"),
-					Company:   String("c"),
-					Blog:      String("b"),
-					Location:  String("l"),
-					Email:     String("e"),
-				},
-				CreatedAt: &Timestamp{time.Date(2024, time.July, 21, 19, 33, 8, 0, time.UTC)},
-				UpdatedAt: &Timestamp{time.Date(2024, time.July, 21, 19, 33, 8, 0, time.UTC)},
-			},
-		},
-	}
-	if !cmp.Equal(apps, want) {
-		t.Errorf("Organizations.ListCustomRepoRoles returned %+v, want %+v", apps, want)
-	}
-
-	const methodName = "ListCustomRepoRoles"
-	testBadOptions(t, methodName, func() (err error) {
-		_, _, err = client.Organizations.ListCustomRepoRoles(ctx, "\no")
-		return err
-	})
-
-	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
-		got, resp, err := client.Organizations.ListCustomRepoRoles(ctx, "o")
-		if got != nil {
-			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
-		}
-		return resp, err
-	})
-}
-
-func TestOrganizationsService_CreateCustomRepoRole(t *testing.T) {
-	client, mux, _, teardown := setup()
-	defer teardown()
-
-	mux.HandleFunc("/orgs/o/custom-repository-roles", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, "POST")
-		fmt.Fprint(w, `{"id":8030,"name":"Labeler","description":"A role for issue and PR labelers","base_role":"read","permissions":["add_label"]}`)
-	})
-
-	ctx := context.Background()
-
-	opts := &CreateOrUpdateCustomRepoRoleOptions{
-		Name:        String("Labeler"),
-		Description: String("A role for issue and PR labelers"),
-		BaseRole:    String("read"),
-		Permissions: []string{"add_label"},
-	}
-	apps, _, err := client.Organizations.CreateCustomRepoRole(ctx, "o", opts)
-	if err != nil {
-		t.Errorf("Organizations.CreateCustomRepoRole returned error: %v", err)
-	}
-
-	want := &CustomRepoRoles{ID: Int64(8030), Name: String("Labeler"), BaseRole: String("read"), Permissions: []string{"add_label"}, Description: String("A role for issue and PR labelers")}
-
-	if !cmp.Equal(apps, want) {
-		t.Errorf("Organizations.CreateCustomRepoRole returned %+v, want %+v", apps, want)
-	}
-
-	const methodName = "CreateCustomRepoRole"
-	testBadOptions(t, methodName, func() (err error) {
-		_, _, err = client.Organizations.CreateCustomRepoRole(ctx, "\no", nil)
-		return err
-	})
-
-	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
-		got, resp, err := client.Organizations.CreateCustomRepoRole(ctx, "o", nil)
-		if got != nil {
-			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
-		}
-		return resp, err
-	})
-}
-
-func TestOrganizationsService_UpdateCustomRepoRole(t *testing.T) {
-	client, mux, _, teardown := setup()
-	defer teardown()
-
-	mux.HandleFunc("/orgs/o/custom-repository-roles/8030", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, "PATCH")
-		fmt.Fprint(w, `{"id":8030,"name":"Updated Name","description":"Updated Description","base_role":"read","permissions":["add_label"]}`)
-	})
-
-	ctx := context.Background()
-
-	opts := &CreateOrUpdateCustomRepoRoleOptions{
-		Name:        String("Updated Name"),
-		Description: String("Updated Description"),
-	}
-	apps, _, err := client.Organizations.UpdateCustomRepoRole(ctx, "o", 8030, opts)
-	if err != nil {
-		t.Errorf("Organizations.UpdateCustomRepoRole returned error: %v", err)
-	}
-
-	want := &CustomRepoRoles{ID: Int64(8030), Name: String("Updated Name"), BaseRole: String("read"), Permissions: []string{"add_label"}, Description: String("Updated Description")}
-
-	if !cmp.Equal(apps, want) {
-		t.Errorf("Organizations.UpdateCustomRepoRole returned %+v, want %+v", apps, want)
-	}
-
-	const methodName = "UpdateCustomRepoRole"
-	testBadOptions(t, methodName, func() (err error) {
-		_, _, err = client.Organizations.UpdateCustomRepoRole(ctx, "\no", 8030, nil)
-		return err
-	})
-
-	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
-		got, resp, err := client.Organizations.UpdateCustomRepoRole(ctx, "o", 8030, nil)
-		if got != nil {
-			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
-		}
-		return resp, err
-	})
-}
-
-func TestOrganizationsService_DeleteCustomRepoRole(t *testing.T) {
-	client, mux, _, teardown := setup()
-	defer teardown()
-
-	mux.HandleFunc("/orgs/o/custom-repository-roles/8030", func(w http.ResponseWriter, r *http.Request) {
-		testMethod(t, r, "DELETE")
-		w.WriteHeader(http.StatusNoContent)
-	})
-
-	ctx := context.Background()
-
-	resp, err := client.Organizations.DeleteCustomRepoRole(ctx, "o", 8030)
-	if err != nil {
-		t.Errorf("Organizations.DeleteCustomRepoRole returned error: %v", err)
-	}
-
-	if !cmp.Equal(resp.StatusCode, 204) {
-		t.Errorf("Organizations.DeleteCustomRepoRole returned  status code %+v, want %+v", resp.StatusCode, "204")
-	}
-
-	const methodName = "DeleteCustomRepoRole"
-	testBadOptions(t, methodName, func() (err error) {
-		_, err = client.Organizations.DeleteCustomRepoRole(ctx, "\no", 8030)
-		return err
-	})
-}
-
 func TestOrganizationsService_ListTeamsAssignedToOrgRole(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()
diff --git a/github/repos_releases_test.go b/github/repos_releases_test.go
index d7efd096..2cca2bd5 100644
--- a/github/repos_releases_test.go
+++ b/github/repos_releases_test.go
@@ -12,7 +12,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"os"
 	"strings"
 	"testing"
 
@@ -707,11 +706,7 @@ func TestRepositoriesService_UploadReleaseAsset(t *testing.T) {
 			fmt.Fprintf(w, `{"id":1}`)
 		})
 
-		file, dir, err := openTestFile(test.fileName, "Upload me !\n")
-		if err != nil {
-			t.Fatalf("Unable to create temp file: %v", err)
-		}
-		defer os.RemoveAll(dir)
+		file := openTestFile(t, test.fileName, "Upload me !\n")
 
 		ctx := context.Background()
 		asset, _, err := client.Repositories.UploadReleaseAsset(ctx, "o", "r", int64(key), test.uploadOpts, file)
diff --git a/github/repos_rules_test.go b/github/repos_rules_test.go
index 8e123ed0..6daf7534 100644
--- a/github/repos_rules_test.go
+++ b/github/repos_rules_test.go
@@ -312,6 +312,48 @@ func TestRepositoryRule_UnmarshalJSON(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		"Valid max_file_path_length params": {
+			data: `{"type":"max_file_path_length","parameters":{"max_file_path_length": 255}}`,
+			want: NewMaxFilePathLengthRule(&RuleMaxFilePathLengthParameters{
+				MaxFilePathLength: 255,
+			}),
+		},
+		"Invalid max_file_path_length params": {
+			data: `{"type":"max_file_path_length","parameters":{"max_file_path_length": "255"}}`,
+			want: &RepositoryRule{
+				Type:       "max_file_path_length",
+				Parameters: nil,
+			},
+			wantErr: true,
+		},
+		"Valid file_extension_restriction params": {
+			data: `{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe"]}}`,
+			want: NewFileExtensionRestrictionRule(&RuleFileExtensionRestrictionParameters{
+				RestrictedFileExtensions: []string{".exe"},
+			}),
+		},
+		"Invalid file_extension_restriction params": {
+			data: `{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":true}}`,
+			want: &RepositoryRule{
+				Type:       "file_extension_restriction",
+				Parameters: nil,
+			},
+			wantErr: true,
+		},
+		"Valid max_file_size params": {
+			data: `{"type":"max_file_size","parameters":{"max_file_size": 1024}}`,
+			want: NewMaxFileSizeRule(&RuleMaxFileSizeParameters{
+				MaxFileSize: 1024,
+			}),
+		},
+		"Invalid max_file_size params": {
+			data: `{"type":"max_file_size","parameters":{"max_file_size": "1024"}}`,
+			want: &RepositoryRule{
+				Type:       "max_file_size",
+				Parameters: nil,
+			},
+			wantErr: true,
+		},
 	}
 
 	for name, tc := range tests {
@@ -539,6 +581,94 @@ func TestRepositoriesService_CreateRuleset(t *testing.T) {
 	})
 }
 
+func TestRepositoriesService_CreateRulesetWithPushRules(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/repo/rulesets", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		fmt.Fprint(w, `{
+			"id": 42,
+			"name": "ruleset",
+			"source_type": "Repository",
+			"source": "o/repo",
+			"enforcement": "enabled",
+			"target": "push",
+			"rules": [
+				{
+					"type": "file_path_restriction",
+					"parameters": {
+						"restricted_file_paths": ["/a/file"]
+					}
+				},
+				{
+					"type": "max_file_path_length",
+					"parameters": {
+						"max_file_path_length": 255
+					}
+				},
+				{
+					"type": "file_extension_restriction",
+					"parameters": {
+						"restricted_file_extensions": [".exe"]
+					}
+				},
+				{
+					"type": "max_file_size",
+					"parameters": {
+						"max_file_size": 1024
+					}
+				}
+			]
+		}`)
+	})
+
+	ctx := context.Background()
+	ruleSet, _, err := client.Repositories.CreateRuleset(ctx, "o", "repo", &Ruleset{
+		Name:        "ruleset",
+		Enforcement: "enabled",
+	})
+	if err != nil {
+		t.Errorf("Repositories.CreateRuleset returned error: %v", err)
+	}
+
+	want := &Ruleset{
+		ID:          Int64(42),
+		Name:        "ruleset",
+		SourceType:  String("Repository"),
+		Source:      "o/repo",
+		Target:      String("push"),
+		Enforcement: "enabled",
+		Rules: []*RepositoryRule{
+			NewFilePathRestrictionRule(&RuleFileParameters{
+				RestrictedFilePaths: &[]string{"/a/file"},
+			}),
+			NewMaxFilePathLengthRule(&RuleMaxFilePathLengthParameters{
+				MaxFilePathLength: 255,
+			}),
+			NewFileExtensionRestrictionRule(&RuleFileExtensionRestrictionParameters{
+				RestrictedFileExtensions: []string{".exe"},
+			}),
+			NewMaxFileSizeRule(&RuleMaxFileSizeParameters{
+				MaxFileSize: 1024,
+			}),
+		},
+	}
+	if !cmp.Equal(ruleSet, want) {
+		t.Errorf("Repositories.CreateRuleset returned %+v, want %+v", ruleSet, want)
+	}
+
+	const methodName = "CreateRuleset"
+
+	testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) {
+		got, resp, err := client.Repositories.CreateRuleset(ctx, "o", "repo", &Ruleset{})
+		if got != nil {
+			t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got)
+		}
+		return resp, err
+	})
+}
+
 func TestRepositoriesService_GetRuleset(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()