Repository: integrations/terraform-provider-github

Issue:
[FEAT]: add file_path_restriction conditionals to repository rulesets
### Describe the need

Add the ability to add conditionals to [repository rulesets](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_ruleset) using file_path_restriction, this requires #2359 to be implemented first.

Once #2359 is merged I will do a PR to add the update 

### SDK Version

go-github V64

### API Version

2022-11-28 of GitHub rest API

### Relevant log output

_No response_

### Code of Conduct

- [X] I agree to follow this project's Code of Conduct

Discussion:
@github-actions: 👋 Hey Friends, this issue has been automatically marked as `stale` because it has no recent activity. It will be closed if no further activity occurs. Please add the `Status: Pinned` label if you feel that this issue needs to remain open/active. Thank you for your contributions and help in keeping things tidy!@Okeanos: This is still relevant and a PR exists to implement it.@michalhepner: Is it possible to push this forward? #2359  has been implemented and #2415 is still awaiting.@dorkamotorka: This would be nice :)

Solution patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 85d9a531..a14f7ef0 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -149,6 +149,9 @@ export GITHUB_OWNER=
 # enable testing of enterprise appliances
 export GITHUB_BASE_URL=
 
+# enable testing of GitHub Paid features, these normally also require an organization e.g. repository push rulesets
+export GITHUB_PAID_FEATURES=true
+
 # leverage helper accounts for tests requiring them
 # examples include:
 # - https://github.com/github-terraform-test-user
diff --git a/github/provider_utils.go b/github/provider_utils.go
index 2e3b44a0..558a8b1f 100644
--- a/github/provider_utils.go
+++ b/github/provider_utils.go
@@ -9,6 +9,7 @@ import (
 
 var testCollaborator = os.Getenv("GITHUB_TEST_COLLABORATOR")
 var isEnterprise = os.Getenv("ENTERPRISE_ACCOUNT")
+var isPaidPlan = os.Getenv("GITHUB_PAID_FEATURES")
 var testEnterprise = os.Getenv("ENTERPRISE_SLUG")
 var testOrganization = testOrganizationFunc()
 var testOwner = os.Getenv("GITHUB_OWNER")
diff --git a/github/resource_github_repository_ruleset.go b/github/resource_github_repository_ruleset.go
index cac66302..def53374 100644
--- a/github/resource_github_repository_ruleset.go
+++ b/github/resource_github_repository_ruleset.go
@@ -34,8 +34,8 @@ func resourceGithubRepositoryRuleset() *schema.Resource {
 			"target": {
 				Type:         schema.TypeString,
 				Required:     true,
-				ValidateFunc: validation.StringInSlice([]string{"branch", "tag"}, false),
-				Description:  "Possible values are `branch` and `tag`.",
+				ValidateFunc: validation.StringInSlice([]string{"branch", "push", "tag"}, false),
+				Description:  "Possible values are `branch`, `push` and `tag`.",
 			},
 			"repository": {
 				Type:        schema.TypeString,
@@ -509,6 +509,59 @@ func resourceGithubRepositoryRuleset() *schema.Resource {
 								},
 							},
 						},
+						"file_path_restriction": {
+							Type:        schema.TypeList,
+							Optional:    true,
+							MaxItems:    1,
+							Description: "Prevent commits that include changes in specified file paths from being pushed to the commit graph.",
+							Elem: &schema.Resource{
+								Schema: map[string]*schema.Schema{
+									"restricted_file_paths": {
+										Type:        schema.TypeList,
+										MinItems:    1,
+										Required:    true,
+										Description: "The file paths that are restricted from being pushed to the commit graph.",
+										Elem: &schema.Schema{
+											Type: schema.TypeString,
+										},
+									},
+								},
+							},
+						},
+						"max_file_size": {
+							Type:        schema.TypeList,
+							Optional:    true,
+							MaxItems:    1,
+							Description: "Prevent pushes based on file size.",
+							Elem: &schema.Resource{
+								Schema: map[string]*schema.Schema{
+									"max_file_size": {
+										Type:        schema.TypeInt,
+										Required:    true,
+										Description: "The maximum allowed size of a file in bytes.",
+									},
+								},
+							},
+						},
+						"file_extension_restriction": {
+							Type:        schema.TypeList,
+							Optional:    true,
+							MaxItems:    1,
+							Description: "Prevent pushes based on file extensions.",
+							Elem: &schema.Resource{
+								Schema: map[string]*schema.Schema{
+									"restricted_file_extensions": {
+										Type:        schema.TypeSet,
+										MinItems:    1,
+										Required:    true,
+										Description: "A list of file extensions.",
+										Elem: &schema.Schema{
+											Type: schema.TypeString,
+										},
+									},
+								},
+							},
+						},
 					},
 				},
 			},
diff --git a/github/respository_rules_utils.go b/github/respository_rules_utils.go
index ec6e3e58..1b798be6 100644
--- a/github/respository_rules_utils.go
+++ b/github/respository_rules_utils.go
@@ -413,6 +413,43 @@ func expandRules(input []interface{}, org bool) []*github.RepositoryRule {
 		rulesSlice = append(rulesSlice, github.NewRequiredCodeScanningRule(params))
 	}
 
+	// file_path_restriction rule
+	if v, ok := rulesMap["file_path_restriction"].([]interface{}); ok && len(v) != 0 {
+		filePathRestrictionMap := v[0].(map[string]interface{})
+		restrictedFilePaths := make([]string, 0)
+		for _, path := range filePathRestrictionMap["restricted_file_paths"].([]interface{}) {
+			restrictedFilePaths = append(restrictedFilePaths, path.(string))
+		}
+		params := &github.RuleFileParameters{
+			RestrictedFilePaths: &restrictedFilePaths,
+		}
+		rulesSlice = append(rulesSlice, github.NewFilePathRestrictionRule(params))
+	}
+
+	// max_file_size rule
+	if v, ok := rulesMap["max_file_size"].([]interface{}); ok && len(v) != 0 {
+		maxFileSizeMap := v[0].(map[string]interface{})
+		maxFileSize := int64(maxFileSizeMap["max_file_size"].(float64))
+		params := &github.RuleMaxFileSizeParameters{
+			MaxFileSize: maxFileSize,
+		}
+		rulesSlice = append(rulesSlice, github.NewMaxFileSizeRule(params))
+
+	}
+
+	// file_extension_restriction rule
+	if v, ok := rulesMap["file_extension_restriction"].([]interface{}); ok && len(v) != 0 {
+		fileExtensionRestrictionMap := v[0].(map[string]interface{})
+		restrictedFileExtensions := make([]string, 0)
+		for _, extension := range fileExtensionRestrictionMap["restricted_file_extensions"].([]interface{}) {
+			restrictedFileExtensions = append(restrictedFileExtensions, extension.(string))
+		}
+		params := &github.RuleFileExtensionRestrictionParameters{
+			RestrictedFileExtensions: restrictedFileExtensions,
+		}
+		rulesSlice = append(rulesSlice, github.NewFileExtensionRestrictionRule(params))
+	}
+
 	return rulesSlice
 }
 
@@ -588,6 +625,17 @@ func flattenRules(rules []*github.RepositoryRule, org bool) []interface{} {
 			rule["min_entries_to_merge"] = params.MinEntriesToMerge
 			rule["min_entries_to_merge_wait_minutes"] = params.MinEntriesToMergeWaitMinutes
 			rulesMap[v.Type] = []map[string]interface{}{rule}
+
+		case "file_path_restriction":
+			var params github.RuleFileParameters
+			err := json.Unmarshal(*v.Parameters, &params)
+			if err != nil {
+				log.Printf("[INFO] Unexpected error unmarshalling rule %s with parameters: %v",
+					v.Type, v.Parameters)
+			}
+			rule := make(map[string]interface{})
+			rule["restricted_file_paths"] = params.GetRestrictedFilePaths()
+			rulesMap[v.Type] = []map[string]interface{}{rule}
 		}
 	}
 
diff --git a/website/docs/r/repository_ruleset.html.markdown b/website/docs/r/repository_ruleset.html.markdown
index dfb34208..ff3466e6 100644
--- a/website/docs/r/repository_ruleset.html.markdown
+++ b/website/docs/r/repository_ruleset.html.markdown
@@ -52,6 +52,28 @@ resource "github_repository_ruleset" "example" {
 
   }
 }
+
+# Example with push ruleset
+resource "github_repository_ruleset" "example_push" {
+  name        = "example_push"
+  repository  = github_repository.example.name
+  target      = "push"
+  enforcement = "active"
+
+  rules {
+    file_path_restriction {
+      restricted_file_paths = [".github/workflows/*", "*.env"]
+    }
+    
+    max_file_size {
+      max_file_size = 104857600  # 100 MB in bytes
+    }
+    
+    file_extension_restriction {
+      restricted_file_extensions = ["*.exe", "*.dll", "*.so"]
+    }
+  }
+}
 ```
 
 ## Argument Reference
@@ -62,7 +84,7 @@ resource "github_repository_ruleset" "example" {
 
 * `rules` - (Required) (Block List, Min: 1, Max: 1) Rules within the ruleset. (see [below for nested schema](#rules))
 
-* `target` - (Required) (String) Possible values are `branch` and `tag`.
+* `target` - (Required) (String) Possible values are `branch`, `tag` and `push`.
 
 * `bypass_actors` - (Optional) (Block List) The actors that can bypass the rules in this ruleset. (see [below for nested schema](#bypass_actors))
 
@@ -104,6 +126,10 @@ The `rules` block supports the following:
 
 * `required_code_scanning` - (Optional) (Block List, Max: 1) Define which tools must provide code scanning results before the reference is updated. When configured, code scanning must be enabled and have results for both the commit and the reference being updated. Multiple code scanning tools can be specified. (see [below for nested schema](#rules.required_code_scanning))
 
+* `file_path_restriction` - (Optional) (Block List, Max 1) Parameters to be used for the file_path_restriction rule. When enabled restricts access to files within the repository. (See [below for nested schema](#rules.file_path_restriction))
+
+* `max_file_size` - (Optional) (Block List, Max 1) Parameters to be used for the max_file_size rule. When enabled restricts the maximum size of a file that can be pushed to the repository. (See [below for nested schema](#rules.max_file_size))
+* `file_extension_restriction` - (Optional) (Block List, Max: 1) Prevent commits that include files with specified file extensions from being pushed to the commit graph. This rule only applies to rulesets with target `push`. (see [below for nested schema](#rules.file_extension_restriction))
 * `update` - (Optional) (Boolean) Only allow users with bypass permission to update matching refs.
 
 * `update_allows_fetch_and_merge` - (Optional) (Boolean) Branch can pull changes from its upstream repository. This is only applicable to forked repositories. Requires `update` to be set to `true`. Note: behaviour is affected by a known bug on the GitHub side which may cause issues when using this parameter.
@@ -216,6 +242,18 @@ The `rules` block supports the following:
 
 * `tool` - (Required) (String) The name of a code scanning tool.
 
+#### rules.file_path_restriction ####
+
+* `restricted_file_paths` - (Required) (Block Set, Min: 1) The file paths that are restricted from being pushed to the commit graph.
+
+#### rules.max_file_size ####
+
+* `max_file_size` - (Required) (Integer) The maximum allowed size, in bytes, of a file.
+
+#### rules.file_extension_restriction ####
+
+* `restricted_file_extensions` - (Required) (Block Set, Min: 1) The file extensions that are restricted from being pushed to the commit graph.
+
 #### bypass_actors ####
 
 * `actor_id` - (Required) (Number) The ID of the actor that can bypass a ruleset. If `actor_type` is `Integration`, `actor_id` is a GitHub App ID. App ID can be obtained by following instructions from the [Get an App API docs](https://docs.github.com/en/rest/apps/apps?apiVersion=2022-11-28#get-an-app)



Test patch:
diff --git a/github/resource_github_repository_ruleset_test.go b/github/resource_github_repository_ruleset_test.go
index 01c61f91..29957722 100644
--- a/github/resource_github_repository_ruleset_test.go
+++ b/github/resource_github_repository_ruleset_test.go
@@ -416,6 +416,82 @@ func TestGithubRepositoryRulesets(t *testing.T) {
 		})
 
 	})
+	t.Run("Creates a push repository ruleset without errors", func(t *testing.T) {
+		if isPaidPlan != "true" {
+			t.Skip("Skipping because `GITHUB_PAID_FEATURES` is not set to true")
+		}
+		config := fmt.Sprintf(`
+			 resource "github_repository" "test" {
+				 name                 = "tf-acc-test-%s"
+				 auto_init            = false
+				 visibility           = "internal"
+				 vulnerability_alerts = true
+			 }
+
+			 resource "github_repository_ruleset" "test_push" {
+				 name        = "test-push"
+				 repository  = github_repository.test.id
+				 target      = "push"
+				 enforcement = "active"
+
+				 rules {
+					file_path_restriction {
+					  restricted_file_paths = ["test.txt"]
+					 }
+					max_file_size {
+					  max_file_size = 1048576
+					}
+					file_extension_restriction {
+					   restricted_file_extensions = ["*.zip"]
+					}
+				 }
+			 }
+
+		`, randomID)
+		check := resource.ComposeTestCheckFunc(
+			resource.TestCheckResourceAttr(
+				"github_repository_ruleset.test_push", "name",
+				"test-push",
+			),
+			resource.TestCheckResourceAttr(
+				"github_repository_ruleset.test_push", "target",
+				"push",
+			),
+			resource.TestCheckResourceAttr(
+				"github_repository_ruleset.test_push", "rules.0.file_path_restriction.0.restricted_file_paths.0",
+				"test.txt",
+			),
+			resource.TestCheckResourceAttr(
+				"github_repository_ruleset.test_push", "rules.0.max_file_size.0.max_file_size",
+				"1048576",
+			),
+			resource.TestCheckResourceAttr(
+				"github_repository_ruleset.test_push", "rules.0.file_extension_restriction.0.restricted_file_extensions.0",
+				"*.zip",
+			),
+		)
+		testCase := func(t *testing.T, mode string) {
+			resource.Test(t, resource.TestCase{
+				PreCheck:  func() { skipUnlessMode(t, mode) },
+				Providers: testAccProviders,
+				Steps: []resource.TestStep{
+					{
+						Config: config,
+						Check:  check,
+					},
+				},
+			})
+		}
+		t.Run("with an anonymous account", func(t *testing.T) {
+			t.Skip("anonymous account not supported for this operation")
+		})
+		t.Run("with an individual account", func(t *testing.T) {
+			t.Skip("individual account not supported for this operation")
+		})
+		t.Run("with a paid plan in an organization", func(t *testing.T) {
+			testCase(t, organization)
+		})
+	})
 
 	t.Run("Creates repository ruleset with merge queue SQUASH method", func(t *testing.T) {