From a06f75a5d0d9904c0ee2d6b7823725ae1006fa43 Mon Sep 17 00:00:00 2001 From: markszabo <12611720+markszabo@users.noreply.github.com> Date: Fri, 21 Nov 2025 10:06:31 +0900 Subject: [PATCH 1/4] Add ValuesEditableBy support to github_organization_custom_properties --- ...e_github_organization_custom_properties.go | 6 +++ ...e_github_organization_custom_properties.go | 19 +++++++- ...hub_organization_custom_properties_test.go | 47 +++++++++++++++++++ ...ganization_custom_properties.html.markdown | 4 +- ...ganization_custom_properties.html.markdown | 16 +++++++ 5 files changed, 89 insertions(+), 3 deletions(-) diff --git a/github/data_source_github_organization_custom_properties.go b/github/data_source_github_organization_custom_properties.go index 42786cdd3d..10bd8d38d9 100644 --- a/github/data_source_github_organization_custom_properties.go +++ b/github/data_source_github_organization_custom_properties.go @@ -40,6 +40,11 @@ func dataSourceGithubOrganizationCustomProperties() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "values_editable_by": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, }, } } @@ -66,6 +71,7 @@ func dataSourceGithubOrganizationCustomPropertiesRead(d *schema.ResourceData, me _ = d.Set("property_name", propertyAttributes.PropertyName) _ = d.Set("required", propertyAttributes.Required) _ = d.Set("value_type", propertyAttributes.ValueType) + _ = d.Set("values_editable_by", propertyAttributes.ValuesEditableBy) return nil } diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index a20b5d4ef1..0d2dd90c03 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -59,6 +59,12 @@ func resourceGithubOrganizationCustomProperties() *schema.Resource { Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "values_editable_by": { + Description: "Who can edit the values of the custom property. Can be one of 'org_actors' or 'org_and_repo_actors'. If not specified, the default is 'org_actors' (only organization owners can edit values)", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, }, } } @@ -79,14 +85,22 @@ func resourceGithubCustomPropertiesCreate(d *schema.ResourceData, meta any) erro allowedValuesString = append(allowedValuesString, v.(string)) } - customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, d.Get("property_name").(string), &github.CustomProperty{ + customProperty := &github.CustomProperty{ PropertyName: &propertyName, ValueType: valueType, Required: &required, DefaultValue: &defaultValue, Description: &description, AllowedValues: allowedValuesString, - }) + } + + // Set ValuesEditableBy if provided + if valuesEditableBy, ok := d.GetOk("values_editable_by"); ok { + valuesEditableByStr := valuesEditableBy.(string) + customProperty.ValuesEditableBy = &valuesEditableByStr + } + + customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, d.Get("property_name").(string), customProperty) if err != nil { return err } @@ -112,6 +126,7 @@ func resourceGithubCustomPropertiesRead(d *schema.ResourceData, meta any) error _ = d.Set("property_name", customProperty.PropertyName) _ = d.Set("required", customProperty.Required) _ = d.Set("value_type", customProperty.ValueType) + _ = d.Set("values_editable_by", customProperty.ValuesEditableBy) return nil } diff --git a/github/resource_github_organization_custom_properties_test.go b/github/resource_github_organization_custom_properties_test.go index 87faed79ef..c8e5ba0e80 100644 --- a/github/resource_github_organization_custom_properties_test.go +++ b/github/resource_github_organization_custom_properties_test.go @@ -151,4 +151,51 @@ func TestAccGithubOrganizationCustomProperties(t *testing.T) { testCase(t, organization) }) }) + + t.Run("creates custom property with values_editable_by without error", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestValuesEditableBy" + value_type = "string" + required = false + description = "Test property for values_editable_by" + values_editable_by = "org_and_repo_actors" + }` + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_custom_properties.test", + "property_name", "TestValuesEditableBy", + ), + resource.TestCheckResourceAttr( + "github_organization_custom_properties.test", + "values_editable_by", "org_and_repo_actors", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) } diff --git a/website/docs/d/organization_custom_properties.html.markdown b/website/docs/d/organization_custom_properties.html.markdown index 5ee3cfd42c..783eda2ad0 100644 --- a/website/docs/d/organization_custom_properties.html.markdown +++ b/website/docs/d/organization_custom_properties.html.markdown @@ -35,4 +35,6 @@ The following arguments are supported: * `default_value` - The default value of the custom property. -* `allowed_values` - List of allowed values for the custom property. Only populated when `value_type` is `single_select` or `multi_select`. \ No newline at end of file +* `allowed_values` - List of allowed values for the custom property. Only populated when `value_type` is `single_select` or `multi_select`. + +* `values_editable_by` - Who can edit the values of the custom property. Can be one of `org_actors` or `org_and_repo_actors`. \ No newline at end of file diff --git a/website/docs/r/organization_custom_properties.html.markdown b/website/docs/r/organization_custom_properties.html.markdown index f87f4a2df6..41ec8d85a9 100644 --- a/website/docs/r/organization_custom_properties.html.markdown +++ b/website/docs/r/organization_custom_properties.html.markdown @@ -28,6 +28,20 @@ resource "github_organization_custom_properties" "environment" { } ``` +## Example Usage - Allow Repository Actors to Edit + +This example shows how to allow repository administrators to edit the property values: + +```hcl +resource "github_organization_custom_properties" "team_contact" { + property_name = "team_contact" + value_type = "string" + required = false + description = "Contact information for the team managing this repository" + values_editable_by = "org_and_repo_actors" +} +``` + ## Example Usage - Text Property ```hcl @@ -67,6 +81,8 @@ The following arguments are supported: * `allowed_values` - (Optional) List of allowed values for the custom property. Only applicable when `value_type` is `single_select` or `multi_select`. +* `values_editable_by` - (Optional) Who can edit the values of the custom property. Can be one of `org_actors` or `org_and_repo_actors`. When set to `org_actors` (the default), only organization owners can edit the property values on repositories. When set to `org_and_repo_actors`, both organization owners and repository administrators with the custom properties permission can edit the values. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: From 6b8fad118904553874b719132cd13d9df46ae761 Mon Sep 17 00:00:00 2001 From: markszabo <12611720+markszabo@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:35:20 +0900 Subject: [PATCH 2/4] Validate values --- github/resource_github_organization_custom_properties.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index 0d2dd90c03..fe1941e35d 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -60,10 +60,11 @@ func resourceGithubOrganizationCustomProperties() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, }, "values_editable_by": { - Description: "Who can edit the values of the custom property. Can be one of 'org_actors' or 'org_and_repo_actors'. If not specified, the default is 'org_actors' (only organization owners can edit values)", - Type: schema.TypeString, - Optional: true, - Computed: true, + Description: "Who can edit the values of the custom property. Can be one of 'org_actors' or 'org_and_repo_actors'. If not specified, the default is 'org_actors' (only organization owners can edit values)", + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: validateValueFunc([]string{"org_actors", "org_and_repo_actors"}), }, }, } From 08c48700dcc146615e8ba8f2d5349efd3a193f0e Mon Sep 17 00:00:00 2001 From: markszabo <12611720+markszabo@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:35:34 +0900 Subject: [PATCH 3/4] Add more tests --- ...hub_organization_custom_properties_test.go | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) diff --git a/github/resource_github_organization_custom_properties_test.go b/github/resource_github_organization_custom_properties_test.go index c8e5ba0e80..4d5598d62d 100644 --- a/github/resource_github_organization_custom_properties_test.go +++ b/github/resource_github_organization_custom_properties_test.go @@ -2,11 +2,50 @@ package github import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +func TestAccGithubOrganizationCustomPropertiesValidation(t *testing.T) { + t.Run("rejects invalid values_editable_by value", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestInvalidValuesEditableBy" + value_type = "string" + required = false + description = "Test invalid values_editable_by" + values_editable_by = "invalid_value" + }` + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("invalid_value is an invalid value"), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) +} + func TestAccGithubOrganizationCustomProperties(t *testing.T) { t.Run("creates custom property without error", func(t *testing.T) { config := ` @@ -198,4 +237,181 @@ func TestAccGithubOrganizationCustomProperties(t *testing.T) { testCase(t, organization) }) }) + + t.Run("backward compatibility - property without values_editable_by defaults correctly", func(t *testing.T) { + config := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestBackwardCompat" + value_type = "string" + required = false + description = "Test property without values_editable_by" + }` + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_custom_properties.test", + "property_name", "TestBackwardCompat", + ), + // When not specified, API returns "org_actors" as the default + resource.TestCheckResourceAttr( + "github_organization_custom_properties.test", + "values_editable_by", "org_actors", + ), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("update values_editable_by from org_actors to org_and_repo_actors", func(t *testing.T) { + configBefore := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestUpdateValuesEditableBy" + value_type = "string" + required = false + description = "Test updating values_editable_by" + values_editable_by = "org_actors" + }` + + configAfter := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestUpdateValuesEditableBy" + value_type = "string" + required = false + description = "Test updating values_editable_by" + values_editable_by = "org_and_repo_actors" + }` + + const resourceName = "github_organization_custom_properties.test" + + checkBefore := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "values_editable_by", "org_actors"), + ) + checkAfter := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "values_editable_by", "org_and_repo_actors"), + ) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: configBefore, + Check: checkBefore, + }, + { + Config: configAfter, + Check: checkAfter, + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) + + t.Run("imports existing property with values_editable_by set via UI", func(t *testing.T) { + // This test simulates a scenario where values_editable_by was set to + // org_and_repo_actors in the GitHub UI before Terraform support was added. + // The resource config intentionally omits values_editable_by to verify + // Terraform can read and maintain the existing value from the API. + + configWithoutField := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestImportWithUISet" + value_type = "string" + required = false + description = "Test property set via UI" + }` + + // After import, we explicitly set the value in config to match what's in the API + configWithField := ` + resource "github_organization_custom_properties" "test" { + property_name = "TestImportWithUISet" + value_type = "string" + required = false + description = "Test property set via UI" + values_editable_by = "org_and_repo_actors" + }` + + const resourceName = "github_organization_custom_properties.test" + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + // First, create a property with values_editable_by set + Config: configWithField, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "values_editable_by", "org_and_repo_actors"), + ), + }, + { + // Simulate the scenario: config doesn't have values_editable_by + // (as it would have been before Terraform support was added) + // Terraform should read the existing value from the API + Config: configWithoutField, + Check: resource.ComposeTestCheckFunc( + // Terraform should still see the value from the API + resource.TestCheckResourceAttr(resourceName, "values_editable_by", "org_and_repo_actors"), + ), + }, + { + // Now add it back to the config - should be no changes needed + Config: configWithField, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "values_editable_by", "org_and_repo_actors"), + ), + }, + }, + }) + } + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + + t.Run("with an individual account", func(t *testing.T) { + t.Skip("individual account not supported for this operation") + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + }) } From 41c700da60c12a2413f56fb714b80e789dbcfd9b Mon Sep 17 00:00:00 2001 From: markszabo <12611720+markszabo@users.noreply.github.com> Date: Tue, 25 Nov 2025 11:54:52 +0900 Subject: [PATCH 4/4] Use value parsing the same way the others are parsed --- ...e_github_organization_custom_properties.go | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index fe1941e35d..da400db9dd 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -85,20 +85,16 @@ func resourceGithubCustomPropertiesCreate(d *schema.ResourceData, meta any) erro for _, v := range allowedValues { allowedValuesString = append(allowedValuesString, v.(string)) } + valuesEditableBy := d.Get("values_editable_by").(string) customProperty := &github.CustomProperty{ - PropertyName: &propertyName, - ValueType: valueType, - Required: &required, - DefaultValue: &defaultValue, - Description: &description, - AllowedValues: allowedValuesString, - } - - // Set ValuesEditableBy if provided - if valuesEditableBy, ok := d.GetOk("values_editable_by"); ok { - valuesEditableByStr := valuesEditableBy.(string) - customProperty.ValuesEditableBy = &valuesEditableByStr + PropertyName: &propertyName, + ValueType: valueType, + Required: &required, + DefaultValue: &defaultValue, + Description: &description, + AllowedValues: allowedValuesString, + ValuesEditableBy: &valuesEditableBy, } customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, d.Get("property_name").(string), customProperty)