From 42ae4fe2918071bae627539203680b2187a413a3 Mon Sep 17 00:00:00 2001 From: Clemens W Date: Wed, 26 Jun 2024 15:57:36 +0200 Subject: [PATCH] Add support in rulesets for including or excluding repos based on properties (#3194) --- github/github-accessors.go | 8 + github/github-accessors_test.go | 7 + github/orgs_rules_test.go | 530 ++++++++++++++++++++++++++++++++ github/repos_rules.go | 21 +- 4 files changed, 562 insertions(+), 4 deletions(-) diff --git a/github/github-accessors.go b/github/github-accessors.go index 5a69f27f8cf..5118791cdd2 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -20878,6 +20878,14 @@ func (r *RulesetConditions) GetRepositoryName() *RulesetRepositoryNamesCondition return r.RepositoryName } +// GetRepositoryProperty returns the RepositoryProperty field. +func (r *RulesetConditions) GetRepositoryProperty() *RulesetRepositoryPropertyConditionParameters { + if r == nil { + return nil + } + return r.RepositoryProperty +} + // GetHRef returns the HRef field if it's non-nil, zero value otherwise. func (r *RulesetLink) GetHRef() string { if r == nil || r.HRef == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index bfe3c61e0f9..03ebada64b1 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -24262,6 +24262,13 @@ func TestRulesetConditions_GetRepositoryName(tt *testing.T) { r.GetRepositoryName() } +func TestRulesetConditions_GetRepositoryProperty(tt *testing.T) { + r := &RulesetConditions{} + r.GetRepositoryProperty() + r = nil + r.GetRepositoryProperty() +} + func TestRulesetLink_GetHRef(tt *testing.T) { var zeroValue string r := &RulesetLink{HRef: &zeroValue} diff --git a/github/orgs_rules_test.go b/github/orgs_rules_test.go index 0ed54ca8d27..d45469f27e7 100644 --- a/github/orgs_rules_test.go +++ b/github/orgs_rules_test.go @@ -395,7 +395,342 @@ func TestOrganizationsService_CreateOrganizationRuleset_RepoNames(t *testing.T) return resp, err }) } +func TestOrganizationsService_CreateOrganizationRuleset_RepoProperty(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + mux.HandleFunc("/orgs/o/rulesets", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + fmt.Fprint(w, `{ + "id": 21, + "name": "ruleset", + "target": "branch", + "source_type": "Organization", + "source": "o", + "enforcement": "active", + "bypass_actors": [ + { + "actor_id": 234, + "actor_type": "Team" + } + ], + "conditions": { + "repository_property": { + "include": [ + { + "name": "testIncludeProp", + "source": "custom", + "property_values": [ + "true" + ] + } + ], + "exclude": [ + { + "name": "testExcludeProp", + "source": "custom", + "property_values": [ + "false" + ] + } + ] + } + }, + "rules": [ + { + "type": "creation" + }, + { + "type": "update", + "parameters": { + "update_allows_fetch_and_merge": true + } + }, + { + "type": "deletion" + }, + { + "type": "merge_queue" + }, + { + "type": "required_linear_history" + }, + { + "type": "required_deployments", + "parameters": { + "required_deployment_environments": ["test"] + } + }, + { + "type": "required_signatures" + }, + { + "type": "pull_request", + "parameters": { + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": true, + "require_last_push_approval": true, + "required_approving_review_count": 1, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "required_status_checks": [ + { + "context": "test", + "integration_id": 1 + } + ], + "strict_required_status_checks_policy": true + } + }, + { + "type": "non_fast_forward" + }, + { + "type": "commit_message_pattern", + "parameters": { + "name": "avoid test commits", + "negate": true, + "operator": "starts_with", + "pattern": "[test]" + } + }, + { + "type": "commit_author_email_pattern", + "parameters": { + "operator": "contains", + "pattern": "github" + } + }, + { + "type": "committer_email_pattern", + "parameters": { + "name": "avoid commit emails", + "negate": true, + "operator": "ends_with", + "pattern": "abc" + } + }, + { + "type": "branch_name_pattern", + "parameters": { + "name": "avoid branch names", + "negate": true, + "operator": "regex", + "pattern": "github$" + } + }, + { + "type": "tag_name_pattern", + "parameters": { + "name": "avoid tag names", + "negate": true, + "operator": "contains", + "pattern": "github" + } + } + ] + }`) + }) + + ctx := context.Background() + ruleset, _, err := client.Organizations.CreateOrganizationRuleset(ctx, "o", &Ruleset{ + ID: Int64(21), + Name: "ruleset", + Target: String("branch"), + SourceType: String("Organization"), + Source: "o", + Enforcement: "active", + BypassActors: []*BypassActor{ + { + ActorID: Int64(234), + ActorType: String("Team"), + }, + }, + Conditions: &RulesetConditions{ + RepositoryProperty: &RulesetRepositoryPropertyConditionParameters{ + Include: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testIncludeProp", + Values: []string{"true"}, + }, + }, + Exclude: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testExcludeProp", + Values: []string{"false"}, + }, + }, + }, + }, + Rules: []*RepositoryRule{ + NewCreationRule(), + NewUpdateRule(&UpdateAllowsFetchAndMergeRuleParameters{ + UpdateAllowsFetchAndMerge: true, + }), + NewDeletionRule(), + NewMergeQueueRule(), + NewRequiredLinearHistoryRule(), + NewRequiredDeploymentsRule(&RequiredDeploymentEnvironmentsRuleParameters{ + RequiredDeploymentEnvironments: []string{"test"}, + }), + NewRequiredSignaturesRule(), + NewPullRequestRule(&PullRequestRuleParameters{ + RequireCodeOwnerReview: true, + RequireLastPushApproval: true, + RequiredApprovingReviewCount: 1, + RequiredReviewThreadResolution: true, + DismissStaleReviewsOnPush: true, + }), + NewRequiredStatusChecksRule(&RequiredStatusChecksRuleParameters{ + RequiredStatusChecks: []RuleRequiredStatusChecks{ + { + Context: "test", + IntegrationID: Int64(1), + }, + }, + StrictRequiredStatusChecksPolicy: true, + }), + NewNonFastForwardRule(), + NewCommitMessagePatternRule(&RulePatternParameters{ + Name: String("avoid test commits"), + Negate: Bool(true), + Operator: "starts_with", + Pattern: "[test]", + }), + NewCommitAuthorEmailPatternRule(&RulePatternParameters{ + Operator: "contains", + Pattern: "github", + }), + NewCommitterEmailPatternRule(&RulePatternParameters{ + Name: String("avoid commit emails"), + Negate: Bool(true), + Operator: "ends_with", + Pattern: "abc", + }), + NewBranchNamePatternRule(&RulePatternParameters{ + Name: String("avoid branch names"), + Negate: Bool(true), + Operator: "regex", + Pattern: "github$", + }), + NewTagNamePatternRule(&RulePatternParameters{ + Name: String("avoid tag names"), + Negate: Bool(true), + Operator: "contains", + Pattern: "github", + }), + }, + }) + if err != nil { + t.Errorf("Organizations.CreateOrganizationRuleset returned error: %v", err) + } + + want := &Ruleset{ + ID: Int64(21), + Name: "ruleset", + Target: String("branch"), + SourceType: String("Organization"), + Source: "o", + Enforcement: "active", + BypassActors: []*BypassActor{ + { + ActorID: Int64(234), + ActorType: String("Team"), + }, + }, + Conditions: &RulesetConditions{ + RepositoryProperty: &RulesetRepositoryPropertyConditionParameters{ + Include: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testIncludeProp", + Values: []string{"true"}, + }, + }, + Exclude: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testExcludeProp", + Values: []string{"false"}, + }, + }, + }, + }, + Rules: []*RepositoryRule{ + NewCreationRule(), + NewUpdateRule(&UpdateAllowsFetchAndMergeRuleParameters{ + UpdateAllowsFetchAndMerge: true, + }), + NewDeletionRule(), + NewMergeQueueRule(), + NewRequiredLinearHistoryRule(), + NewRequiredDeploymentsRule(&RequiredDeploymentEnvironmentsRuleParameters{ + RequiredDeploymentEnvironments: []string{"test"}, + }), + NewRequiredSignaturesRule(), + NewPullRequestRule(&PullRequestRuleParameters{ + RequireCodeOwnerReview: true, + RequireLastPushApproval: true, + RequiredApprovingReviewCount: 1, + RequiredReviewThreadResolution: true, + DismissStaleReviewsOnPush: true, + }), + NewRequiredStatusChecksRule(&RequiredStatusChecksRuleParameters{ + RequiredStatusChecks: []RuleRequiredStatusChecks{ + { + Context: "test", + IntegrationID: Int64(1), + }, + }, + StrictRequiredStatusChecksPolicy: true, + }), + NewNonFastForwardRule(), + NewCommitMessagePatternRule(&RulePatternParameters{ + Name: String("avoid test commits"), + Negate: Bool(true), + Operator: "starts_with", + Pattern: "[test]", + }), + NewCommitAuthorEmailPatternRule(&RulePatternParameters{ + Operator: "contains", + Pattern: "github", + }), + NewCommitterEmailPatternRule(&RulePatternParameters{ + Name: String("avoid commit emails"), + Negate: Bool(true), + Operator: "ends_with", + Pattern: "abc", + }), + NewBranchNamePatternRule(&RulePatternParameters{ + Name: String("avoid branch names"), + Negate: Bool(true), + Operator: "regex", + Pattern: "github$", + }), + NewTagNamePatternRule(&RulePatternParameters{ + Name: String("avoid tag names"), + Negate: Bool(true), + Operator: "contains", + Pattern: "github", + }), + }, + } + if !cmp.Equal(ruleset, want) { + t.Errorf("Organizations.CreateOrganizationRuleset returned %+v, want %+v", ruleset, want) + } + + const methodName = "CreateOrganizationRuleset" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.CreateOrganizationRuleset(ctx, "o", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} func TestOrganizationsService_CreateOrganizationRuleset_RepoIDs(t *testing.T) { client, mux, _, teardown := setup() defer teardown() @@ -807,6 +1142,94 @@ func TestOrganizationsService_GetOrganizationRuleset(t *testing.T) { }) } +func TestOrganizationsService_GetOrganizationRulesetWithRepoPropCondition(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/rulesets/26110", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "id": 26110, + "name": "test ruleset", + "target": "branch", + "source_type": "Organization", + "source": "o", + "enforcement": "active", + "bypass_mode": "none", + "node_id": "nid", + "_links": { + "self": { + "href": "https://api.github.com/orgs/o/rulesets/26110" + } + }, + "conditions": { + "repository_property": { + "exclude": [], + "include": [ + { + "name": "testIncludeProp", + "source": "custom", + "property_values": [ + "true" + ] + } + ] + } + }, + "rules": [ + { + "type": "creation" + } + ] + }`) + }) + + ctx := context.Background() + rulesets, _, err := client.Organizations.GetOrganizationRuleset(ctx, "o", 26110) + if err != nil { + t.Errorf("Organizations.GetOrganizationRepositoryRuleset returned error: %v", err) + } + + want := &Ruleset{ + ID: Int64(26110), + Name: "test ruleset", + Target: String("branch"), + SourceType: String("Organization"), + Source: "o", + Enforcement: "active", + NodeID: String("nid"), + Links: &RulesetLinks{ + Self: &RulesetLink{HRef: String("https://api.github.com/orgs/o/rulesets/26110")}, + }, + Conditions: &RulesetConditions{ + RepositoryProperty: &RulesetRepositoryPropertyConditionParameters{ + Include: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testIncludeProp", + Values: []string{"true"}, + }, + }, + Exclude: []RulesetRepositoryPropertyTargetParameters{}, + }, + }, + Rules: []*RepositoryRule{ + NewCreationRule(), + }, + } + if !cmp.Equal(rulesets, want) { + t.Errorf("Organizations.GetOrganizationRuleset returned %+v, want %+v", rulesets, want) + } + + const methodName = "GetOrganizationRuleset" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.GetOrganizationRuleset(ctx, "o", 26110) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} func TestOrganizationsService_UpdateOrganizationRuleset(t *testing.T) { client, mux, _, teardown := setup() defer teardown() @@ -922,6 +1345,113 @@ func TestOrganizationsService_UpdateOrganizationRuleset(t *testing.T) { }) } +func TestOrganizationsService_UpdateOrganizationRulesetWithRepoProp(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/rulesets/26110", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{ + "id": 26110, + "name": "test ruleset", + "target": "branch", + "source_type": "Organization", + "source": "o", + "enforcement": "active", + "bypass_mode": "none", + "node_id": "nid", + "_links": { + "self": { + "href": "https://api.github.com/orgs/o/rulesets/26110" + } + }, + "conditions": { + "repository_property": { + "exclude": [], + "include": [ + { + "name": "testIncludeProp", + "source": "custom", + "property_values": [ + "true" + ] + } + ] + } + }, + "rules": [ + { + "type": "creation" + } + ] + }`) + }) + + ctx := context.Background() + rulesets, _, err := client.Organizations.UpdateOrganizationRuleset(ctx, "o", 26110, &Ruleset{ + Name: "test ruleset", + Target: String("branch"), + Enforcement: "active", + Conditions: &RulesetConditions{ + RepositoryProperty: &RulesetRepositoryPropertyConditionParameters{ + Include: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testIncludeProp", + Values: []string{"true"}, + }, + }, + Exclude: []RulesetRepositoryPropertyTargetParameters{}, + }, + }, + Rules: []*RepositoryRule{ + NewCreationRule(), + }, + }) + + if err != nil { + t.Errorf("Organizations.UpdateOrganizationRuleset returned error: %v", err) + } + + want := &Ruleset{ + ID: Int64(26110), + Name: "test ruleset", + Target: String("branch"), + SourceType: String("Organization"), + Source: "o", + Enforcement: "active", + NodeID: String("nid"), + Links: &RulesetLinks{ + Self: &RulesetLink{HRef: String("https://api.github.com/orgs/o/rulesets/26110")}, + }, + Conditions: &RulesetConditions{ + RepositoryProperty: &RulesetRepositoryPropertyConditionParameters{ + Include: []RulesetRepositoryPropertyTargetParameters{ + { + Name: "testIncludeProp", + Values: []string{"true"}, + }, + }, + Exclude: []RulesetRepositoryPropertyTargetParameters{}, + }, + }, + Rules: []*RepositoryRule{ + NewCreationRule(), + }, + } + if !cmp.Equal(rulesets, want) { + t.Errorf("Organizations.UpdateOrganizationRuleset returned %+v, want %+v", rulesets, want) + } + + const methodName = "UpdateOrganizationRuleset" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.UpdateOrganizationRuleset(ctx, "o", 26110, nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} func TestOrganizationsService_DeleteOrganizationRuleset(t *testing.T) { client, mux, _, teardown := setup() defer teardown() diff --git a/github/repos_rules.go b/github/repos_rules.go index 6f046a356a6..e9148f2f03b 100644 --- a/github/repos_rules.go +++ b/github/repos_rules.go @@ -48,12 +48,25 @@ type RulesetRepositoryIDsConditionParameters struct { RepositoryIDs []int64 `json:"repository_ids,omitempty"` } +// RulesetRepositoryPropertyTargetParameters represents a repository_property name and values to be used for targeting. +type RulesetRepositoryPropertyTargetParameters struct { + Name string `json:"name"` + Values []string `json:"property_values"` +} + +// RulesetRepositoryPropertyConditionParameters represents the conditions object for repository_property. +type RulesetRepositoryPropertyConditionParameters struct { + Include []RulesetRepositoryPropertyTargetParameters `json:"include"` + Exclude []RulesetRepositoryPropertyTargetParameters `json:"exclude"` +} + // RulesetConditions represents the conditions object in a ruleset. -// Set either RepositoryName or RepositoryID, not both. +// Set either RepositoryName or RepositoryID or RepositoryProperty, not more than one. type RulesetConditions struct { - RefName *RulesetRefConditionParameters `json:"ref_name,omitempty"` - RepositoryName *RulesetRepositoryNamesConditionParameters `json:"repository_name,omitempty"` - RepositoryID *RulesetRepositoryIDsConditionParameters `json:"repository_id,omitempty"` + RefName *RulesetRefConditionParameters `json:"ref_name,omitempty"` + RepositoryName *RulesetRepositoryNamesConditionParameters `json:"repository_name,omitempty"` + RepositoryID *RulesetRepositoryIDsConditionParameters `json:"repository_id,omitempty"` + RepositoryProperty *RulesetRepositoryPropertyConditionParameters `json:"repository_property,omitempty"` } // RulePatternParameters represents the rule pattern parameters.