From cb37dbdeaaed39878a6f725134eaa1e9a5889344 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 6 Aug 2024 22:45:07 +0100 Subject: [PATCH 01/20] Define schema for repository_property Refactor expandConditions to reduce complexity Refactor logic to reduce the cognitive complexity and add logic to handle the repository_property field Flatten conditions for repository_property and fix schemas Add test case when ruleset use repository_property Refactor repository property conditions to make them optional Flatten update Target parameters to allow the detection of changes when remote resource is updated Update documentation --- .../resource_github_organization_ruleset.go | 79 +++++++++++++++++-- ...source_github_organization_ruleset_test.go | 56 +++++++++++++ github/util_rules.go | 73 +++++++++++++++++ .../docs/r/organization_ruleset.html.markdown | 21 ++++- 4 files changed, 220 insertions(+), 9 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 1fac297d03..5f5ea6eeaf 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -94,7 +94,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, - Description: "Parameters for an organization ruleset condition. `ref_name` is required for `branch` and `tag` targets, but must not be set for `push` targets. One of `repository_name` or `repository_id` is always required.", + Description: "Parameters for an organization ruleset condition. Push rulesets require exactly one of: repository_id, repository_name, or repository_property. Branch/tag rulesets require ref_name AND exactly one repository targeting option.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "ref_name": { @@ -123,13 +123,80 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, }, + "repository_property": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Conditions to target repositories by custom or system properties.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "include": { + Type: schema.TypeList, + Optional: true, + Description: "The repository properties and values to include. All of these properties must match for the condition to pass.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the repository property to target.", + }, + "property_values": { + Type: schema.TypeList, + Required: true, + Description: "The values to match for the repository property.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "source": { + Type: schema.TypeString, + Optional: true, + Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", + Default: "custom", + }, + }, + }, + }, + "exclude": { + Type: schema.TypeList, + Optional: true, + Description: "The repository properties and values to exclude. The condition will not pass if any of these properties match.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + + "name": { + Type: schema.TypeString, + Required: true, + Description: "The name of the repository property to target.", + }, + "property_values": { + Type: schema.TypeList, + Required: true, + Description: "The values to match for the repository property.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "source": { + Type: schema.TypeString, + Optional: true, + Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", + Default: "custom", + ValidateFunc: validation.StringInSlice([]string{"custom", "system"}, false), + }, + }, + }, + }, + }, + }, + }, "repository_name": { Type: schema.TypeList, Optional: true, MaxItems: 1, Description: "Targets repositories that match the specified name patterns.", - ExactlyOneOf: []string{"conditions.0.repository_id"}, - AtLeastOneOf: []string{"conditions.0.repository_id"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "include": { @@ -158,9 +225,9 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_id": { - Type: schema.TypeList, - Optional: true, - Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + Type: schema.TypeList, + Optional: true, + Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", Elem: &schema.Schema{ Type: schema.TypeInt, }, diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index 6f8dd502c5..2412650497 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -182,6 +182,62 @@ resource "github_organization_ruleset" "test" { }) }) + t.Run("create_ruleset_with_repository_property", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + rulesetName := fmt.Sprintf("%s-repo-prop-ruleset-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_organization_ruleset" "test" { + name = "%s" + target = "branch" + enforcement = "active" + + conditions { + repository_property { + include = [{ + name = "team" + source = "custom" + property_values = ["blue"] + }] + exclude = [] + } + + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + rules { + creation = true + update = true + deletion = true + required_linear_history = true + } +} +`, rulesetName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "target", "branch"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "enforcement", "active"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "team"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "blue"), + ), + }, + }, + }) + }) + t.Run("create_push_ruleset", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-push-ruleset-%s", testResourcePrefix, randomID) diff --git a/github/util_rules.go b/github/util_rules.go index 71c08b2ea1..ecc2b8c79f 100644 --- a/github/util_rules.go +++ b/github/util_rules.go @@ -250,12 +250,61 @@ func expandConditions(input []any, org bool) *github.RepositoryRulesetConditions } rulesetConditions.RepositoryID = &github.RepositoryRulesetRepositoryIDsConditionParameters{RepositoryIDs: repositoryIDs} + } else if v, ok := inputConditions["repository_property"].([]any); ok && v != nil && len(v) != 0 { + rulesetConditions.RepositoryProperty = expandRepositoryPropertyConditions(v) } } return rulesetConditions } +func expandRepositoryPropertyConditions(v []any) *github.RepositoryRulesetRepositoryPropertyConditionParameters { + repositoryProperties := v[0].(map[string]any) + include := make([]*github.RepositoryRulesetRepositoryPropertyTargetParameters, 0) + exclude := make([]*github.RepositoryRulesetRepositoryPropertyTargetParameters, 0) + + for _, v := range repositoryProperties["include"].([]any) { + if v != nil { + propertyMap := v.(map[string]any) + propertyValues := make([]string, 0) + for _, pv := range propertyMap["property_values"].([]any) { + if pv != nil { + propertyValues = append(propertyValues, pv.(string)) + } + } + property := &github.RepositoryRulesetRepositoryPropertyTargetParameters{ + Name: propertyMap["name"].(string), + Source: github.Ptr(propertyMap["source"].(string)), + PropertyValues: propertyValues, + } + include = append(include, property) + } + } + + for _, v := range repositoryProperties["exclude"].([]any) { + if v != nil { + propertyMap := v.(map[string]any) + propertyValues := make([]string, 0) + for _, pv := range propertyMap["property_values"].([]any) { + if pv != nil { + propertyValues = append(propertyValues, pv.(string)) + } + } + property := &github.RepositoryRulesetRepositoryPropertyTargetParameters{ + Name: propertyMap["name"].(string), + Source: github.Ptr(propertyMap["source"].(string)), + PropertyValues: propertyValues, + } + exclude = append(exclude, property) + } + } + + return &github.RepositoryRulesetRepositoryPropertyConditionParameters{ + Include: include, + Exclude: exclude, + } +} + func flattenConditions(ctx context.Context, conditions *github.RepositoryRulesetConditions, org bool) []any { if conditions == nil || reflect.DeepEqual(conditions, &github.RepositoryRulesetConditions{}) { tflog.Debug(ctx, "Conditions are empty, returning empty list") @@ -296,11 +345,35 @@ func flattenConditions(ctx context.Context, conditions *github.RepositoryRuleset if conditions.RepositoryID != nil { conditionsMap["repository_id"] = conditions.RepositoryID.RepositoryIDs } + + if conditions.RepositoryProperty != nil { + repositoryPropertySlice := make([]map[string]any, 0) + + repositoryPropertySlice = append(repositoryPropertySlice, map[string]any{ + "include": flattenRulesetRepositoryPropertyTargetParameters(conditions.RepositoryProperty.Include), + "exclude": flattenRulesetRepositoryPropertyTargetParameters(conditions.RepositoryProperty.Exclude), + }) + conditionsMap["repository_property"] = repositoryPropertySlice + } } return []any{conditionsMap} } +func flattenRulesetRepositoryPropertyTargetParameters(input []*github.RepositoryRulesetRepositoryPropertyTargetParameters) []map[string]any { + result := make([]map[string]any, 0) + + for _, v := range input { + propertyMap := make(map[string]any) + propertyMap["name"] = v.Name + propertyMap["source"] = v.GetSource() + propertyMap["property_values"] = v.PropertyValues + result = append(result, propertyMap) + } + + return result +} + func expandRules(input []any, org bool) *github.RepositoryRulesetRules { if len(input) == 0 || input[0] == nil { return &github.RepositoryRulesetRules{} diff --git a/website/docs/r/organization_ruleset.html.markdown b/website/docs/r/organization_ruleset.html.markdown index 5a7e10d506..1cea4ecf18 100644 --- a/website/docs/r/organization_ruleset.html.markdown +++ b/website/docs/r/organization_ruleset.html.markdown @@ -325,10 +325,11 @@ The `rules` block supports the following: #### conditions #### - `ref_name` - (Optional) (Block List, Max: 1) Required for `branch` and `tag` targets. Must NOT be set for `push` targets. (see [below for nested schema](#conditionsref_name)) -- `repository_id` (Optional) (List of Number) The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass. Conflicts with `repository_name`. -- `repository_name` (Optional) (Block List, Max: 1) Conflicts with `repository_id`. (see [below for nested schema](#conditionsrepository_name)) +- `repository_id` (Optional) (List of Number) The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass. +- `repository_name` (Optional) (Block List, Max: 1) Targets repositories that match the specified name patterns. (see [below for nested schema](#conditionsrepository_name)) +- `repository_property` (Optional) (Block List, Max: 1) Targets repositories by custom or system properties. (see [below for nested schema](#conditionsrepository_property)) -One of `repository_id` and `repository_name` must be set for the rule to target any repositories. +Exactly one of `repository_id`, `repository_name`, or `repository_property` must be set for the rule to target repositories. ~> **Note:** For `push` targets, do not include `ref_name` in conditions. Push rulesets operate on file content, not on refs. @@ -344,6 +345,20 @@ One of `repository_id` and `repository_name` must be set for the rule to target - `include` - (Required) (List of String) Array of repository names or patterns to include. One of these patterns must match for the condition to pass. Also accepts `~ALL` to include all repositories. - `protected` - (Optional) (Boolean) Whether renaming of target repositories is prevented. Defaults to `false`. +#### conditions.repository_property #### + +- `include` - (Optional) (List of Repository Properties) The repository properties and values to include. All of these properties must match for the condition to pass. (see [below for nested schema](#conditions.repository_property.properties)) + +- `exclude` - (Optional) (List of Repository Properties) The repository properties and values to exclude. The condition will not pass if any of these properties match.(see [below for nested schema](#conditions.repository_property.properties)) + +#### conditions.repository_property.properties #### + +- `name` (Required) (String) The name of the repository property to target. + +- `property_values` (Required) (Array of String) The values to match for the repository property. + +- `source` (String) The source of the repository property. Defaults to 'custom' if not specified. Can be one of: `custom`, `system` + ## Attributes Reference The following additional attributes are exported: From 35bc82d8c648d03c9a21399bbacc2642978a5b56 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 18 Nov 2025 16:17:30 +0000 Subject: [PATCH 02/20] Apply format in the code base --- github/resource_github_organization_ruleset.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 5f5ea6eeaf..c10847cde7 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -136,7 +136,6 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Description: "The repository properties and values to include. All of these properties must match for the condition to pass.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "name": { Type: schema.TypeString, Required: true, @@ -165,7 +164,6 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Description: "The repository properties and values to exclude. The condition will not pass if any of these properties match.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "name": { Type: schema.TypeString, Required: true, From 49ab0089a8d5fdf14befd2b761160dbb2d1e5d9e Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Sun, 18 Jan 2026 16:30:43 +0000 Subject: [PATCH 03/20] Fix repository_property validation and docs - Add ValidateFunc to include.source field for consistency with exclude - Update docs to mention repository_property as third targeting option - Fix missing space in documentation --- github/resource_github_organization_ruleset.go | 9 +++++---- website/docs/r/organization_ruleset.html.markdown | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index c10847cde7..6f0bed663c 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -150,10 +150,11 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "source": { - Type: schema.TypeString, - Optional: true, - Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", - Default: "custom", + Type: schema.TypeString, + Optional: true, + Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", + Default: "custom", + ValidateFunc: validation.StringInSlice([]string{"custom", "system"}, false), }, }, }, diff --git a/website/docs/r/organization_ruleset.html.markdown b/website/docs/r/organization_ruleset.html.markdown index 1cea4ecf18..e14dc678d2 100644 --- a/website/docs/r/organization_ruleset.html.markdown +++ b/website/docs/r/organization_ruleset.html.markdown @@ -349,7 +349,7 @@ Exactly one of `repository_id`, `repository_name`, or `repository_property` must - `include` - (Optional) (List of Repository Properties) The repository properties and values to include. All of these properties must match for the condition to pass. (see [below for nested schema](#conditions.repository_property.properties)) -- `exclude` - (Optional) (List of Repository Properties) The repository properties and values to exclude. The condition will not pass if any of these properties match.(see [below for nested schema](#conditions.repository_property.properties)) +- `exclude` - (Optional) (List of Repository Properties) The repository properties and values to exclude. The condition will not pass if any of these properties match. (see [below for nested schema](#conditions.repository_property.properties)) #### conditions.repository_property.properties #### From e64d7db6790dee35e09038e98222f613f1e482e2 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Mon, 19 Jan 2026 16:51:36 +0000 Subject: [PATCH 04/20] Support repository_property in push rulesets --- .../resource_github_organization_ruleset.go | 3 ++- github/resource_github_repository_ruleset.go | 3 ++- github/util_ruleset_validation.go | 23 +++++++++++++++---- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 6f0bed663c..2a6db6d251 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-github/v83/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -30,7 +31,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { SchemaVersion: 1, - CustomizeDiff: resourceGithubOrganizationRulesetDiff, + CustomizeDiff: resourceGithubOrganizationRulesetDiff, Schema: map[string]*schema.Schema{ "name": { diff --git a/github/resource_github_repository_ruleset.go b/github/resource_github_repository_ruleset.go index f8eef8e233..3ad063d5a5 100644 --- a/github/resource_github_repository_ruleset.go +++ b/github/resource_github_repository_ruleset.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-github/v83/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -104,7 +105,7 @@ func resourceGithubRepositoryRuleset() *schema.Resource { Schema: map[string]*schema.Schema{ "ref_name": { Type: schema.TypeList, - Required: true, + Optional: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ diff --git a/github/util_ruleset_validation.go b/github/util_ruleset_validation.go index 1e784be97a..1668cf16c6 100644 --- a/github/util_ruleset_validation.go +++ b/github/util_ruleset_validation.go @@ -171,11 +171,26 @@ func validateConditionsFieldForBranchAndTagTargets(ctx context.Context, target g return fmt.Errorf("ref_name must be set for %s target", target) } - // Repository rulesets don't have repository_name or repository_id, only org rulesets do. + // Repository rulesets don't have repository_name, repository_id, or repository_property - only org rulesets do. if isOrg { - if (conditions["repository_name"] == nil || len(conditions["repository_name"].([]any)) == 0) && (conditions["repository_id"] == nil || len(conditions["repository_id"].([]any)) == 0) { - tflog.Debug(ctx, fmt.Sprintf("Missing repository_name or repository_id for %s target", target), map[string]any{"target": target}) - return fmt.Errorf("either repository_name or repository_id must be set for %s target", target) + hasRepoName := conditions["repository_name"] != nil && len(conditions["repository_name"].([]any)) > 0 + hasRepoId := conditions["repository_id"] != nil && len(conditions["repository_id"].([]any)) > 0 + hasRepoProp := conditions["repository_property"] != nil && len(conditions["repository_property"].([]any)) > 0 + + repoTargetingCount := 0 + if hasRepoName { + repoTargetingCount++ + } + if hasRepoId { + repoTargetingCount++ + } + if hasRepoProp { + repoTargetingCount++ + } + + if repoTargetingCount != 1 { + tflog.Debug(ctx, fmt.Sprintf("Invalid repository targeting for %s target", target), map[string]any{"target": target}) + return fmt.Errorf("exactly one of repository_name, repository_id, or repository_property must be set for %s target", target) } } tflog.Debug(ctx, fmt.Sprintf("Conditions validation passed for %s target", target)) From d83c9b6f66deaa11722e7bec0ff5c7b71dff281a Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Wed, 4 Feb 2026 21:38:43 +0000 Subject: [PATCH 05/20] Remove unused customdiff imports --- .../resource_github_organization_ruleset.go | 25 +++++++++---------- github/resource_github_repository_ruleset.go | 1 - 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 2a6db6d251..cc0372ea12 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -12,7 +12,6 @@ import ( "github.com/google/go-github/v83/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) @@ -31,7 +30,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { SchemaVersion: 1, - CustomizeDiff: resourceGithubOrganizationRulesetDiff, + CustomizeDiff: resourceGithubOrganizationRulesetDiff, Schema: map[string]*schema.Schema{ "name": { @@ -125,10 +124,10 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_property": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Conditions to target repositories by custom or system properties.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Conditions to target repositories by custom or system properties.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "include": { @@ -193,10 +192,10 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_name": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Targets repositories that match the specified name patterns.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Targets repositories that match the specified name patterns.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "include": { @@ -225,9 +224,9 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_id": { - Type: schema.TypeList, - Optional: true, - Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + Type: schema.TypeList, + Optional: true, + Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", Elem: &schema.Schema{ Type: schema.TypeInt, }, diff --git a/github/resource_github_repository_ruleset.go b/github/resource_github_repository_ruleset.go index 3ad063d5a5..629f48cd83 100644 --- a/github/resource_github_repository_ruleset.go +++ b/github/resource_github_repository_ruleset.go @@ -12,7 +12,6 @@ import ( "github.com/google/go-github/v83/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) From b62cfddd0f605e2c134ac4696f2bf6f7142331f5 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Fri, 13 Feb 2026 16:50:04 +0000 Subject: [PATCH 06/20] Replace custom validation with built-in ExactlyOneOf for repo targeting Add ExactlyOneOf/AtLeastOneOf to repository_property, repository_name,repository_id fields. Remove manual validation counting in util_ruleset_validation.Add 3 validation tests for single/multiple/missing repo targeting options. Addresses PR #2356 review feedback - simplifies validation using schema constraints. --- .../resource_github_organization_ruleset.go | 32 +++-- ...source_github_organization_ruleset_test.go | 114 ++++++++++++++++++ github/util_ruleset_validation.go | 23 +--- 3 files changed, 135 insertions(+), 34 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index cc0372ea12..328d44a0aa 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -124,10 +124,12 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_property": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Conditions to target repositories by custom or system properties.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, + AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, + Description: "Conditions to target repositories by custom or system properties.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "include": { @@ -154,7 +156,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Optional: true, Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", Default: "custom", - ValidateFunc: validation.StringInSlice([]string{"custom", "system"}, false), + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"custom", "system"}, false)), }, }, }, @@ -183,7 +185,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Optional: true, Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", Default: "custom", - ValidateFunc: validation.StringInSlice([]string{"custom", "system"}, false), + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"custom", "system"}, false)), }, }, }, @@ -192,10 +194,12 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_name": { - Type: schema.TypeList, - Optional: true, - MaxItems: 1, - Description: "Targets repositories that match the specified name patterns.", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, + AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, + Description: "Targets repositories that match the specified name patterns.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "include": { @@ -224,9 +228,11 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "repository_id": { - Type: schema.TypeList, - Optional: true, - Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + Type: schema.TypeList, + Optional: true, + ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, + AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, + Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", Elem: &schema.Schema{ Type: schema.TypeInt, }, diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index 2412650497..739f5f159b 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -738,6 +738,120 @@ resource "github_organization_ruleset" "test" { }) }) + t.Run("validates_conditions_require_exactly_one_repository_targeting", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + resourceName := "test-multiple-repo-targeting" + config := fmt.Sprintf(` + resource "github_organization_ruleset" "%s" { + name = "test-multiple-targeting-%s" + target = "branch" + enforcement = "active" + + conditions { + ref_name { + include = ["~ALL"] + exclude = [] + } + repository_name { + include = ["~ALL"] + exclude = [] + } + repository_id = [123] + } + + rules { + creation = true + } + } + `, resourceName, randomID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("only one of `conditions.0.repository_id,conditions.0.repository_name,conditions.0.repository_property` can be specified"), + }, + }, + }) + }) + + t.Run("validates_conditions_require_at_least_one_repository_targeting", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + resourceName := "test-no-repo-targeting" + config := fmt.Sprintf(` + resource "github_organization_ruleset" "%s" { + name = "test-no-targeting-%s" + target = "branch" + enforcement = "active" + + conditions { + ref_name { + include = ["~ALL"] + exclude = [] + } + } + + rules { + creation = true + } + } + `, resourceName, randomID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("one of `conditions.0.repository_id,conditions.0.repository_name,conditions.0.repository_property` must be specified"), + }, + }, + }) + }) + + t.Run("validates_repository_property_works_as_single_targeting_option", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + resourceName := "test-repo-property-targeting" + config := fmt.Sprintf(` + resource "github_organization_ruleset" "%s" { + name = "test-property-targeting-%s" + target = "branch" + enforcement = "active" + + conditions { + ref_name { + include = ["~ALL"] + exclude = [] + } + repository_property { + include { + name = "environment" + property_values = ["production"] + source = "custom" + } + } + } + + rules { + creation = true + } + } + `, resourceName, randomID) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: nil, // This should succeed + }, + }, + }) + }) + t.Run("creates_push_ruleset", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%stest-push-%s", testResourcePrefix, randomID) diff --git a/github/util_ruleset_validation.go b/github/util_ruleset_validation.go index 1668cf16c6..eb33651f61 100644 --- a/github/util_ruleset_validation.go +++ b/github/util_ruleset_validation.go @@ -172,27 +172,8 @@ func validateConditionsFieldForBranchAndTagTargets(ctx context.Context, target g } // Repository rulesets don't have repository_name, repository_id, or repository_property - only org rulesets do. - if isOrg { - hasRepoName := conditions["repository_name"] != nil && len(conditions["repository_name"].([]any)) > 0 - hasRepoId := conditions["repository_id"] != nil && len(conditions["repository_id"].([]any)) > 0 - hasRepoProp := conditions["repository_property"] != nil && len(conditions["repository_property"].([]any)) > 0 - - repoTargetingCount := 0 - if hasRepoName { - repoTargetingCount++ - } - if hasRepoId { - repoTargetingCount++ - } - if hasRepoProp { - repoTargetingCount++ - } - - if repoTargetingCount != 1 { - tflog.Debug(ctx, fmt.Sprintf("Invalid repository targeting for %s target", target), map[string]any{"target": target}) - return fmt.Errorf("exactly one of repository_name, repository_id, or repository_property must be set for %s target", target) - } - } + // Note: The built-in ExactlyOneOf/AtLeastOneOf schema validation already ensures exactly one + // repository targeting option is set, so no additional validation is needed here. tflog.Debug(ctx, fmt.Sprintf("Conditions validation passed for %s target", target)) return nil } From 5d32b3f938e9aed80b558532f46087eec746d0bc Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Fri, 13 Feb 2026 16:50:25 +0000 Subject: [PATCH 07/20] Rever unintencial change --- github/resource_github_repository_ruleset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/resource_github_repository_ruleset.go b/github/resource_github_repository_ruleset.go index 629f48cd83..f8eef8e233 100644 --- a/github/resource_github_repository_ruleset.go +++ b/github/resource_github_repository_ruleset.go @@ -104,7 +104,7 @@ func resourceGithubRepositoryRuleset() *schema.Resource { Schema: map[string]*schema.Schema{ "ref_name": { Type: schema.TypeList, - Optional: true, + Required: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ From c0333ba864ea50a44459ae816c76e0a7a5111c1f Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Fri, 13 Feb 2026 17:07:39 +0000 Subject: [PATCH 08/20] Add tests, docs, and fix default handling for repository_property - Add doc example for repository_property usage - Add tests for exclude block, multiple properties, and updates - Fix source field default handling in flatten function to prevent diffs --- ...source_github_organization_ruleset_test.go | 206 ++++++++++++++++++ github/util_rules.go | 6 +- .../docs/r/organization_ruleset.html.markdown | 37 ++++ 3 files changed, 248 insertions(+), 1 deletion(-) diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index 739f5f159b..de983d58fc 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -238,6 +238,212 @@ resource "github_organization_ruleset" "test" { }) }) + t.Run("create_ruleset_with_repository_property_exclude", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + rulesetName := fmt.Sprintf("%s-repo-prop-exclude-ruleset-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_organization_ruleset" "test" { + name = "%s" + target = "branch" + enforcement = "active" + + conditions { + repository_property { + include = [{ + name = "tier" + source = "custom" + property_values = ["premium"] + }] + exclude = [{ + name = "archived" + source = "system" + property_values = ["true"] + }] + } + + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + rules { + required_linear_history = true + } +} +`, rulesetName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", "archived"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.source", "system"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.property_values.0", "true"), + ), + }, + }, + }) + }) + + t.Run("create_ruleset_with_multiple_repository_properties", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + rulesetName := fmt.Sprintf("%s-repo-prop-multiple-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_organization_ruleset" "test" { + name = "%s" + target = "branch" + enforcement = "active" + + conditions { + repository_property { + include = [ + { + name = "environment" + source = "custom" + property_values = ["production"] + }, + { + name = "team" + source = "custom" + property_values = ["backend", "platform"] + } + ] + exclude = [] + } + + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + rules { + required_signatures = true + } +} +`, rulesetName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "2"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "environment"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.name", "team"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.#", "2"), + ), + }, + }, + }) + }) + + t.Run("update_repository_property", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + rulesetName := fmt.Sprintf("%s-repo-prop-update-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_organization_ruleset" "test" { + name = "%s" + target = "branch" + enforcement = "active" + + conditions { + repository_property { + include = [{ + name = "tier" + source = "custom" + property_values = ["basic"] + }] + exclude = [] + } + + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + rules { + creation = true + } +} +`, rulesetName) + + configUpdated := fmt.Sprintf(` +resource "github_organization_ruleset" "test" { + name = "%s" + target = "branch" + enforcement = "active" + + conditions { + repository_property { + include = [{ + name = "tier" + source = "custom" + property_values = ["premium", "enterprise"] + }] + exclude = [{ + name = "archived" + source = "system" + property_values = ["true"] + }] + } + + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + rules { + creation = true + update = true + } +} +`, rulesetName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "basic"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "0"), + ), + }, + { + Config: configUpdated, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "2"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "premium"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.1", "enterprise"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", "archived"), + ), + }, + }, + }) + }) + t.Run("create_push_ruleset", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-push-ruleset-%s", testResourcePrefix, randomID) diff --git a/github/util_rules.go b/github/util_rules.go index ecc2b8c79f..b5006f591a 100644 --- a/github/util_rules.go +++ b/github/util_rules.go @@ -366,7 +366,11 @@ func flattenRulesetRepositoryPropertyTargetParameters(input []*github.Repository for _, v := range input { propertyMap := make(map[string]any) propertyMap["name"] = v.Name - propertyMap["source"] = v.GetSource() + source := v.GetSource() + if source == "" { + source = "custom" + } + propertyMap["source"] = source propertyMap["property_values"] = v.PropertyValues result = append(result, propertyMap) } diff --git a/website/docs/r/organization_ruleset.html.markdown b/website/docs/r/organization_ruleset.html.markdown index e14dc678d2..210d912b20 100644 --- a/website/docs/r/organization_ruleset.html.markdown +++ b/website/docs/r/organization_ruleset.html.markdown @@ -99,6 +99,43 @@ resource "github_organization_ruleset" "example_push" { } } } + +# Example with repository_property targeting +resource "github_organization_ruleset" "example_property" { + name = "example_property" + target = "branch" + enforcement = "active" + + conditions { + ref_name { + include = ["~ALL"] + exclude = [] + } + + repository_property { + include { + name = "environment" + property_values = ["production", "staging"] + source = "custom" + } + include { + name = "team" + property_values = ["backend"] + source = "custom" + } + exclude { + name = "archived" + property_values = ["true"] + source = "system" + } + } + } + + rules { + required_signatures = true + pull_request {} + } +} ``` ## Argument Reference From 59bb126e0b5a7777d4359259526feca9745315f0 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Sat, 14 Feb 2026 14:10:23 +0000 Subject: [PATCH 09/20] Apply suggestions from PR comment --- github/resource_github_organization_ruleset.go | 4 ++-- github/util_ruleset_validation.go | 4 +--- website/docs/r/organization_ruleset.html.markdown | 4 ++-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 328d44a0aa..73c5322aff 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -164,7 +164,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { "exclude": { Type: schema.TypeList, Optional: true, - Description: "The repository properties and values to exclude. The condition will not pass if any of these properties match.", + Description: "The repository properties and values to exclude. The ruleset will not apply if any of these properties match.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { @@ -232,7 +232,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Optional: true, ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, - Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the condition to pass.", + Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the ruleset to apply.", Elem: &schema.Schema{ Type: schema.TypeInt, }, diff --git a/github/util_ruleset_validation.go b/github/util_ruleset_validation.go index eb33651f61..36247f642c 100644 --- a/github/util_ruleset_validation.go +++ b/github/util_ruleset_validation.go @@ -171,9 +171,7 @@ func validateConditionsFieldForBranchAndTagTargets(ctx context.Context, target g return fmt.Errorf("ref_name must be set for %s target", target) } - // Repository rulesets don't have repository_name, repository_id, or repository_property - only org rulesets do. - // Note: The built-in ExactlyOneOf/AtLeastOneOf schema validation already ensures exactly one - // repository targeting option is set, so no additional validation is needed here. + tflog.Debug(ctx, fmt.Sprintf("Conditions validation passed for %s target", target)) return nil } diff --git a/website/docs/r/organization_ruleset.html.markdown b/website/docs/r/organization_ruleset.html.markdown index 210d912b20..eebb3813dc 100644 --- a/website/docs/r/organization_ruleset.html.markdown +++ b/website/docs/r/organization_ruleset.html.markdown @@ -384,9 +384,9 @@ Exactly one of `repository_id`, `repository_name`, or `repository_property` must #### conditions.repository_property #### -- `include` - (Optional) (List of Repository Properties) The repository properties and values to include. All of these properties must match for the condition to pass. (see [below for nested schema](#conditions.repository_property.properties)) +- `include` - (Optional) (List of Repository Properties) The repository properties and values to include. All of these properties must match for the condition to pass. (see [below for nested schema](#conditionsrepository_propertyproperties)) -- `exclude` - (Optional) (List of Repository Properties) The repository properties and values to exclude. The condition will not pass if any of these properties match. (see [below for nested schema](#conditions.repository_property.properties)) +- `exclude` - (Optional) (List of Repository Properties) The repository properties and values to exclude. The condition will not pass if any of these properties match. (see [below for nested schema](#conditionsrepository_propertyproperties)) #### conditions.repository_property.properties #### From c5085be7260ed258b3ee6de1332326b70727b0b2 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Sat, 14 Feb 2026 16:23:50 +0000 Subject: [PATCH 10/20] Add unit tests --- github/util_rules_test.go | 485 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 485 insertions(+) diff --git a/github/util_rules_test.go b/github/util_rules_test.go index d99e6e4589..c53afc8700 100644 --- a/github/util_rules_test.go +++ b/github/util_rules_test.go @@ -814,3 +814,488 @@ func TestRoundTripRequiredReviewers(t *testing.T) { t.Errorf("Expected reviewer type to be Team after round trip, got %v", reviewerBlock[0]["type"]) } } + +func TestExpandRepositoryPropertyConditions_SingleInclude(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod"}, + }, + }, + "exclude": []any{}, + }, + } + + result := expandRepositoryPropertyConditions(input) + + if result == nil { + t.Fatal("Expected result to not be nil") + } + + if len(result.Include) != 1 { + t.Fatalf("Expected 1 include property, got %d", len(result.Include)) + } + + if len(result.Exclude) != 0 { + t.Fatalf("Expected 0 exclude properties, got %d", len(result.Exclude)) + } + + prop := result.Include[0] + if prop.Name != "env" { + t.Errorf("Expected name to be 'env', got %s", prop.Name) + } + if prop.Source == nil || *prop.Source != "custom" { + t.Errorf("Expected source to be 'custom', got %v", prop.Source) + } + if len(prop.PropertyValues) != 1 || prop.PropertyValues[0] != "prod" { + t.Errorf("Expected property_values to be ['prod'], got %v", prop.PropertyValues) + } +} + +func TestExpandRepositoryPropertyConditions_IncludeAndExclude(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod"}, + }, + }, + "exclude": []any{ + map[string]any{ + "name": "tier", + "source": "system", + "property_values": []any{"free"}, + }, + }, + }, + } + + result := expandRepositoryPropertyConditions(input) + + if result == nil { + t.Fatal("Expected result to not be nil") + } + + if len(result.Include) != 1 { + t.Fatalf("Expected 1 include property, got %d", len(result.Include)) + } + + if len(result.Exclude) != 1 { + t.Fatalf("Expected 1 exclude property, got %d", len(result.Exclude)) + } + + includeProp := result.Include[0] + if includeProp.Name != "env" { + t.Errorf("Expected include name to be 'env', got %s", includeProp.Name) + } + if includeProp.Source == nil || *includeProp.Source != "custom" { + t.Errorf("Expected include source to be 'custom', got %v", includeProp.Source) + } + + excludeProp := result.Exclude[0] + if excludeProp.Name != "tier" { + t.Errorf("Expected exclude name to be 'tier', got %s", excludeProp.Name) + } + if excludeProp.Source == nil || *excludeProp.Source != "system" { + t.Errorf("Expected exclude source to be 'system', got %v", excludeProp.Source) + } +} + +func TestExpandRepositoryPropertyConditions_MultipleValues(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod", "staging", "dev"}, + }, + }, + "exclude": []any{}, + }, + } + + result := expandRepositoryPropertyConditions(input) + + if result == nil { + t.Fatal("Expected result to not be nil") + } + + if len(result.Include) != 1 { + t.Fatalf("Expected 1 include property, got %d", len(result.Include)) + } + + prop := result.Include[0] + if len(prop.PropertyValues) != 3 { + t.Fatalf("Expected 3 property values, got %d", len(prop.PropertyValues)) + } + + expectedValues := []string{"prod", "staging", "dev"} + for i, expected := range expectedValues { + if prop.PropertyValues[i] != expected { + t.Errorf("Expected property_values[%d] to be '%s', got '%s'", i, expected, prop.PropertyValues[i]) + } + } +} + +func TestExpandRepositoryPropertyConditions_MultipleProperties(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod"}, + }, + map[string]any{ + "name": "tier", + "source": "system", + "property_values": []any{"premium", "enterprise"}, + }, + }, + "exclude": []any{}, + }, + } + + result := expandRepositoryPropertyConditions(input) + + if result == nil { + t.Fatal("Expected result to not be nil") + } + + if len(result.Include) != 2 { + t.Fatalf("Expected 2 include properties, got %d", len(result.Include)) + } + + // Check first property + if result.Include[0].Name != "env" { + t.Errorf("Expected first property name to be 'env', got %s", result.Include[0].Name) + } + if len(result.Include[0].PropertyValues) != 1 { + t.Errorf("Expected first property to have 1 value, got %d", len(result.Include[0].PropertyValues)) + } + + // Check second property + if result.Include[1].Name != "tier" { + t.Errorf("Expected second property name to be 'tier', got %s", result.Include[1].Name) + } + if len(result.Include[1].PropertyValues) != 2 { + t.Errorf("Expected second property to have 2 values, got %d", len(result.Include[1].PropertyValues)) + } +} + +func TestExpandRepositoryPropertyConditions_NilElements(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod"}, + }, + nil, + map[string]any{ + "name": "tier", + "source": "system", + "property_values": []any{"premium"}, + }, + }, + "exclude": []any{}, + }, + } + + result := expandRepositoryPropertyConditions(input) + + if result == nil { + t.Fatal("Expected result to not be nil") + } + + // Nil element should be skipped, so we should have 2 properties + if len(result.Include) != 2 { + t.Fatalf("Expected 2 include properties (nil skipped), got %d", len(result.Include)) + } + + if result.Include[0].Name != "env" { + t.Errorf("Expected first property name to be 'env', got %s", result.Include[0].Name) + } + if result.Include[1].Name != "tier" { + t.Errorf("Expected second property name to be 'tier', got %s", result.Include[1].Name) + } +} + +func TestExpandRepositoryPropertyConditions_NilPropertyValues(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod", nil, "staging"}, + }, + }, + "exclude": []any{}, + }, + } + + result := expandRepositoryPropertyConditions(input) + + if result == nil { + t.Fatal("Expected result to not be nil") + } + + if len(result.Include) != 1 { + t.Fatalf("Expected 1 include property, got %d", len(result.Include)) + } + + prop := result.Include[0] + // Nil value should be skipped, so we should have 2 values + if len(prop.PropertyValues) != 2 { + t.Fatalf("Expected 2 property values (nil skipped), got %d", len(prop.PropertyValues)) + } + + if prop.PropertyValues[0] != "prod" { + t.Errorf("Expected first value to be 'prod', got '%s'", prop.PropertyValues[0]) + } + if prop.PropertyValues[1] != "staging" { + t.Errorf("Expected second value to be 'staging', got '%s'", prop.PropertyValues[1]) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters(t *testing.T) { + input := []*github.RepositoryRulesetRepositoryPropertyTargetParameters{ + { + Name: "env", + Source: github.Ptr("custom"), + PropertyValues: []string{"prod", "staging"}, + }, + { + Name: "tier", + Source: github.Ptr("system"), + PropertyValues: []string{"premium"}, + }, + } + + result := flattenRulesetRepositoryPropertyTargetParameters(input) + + if len(result) != 2 { + t.Fatalf("Expected 2 properties, got %d", len(result)) + } + + // Check first property + if result[0]["name"] != "env" { + t.Errorf("Expected first property name to be 'env', got %v", result[0]["name"]) + } + if result[0]["source"] != "custom" { + t.Errorf("Expected first property source to be 'custom', got %v", result[0]["source"]) + } + values := result[0]["property_values"].([]string) + if len(values) != 2 || values[0] != "prod" || values[1] != "staging" { + t.Errorf("Expected first property values to be ['prod', 'staging'], got %v", values) + } + + // Check second property + if result[1]["name"] != "tier" { + t.Errorf("Expected second property name to be 'tier', got %v", result[1]["name"]) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters_EmptySource(t *testing.T) { + input := []*github.RepositoryRulesetRepositoryPropertyTargetParameters{ + { + Name: "env", + Source: github.Ptr(""), + PropertyValues: []string{"prod"}, + }, + } + + result := flattenRulesetRepositoryPropertyTargetParameters(input) + + if len(result) != 1 { + t.Fatalf("Expected 1 property, got %d", len(result)) + } + + // Empty source should default to "custom" + if result[0]["source"] != "custom" { + t.Errorf("Expected source to default to 'custom', got %v", result[0]["source"]) + } +} + +func TestRoundTripRepositoryPropertyConditions(t *testing.T) { + input := []any{ + map[string]any{ + "include": []any{ + map[string]any{ + "name": "env", + "source": "custom", + "property_values": []any{"prod", "staging"}, + }, + map[string]any{ + "name": "tier", + "source": "system", + "property_values": []any{"premium"}, + }, + }, + "exclude": []any{ + map[string]any{ + "name": "region", + "source": "custom", + "property_values": []any{"us-west"}, + }, + }, + }, + } + + // Expand + expanded := expandRepositoryPropertyConditions(input) + + // Flatten + flattenedInclude := flattenRulesetRepositoryPropertyTargetParameters(expanded.Include) + flattenedExclude := flattenRulesetRepositoryPropertyTargetParameters(expanded.Exclude) + + // Verify include + if len(flattenedInclude) != 2 { + t.Fatalf("Expected 2 include properties after round trip, got %d", len(flattenedInclude)) + } + + if flattenedInclude[0]["name"] != "env" { + t.Errorf("Expected first include name to be 'env', got %v", flattenedInclude[0]["name"]) + } + if flattenedInclude[0]["source"] != "custom" { + t.Errorf("Expected first include source to be 'custom', got %v", flattenedInclude[0]["source"]) + } + includeValues := flattenedInclude[0]["property_values"].([]string) + if len(includeValues) != 2 || includeValues[0] != "prod" || includeValues[1] != "staging" { + t.Errorf("Expected first include values to be ['prod', 'staging'], got %v", includeValues) + } + + if flattenedInclude[1]["name"] != "tier" { + t.Errorf("Expected second include name to be 'tier', got %v", flattenedInclude[1]["name"]) + } + + // Verify exclude + if len(flattenedExclude) != 1 { + t.Fatalf("Expected 1 exclude property after round trip, got %d", len(flattenedExclude)) + } + + if flattenedExclude[0]["name"] != "region" { + t.Errorf("Expected exclude name to be 'region', got %v", flattenedExclude[0]["name"]) + } + excludeValues := flattenedExclude[0]["property_values"].([]string) + if len(excludeValues) != 1 || excludeValues[0] != "us-west" { + t.Errorf("Expected exclude values to be ['us-west'], got %v", excludeValues) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters_Empty(t *testing.T) { + // Test nil input + result := flattenRulesetRepositoryPropertyTargetParameters(nil) + if len(result) != 0 { + t.Errorf("Expected empty slice for nil input, got %v", result) + } + + // Test empty slice input + result = flattenRulesetRepositoryPropertyTargetParameters([]*github.RepositoryRulesetRepositoryPropertyTargetParameters{}) + if len(result) != 0 { + t.Errorf("Expected empty slice for empty input, got %v", result) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters_SingleProperty(t *testing.T) { + input := []*github.RepositoryRulesetRepositoryPropertyTargetParameters{ + { + Name: "env", + Source: github.Ptr("system"), + PropertyValues: []string{"prod", "staging"}, + }, + } + + result := flattenRulesetRepositoryPropertyTargetParameters(input) + + if len(result) != 1 { + t.Fatalf("Expected 1 property, got %d", len(result)) + } + + if result[0]["name"] != "env" { + t.Errorf("Expected name to be 'env', got %v", result[0]["name"]) + } + + if result[0]["source"] != "system" { + t.Errorf("Expected source to be 'system', got %v", result[0]["source"]) + } + + values := result[0]["property_values"].([]string) + if len(values) != 2 || values[0] != "prod" || values[1] != "staging" { + t.Errorf("Expected property_values to be ['prod', 'staging'], got %v", values) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters_NilSource(t *testing.T) { + input := []*github.RepositoryRulesetRepositoryPropertyTargetParameters{ + { + Name: "env", + Source: nil, + PropertyValues: []string{"prod"}, + }, + } + + result := flattenRulesetRepositoryPropertyTargetParameters(input) + + if len(result) != 1 { + t.Fatalf("Expected 1 property, got %d", len(result)) + } + + // Nil source should default to "custom" + if result[0]["source"] != "custom" { + t.Errorf("Expected source to default to 'custom' for nil source, got %v", result[0]["source"]) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters_EmptyPropertyValues(t *testing.T) { + input := []*github.RepositoryRulesetRepositoryPropertyTargetParameters{ + { + Name: "env", + Source: github.Ptr("custom"), + PropertyValues: []string{}, + }, + } + + result := flattenRulesetRepositoryPropertyTargetParameters(input) + + if len(result) != 1 { + t.Fatalf("Expected 1 property, got %d", len(result)) + } + + values := result[0]["property_values"].([]string) + if len(values) != 0 { + t.Errorf("Expected property_values to be empty array, got %v", values) + } +} + +func TestFlattenRulesetRepositoryPropertyTargetParameters_NilPropertyValues(t *testing.T) { + input := []*github.RepositoryRulesetRepositoryPropertyTargetParameters{ + { + Name: "env", + Source: github.Ptr("custom"), + PropertyValues: nil, + }, + } + + result := flattenRulesetRepositoryPropertyTargetParameters(input) + + if len(result) != 1 { + t.Fatalf("Expected 1 property, got %d", len(result)) + } + + // Nil PropertyValues should be preserved in the map + values := result[0]["property_values"] + if values != nil { + if valSlice, ok := values.([]string); ok && len(valSlice) != 0 { + t.Errorf("Expected property_values to be nil or empty, got %v", values) + } + } +} From a24d1d2d200da497fcbb009518bbe7b92af116d5 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 17 Feb 2026 10:58:57 +0000 Subject: [PATCH 11/20] Remove redundant condition --- github/resource_github_organization_ruleset.go | 1 - 1 file changed, 1 deletion(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 73c5322aff..69d5ff3508 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -128,7 +128,6 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Optional: true, MaxItems: 1, ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, - AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, Description: "Conditions to target repositories by custom or system properties.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ From 121fb86a60b7750f3933e917b40c91d3bd64beac Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 17 Feb 2026 11:09:03 +0000 Subject: [PATCH 12/20] Updated description based in the GitHub API docs --- github/resource_github_organization_ruleset.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 69d5ff3508..1817cbbc27 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -94,7 +94,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, - Description: "Parameters for an organization ruleset condition. Push rulesets require exactly one of: repository_id, repository_name, or repository_property. Branch/tag rulesets require ref_name AND exactly one repository targeting option.", + Description: "Parameters for an organization ruleset condition.The branch and tag rulesets conditions object should contain both repository_name and ref_name properties, or both repository_id and ref_name properties, or both repository_property and ref_name properties. The push rulesets conditions object does not require the ref_name property.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "ref_name": { From 309f8d3448cc0521a77b97a1791617f11630d948 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 17 Feb 2026 16:14:42 +0000 Subject: [PATCH 13/20] Apply linter and fmt --- github/resource_github_organization_ruleset.go | 18 +++++++++--------- github/util_ruleset_validation.go | 1 - 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 1817cbbc27..5f21e6ca4e 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -94,7 +94,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Type: schema.TypeList, Optional: true, MaxItems: 1, - Description: "Parameters for an organization ruleset condition.The branch and tag rulesets conditions object should contain both repository_name and ref_name properties, or both repository_id and ref_name properties, or both repository_property and ref_name properties. The push rulesets conditions object does not require the ref_name property.", + Description: "Parameters for an organization ruleset condition.The branch and tag rulesets conditions object should contain both repository_name and ref_name properties, or both repository_id and ref_name properties, or both repository_property and ref_name properties. The push rulesets conditions object does not require the ref_name property.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "ref_name": { @@ -151,10 +151,10 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "source": { - Type: schema.TypeString, - Optional: true, - Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", - Default: "custom", + Type: schema.TypeString, + Optional: true, + Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", + Default: "custom", ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"custom", "system"}, false)), }, }, @@ -180,10 +180,10 @@ func resourceGithubOrganizationRuleset() *schema.Resource { }, }, "source": { - Type: schema.TypeString, - Optional: true, - Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", - Default: "custom", + Type: schema.TypeString, + Optional: true, + Description: "The source of the repository property. Defaults to 'custom' if not specified. Can be one of: custom, system", + Default: "custom", ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"custom", "system"}, false)), }, }, diff --git a/github/util_ruleset_validation.go b/github/util_ruleset_validation.go index 36247f642c..81898182d0 100644 --- a/github/util_ruleset_validation.go +++ b/github/util_ruleset_validation.go @@ -171,7 +171,6 @@ func validateConditionsFieldForBranchAndTagTargets(ctx context.Context, target g return fmt.Errorf("ref_name must be set for %s target", target) } - tflog.Debug(ctx, fmt.Sprintf("Conditions validation passed for %s target", target)) return nil } From 1b97fb488f8955753e4a7fc92c5735415b6aa6f7 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 17 Feb 2026 16:27:28 +0000 Subject: [PATCH 14/20] Remove unnecessary checks --- github/resource_github_organization_ruleset.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 5f21e6ca4e..692b014d9c 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -197,7 +197,6 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Optional: true, MaxItems: 1, ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, - AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, Description: "Targets repositories that match the specified name patterns.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -230,7 +229,6 @@ func resourceGithubOrganizationRuleset() *schema.Resource { Type: schema.TypeList, Optional: true, ExactlyOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, - AtLeastOneOf: []string{"conditions.0.repository_property", "conditions.0.repository_name", "conditions.0.repository_id"}, Description: "The repository IDs that the ruleset applies to. One of these IDs must match for the ruleset to apply.", Elem: &schema.Schema{ Type: schema.TypeInt, From 968b1af6e28c48f584e8e99928b2f0d865f1e956 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 17 Feb 2026 17:17:47 +0000 Subject: [PATCH 15/20] Remove tests These tests check validation that was handover to the Terraform built-in validation --- github/util_ruleset_validation_test.go | 31 -------------------------- 1 file changed, 31 deletions(-) diff --git a/github/util_ruleset_validation_test.go b/github/util_ruleset_validation_test.go index 7cf434b5f0..51e3fb51f3 100644 --- a/github/util_ruleset_validation_test.go +++ b/github/util_ruleset_validation_test.go @@ -165,37 +165,6 @@ func Test_validateConditionsFieldForBranchAndTagTargets(t *testing.T) { expectError: true, errorMsg: "ref_name must be set for branch target", }, - { - name: "invalid branch target without repository_name or repository_id", - target: github.RulesetTargetBranch, - conditions: map[string]any{ - "ref_name": []any{map[string]any{"include": []any{"~DEFAULT_BRANCH"}, "exclude": []any{}}}, - }, - expectError: true, - errorMsg: "either repository_name or repository_id must be set for branch target", - }, - { - name: "invalid tag target with nil repository_name and repository_id", - target: github.RulesetTargetTag, - conditions: map[string]any{ - "ref_name": []any{map[string]any{"include": []any{"v*"}, "exclude": []any{}}}, - "repository_name": nil, - "repository_id": nil, - }, - expectError: true, - errorMsg: "either repository_name or repository_id must be set for tag target", - }, - { - name: "invalid branch target with empty repository_name and repository_id slices", - target: github.RulesetTargetBranch, - conditions: map[string]any{ - "ref_name": []any{map[string]any{"include": []any{"~DEFAULT_BRANCH"}, "exclude": []any{}}}, - "repository_name": []any{}, - "repository_id": []any{}, - }, - expectError: true, - errorMsg: "either repository_name or repository_id must be set for branch target", - }, } for _, tt := range tests { From b33f177c919d27d5fa3a9f5462622956fd6f741b Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Tue, 17 Feb 2026 21:21:47 +0000 Subject: [PATCH 16/20] Improve syntax for include and exclude property Improve e2e tests for the ruleset and improve documentation --- .../resource_github_organization_ruleset.go | 2 + ...source_github_organization_ruleset_test.go | 195 ++++++++++++------ .../docs/r/organization_ruleset.html.markdown | 34 +-- 3 files changed, 158 insertions(+), 73 deletions(-) diff --git a/github/resource_github_organization_ruleset.go b/github/resource_github_organization_ruleset.go index 692b014d9c..1560fb3a1e 100644 --- a/github/resource_github_organization_ruleset.go +++ b/github/resource_github_organization_ruleset.go @@ -134,6 +134,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { "include": { Type: schema.TypeList, Optional: true, + ConfigMode: schema.SchemaConfigModeAttr, Description: "The repository properties and values to include. All of these properties must match for the condition to pass.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -163,6 +164,7 @@ func resourceGithubOrganizationRuleset() *schema.Resource { "exclude": { Type: schema.TypeList, Optional: true, + ConfigMode: schema.SchemaConfigModeAttr, Description: "The repository properties and values to exclude. The ruleset will not apply if any of these properties match.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index de983d58fc..b2900ccc63 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -1,10 +1,12 @@ package github import ( + "context" "fmt" "regexp" "testing" + "github.com/google/go-github/v82/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -250,15 +252,11 @@ resource "github_organization_ruleset" "test" { conditions { repository_property { - include = [{ - name = "tier" - source = "custom" - property_values = ["premium"] - }] + include = [] exclude = [{ - name = "archived" - source = "system" - property_values = ["true"] + name = "team" + source = "custom" + property_values = ["red"] }] } @@ -282,11 +280,11 @@ resource "github_organization_ruleset" "test" { Config: config, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "0"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", "archived"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.source", "system"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.property_values.0", "true"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", "team"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.source", "custom"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.property_values.0", "red"), ), }, }, @@ -297,6 +295,45 @@ resource "github_organization_ruleset" "test" { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-multiple-%s", testResourcePrefix, randomID) + // Setup: Create custom properties for testing + meta, err := getTestMeta() + if err != nil { + t.Fatalf("Error getting test meta: %v", err) + } + + ctx := context.Background() + org := testAccConf.owner + + // Create test properties + properties := []struct { + name string + values []string + }{ + {name: "e2e_test_environment", values: []string{"production", "staging"}}, + {name: "e2e_test_tier", values: []string{"premium", "enterprise"}}, + } + + for _, prop := range properties { + _, _, err := meta.v3client.Organizations.CreateOrUpdateCustomProperty(ctx, org, prop.name, &github.CustomProperty{ + ValueType: github.PropertyValueTypeSingleSelect, + Required: github.Ptr(false), + AllowedValues: prop.values, + }) + if err != nil { + t.Logf("Warning: Could not create custom property %s (may already exist): %v", prop.name, err) + } + } + + // Cleanup: Remove custom properties after test + defer func() { + for _, prop := range properties { + _, err := meta.v3client.Organizations.RemoveCustomProperty(ctx, org, prop.name) + if err != nil { + t.Logf("Warning: Could not remove custom property %s: %v", prop.name, err) + } + } + }() + config := fmt.Sprintf(` resource "github_organization_ruleset" "test" { name = "%s" @@ -307,14 +344,14 @@ resource "github_organization_ruleset" "test" { repository_property { include = [ { - name = "environment" + name = "e2e_test_environment" source = "custom" property_values = ["production"] }, { - name = "team" + name = "e2e_test_tier" source = "custom" - property_values = ["backend", "platform"] + property_values = ["premium", "enterprise"] } ] exclude = [] @@ -341,10 +378,15 @@ resource "github_organization_ruleset" "test" { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "environment"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "e2e_test_environment"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.name", "team"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.name", "e2e_test_tier"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.source", "custom"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.#", "2"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.0", "premium"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.1", "enterprise"), ), }, }, @@ -364,9 +406,9 @@ resource "github_organization_ruleset" "test" { conditions { repository_property { include = [{ - name = "tier" + name = "team" source = "custom" - property_values = ["basic"] + property_values = ["blue"] }] exclude = [] } @@ -392,15 +434,11 @@ resource "github_organization_ruleset" "test" { conditions { repository_property { include = [{ - name = "tier" + name = "team" source = "custom" - property_values = ["premium", "enterprise"] - }] - exclude = [{ - name = "archived" - source = "system" - property_values = ["true"] + property_values = ["backend", "platform"] }] + exclude = [] } ref_name { @@ -425,7 +463,7 @@ resource "github_organization_ruleset" "test" { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "basic"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "blue"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "0"), ), }, @@ -434,10 +472,9 @@ resource "github_organization_ruleset" "test" { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "premium"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.1", "enterprise"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", "archived"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "backend"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.1", "platform"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "0"), ), }, }, @@ -977,7 +1014,7 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - ExpectError: regexp.MustCompile("only one of `conditions.0.repository_id,conditions.0.repository_name,conditions.0.repository_property` can be specified"), + ExpectError: regexp.MustCompile(`(?s)only one of.*conditions\.0\.repository_id.*conditions\.0\.repository_name.*conditions\.0\.repository_property.*can be specified`), }, }, }) @@ -1011,7 +1048,7 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - ExpectError: regexp.MustCompile("one of `conditions.0.repository_id,conditions.0.repository_name,conditions.0.repository_property` must be specified"), + ExpectError: regexp.MustCompile(`(?s)one of.*conditions\.0\.repository_id.*conditions\.0\.repository_name.*conditions\.0\.repository_property.*must be specified`), }, }, }) @@ -1019,40 +1056,82 @@ resource "github_organization_ruleset" "test" { t.Run("validates_repository_property_works_as_single_targeting_option", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - resourceName := "test-repo-property-targeting" - config := fmt.Sprintf(` - resource "github_organization_ruleset" "%s" { - name = "test-property-targeting-%s" - target = "branch" - enforcement = "active" + rulesetName := fmt.Sprintf("%s-repo-prop-only-%s", testResourcePrefix, randomID) - conditions { - ref_name { - include = ["~ALL"] - exclude = [] - } - repository_property { - include { - name = "environment" - property_values = ["production"] - source = "custom" - } - } - } + // Setup: Create custom property for testing + meta, err := getTestMeta() + if err != nil { + t.Fatalf("Error getting test meta: %v", err) + } - rules { - creation = true - } + ctx := context.Background() + org := testAccConf.owner + propName := "e2e_test_environment" + propValues := []string{"production", "staging"} + + _, _, err = meta.v3client.Organizations.CreateOrUpdateCustomProperty(ctx, org, propName, &github.CustomProperty{ + ValueType: github.PropertyValueTypeSingleSelect, + Required: github.Ptr(false), + AllowedValues: propValues, + }) + if err != nil { + t.Logf("Warning: Could not create custom property %s (may already exist): %v", propName, err) + } + + // Cleanup: Remove custom property after test + defer func() { + _, err := meta.v3client.Organizations.RemoveCustomProperty(ctx, org, propName) + if err != nil { + t.Logf("Warning: Could not remove custom property %s: %v", propName, err) } - `, resourceName, randomID) + }() + + config := fmt.Sprintf(` +resource "github_organization_ruleset" "test" { + name = "%s" + target = "branch" + enforcement = "active" + + conditions { + repository_property { + include = [{ + name = "e2e_test_environment" + source = "custom" + property_values = ["production", "staging"] + }] + exclude = [] + } + + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + rules { + creation = true + update = true + } +} +`, rulesetName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: config, - ExpectError: nil, // This should succeed + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "target", "branch"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "enforcement", "active"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "e2e_test_environment"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "2"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.1", "staging"), + ), }, }, }) diff --git a/website/docs/r/organization_ruleset.html.markdown b/website/docs/r/organization_ruleset.html.markdown index eebb3813dc..f0a052a159 100644 --- a/website/docs/r/organization_ruleset.html.markdown +++ b/website/docs/r/organization_ruleset.html.markdown @@ -113,21 +113,25 @@ resource "github_organization_ruleset" "example_property" { } repository_property { - include { - name = "environment" - property_values = ["production", "staging"] - source = "custom" - } - include { - name = "team" - property_values = ["backend"] - source = "custom" - } - exclude { - name = "archived" - property_values = ["true"] - source = "system" - } + include = [ + { + name = "environment" + property_values = ["production", "staging"] + source = "custom" + }, + { + name = "team" + property_values = ["backend"] + source = "custom" + } + ] + exclude = [ + { + name = "archived" + property_values = ["true"] + source = "system" + } + ] } } From 3d687708623e87f6f805ff19d1f1a6bca90d3f2b Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Mon, 23 Feb 2026 08:52:54 +0000 Subject: [PATCH 17/20] Use t context --- github/resource_github_organization_ruleset_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index b2900ccc63..4c8cd5029c 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -301,7 +301,7 @@ resource "github_organization_ruleset" "test" { t.Fatalf("Error getting test meta: %v", err) } - ctx := context.Background() + ctx := t.Context() org := testAccConf.owner // Create test properties From 24d2bcb5b4d520e5bc010828e586062c6c65037a Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Mon, 23 Feb 2026 10:22:18 +0000 Subject: [PATCH 18/20] Rewrite tests using the resource_github_organization_custom_properties Setup the tests case using the config instead of using the API --- ...source_github_organization_ruleset_test.go | 185 +++++++++--------- 1 file changed, 93 insertions(+), 92 deletions(-) diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index 4c8cd5029c..6fabea0f77 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -1,12 +1,10 @@ package github import ( - "context" "fmt" "regexp" "testing" - "github.com/google/go-github/v82/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -187,17 +185,25 @@ resource "github_organization_ruleset" "test" { t.Run("create_ruleset_with_repository_property", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-ruleset-%s", testResourcePrefix, randomID) + propName := fmt.Sprintf("e2e_test_team_%s", randomID) config := fmt.Sprintf(` +resource "github_organization_custom_properties" "team" { + property_name = "%[2]s" + value_type = "single_select" + required = false + allowed_values = ["blue", "red", "backend", "platform"] +} + resource "github_organization_ruleset" "test" { - name = "%s" + name = "%[1]s" target = "branch" enforcement = "active" conditions { repository_property { include = [{ - name = "team" + name = "%[2]s" source = "custom" property_values = ["blue"] }] @@ -216,8 +222,10 @@ resource "github_organization_ruleset" "test" { deletion = true required_linear_history = true } + + depends_on = [github_organization_custom_properties.team] } -`, rulesetName) +`, rulesetName, propName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -230,7 +238,7 @@ resource "github_organization_ruleset" "test" { resource.TestCheckResourceAttr("github_organization_ruleset.test", "target", "branch"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "enforcement", "active"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "team"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", propName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "blue"), @@ -243,10 +251,18 @@ resource "github_organization_ruleset" "test" { t.Run("create_ruleset_with_repository_property_exclude", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-exclude-ruleset-%s", testResourcePrefix, randomID) + propName := fmt.Sprintf("e2e_test_team_%s", randomID) config := fmt.Sprintf(` +resource "github_organization_custom_properties" "team" { + property_name = "%[2]s" + value_type = "single_select" + required = false + allowed_values = ["blue", "red", "backend", "platform"] +} + resource "github_organization_ruleset" "test" { - name = "%s" + name = "%[1]s" target = "branch" enforcement = "active" @@ -254,7 +270,7 @@ resource "github_organization_ruleset" "test" { repository_property { include = [] exclude = [{ - name = "team" + name = "%[2]s" source = "custom" property_values = ["red"] }] @@ -269,8 +285,10 @@ resource "github_organization_ruleset" "test" { rules { required_linear_history = true } + + depends_on = [github_organization_custom_properties.team] } -`, rulesetName) +`, rulesetName, propName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -282,7 +300,7 @@ resource "github_organization_ruleset" "test" { resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "0"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", "team"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", propName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.source", "custom"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.property_values.0", "red"), ), @@ -294,49 +312,26 @@ resource "github_organization_ruleset" "test" { t.Run("create_ruleset_with_multiple_repository_properties", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-multiple-%s", testResourcePrefix, randomID) + propEnvironmentName := fmt.Sprintf("e2e_test_environment_%s", randomID) + propTierName := fmt.Sprintf("e2e_test_tier_%s", randomID) - // Setup: Create custom properties for testing - meta, err := getTestMeta() - if err != nil { - t.Fatalf("Error getting test meta: %v", err) - } - - ctx := t.Context() - org := testAccConf.owner - - // Create test properties - properties := []struct { - name string - values []string - }{ - {name: "e2e_test_environment", values: []string{"production", "staging"}}, - {name: "e2e_test_tier", values: []string{"premium", "enterprise"}}, - } - - for _, prop := range properties { - _, _, err := meta.v3client.Organizations.CreateOrUpdateCustomProperty(ctx, org, prop.name, &github.CustomProperty{ - ValueType: github.PropertyValueTypeSingleSelect, - Required: github.Ptr(false), - AllowedValues: prop.values, - }) - if err != nil { - t.Logf("Warning: Could not create custom property %s (may already exist): %v", prop.name, err) - } - } + config := fmt.Sprintf(` +resource "github_organization_custom_properties" "environment" { + property_name = "%[2]s" + value_type = "single_select" + required = false + allowed_values = ["production", "staging"] +} - // Cleanup: Remove custom properties after test - defer func() { - for _, prop := range properties { - _, err := meta.v3client.Organizations.RemoveCustomProperty(ctx, org, prop.name) - if err != nil { - t.Logf("Warning: Could not remove custom property %s: %v", prop.name, err) - } - } - }() +resource "github_organization_custom_properties" "tier" { + property_name = "%[3]s" + value_type = "single_select" + required = false + allowed_values = ["premium", "enterprise"] +} - config := fmt.Sprintf(` resource "github_organization_ruleset" "test" { - name = "%s" + name = "%[1]s" target = "branch" enforcement = "active" @@ -344,12 +339,12 @@ resource "github_organization_ruleset" "test" { repository_property { include = [ { - name = "e2e_test_environment" + name = "%[2]s" source = "custom" property_values = ["production"] }, { - name = "e2e_test_tier" + name = "%[3]s" source = "custom" property_values = ["premium", "enterprise"] } @@ -366,8 +361,13 @@ resource "github_organization_ruleset" "test" { rules { required_signatures = true } + + depends_on = [ + github_organization_custom_properties.environment, + github_organization_custom_properties.tier, + ] } -`, rulesetName) +`, rulesetName, propEnvironmentName, propTierName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -378,11 +378,11 @@ resource "github_organization_ruleset" "test" { Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "e2e_test_environment"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", propEnvironmentName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.name", "e2e_test_tier"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.name", propTierName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.source", "custom"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.#", "2"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.0", "premium"), @@ -396,17 +396,25 @@ resource "github_organization_ruleset" "test" { t.Run("update_repository_property", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-update-%s", testResourcePrefix, randomID) + propName := fmt.Sprintf("e2e_test_team_%s", randomID) config := fmt.Sprintf(` +resource "github_organization_custom_properties" "team" { + property_name = "%[2]s" + value_type = "single_select" + required = false + allowed_values = ["blue", "red", "backend", "platform"] +} + resource "github_organization_ruleset" "test" { - name = "%s" + name = "%[1]s" target = "branch" enforcement = "active" conditions { repository_property { include = [{ - name = "team" + name = "%[2]s" source = "custom" property_values = ["blue"] }] @@ -422,19 +430,28 @@ resource "github_organization_ruleset" "test" { rules { creation = true } + + depends_on = [github_organization_custom_properties.team] } -`, rulesetName) +`, rulesetName, propName) configUpdated := fmt.Sprintf(` +resource "github_organization_custom_properties" "team" { + property_name = "%[2]s" + value_type = "single_select" + required = false + allowed_values = ["blue", "red", "backend", "platform"] +} + resource "github_organization_ruleset" "test" { - name = "%s" + name = "%[1]s" target = "branch" enforcement = "active" conditions { repository_property { include = [{ - name = "team" + name = "%[2]s" source = "custom" property_values = ["backend", "platform"] }] @@ -451,8 +468,10 @@ resource "github_organization_ruleset" "test" { creation = true update = true } + + depends_on = [github_organization_custom_properties.team] } -`, rulesetName) +`, rulesetName, propName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -1057,45 +1076,25 @@ resource "github_organization_ruleset" "test" { t.Run("validates_repository_property_works_as_single_targeting_option", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-only-%s", testResourcePrefix, randomID) - - // Setup: Create custom property for testing - meta, err := getTestMeta() - if err != nil { - t.Fatalf("Error getting test meta: %v", err) - } - - ctx := context.Background() - org := testAccConf.owner - propName := "e2e_test_environment" - propValues := []string{"production", "staging"} - - _, _, err = meta.v3client.Organizations.CreateOrUpdateCustomProperty(ctx, org, propName, &github.CustomProperty{ - ValueType: github.PropertyValueTypeSingleSelect, - Required: github.Ptr(false), - AllowedValues: propValues, - }) - if err != nil { - t.Logf("Warning: Could not create custom property %s (may already exist): %v", propName, err) - } - - // Cleanup: Remove custom property after test - defer func() { - _, err := meta.v3client.Organizations.RemoveCustomProperty(ctx, org, propName) - if err != nil { - t.Logf("Warning: Could not remove custom property %s: %v", propName, err) - } - }() + propName := fmt.Sprintf("e2e_test_environment_%s", randomID) config := fmt.Sprintf(` +resource "github_organization_custom_properties" "environment" { + property_name = "%[2]s" + value_type = "single_select" + required = false + allowed_values = ["production", "staging"] +} + resource "github_organization_ruleset" "test" { - name = "%s" + name = "%[1]s" target = "branch" enforcement = "active" conditions { repository_property { include = [{ - name = "e2e_test_environment" + name = "%[2]s" source = "custom" property_values = ["production", "staging"] }] @@ -1112,8 +1111,10 @@ resource "github_organization_ruleset" "test" { creation = true update = true } + + depends_on = [github_organization_custom_properties.environment] } -`, rulesetName) +`, rulesetName, propName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -1126,7 +1127,7 @@ resource "github_organization_ruleset" "test" { resource.TestCheckResourceAttr("github_organization_ruleset.test", "target", "branch"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "enforcement", "active"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", "e2e_test_environment"), + resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", propName), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "2"), resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), From 6209daab4ffee508783234841e7f3fb1b4d0cf64 Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Mon, 23 Feb 2026 23:03:47 +0000 Subject: [PATCH 19/20] Refactor repository property names in organization ruleset tests for consistency --- ...source_github_organization_ruleset_test.go | 35 ++++++------------- 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index 6fabea0f77..c65556593c 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -185,7 +185,7 @@ resource "github_organization_ruleset" "test" { t.Run("create_ruleset_with_repository_property", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-ruleset-%s", testResourcePrefix, randomID) - propName := fmt.Sprintf("e2e_test_team_%s", randomID) + propName := fmt.Sprintf("%s_team_%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_custom_properties" "team" { @@ -203,7 +203,7 @@ resource "github_organization_ruleset" "test" { conditions { repository_property { include = [{ - name = "%[2]s" + name = github_organization_custom_properties.team.property_name source = "custom" property_values = ["blue"] }] @@ -222,8 +222,6 @@ resource "github_organization_ruleset" "test" { deletion = true required_linear_history = true } - - depends_on = [github_organization_custom_properties.team] } `, rulesetName, propName) @@ -251,7 +249,7 @@ resource "github_organization_ruleset" "test" { t.Run("create_ruleset_with_repository_property_exclude", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-exclude-ruleset-%s", testResourcePrefix, randomID) - propName := fmt.Sprintf("e2e_test_team_%s", randomID) + propName := fmt.Sprintf("%s_team_%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_custom_properties" "team" { @@ -270,7 +268,7 @@ resource "github_organization_ruleset" "test" { repository_property { include = [] exclude = [{ - name = "%[2]s" + name = github_organization_custom_properties.team.property_name source = "custom" property_values = ["red"] }] @@ -285,8 +283,6 @@ resource "github_organization_ruleset" "test" { rules { required_linear_history = true } - - depends_on = [github_organization_custom_properties.team] } `, rulesetName, propName) @@ -312,8 +308,8 @@ resource "github_organization_ruleset" "test" { t.Run("create_ruleset_with_multiple_repository_properties", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-multiple-%s", testResourcePrefix, randomID) - propEnvironmentName := fmt.Sprintf("e2e_test_environment_%s", randomID) - propTierName := fmt.Sprintf("e2e_test_tier_%s", randomID) + propEnvironmentName := fmt.Sprintf("%s_environment_%s", testResourcePrefix, randomID) + propTierName := fmt.Sprintf("%s_tier_%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_custom_properties" "environment" { @@ -339,12 +335,12 @@ resource "github_organization_ruleset" "test" { repository_property { include = [ { - name = "%[2]s" + name = github_organization_custom_properties.environment.property_name source = "custom" property_values = ["production"] }, { - name = "%[3]s" + name = github_organization_custom_properties.tier.property_name source = "custom" property_values = ["premium", "enterprise"] } @@ -361,11 +357,6 @@ resource "github_organization_ruleset" "test" { rules { required_signatures = true } - - depends_on = [ - github_organization_custom_properties.environment, - github_organization_custom_properties.tier, - ] } `, rulesetName, propEnvironmentName, propTierName) @@ -396,7 +387,7 @@ resource "github_organization_ruleset" "test" { t.Run("update_repository_property", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-update-%s", testResourcePrefix, randomID) - propName := fmt.Sprintf("e2e_test_team_%s", randomID) + propName := fmt.Sprintf("%s_team_%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_custom_properties" "team" { @@ -414,7 +405,7 @@ resource "github_organization_ruleset" "test" { conditions { repository_property { include = [{ - name = "%[2]s" + name = github_organization_custom_properties.team.property_name source = "custom" property_values = ["blue"] }] @@ -430,8 +421,6 @@ resource "github_organization_ruleset" "test" { rules { creation = true } - - depends_on = [github_organization_custom_properties.team] } `, rulesetName, propName) @@ -451,7 +440,7 @@ resource "github_organization_ruleset" "test" { conditions { repository_property { include = [{ - name = "%[2]s" + name = github_organization_custom_properties.team.property_name source = "custom" property_values = ["backend", "platform"] }] @@ -468,8 +457,6 @@ resource "github_organization_ruleset" "test" { creation = true update = true } - - depends_on = [github_organization_custom_properties.team] } `, rulesetName, propName) From 91298adb95ababb24b38687d328353aec79828dc Mon Sep 17 00:00:00 2001 From: Stephane Moser Date: Mon, 23 Feb 2026 23:40:50 +0000 Subject: [PATCH 20/20] Refactor tests to use state checks for repository property validation in organization ruleset --- ...source_github_organization_ruleset_test.go | 102 ++++++++---------- 1 file changed, 46 insertions(+), 56 deletions(-) diff --git a/github/resource_github_organization_ruleset_test.go b/github/resource_github_organization_ruleset_test.go index c65556593c..4a08230946 100644 --- a/github/resource_github_organization_ruleset_test.go +++ b/github/resource_github_organization_ruleset_test.go @@ -8,6 +8,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubOrganizationRuleset(t *testing.T) { @@ -231,16 +234,14 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "target", "branch"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "enforcement", "active"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", propName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "blue"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("name"), knownvalue.StringExact(rulesetName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("target"), knownvalue.StringExact("branch")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("enforcement"), knownvalue.StringExact("active")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("name"), knownvalue.StringExact(propName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("source"), knownvalue.StringExact("custom")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("blue")), + }, }, }, }) @@ -292,14 +293,12 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "0"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.name", propName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.source", "custom"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.0.property_values.0", "red"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("name"), knownvalue.StringExact(rulesetName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("exclude").AtSliceIndex(0).AtMapKey("name"), knownvalue.StringExact(propName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("exclude").AtSliceIndex(0).AtMapKey("source"), knownvalue.StringExact("custom")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("exclude").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("red")), + }, }, }, }) @@ -366,19 +365,16 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", propEnvironmentName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.name", propTierName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.source", "custom"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.0", "premium"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.1.property_values.1", "enterprise"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("name"), knownvalue.StringExact(rulesetName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("name"), knownvalue.StringExact(propEnvironmentName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("source"), knownvalue.StringExact("custom")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("production")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(1).AtMapKey("name"), knownvalue.StringExact(propTierName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(1).AtMapKey("source"), knownvalue.StringExact("custom")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(1).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("premium")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(1).AtMapKey("property_values").AtSliceIndex(1), knownvalue.StringExact("enterprise")), + }, }, }, }) @@ -466,22 +462,18 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "blue"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "0"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("name"), knownvalue.StringExact(rulesetName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("blue")), + }, }, { Config: configUpdated, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "backend"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.1", "platform"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.exclude.#", "0"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("name"), knownvalue.StringExact(rulesetName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("backend")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(1), knownvalue.StringExact("platform")), + }, }, }, }) @@ -1063,7 +1055,7 @@ resource "github_organization_ruleset" "test" { t.Run("validates_repository_property_works_as_single_targeting_option", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) rulesetName := fmt.Sprintf("%s-repo-prop-only-%s", testResourcePrefix, randomID) - propName := fmt.Sprintf("e2e_test_environment_%s", randomID) + propName := fmt.Sprintf("%s_environment_%s", testResourcePrefix, randomID) config := fmt.Sprintf(` resource "github_organization_custom_properties" "environment" { @@ -1109,17 +1101,15 @@ resource "github_organization_ruleset" "test" { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_ruleset.test", "name", rulesetName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "target", "branch"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "enforcement", "active"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.#", "1"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.name", propName), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.source", "custom"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.#", "2"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.0", "production"), - resource.TestCheckResourceAttr("github_organization_ruleset.test", "conditions.0.repository_property.0.include.0.property_values.1", "staging"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("name"), knownvalue.StringExact(rulesetName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("target"), knownvalue.StringExact("branch")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("enforcement"), knownvalue.StringExact("active")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("name"), knownvalue.StringExact(propName)), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("source"), knownvalue.StringExact("custom")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(0), knownvalue.StringExact("production")), + statecheck.ExpectKnownValue("github_organization_ruleset.test", tfjsonpath.New("conditions").AtSliceIndex(0).AtMapKey("repository_property").AtSliceIndex(0).AtMapKey("include").AtSliceIndex(0).AtMapKey("property_values").AtSliceIndex(1), knownvalue.StringExact("staging")), + }, }, }, })