From 92cb38c119a22a7f9a64431856b8cede99094e3d Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Wed, 3 Dec 2025 18:23:45 +0000 Subject: [PATCH 1/3] feat: Add repository target to ruleset Signed-off-by: Steve Hipwell --- github/github-accessors.go | 40 +++++++ github/github-accessors_test.go | 40 +++++++ github/rules.go | 192 +++++++++++++++++++++++++++----- github/rules_test.go | 51 ++++++++- 4 files changed, 295 insertions(+), 28 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index bb937fc4ab6..76af1d2d588 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -25094,6 +25094,46 @@ func (r *RepositoryRulesetRules) GetPullRequest() *PullRequestRuleParameters { return r.PullRequest } +// GetRepositoryCreate returns the RepositoryCreate field. +func (r *RepositoryRulesetRules) GetRepositoryCreate() *EmptyRuleParameters { + if r == nil { + return nil + } + return r.RepositoryCreate +} + +// GetRepositoryDelete returns the RepositoryDelete field. +func (r *RepositoryRulesetRules) GetRepositoryDelete() *EmptyRuleParameters { + if r == nil { + return nil + } + return r.RepositoryDelete +} + +// GetRepositoryName returns the RepositoryName field. +func (r *RepositoryRulesetRules) GetRepositoryName() *SimplePatternRuleParameters { + if r == nil { + return nil + } + return r.RepositoryName +} + +// GetRepositoryTransfer returns the RepositoryTransfer field. +func (r *RepositoryRulesetRules) GetRepositoryTransfer() *EmptyRuleParameters { + if r == nil { + return nil + } + return r.RepositoryTransfer +} + +// GetRepositoryVisibility returns the RepositoryVisibility field. +func (r *RepositoryRulesetRules) GetRepositoryVisibility() *RepositoryVisibilityRuleParameters { + if r == nil { + return nil + } + return r.RepositoryVisibility +} + // GetRequiredDeployments returns the RequiredDeployments field. func (r *RepositoryRulesetRules) GetRequiredDeployments() *RequiredDeploymentsRuleParameters { if r == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 76059c647c6..937aff97dc9 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -32370,6 +32370,46 @@ func TestRepositoryRulesetRules_GetPullRequest(tt *testing.T) { r.GetPullRequest() } +func TestRepositoryRulesetRules_GetRepositoryCreate(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryCreate() + r = nil + r.GetRepositoryCreate() +} + +func TestRepositoryRulesetRules_GetRepositoryDelete(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryDelete() + r = nil + r.GetRepositoryDelete() +} + +func TestRepositoryRulesetRules_GetRepositoryName(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryName() + r = nil + r.GetRepositoryName() +} + +func TestRepositoryRulesetRules_GetRepositoryTransfer(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryTransfer() + r = nil + r.GetRepositoryTransfer() +} + +func TestRepositoryRulesetRules_GetRepositoryVisibility(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetRepositoryVisibility() + r = nil + r.GetRepositoryVisibility() +} + func TestRepositoryRulesetRules_GetRequiredDeployments(tt *testing.T) { tt.Parallel() r := &RepositoryRulesetRules{} diff --git a/github/rules.go b/github/rules.go index 9c0df3192d0..2c591be4011 100644 --- a/github/rules.go +++ b/github/rules.go @@ -7,7 +7,7 @@ package github import ( "encoding/json" - "reflect" + "fmt" ) // RulesetTarget represents a GitHub ruleset target. @@ -15,9 +15,10 @@ type RulesetTarget string // This is the set of GitHub ruleset targets. const ( - RulesetTargetBranch RulesetTarget = "branch" - RulesetTargetTag RulesetTarget = "tag" - RulesetTargetPush RulesetTarget = "push" + RulesetTargetBranch RulesetTarget = "branch" + RulesetTargetTag RulesetTarget = "tag" + RulesetTargetPush RulesetTarget = "push" + RulesetTargetRepository RulesetTarget = "repository" ) // RulesetSourceType represents a GitHub ruleset source type. @@ -68,27 +69,37 @@ type RepositoryRuleType string // This is the set of GitHub ruleset rule types. const ( + // Branch or tag target rules + RulesetRuleTypeBranchNamePattern RepositoryRuleType = "branch_name_pattern" + RulesetRuleTypeCodeScanning RepositoryRuleType = "code_scanning" + RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" + RulesetRuleTypeCommitMessagePattern RepositoryRuleType = "commit_message_pattern" + RulesetRuleTypeCommitterEmailPattern RepositoryRuleType = "committer_email_pattern" RulesetRuleTypeCreation RepositoryRuleType = "creation" - RulesetRuleTypeUpdate RepositoryRuleType = "update" RulesetRuleTypeDeletion RepositoryRuleType = "deletion" - RulesetRuleTypeRequiredLinearHistory RepositoryRuleType = "required_linear_history" RulesetRuleTypeMergeQueue RepositoryRuleType = "merge_queue" + RulesetRuleTypeNonFastForward RepositoryRuleType = "non_fast_forward" + RulesetRuleTypePullRequest RepositoryRuleType = "pull_request" RulesetRuleTypeRequiredDeployments RepositoryRuleType = "required_deployments" + RulesetRuleTypeRequiredLinearHistory RepositoryRuleType = "required_linear_history" RulesetRuleTypeRequiredSignatures RepositoryRuleType = "required_signatures" - RulesetRuleTypePullRequest RepositoryRuleType = "pull_request" RulesetRuleTypeRequiredStatusChecks RepositoryRuleType = "required_status_checks" - RulesetRuleTypeNonFastForward RepositoryRuleType = "non_fast_forward" - RulesetRuleTypeCommitMessagePattern RepositoryRuleType = "commit_message_pattern" - RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" - RulesetRuleTypeCommitterEmailPattern RepositoryRuleType = "committer_email_pattern" - RulesetRuleTypeBranchNamePattern RepositoryRuleType = "branch_name_pattern" RulesetRuleTypeTagNamePattern RepositoryRuleType = "tag_name_pattern" + RulesetRuleTypeUpdate RepositoryRuleType = "update" + RulesetRuleTypeWorkflows RepositoryRuleType = "workflows" + + // Push target rules + RulesetRuleTypeFileExtensionRestriction RepositoryRuleType = "file_extension_restriction" RulesetRuleTypeFilePathRestriction RepositoryRuleType = "file_path_restriction" RulesetRuleTypeMaxFilePathLength RepositoryRuleType = "max_file_path_length" - RulesetRuleTypeFileExtensionRestriction RepositoryRuleType = "file_extension_restriction" RulesetRuleTypeMaxFileSize RepositoryRuleType = "max_file_size" - RulesetRuleTypeWorkflows RepositoryRuleType = "workflows" - RulesetRuleTypeCodeScanning RepositoryRuleType = "code_scanning" + + // Repository target rules + RulesetRuleTypeRepositoryCreate RepositoryRuleType = "repository_create" + RulesetRuleTypeRepositoryDelete RepositoryRuleType = "repository_delete" + RulesetRuleTypeRepositoryName RepositoryRuleType = "repository_name" + RulesetRuleTypeRepositoryTransfer RepositoryRuleType = "repository_transfer" + RulesetRuleTypeRepositoryVisibility RepositoryRuleType = "repository_visibility" ) // MergeGroupingStrategy models a GitHub merge grouping strategy. @@ -277,6 +288,7 @@ type RepositoryRule struct { // RepositoryRulesetRules represents a GitHub ruleset rules object. // This type doesn't have JSON annotations as it uses custom marshaling. type RepositoryRulesetRules struct { + // Branch or tag target rules Creation *EmptyRuleParameters Update *UpdateRuleParameters Deletion *EmptyRuleParameters @@ -292,17 +304,27 @@ type RepositoryRulesetRules struct { CommitterEmailPattern *PatternRuleParameters BranchNamePattern *PatternRuleParameters TagNamePattern *PatternRuleParameters + Workflows *WorkflowsRuleParameters + CodeScanning *CodeScanningRuleParameters + + // Push target rules + FileExtensionRestriction *FileExtensionRestrictionRuleParameters FilePathRestriction *FilePathRestrictionRuleParameters MaxFilePathLength *MaxFilePathLengthRuleParameters - FileExtensionRestriction *FileExtensionRestrictionRuleParameters MaxFileSize *MaxFileSizeRuleParameters - Workflows *WorkflowsRuleParameters - CodeScanning *CodeScanningRuleParameters + + // Repository target rules + RepositoryCreate *EmptyRuleParameters + RepositoryDelete *EmptyRuleParameters + RepositoryName *SimplePatternRuleParameters + RepositoryTransfer *EmptyRuleParameters + RepositoryVisibility *RepositoryVisibilityRuleParameters } // BranchRules represents the rules active for a GitHub repository branch. // This type doesn't have JSON annotations as it uses custom marshaling. type BranchRules struct { + // Branch or tag target rules Creation []*BranchRuleMetadata Update []*UpdateBranchRule Deletion []*BranchRuleMetadata @@ -318,12 +340,14 @@ type BranchRules struct { CommitterEmailPattern []*PatternBranchRule BranchNamePattern []*PatternBranchRule TagNamePattern []*PatternBranchRule + Workflows []*WorkflowsBranchRule + CodeScanning []*CodeScanningBranchRule + + // Push target rules + FileExtensionRestriction []*FileExtensionRestrictionBranchRule FilePathRestriction []*FilePathRestrictionBranchRule MaxFilePathLength []*MaxFilePathLengthBranchRule - FileExtensionRestriction []*FileExtensionRestrictionBranchRule MaxFileSize []*MaxFileSizeBranchRule - Workflows []*WorkflowsBranchRule - CodeScanning []*CodeScanningBranchRule } // BranchRuleMetadata represents the metadata for a branch rule. @@ -522,6 +546,18 @@ type RuleCodeScanningTool struct { Tool string `json:"tool"` } +// SimplePatternRuleParameters represents the parameters for a simple pattern rule. +type SimplePatternRuleParameters struct { + Negate bool `json:"negate"` + Pattern string `json:"pattern"` +} + +// RepositoryVisibilityRuleParameters represents the repository visibility rule parameters. +type RepositoryVisibilityRuleParameters struct { + Internal bool `json:"internal"` + Private bool `json:"private"` +} + // repositoryRulesetRuleWrapper is a helper type to marshal & unmarshal a ruleset rule. type repositoryRulesetRuleWrapper struct { Type RepositoryRuleType `json:"type"` @@ -702,17 +738,74 @@ func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { rawRules = append(rawRules, json.RawMessage(bytes)) } + if r.RepositoryCreate != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryCreate, r.RepositoryCreate) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryDelete != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryDelete, r.RepositoryDelete) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryName != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryName, r.RepositoryName) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryTransfer != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryTransfer, r.RepositoryTransfer) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + + if r.RepositoryVisibility != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryVisibility, r.RepositoryVisibility) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + return json.Marshal(rawRules) } // marshalRepositoryRulesetRule is a helper function to marshal a ruleset rule. -// -// TODO: Benchmark the code that uses reflection. -// TODO: Use a generator here instead of reflection if there is a significant performance hit. func marshalRepositoryRulesetRule[T any](t RepositoryRuleType, params T) ([]byte, error) { - paramsType := reflect.TypeFor[T]() + hasParams := true + + switch t { + case RulesetRuleTypeCreation, + RulesetRuleTypeDeletion, + RulesetRuleTypeRequiredLinearHistory, + RulesetRuleTypeRequiredSignatures, + RulesetRuleTypeNonFastForward, + RulesetRuleTypeRepositoryCreate, + RulesetRuleTypeRepositoryDelete, + RulesetRuleTypeRepositoryTransfer: + hasParams = false + case RulesetRuleTypeUpdate: + paramsTyped, ok := any(params).(*UpdateRuleParameters) + if !ok { + return nil, fmt.Errorf("expected UpdateRuleParameters for rule type %s", t) + } + if paramsTyped == nil || *paramsTyped == (UpdateRuleParameters{}) { + hasParams = false + } + } - if paramsType.Kind() == reflect.Pointer && (reflect.ValueOf(params).IsNil() || reflect.ValueOf(params).Elem().IsZero()) { + if !hasParams { return json.Marshal(repositoryRulesetRuleWrapper{Type: t}) } @@ -872,6 +965,28 @@ func (r *RepositoryRulesetRules) UnmarshalJSON(data []byte) error { return err } } + case RulesetRuleTypeRepositoryCreate: + r.RepositoryCreate = &EmptyRuleParameters{} + case RulesetRuleTypeRepositoryDelete: + r.RepositoryDelete = &EmptyRuleParameters{} + case RulesetRuleTypeRepositoryName: + r.RepositoryName = &SimplePatternRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, r.RepositoryName); err != nil { + return err + } + } + case RulesetRuleTypeRepositoryTransfer: + r.RepositoryTransfer = &EmptyRuleParameters{} + case RulesetRuleTypeRepositoryVisibility: + r.RepositoryVisibility = &RepositoryVisibilityRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, r.RepositoryVisibility); err != nil { + return err + } + } } } @@ -1251,6 +1366,31 @@ func (r *RepositoryRule) UnmarshalJSON(data []byte) error { } } + r.Parameters = p + case RulesetRuleTypeRepositoryCreate: + r.Parameters = nil + case RulesetRuleTypeRepositoryDelete: + r.Parameters = nil + case RulesetRuleTypeRepositoryName: + p := &SimplePatternRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, p); err != nil { + return err + } + } + + r.Parameters = p + case RulesetRuleTypeRepositoryTransfer: + r.Parameters = nil + case RulesetRuleTypeRepositoryVisibility: + p := &RepositoryVisibilityRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, p); err != nil { + return err + } + } r.Parameters = p } diff --git a/github/rules_test.go b/github/rules_test.go index 562d385ec74..cd0f2ae2377 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -122,8 +122,13 @@ func TestRulesetRules(t *testing.T) { }, }, }, + RepositoryCreate: &EmptyRuleParameters{}, + RepositoryDelete: &EmptyRuleParameters{}, + RepositoryName: &SimplePatternRuleParameters{Pattern: "^test-.+", Negate: false}, + RepositoryTransfer: &EmptyRuleParameters{}, + RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}]`, + `[{"type":"creation"},{"type":"update"},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"required_status_checks":[{"context":"test1"},{"context":"test2"}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"workflows":[{"path":".github/workflows/test1.yaml"},{"path":".github/workflows/test2.yaml"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, { "all_rules_with_all_params", @@ -235,8 +240,13 @@ func TestRulesetRules(t *testing.T) { }, }, }, + RepositoryCreate: &EmptyRuleParameters{}, + RepositoryDelete: &EmptyRuleParameters{}, + RepositoryName: &SimplePatternRuleParameters{Pattern: "^test-.+", Negate: false}, + RepositoryTransfer: &EmptyRuleParameters{}, + RepositoryVisibility: &RepositoryVisibilityRuleParameters{Internal: false, Private: false}, }, - `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}]`, + `[{"type":"creation"},{"type":"update","parameters":{"update_allows_fetch_and_merge":true}},{"type":"deletion"},{"type":"required_linear_history"},{"type":"merge_queue","parameters":{"check_response_timeout_minutes":5,"grouping_strategy":"ALLGREEN","max_entries_to_build":10,"max_entries_to_merge":20,"merge_method":"SQUASH","min_entries_to_merge":1,"min_entries_to_merge_wait_minutes":15}},{"type":"required_deployments","parameters":{"required_deployment_environments":["test1","test2"]}},{"type":"required_signatures"},{"type":"pull_request","parameters":{"allowed_merge_methods":["squash","rebase"],"automatic_copilot_code_review_enabled":false,"dismiss_stale_reviews_on_push":true,"require_code_owner_review":true,"require_last_push_approval":true,"required_approving_review_count":2,"required_review_thread_resolution":true}},{"type":"required_status_checks","parameters":{"do_not_enforce_on_create":true,"required_status_checks":[{"context":"test1","integration_id":1},{"context":"test2","integration_id":2}],"strict_required_status_checks_policy":true}},{"type":"non_fast_forward"},{"type":"commit_message_pattern","parameters":{"name":"cmp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"commit_author_email_pattern","parameters":{"name":"caep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"committer_email_pattern","parameters":{"name":"cep","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"branch_name_pattern","parameters":{"name":"bp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"tag_name_pattern","parameters":{"name":"tp","negate":false,"operator":"starts_with","pattern":"test"}},{"type":"file_path_restriction","parameters":{"restricted_file_paths":["test1","test2"]}},{"type":"max_file_path_length","parameters":{"max_file_path_length":512}},{"type":"file_extension_restriction","parameters":{"restricted_file_extensions":[".exe",".pkg"]}},{"type":"max_file_size","parameters":{"max_file_size":1024}},{"type":"workflows","parameters":{"do_not_enforce_on_create":true,"workflows":[{"path":".github/workflows/test1.yaml","ref":"main","repository_id":1,"sha":"aaaa"},{"path":".github/workflows/test2.yaml","ref":"main","repository_id":2,"sha":"bbbb"}]}},{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}},{"type":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"internal":false,"private":false}}]`, }, } @@ -919,6 +929,43 @@ func TestRepositoryRule(t *testing.T) { }, `{"type":"code_scanning","parameters":{"code_scanning_tools":[{"alerts_threshold":"all","security_alerts_threshold":"all","tool":"test"},{"alerts_threshold":"none","security_alerts_threshold":"none","tool":"test"}]}}`, }, + { + "repository_create", + &RepositoryRule{Type: RulesetRuleTypeRepositoryCreate, Parameters: nil}, + `{"type":"repository_create"}`, + }, + { + "repository_delete", + &RepositoryRule{Type: RulesetRuleTypeRepositoryDelete, Parameters: nil}, + `{"type":"repository_delete"}`, + }, + { + "repository_name", + &RepositoryRule{ + Type: RulesetRuleTypeRepositoryName, + Parameters: &SimplePatternRuleParameters{ + Negate: false, + Pattern: "^test-.+", + }, + }, + `{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}}`, + }, + { + "repository_transfer", + &RepositoryRule{Type: RulesetRuleTypeRepositoryTransfer, Parameters: nil}, + `{"type":"repository_transfer"}`, + }, + { + "repository_visibility", + &RepositoryRule{ + Type: RulesetRuleTypeRepositoryVisibility, + Parameters: &RepositoryVisibilityRuleParameters{ + Internal: false, + Private: false, + }, + }, + `{"type":"repository_visibility","parameters":{"internal":false,"private":false}}`, + }, } t.Run("UnmarshalJSON", func(t *testing.T) { From 84e213879fffa97b6d35b8c6dbf28e4a7336c139 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Wed, 3 Dec 2025 19:11:43 +0000 Subject: [PATCH 2/3] fix: Correct lint errors Signed-off-by: Steve Hipwell --- github/rules.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/github/rules.go b/github/rules.go index 2c591be4011..598fa29c36a 100644 --- a/github/rules.go +++ b/github/rules.go @@ -69,7 +69,7 @@ type RepositoryRuleType string // This is the set of GitHub ruleset rule types. const ( - // Branch or tag target rules + // Branch or tag target rules. RulesetRuleTypeBranchNamePattern RepositoryRuleType = "branch_name_pattern" RulesetRuleTypeCodeScanning RepositoryRuleType = "code_scanning" RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" @@ -88,13 +88,13 @@ const ( RulesetRuleTypeUpdate RepositoryRuleType = "update" RulesetRuleTypeWorkflows RepositoryRuleType = "workflows" - // Push target rules + // Push target rules. RulesetRuleTypeFileExtensionRestriction RepositoryRuleType = "file_extension_restriction" RulesetRuleTypeFilePathRestriction RepositoryRuleType = "file_path_restriction" RulesetRuleTypeMaxFilePathLength RepositoryRuleType = "max_file_path_length" RulesetRuleTypeMaxFileSize RepositoryRuleType = "max_file_size" - // Repository target rules + // Repository target rules. RulesetRuleTypeRepositoryCreate RepositoryRuleType = "repository_create" RulesetRuleTypeRepositoryDelete RepositoryRuleType = "repository_delete" RulesetRuleTypeRepositoryName RepositoryRuleType = "repository_name" @@ -288,7 +288,7 @@ type RepositoryRule struct { // RepositoryRulesetRules represents a GitHub ruleset rules object. // This type doesn't have JSON annotations as it uses custom marshaling. type RepositoryRulesetRules struct { - // Branch or tag target rules + // Branch or tag target rules. Creation *EmptyRuleParameters Update *UpdateRuleParameters Deletion *EmptyRuleParameters @@ -307,13 +307,13 @@ type RepositoryRulesetRules struct { Workflows *WorkflowsRuleParameters CodeScanning *CodeScanningRuleParameters - // Push target rules + // Push target rules. FileExtensionRestriction *FileExtensionRestrictionRuleParameters FilePathRestriction *FilePathRestrictionRuleParameters MaxFilePathLength *MaxFilePathLengthRuleParameters MaxFileSize *MaxFileSizeRuleParameters - // Repository target rules + // Repository target rules. RepositoryCreate *EmptyRuleParameters RepositoryDelete *EmptyRuleParameters RepositoryName *SimplePatternRuleParameters @@ -324,7 +324,7 @@ type RepositoryRulesetRules struct { // BranchRules represents the rules active for a GitHub repository branch. // This type doesn't have JSON annotations as it uses custom marshaling. type BranchRules struct { - // Branch or tag target rules + // Branch or tag target rules. Creation []*BranchRuleMetadata Update []*UpdateBranchRule Deletion []*BranchRuleMetadata @@ -343,7 +343,7 @@ type BranchRules struct { Workflows []*WorkflowsBranchRule CodeScanning []*CodeScanningBranchRule - // Push target rules + // Push target rules. FileExtensionRestriction []*FileExtensionRestrictionBranchRule FilePathRestriction []*FilePathRestrictionBranchRule MaxFilePathLength []*MaxFilePathLengthBranchRule From 21fb8dff7c71517f0260f477d4383e70d7118a04 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Wed, 3 Dec 2025 19:53:05 +0000 Subject: [PATCH 3/3] fix: Correct review Signed-off-by: Steve Hipwell --- github/rules.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/rules.go b/github/rules.go index 598fa29c36a..2326ed0d7ac 100644 --- a/github/rules.go +++ b/github/rules.go @@ -798,7 +798,7 @@ func marshalRepositoryRulesetRule[T any](t RepositoryRuleType, params T) ([]byte case RulesetRuleTypeUpdate: paramsTyped, ok := any(params).(*UpdateRuleParameters) if !ok { - return nil, fmt.Errorf("expected UpdateRuleParameters for rule type %s", t) + return nil, fmt.Errorf("expected UpdateRuleParameters for rule type %v", t) } if paramsTyped == nil || *paramsTyped == (UpdateRuleParameters{}) { hasParams = false