@@ -12,6 +12,7 @@ import (
1212 "github.com/google/go-github/v77/github"
1313 "github.com/hashicorp/terraform-plugin-log/tflog"
1414 "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
15+ "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
1516 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1617 "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1718)
@@ -28,7 +29,10 @@ func resourceGithubOrganizationRuleset() *schema.Resource {
2829
2930 SchemaVersion : 1 ,
3031
31- CustomizeDiff : validateConditionsFieldBasedOnTarget ,
32+ CustomizeDiff : customdiff .All (
33+ validateConditionsFieldBasedOnTarget ,
34+ validateRulesFieldBasedOnTarget ,
35+ ),
3236
3337 Schema : map [string ]* schema.Schema {
3438 "name" : {
@@ -781,3 +785,124 @@ func validateConditionsFieldBasedOnTarget(ctx context.Context, d *schema.Resourc
781785 }
782786 return nil
783787}
788+
789+ // branchTagOnlyRules contains rules that are only valid for branch and tag targets.
790+ //
791+ // These rules apply to ref-based operations (branches and tags) and are not supported
792+ // for push rulesets which operate on file content.
793+ //
794+ // To verify/maintain this list:
795+ // 1. Check the GitHub API documentation for organization rulesets:
796+ // https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-ruleset
797+ // 2. The API docs don't clearly separate push vs branch/tag rules. To verify,
798+ // attempt to create a push ruleset via API or UI with each rule type.
799+ // Push rulesets will reject branch/tag rules with "Invalid rule '<name>'" error.
800+ // 3. Generally, push rules deal with file content (paths, sizes, extensions),
801+ // while branch/tag rules deal with ref lifecycle and merge requirements.
802+ var branchTagOnlyRules = []string {
803+ "creation" ,
804+ "update" ,
805+ "deletion" ,
806+ "required_linear_history" ,
807+ "required_signatures" ,
808+ "pull_request" ,
809+ "required_status_checks" ,
810+ "non_fast_forward" ,
811+ "commit_message_pattern" ,
812+ "commit_author_email_pattern" ,
813+ "committer_email_pattern" ,
814+ "branch_name_pattern" ,
815+ "tag_name_pattern" ,
816+ "required_workflows" ,
817+ "required_code_scanning" ,
818+ }
819+
820+ // pushOnlyRules contains rules that are only valid for push targets.
821+ //
822+ // These rules apply to push operations and control what content can be pushed
823+ // to repositories. They are not supported for branch or tag rulesets.
824+ //
825+ // To verify/maintain this list:
826+ // 1. Check the GitHub API documentation for organization rulesets:
827+ // https://docs.github.com/en/rest/orgs/rules?apiVersion=2022-11-28#create-an-organization-repository-ruleset
828+ // 2. The API docs don't clearly separate push vs branch/tag rules. To verify,
829+ // attempt to create a branch ruleset via API or UI with each rule type.
830+ // Branch rulesets will reject push-only rules with an error.
831+ // 3. Push rules control file content: paths, sizes, extensions, path lengths.
832+ var pushOnlyRules = []string {
833+ "file_path_restriction" ,
834+ "max_file_path_length" ,
835+ "file_extension_restriction" ,
836+ "max_file_size" ,
837+ }
838+
839+ func validateRulesForPushTarget (ctx context.Context , d * schema.ResourceDiff , _ any ) error {
840+ tflog .Debug (ctx , "Validating rules for push target" )
841+ rulesRaw := d .Get ("rules" ).([]any )
842+ if len (rulesRaw ) == 0 {
843+ tflog .Debug (ctx , "No rules block, skipping validation" )
844+ return nil
845+ }
846+
847+ rules := rulesRaw [0 ].(map [string ]any )
848+
849+ for _ , ruleName := range branchTagOnlyRules {
850+ ruleValue := rules [ruleName ]
851+ if ruleValue == nil {
852+ continue
853+ }
854+ switch v := ruleValue .(type ) {
855+ case bool :
856+ if v {
857+ tflog .Debug (ctx , "Invalid rule for push target" , map [string ]any {"rule" : ruleName , "value" : v })
858+ return fmt .Errorf ("rule %q is not valid for push target; push targets only support: %v" , ruleName , pushOnlyRules )
859+ }
860+ case []any :
861+ if len (v ) > 0 {
862+ tflog .Debug (ctx , "Invalid rule for push target" , map [string ]any {"rule" : ruleName , "value" : v })
863+ return fmt .Errorf ("rule %q is not valid for push target; push targets only support: %v" , ruleName , pushOnlyRules )
864+ }
865+ }
866+ }
867+ tflog .Debug (ctx , "Rules validation passed for push target" )
868+ return nil
869+ }
870+
871+ func validateRulesForBranchAndTagTargets (ctx context.Context , d * schema.ResourceDiff , _ any ) error {
872+ target := d .Get ("target" ).(string )
873+ tflog .Debug (ctx , "Validating rules for branch/tag target" , map [string ]any {"target" : target })
874+ rulesRaw := d .Get ("rules" ).([]any )
875+ if len (rulesRaw ) == 0 {
876+ tflog .Debug (ctx , "No rules block, skipping validation" )
877+ return nil
878+ }
879+
880+ rules := rulesRaw [0 ].(map [string ]any )
881+
882+ for _ , ruleName := range pushOnlyRules {
883+ ruleValue := rules [ruleName ]
884+ if ruleValue == nil {
885+ continue
886+ }
887+ if ruleList , ok := ruleValue .([]any ); ok && len (ruleList ) > 0 {
888+ tflog .Debug (ctx , "Invalid rule for branch/tag target" , map [string ]any {"rule" : ruleName , "target" : target })
889+ return fmt .Errorf ("rule %q is only valid for push target, not for %s target" , ruleName , target )
890+ }
891+ }
892+ tflog .Debug (ctx , "Rules validation passed for branch/tag target" , map [string ]any {"target" : target })
893+ return nil
894+ }
895+
896+ func validateRulesFieldBasedOnTarget (ctx context.Context , d * schema.ResourceDiff , meta any ) error {
897+ target := d .Get ("target" ).(string )
898+ tflog .Debug (ctx , "Validating rules field based on target" , map [string ]any {"target" : target })
899+
900+ switch target {
901+ case "branch" , "tag" :
902+ return validateRulesForBranchAndTagTargets (ctx , d , meta )
903+ case "push" :
904+ return validateRulesForPushTarget (ctx , d , meta )
905+ }
906+
907+ return nil
908+ }
0 commit comments