diff --git a/github/github-accessors.go b/github/github-accessors.go index 76af1d2d588..65a2163d9b5 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -25022,6 +25022,14 @@ func (r *RepositoryRulesetRules) GetCommitterEmailPattern() *PatternRuleParamete return r.CommitterEmailPattern } +// GetCopilotCodeReview returns the CopilotCodeReview field. +func (r *RepositoryRulesetRules) GetCopilotCodeReview() *CopilotCodeReviewRuleParameters { + if r == nil { + return nil + } + return r.CopilotCodeReview +} + // GetCreation returns the Creation field. func (r *RepositoryRulesetRules) GetCreation() *EmptyRuleParameters { if r == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 937aff97dc9..03fa75fcc0a 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -32298,6 +32298,14 @@ func TestRepositoryRulesetRules_GetCommitterEmailPattern(tt *testing.T) { r.GetCommitterEmailPattern() } +func TestRepositoryRulesetRules_GetCopilotCodeReview(tt *testing.T) { + tt.Parallel() + r := &RepositoryRulesetRules{} + r.GetCopilotCodeReview() + r = nil + r.GetCopilotCodeReview() +} + func TestRepositoryRulesetRules_GetCreation(tt *testing.T) { tt.Parallel() r := &RepositoryRulesetRules{} diff --git a/github/rules.go b/github/rules.go index 2326ed0d7ac..76d6cbc0095 100644 --- a/github/rules.go +++ b/github/rules.go @@ -75,6 +75,7 @@ const ( RulesetRuleTypeCommitAuthorEmailPattern RepositoryRuleType = "commit_author_email_pattern" RulesetRuleTypeCommitMessagePattern RepositoryRuleType = "commit_message_pattern" RulesetRuleTypeCommitterEmailPattern RepositoryRuleType = "committer_email_pattern" + RulesetRuleTypeCopilotCodeReview RepositoryRuleType = "copilot_code_review" RulesetRuleTypeCreation RepositoryRuleType = "creation" RulesetRuleTypeDeletion RepositoryRuleType = "deletion" RulesetRuleTypeMergeQueue RepositoryRuleType = "merge_queue" @@ -306,6 +307,7 @@ type RepositoryRulesetRules struct { TagNamePattern *PatternRuleParameters Workflows *WorkflowsRuleParameters CodeScanning *CodeScanningRuleParameters + CopilotCodeReview *CopilotCodeReviewRuleParameters // Push target rules. FileExtensionRestriction *FileExtensionRestrictionRuleParameters @@ -539,6 +541,12 @@ type CodeScanningRuleParameters struct { CodeScanningTools []*RuleCodeScanningTool `json:"code_scanning_tools"` } +// CopilotCodeReviewRuleParameters represents the copilot_code_review rule parameters. +type CopilotCodeReviewRuleParameters struct { + ReviewNewPushes bool `json:"review_new_pushes"` + ReviewDraftPullRequests bool `json:"review_draft_pull_requests"` +} + // RuleCodeScanningTool represents a single code scanning tool for the code scanning parameters. type RuleCodeScanningTool struct { AlertsThreshold CodeScanningAlertsThreshold `json:"alerts_threshold"` @@ -566,9 +574,9 @@ type repositoryRulesetRuleWrapper struct { // MarshalJSON is a custom JSON marshaler for RulesetRules. func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { - // The RepositoryRulesetRules type marshals to between 1 and 21 rules. + // The RepositoryRulesetRules type marshals to between 1 and 22 rules. // If new rules are added to RepositoryRulesetRules the capacity below needs increasing - rawRules := make([]json.RawMessage, 0, 21) + rawRules := make([]json.RawMessage, 0, 22) if r.Creation != nil { bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeCreation, r.Creation) @@ -738,6 +746,14 @@ func (r *RepositoryRulesetRules) MarshalJSON() ([]byte, error) { rawRules = append(rawRules, json.RawMessage(bytes)) } + if r.CopilotCodeReview != nil { + bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeCopilotCodeReview, r.CopilotCodeReview) + if err != nil { + return nil, err + } + rawRules = append(rawRules, json.RawMessage(bytes)) + } + if r.RepositoryCreate != nil { bytes, err := marshalRepositoryRulesetRule(RulesetRuleTypeRepositoryCreate, r.RepositoryCreate) if err != nil { @@ -965,6 +981,14 @@ func (r *RepositoryRulesetRules) UnmarshalJSON(data []byte) error { return err } } + case RulesetRuleTypeCopilotCodeReview: + r.CopilotCodeReview = &CopilotCodeReviewRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, r.CopilotCodeReview); err != nil { + return err + } + } case RulesetRuleTypeRepositoryCreate: r.RepositoryCreate = &EmptyRuleParameters{} case RulesetRuleTypeRepositoryDelete: @@ -1366,6 +1390,16 @@ func (r *RepositoryRule) UnmarshalJSON(data []byte) error { } } + r.Parameters = p + case RulesetRuleTypeCopilotCodeReview: + p := &CopilotCodeReviewRuleParameters{} + + if w.Parameters != nil { + if err := json.Unmarshal(w.Parameters, p); err != nil { + return err + } + } + r.Parameters = p case RulesetRuleTypeRepositoryCreate: r.Parameters = nil diff --git a/github/rules_test.go b/github/rules_test.go index cd0f2ae2377..e0b091ecbd0 100644 --- a/github/rules_test.go +++ b/github/rules_test.go @@ -122,13 +122,17 @@ func TestRulesetRules(t *testing.T) { }, }, }, + CopilotCodeReview: &CopilotCodeReviewRuleParameters{ + ReviewNewPushes: true, + ReviewDraftPullRequests: false, + }, 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":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"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":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"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", @@ -240,13 +244,17 @@ func TestRulesetRules(t *testing.T) { }, }, }, + CopilotCodeReview: &CopilotCodeReviewRuleParameters{ + ReviewNewPushes: true, + ReviewDraftPullRequests: false, + }, 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":"repository_create"},{"type":"repository_delete"},{"type":"repository_name","parameters":{"negate":false,"pattern":"^test-.+"}},{"type":"repository_transfer"},{"type":"repository_visibility","parameters":{"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":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}},{"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}}]`, }, } @@ -298,6 +306,39 @@ func TestRulesetRules(t *testing.T) { }) } }) + + t.Run("UnmarshalJSON_Error", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + }{ + { + "invalid_copilot_code_review_bool", + `[{"type":"copilot_code_review","parameters":{"review_new_pushes":"invalid_bool"}}]`, + }, + { + "invalid_copilot_code_review_draft_pr", + `[{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":"not_a_bool"}}]`, + }, + { + "invalid_copilot_code_review_parameters", + `[{"type":"copilot_code_review","parameters":"not_an_object"}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := &RepositoryRulesetRules{} + err := json.Unmarshal([]byte(tt.json), got) + if err == nil { + t.Errorf("Expected error unmarshaling %q, got nil", tt.json) + } + }) + } + }) } func TestBranchRules(t *testing.T) { @@ -929,6 +970,25 @@ 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"}]}}`, }, + { + "copilot_code_review", + &RepositoryRule{ + Type: RulesetRuleTypeCopilotCodeReview, + Parameters: &CopilotCodeReviewRuleParameters{ + ReviewNewPushes: true, + ReviewDraftPullRequests: false, + }, + }, + `{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":false}}`, + }, + { + "copilot_code_review_empty_params", + &RepositoryRule{ + Type: RulesetRuleTypeCopilotCodeReview, + Parameters: &CopilotCodeReviewRuleParameters{}, + }, + `{"type":"copilot_code_review","parameters":{"review_new_pushes":false,"review_draft_pull_requests":false}}`, + }, { "repository_create", &RepositoryRule{Type: RulesetRuleTypeRepositoryCreate, Parameters: nil}, @@ -992,4 +1052,37 @@ func TestRepositoryRule(t *testing.T) { }) } }) + + t.Run("UnmarshalJSON_Error", func(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + json string + }{ + { + "invalid_copilot_code_review_bool", + `{"type":"copilot_code_review","parameters":{"review_new_pushes":"invalid_bool"}}`, + }, + { + "invalid_copilot_code_review_draft_pr", + `{"type":"copilot_code_review","parameters":{"review_new_pushes":true,"review_draft_pull_requests":"not_a_bool"}}`, + }, + { + "invalid_copilot_code_review_parameters", + `{"type":"copilot_code_review","parameters":"not_an_object"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := &RepositoryRule{} + err := json.Unmarshal([]byte(tt.json), got) + if err == nil { + t.Errorf("Expected error unmarshaling %q, got nil", tt.json) + } + }) + } + }) }