From 0c9238bc50b78832df934f464f3500da58d554f7 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Fri, 27 Feb 2026 17:55:46 +0100 Subject: [PATCH 01/17] docs: update organization_custom_properties documentation --- .../organization_custom_properties.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/docs/resources/organization_custom_properties.md b/docs/resources/organization_custom_properties.md index f27d8af532..8bd10d1486 100644 --- a/docs/resources/organization_custom_properties.md +++ b/docs/resources/organization_custom_properties.md @@ -64,13 +64,26 @@ resource "github_organization_custom_properties" "archived" { } ``` +~> **Note:** This resource requires the provider to be configured with an organization owner. Individual user accounts are not supported. + +## Argument Reference + +```hcl +resource "github_organization_custom_properties" "docs_link" { + property_name = "docs_link" + value_type = "url" + required = false + description = "Link to the documentation for this repository" +} +``` + ## Argument Reference The following arguments are supported: -- `property_name` - (Required) The name of the custom property. +* `property_name` - (Required) The name of the custom property. Changing this will force the resource to be recreated. -- `value_type` - (Optional) The type of the custom property. Can be one of `string`, `single_select`, `multi_select`, or `true_false`. Defaults to `string`. +* `value_type` - (Required) The type of the custom property. Can be one of `string`, `single_select`, `multi_select`, `true_false`, or `url`. Changing this will force the resource to be recreated. - `required` - (Optional) Whether the custom property is required. Defaults to `false`. @@ -78,7 +91,7 @@ The following arguments are supported: - `default_value` - (Optional) The default value of the custom property. -- `allowed_values` - (Optional) List of allowed values for the custom property. Only applicable when `value_type` is `single_select` or `multi_select`. +* `allowed_values` - (Optional) List of allowed values for the custom property. Required when `value_type` is `single_select` or `multi_select`, and must not be set for other value types. - `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. From a0b4b95b73df3eb4201091a53927467c570db96c Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Sun, 14 Jun 2026 17:55:21 +0200 Subject: [PATCH 02/17] feat(custom_property): add support for organization repository custom property --- ...organization_repository_custom_property.md | 34 ++ ...organization_repository_custom_property.md | 79 +++++ .../example_1.tf | 3 + .../example_1.tf | 12 + .../example_2.tf | 6 + .../example_3.tf | 6 + .../import.sh | 1 + ...organization_repository_custom_property.go | 92 ++++++ ...ization_repository_custom_property_test.go | 46 +++ github/provider.go | 2 + ...organization_repository_custom_property.go | 265 ++++++++++++++++ ...ization_repository_custom_property_test.go | 294 ++++++++++++++++++ ...ization_repository_custom_property.md.tmpl | 15 + ...ization_repository_custom_property.md.tmpl | 32 ++ 14 files changed, 887 insertions(+) create mode 100644 docs/data-sources/organization_repository_custom_property.md create mode 100644 docs/resources/organization_repository_custom_property.md create mode 100644 examples/data-sources/organization_repository_custom_property/example_1.tf create mode 100644 examples/resources/organization_repository_custom_property/example_1.tf create mode 100644 examples/resources/organization_repository_custom_property/example_2.tf create mode 100644 examples/resources/organization_repository_custom_property/example_3.tf create mode 100644 examples/resources/organization_repository_custom_property/import.sh create mode 100644 github/data_source_github_organization_repository_custom_property.go create mode 100644 github/data_source_github_organization_repository_custom_property_test.go create mode 100644 github/resource_github_organization_repository_custom_property.go create mode 100644 github/resource_github_organization_repository_custom_property_test.go create mode 100644 templates/data-sources/organization_repository_custom_property.md.tmpl create mode 100644 templates/resources/organization_repository_custom_property.md.tmpl diff --git a/docs/data-sources/organization_repository_custom_property.md b/docs/data-sources/organization_repository_custom_property.md new file mode 100644 index 0000000000..61dceee182 --- /dev/null +++ b/docs/data-sources/organization_repository_custom_property.md @@ -0,0 +1,34 @@ +--- +page_title: "github_organization_repository_custom_property (Data Source) - GitHub" +description: |- + Looks up a GitHub organization custom property definition. +--- + +# github_organization_repository_custom_property (Data Source) + +Looks up a single GitHub organization custom property definition by name. + +## Example Usage + +```terraform +data "github_organization_repository_custom_property" "environment" { + property_name = "environment" +} +``` + + +## Schema + +### Required + +- `property_name` (String) Name of the custom property to look up. + +### Read-Only + +- `allowed_values` (List of String) Allowed values when `value_type` is `single_select` or `multi_select`. +- `default_value` (String) Default value applied to repositories that do not explicitly set the property. +- `description` (String) Short description of the custom property. +- `id` (String) The ID of this resource. +- `required` (Boolean) Whether the custom property must be set on every repository. +- `value_type` (String) Type of the custom property. +- `values_editable_by` (String) Who can edit values of this property on repositories. diff --git a/docs/resources/organization_repository_custom_property.md b/docs/resources/organization_repository_custom_property.md new file mode 100644 index 0000000000..4fbc78e0ab --- /dev/null +++ b/docs/resources/organization_repository_custom_property.md @@ -0,0 +1,79 @@ +--- +page_title: "github_organization_repository_custom_property (Resource) - GitHub" +description: |- + Manages a GitHub organization custom property definition. +--- + +# github_organization_repository_custom_property (Resource) + +Manages a single GitHub organization custom property definition. Repositories +in the organization can subsequently be tagged with values for this property +via the `github_repository_custom_property` resource or directly through the +GitHub UI / API. + +## Example Usage + +```terraform +resource "github_organization_repository_custom_property" "environment" { + property_name = "environment" + value_type = "single_select" + required = true + description = "The deployment environment for this repository" + default_value = "development" + allowed_values = [ + "development", + "staging", + "production", + ] +} +``` + +## Example Usage - Allow Repository Actors to Edit + +```terraform +resource "github_organization_repository_custom_property" "team_contact" { + property_name = "team_contact" + value_type = "string" + description = "Contact information for the team managing this repository" + values_editable_by = "org_and_repo_actors" +} +``` + +## Example Usage - Boolean Property + +```terraform +resource "github_organization_repository_custom_property" "archived" { + property_name = "archived" + value_type = "true_false" + description = "Whether this repository is archived" + default_value = "false" +} +``` + + +## Schema + +### Required + +- `property_name` (String) Name of the custom property. +- `value_type` (String) Type of the custom property. One of: [string single_select multi_select true_false url]. + +### Optional + +- `allowed_values` (List of String) Allowed values for `single_select` and `multi_select` property types. Must be omitted for other types. +- `default_value` (String) Default value applied to repositories that do not explicitly set the property. +- `description` (String) Short description of the custom property. +- `required` (Boolean) Whether the custom property must be set on every repository. When true, `default_value` must be provided. +- `values_editable_by` (String) Who can edit values of this property on repositories. One of: [org_actors org_and_repo_actors]. Defaults to `org_actors` server-side. + +### Read-Only + +- `id` (String) The ID of this resource. + +## Import + +Organization custom properties can be imported using the property name: + +```shell +terraform import github_organization_repository_custom_property.environment environment +``` diff --git a/examples/data-sources/organization_repository_custom_property/example_1.tf b/examples/data-sources/organization_repository_custom_property/example_1.tf new file mode 100644 index 0000000000..cc5c5b3456 --- /dev/null +++ b/examples/data-sources/organization_repository_custom_property/example_1.tf @@ -0,0 +1,3 @@ +data "github_organization_repository_custom_property" "environment" { + property_name = "environment" +} diff --git a/examples/resources/organization_repository_custom_property/example_1.tf b/examples/resources/organization_repository_custom_property/example_1.tf new file mode 100644 index 0000000000..7ee5abdf01 --- /dev/null +++ b/examples/resources/organization_repository_custom_property/example_1.tf @@ -0,0 +1,12 @@ +resource "github_organization_repository_custom_property" "environment" { + property_name = "environment" + value_type = "single_select" + required = true + description = "The deployment environment for this repository" + default_value = "development" + allowed_values = [ + "development", + "staging", + "production", + ] +} diff --git a/examples/resources/organization_repository_custom_property/example_2.tf b/examples/resources/organization_repository_custom_property/example_2.tf new file mode 100644 index 0000000000..6543a9b57d --- /dev/null +++ b/examples/resources/organization_repository_custom_property/example_2.tf @@ -0,0 +1,6 @@ +resource "github_organization_repository_custom_property" "team_contact" { + property_name = "team_contact" + value_type = "string" + description = "Contact information for the team managing this repository" + values_editable_by = "org_and_repo_actors" +} diff --git a/examples/resources/organization_repository_custom_property/example_3.tf b/examples/resources/organization_repository_custom_property/example_3.tf new file mode 100644 index 0000000000..4e2246162e --- /dev/null +++ b/examples/resources/organization_repository_custom_property/example_3.tf @@ -0,0 +1,6 @@ +resource "github_organization_repository_custom_property" "archived" { + property_name = "archived" + value_type = "true_false" + description = "Whether this repository is archived" + default_value = "false" +} diff --git a/examples/resources/organization_repository_custom_property/import.sh b/examples/resources/organization_repository_custom_property/import.sh new file mode 100644 index 0000000000..0f602621a8 --- /dev/null +++ b/examples/resources/organization_repository_custom_property/import.sh @@ -0,0 +1 @@ +terraform import github_organization_repository_custom_property.environment environment diff --git a/github/data_source_github_organization_repository_custom_property.go b/github/data_source_github_organization_repository_custom_property.go new file mode 100644 index 0000000000..3b35f0e2fe --- /dev/null +++ b/github/data_source_github_organization_repository_custom_property.go @@ -0,0 +1,92 @@ +package github + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/google/go-github/v89/github" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { + return &schema.Resource{ + Description: "Looks up a single GitHub organization custom property definition by name.", + ReadContext: dataSourceGithubOrganizationRepositoryCustomPropertyRead, + + Schema: map[string]*schema.Schema{ + "property_name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the custom property to look up.", + }, + "value_type": { + Type: schema.TypeString, + Computed: true, + Description: "Type of the custom property.", + }, + "required": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the custom property must be set on every repository.", + }, + "default_value": { + Type: schema.TypeString, + Computed: true, + Description: "Default value applied to repositories that do not explicitly set the property.", + }, + "description": { + Type: schema.TypeString, + Computed: true, + Description: "Short description of the custom property.", + }, + "allowed_values": { + Type: schema.TypeList, + Computed: true, + Description: "Allowed values when `value_type` is `single_select` or `multi_select`.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "values_editable_by": { + Type: schema.TypeString, + Computed: true, + Description: "Who can edit values of this property on repositories.", + }, + }, + } +} + +func dataSourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if err := checkOrganization(meta); err != nil { + return diag.FromErr(err) + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + propertyName := d.Get("property_name").(string) + + tflog.Debug(ctx, "Reading organization custom property", map[string]any{"org": orgName, "property": propertyName}) + + cp, _, err := client.Organizations.GetCustomProperty(ctx, orgName, propertyName) + if err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { + return diag.FromErr(fmt.Errorf("organization custom property %q not found in %q", propertyName, orgName)) + } + return diag.FromErr(fmt.Errorf("error reading organization custom property %q: %w", propertyName, err)) + } + + if !slices.Contains([]github.PropertyValueType{ + github.PropertyValueTypeSingleSelect, + github.PropertyValueTypeMultiSelect, + }, cp.ValueType) { + cp.AllowedValues = nil + } + + if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/github/data_source_github_organization_repository_custom_property_test.go b/github/data_source_github_organization_repository_custom_property_test.go new file mode 100644 index 0000000000..1279c2376b --- /dev/null +++ b/github/data_source_github_organization_repository_custom_property_test.go @@ -0,0 +1,46 @@ +package github + +import ( + "testing" + + "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 TestAccGithubOrganizationRepositoryCustomPropertyDataSource(t *testing.T) { + const dataAddr = "data.github_organization_repository_custom_property.test" + + t.Run("reads a property created by the resource", func(t *testing.T) { + config := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-ds" + value_type = "single_select" + description = "tf-acc-test data source" + allowed_values = ["a", "b"] + } + + data "github_organization_repository_custom_property" "test" { + property_name = github_organization_repository_custom_property.test.property_name + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("value_type"), knownvalue.StringExact("single_select")), + statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("description"), knownvalue.StringExact("tf-acc-test data source")), + statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("allowed_values"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("a"), + knownvalue.StringExact("b"), + })), + }, + }, + }, + }) + }) +} diff --git a/github/provider.go b/github/provider.go index 73e93dc5e5..93427eb3c4 100644 --- a/github/provider.go +++ b/github/provider.go @@ -213,6 +213,7 @@ func NewProvider(version, commit string) func() *schema.Provider { "github_organization_custom_role": resourceGithubOrganizationCustomRole(), "github_organization_custom_properties": resourceGithubOrganizationCustomProperties(), "github_organization_project": resourceGithubOrganizationProject(), + "github_organization_repository_custom_property": resourceGithubOrganizationRepositoryCustomProperty(), "github_organization_repository_role": resourceGithubOrganizationRepositoryRole(), "github_organization_role": resourceGithubOrganizationRole(), "github_organization_role_team": resourceGithubOrganizationRoleTeam(), @@ -302,6 +303,7 @@ func NewProvider(version, commit string) func() *schema.Provider { "github_organization_ip_allow_list": dataSourceGithubOrganizationIpAllowList(), "github_organization_members": dataSourceGithubOrganizationMembers(), "github_organization_repositories": dataSourceGithubOrganizationRepositories(), + "github_organization_repository_custom_property": dataSourceGithubOrganizationRepositoryCustomProperty(), "github_organization_repository_role": dataSourceGithubOrganizationRepositoryRole(), "github_organization_repository_roles": dataSourceGithubOrganizationRepositoryRoles(), "github_organization_role": dataSourceGithubOrganizationRole(), diff --git a/github/resource_github_organization_repository_custom_property.go b/github/resource_github_organization_repository_custom_property.go new file mode 100644 index 0000000000..574e839e6d --- /dev/null +++ b/github/resource_github_organization_repository_custom_property.go @@ -0,0 +1,265 @@ +package github + +import ( + "context" + "errors" + "fmt" + "slices" + + "github.com/google/go-github/v89/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" +) + +var organizationCustomPropertyValueTypes = []string{ + string(github.PropertyValueTypeString), + string(github.PropertyValueTypeSingleSelect), + string(github.PropertyValueTypeMultiSelect), + string(github.PropertyValueTypeTrueFalse), + string(github.PropertyValueTypeURL), +} + +var organizationCustomPropertyValuesEditableBy = []string{"org_actors", "org_and_repo_actors"} + +func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { + return &schema.Resource{ + Description: "Manages a GitHub organization custom property definition. Custom properties defined here can later be assigned values on individual repositories.", + + CreateContext: resourceGithubOrganizationRepositoryCustomPropertyCreate, + ReadContext: resourceGithubOrganizationRepositoryCustomPropertyRead, + UpdateContext: resourceGithubOrganizationRepositoryCustomPropertyUpdate, + DeleteContext: resourceGithubOrganizationRepositoryCustomPropertyDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + CustomizeDiff: customdiff.All(resourceGithubOrganizationRepositoryCustomPropertyDiff), + + Schema: map[string]*schema.Schema{ + "property_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the custom property.", + }, + "value_type": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: fmt.Sprintf("Type of the custom property. One of: %v.", organizationCustomPropertyValueTypes), + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(organizationCustomPropertyValueTypes, false)), + }, + "required": { + Type: schema.TypeBool, + Optional: true, + Description: "Whether the custom property must be set on every repository. When true, `default_value` must be provided.", + }, + "default_value": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Default value applied to repositories that do not explicitly set the property.", + }, + "description": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Short description of the custom property.", + }, + "allowed_values": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Description: "Allowed values for `single_select` and `multi_select` property types. Must be omitted for other types.", + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "values_editable_by": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: fmt.Sprintf("Who can edit values of this property on repositories. One of: %v. Defaults to `org_actors` server-side.", organizationCustomPropertyValuesEditableBy), + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice(organizationCustomPropertyValuesEditableBy, false)), + }, + }, + } +} + +func resourceGithubOrganizationRepositoryCustomPropertyDiff(ctx context.Context, d *schema.ResourceDiff, _ any) error { + if !d.NewValueKnown("value_type") || !d.NewValueKnown("allowed_values") { + return nil + } + + valueType := github.PropertyValueType(d.Get("value_type").(string)) + allowedValues, _ := d.Get("allowed_values").([]any) + + selectType := valueType == github.PropertyValueTypeSingleSelect || valueType == github.PropertyValueTypeMultiSelect + + if selectType && len(allowedValues) == 0 { + return fmt.Errorf("allowed_values is required when value_type is %q", valueType) + } + if !selectType && len(allowedValues) > 0 { + return fmt.Errorf("allowed_values must not be set when value_type is %q", valueType) + } + + return nil +} + +func buildOrganizationRepositoryCustomProperty(d *schema.ResourceData) *github.CustomProperty { + propertyName := d.Get("property_name").(string) + valueType := github.PropertyValueType(d.Get("value_type").(string)) + required := d.Get("required").(bool) + description := d.Get("description").(string) + + cp := &github.CustomProperty{ + PropertyName: &propertyName, + ValueType: valueType, + Required: &required, + Description: &description, + } + + if v, ok := d.GetOk("default_value"); ok { + s := v.(string) + cp.DefaultValue = &s + } + + if v, ok := d.GetOk("allowed_values"); ok { + cp.AllowedValues = expandStringList(v.([]any)) + } + + if v, ok := d.GetOk("values_editable_by"); ok { + s := v.(string) + cp.ValuesEditableBy = &s + } + + return cp +} + +func setOrganizationRepositoryCustomPropertyState(d *schema.ResourceData, cp *github.CustomProperty) error { + defaultValue, _ := cp.DefaultValueString() + + d.SetId(cp.GetPropertyName()) + + for _, set := range []struct { + key string + value any + }{ + {"property_name", cp.GetPropertyName()}, + {"value_type", string(cp.ValueType)}, + {"required", cp.GetRequired()}, + {"description", cp.GetDescription()}, + {"default_value", defaultValue}, + {"allowed_values", cp.AllowedValues}, + {"values_editable_by", cp.GetValuesEditableBy()}, + } { + if err := d.Set(set.key, set.value); err != nil { + return err + } + } + + return nil +} + +func resourceGithubOrganizationRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if err := checkOrganization(meta); err != nil { + return diag.FromErr(err) + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + propertyName := d.Get("property_name").(string) + + tflog.Debug(ctx, "Creating organization custom property", map[string]any{"org": orgName, "property": propertyName}) + + cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, orgName, propertyName, buildOrganizationRepositoryCustomProperty(d)) + if err != nil { + return diag.FromErr(fmt.Errorf("error creating organization custom property %q: %w", propertyName, err)) + } + + if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if err := checkOrganization(meta); err != nil { + return diag.FromErr(err) + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + propertyName := d.Id() + + cp, _, err := client.Organizations.GetCustomProperty(ctx, orgName, propertyName) + if err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { + tflog.Info(ctx, "Removing organization custom property from state because it no longer exists", map[string]any{"org": orgName, "property": propertyName}) + d.SetId("") + return nil + } + return diag.FromErr(fmt.Errorf("error reading organization custom property %q: %w", propertyName, err)) + } + + // Sentinel: the API ignores allowed_values for non-select types, so don't + // import a phantom empty list into state. Only persist it when meaningful. + if !slices.Contains([]github.PropertyValueType{ + github.PropertyValueTypeSingleSelect, + github.PropertyValueTypeMultiSelect, + }, cp.ValueType) { + cp.AllowedValues = nil + } + + if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubOrganizationRepositoryCustomPropertyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if err := checkOrganization(meta); err != nil { + return diag.FromErr(err) + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + propertyName := d.Get("property_name").(string) + + tflog.Debug(ctx, "Updating organization custom property", map[string]any{"org": orgName, "property": propertyName}) + + cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, orgName, propertyName, buildOrganizationRepositoryCustomProperty(d)) + if err != nil { + return diag.FromErr(fmt.Errorf("error updating organization custom property %q: %w", propertyName, err)) + } + + if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubOrganizationRepositoryCustomPropertyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + if err := checkOrganization(meta); err != nil { + return diag.FromErr(err) + } + + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + propertyName := d.Get("property_name").(string) + + tflog.Debug(ctx, "Deleting organization custom property", map[string]any{"org": orgName, "property": propertyName}) + + if _, err := client.Organizations.RemoveCustomProperty(ctx, orgName, propertyName); err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { + return nil + } + return diag.FromErr(fmt.Errorf("error deleting organization custom property %q: %w", propertyName, err)) + } + + return nil +} diff --git a/github/resource_github_organization_repository_custom_property_test.go b/github/resource_github_organization_repository_custom_property_test.go new file mode 100644 index 0000000000..eb9fd77300 --- /dev/null +++ b/github/resource_github_organization_repository_custom_property_test.go @@ -0,0 +1,294 @@ +package github + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" +) + +func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { + const resourceAddr = "github_organization_repository_custom_property.test" + + t.Run("creates a string property without error", func(t *testing.T) { + config := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-string" + value_type = "string" + description = "tf-acc-test string property" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("property_name"), knownvalue.StringExact("tf-acc-test-string")), + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("value_type"), knownvalue.StringExact("string")), + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_actors")), + }, + }, + }, + }) + }) + + t.Run("creates a single_select property and grows allowed_values", func(t *testing.T) { + configBefore := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-single-select" + value_type = "single_select" + description = "tf-acc-test single_select property" + allowed_values = ["one"] + }` + configAfter := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-single-select" + value_type = "single_select" + description = "tf-acc-test single_select property updated" + allowed_values = ["one", "two"] + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configBefore, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("allowed_values"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("one"), + })), + }, + }, + { + Config: configAfter, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("allowed_values"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("one"), + knownvalue.StringExact("two"), + })), + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("description"), knownvalue.StringExact("tf-acc-test single_select property updated")), + }, + }, + }, + }) + }) + + t.Run("imports without error", func(t *testing.T) { + config := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-import" + value_type = "string" + description = "tf-acc-test import" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + {Config: config}, + { + ResourceName: resourceAddr, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("forces new when property_name changes", func(t *testing.T) { + before := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-rename-a" + value_type = "string" + }` + after := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-rename-b" + value_type = "string" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + {Config: before}, + { + Config: after, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + }, + }, + }) + }) + + t.Run("forces new when value_type changes", func(t *testing.T) { + before := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-retype" + value_type = "string" + }` + after := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-retype" + value_type = "single_select" + allowed_values = ["x"] + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + {Config: before}, + { + Config: after, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionDestroyBeforeCreate), + }, + }, + }, + }, + }) + }) + + t.Run("rejects allowed_values on string type", func(t *testing.T) { + config := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-invalid" + value_type = "string" + allowed_values = ["nope"] + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("allowed_values must not be set"), + }, + }, + }) + }) + + t.Run("requires allowed_values on single_select type", func(t *testing.T) { + config := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-missing-allowed" + value_type = "single_select" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("allowed_values is required"), + }, + }, + }) + }) + + t.Run("rejects invalid values_editable_by", func(t *testing.T) { + config := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-bad-editable" + value_type = "string" + values_editable_by = "nope" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("nope"), + }, + }, + }) + }) + + t.Run("updates values_editable_by from org_actors to org_and_repo_actors", func(t *testing.T) { + before := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-editable" + value_type = "string" + values_editable_by = "org_actors" + }` + after := ` + resource "github_organization_repository_custom_property" "test" { + property_name = "tf-acc-test-editable" + value_type = "string" + values_editable_by = "org_and_repo_actors" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: before, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_actors")), + }, + }, + { + Config: after, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), + }, + }, + }, + }) + }) + + t.Run("retains values_editable_by set out-of-band when omitted from config", func(t *testing.T) { + // Mirrors the upstream behaviour where a value set via the UI before + // Terraform managed the property is reflected back into state via the + // Computed attribute even when the config omits it. + propertyName := "tf-acc-test-ui-set" + configWithField := fmt.Sprintf(` + resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + values_editable_by = "org_and_repo_actors" + }`, propertyName) + configWithoutField := fmt.Sprintf(` + resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + }`, propertyName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configWithField, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), + }, + }, + { + Config: configWithoutField, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), + }, + }, + }, + }) + }) +} diff --git a/templates/data-sources/organization_repository_custom_property.md.tmpl b/templates/data-sources/organization_repository_custom_property.md.tmpl new file mode 100644 index 0000000000..3825d2a462 --- /dev/null +++ b/templates/data-sources/organization_repository_custom_property.md.tmpl @@ -0,0 +1,15 @@ +--- +page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +description: |- + Looks up a GitHub organization custom property definition. +--- + +# {{.Name}} ({{.Type}}) + +Looks up a single GitHub organization custom property definition by name. + +## Example Usage + +{{ tffile "examples/data-sources/organization_repository_custom_property/example_1.tf" }} + +{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/organization_repository_custom_property.md.tmpl b/templates/resources/organization_repository_custom_property.md.tmpl new file mode 100644 index 0000000000..9be0841085 --- /dev/null +++ b/templates/resources/organization_repository_custom_property.md.tmpl @@ -0,0 +1,32 @@ +--- +page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +description: |- + Manages a GitHub organization custom property definition. +--- + +# {{.Name}} ({{.Type}}) + +Manages a single GitHub organization custom property definition. Repositories +in the organization can subsequently be tagged with values for this property +via the `github_repository_custom_property` resource or directly through the +GitHub UI / API. + +## Example Usage + +{{ tffile "examples/resources/organization_repository_custom_property/example_1.tf" }} + +## Example Usage - Allow Repository Actors to Edit + +{{ tffile "examples/resources/organization_repository_custom_property/example_2.tf" }} + +## Example Usage - Boolean Property + +{{ tffile "examples/resources/organization_repository_custom_property/example_3.tf" }} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +Organization custom properties can be imported using the property name: + +{{ codefile "shell" "examples/resources/organization_repository_custom_property/import.sh" }} From bc12df1eedc741814be271869b0def1eca3722bb Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Sun, 14 Jun 2026 17:55:51 +0200 Subject: [PATCH 03/17] chore(docs): mark organization custom properties as deprecated and update references --- .../organization_custom_properties.md | 2 ++ .../organization_custom_properties.md | 21 +++++-------------- ...e_github_organization_custom_properties.go | 3 ++- ...e_github_organization_custom_properties.go | 1 + .../organization_custom_properties.md.tmpl | 2 ++ .../organization_custom_properties.md.tmpl | 2 ++ 6 files changed, 14 insertions(+), 17 deletions(-) diff --git a/docs/data-sources/organization_custom_properties.md b/docs/data-sources/organization_custom_properties.md index 1564509fd7..0c7324fa5d 100644 --- a/docs/data-sources/organization_custom_properties.md +++ b/docs/data-sources/organization_custom_properties.md @@ -6,6 +6,8 @@ description: |- # github_organization_custom_properties (Data Source) +~> **Deprecated:** Use the singular [`github_organization_repository_custom_property`](organization_repository_custom_property) data source instead. This data source will be removed in a future major release. + Use this data source to retrieve information about a GitHub organization custom property. ## Example Usage diff --git a/docs/resources/organization_custom_properties.md b/docs/resources/organization_custom_properties.md index 8bd10d1486..39b1ff5505 100644 --- a/docs/resources/organization_custom_properties.md +++ b/docs/resources/organization_custom_properties.md @@ -6,6 +6,8 @@ description: |- # github_organization_custom_properties (Resource) +~> **Deprecated:** Use the singular [`github_organization_repository_custom_property`](organization_repository_custom_property) resource instead. This resource will be removed in a future major release. + This resource allows you to create and manage custom properties for a GitHub organization. Custom properties enable you to add metadata to repositories within your organization. You can use custom properties to add context about repositories, such as who owns them, when they expire, or compliance requirements. @@ -64,26 +66,13 @@ resource "github_organization_custom_properties" "archived" { } ``` -~> **Note:** This resource requires the provider to be configured with an organization owner. Individual user accounts are not supported. - -## Argument Reference - -```hcl -resource "github_organization_custom_properties" "docs_link" { - property_name = "docs_link" - value_type = "url" - required = false - description = "Link to the documentation for this repository" -} -``` - ## Argument Reference The following arguments are supported: -* `property_name` - (Required) The name of the custom property. Changing this will force the resource to be recreated. +- `property_name` - (Required) The name of the custom property. -* `value_type` - (Required) The type of the custom property. Can be one of `string`, `single_select`, `multi_select`, `true_false`, or `url`. Changing this will force the resource to be recreated. +- `value_type` - (Optional) The type of the custom property. Can be one of `string`, `single_select`, `multi_select`, or `true_false`. Defaults to `string`. - `required` - (Optional) Whether the custom property is required. Defaults to `false`. @@ -91,7 +80,7 @@ The following arguments are supported: - `default_value` - (Optional) The default value of the custom property. -* `allowed_values` - (Optional) List of allowed values for the custom property. Required when `value_type` is `single_select` or `multi_select`, and must not be set for other value types. +- `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. diff --git a/github/data_source_github_organization_custom_properties.go b/github/data_source_github_organization_custom_properties.go index 8e52187276..cbc76a8df2 100644 --- a/github/data_source_github_organization_custom_properties.go +++ b/github/data_source_github_organization_custom_properties.go @@ -9,7 +9,8 @@ import ( func dataSourceGithubOrganizationCustomProperties() *schema.Resource { return &schema.Resource{ - ReadContext: dataSourceGithubOrganizationCustomPropertiesRead, + DeprecationMessage: "This data source is deprecated and will be removed in a future release. Use github_organization_repository_custom_property (singular) instead.", + ReadContext: dataSourceGithubOrganizationCustomPropertiesRead, Schema: map[string]*schema.Schema{ "property_name": { diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index 84916ab879..7830f802cd 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -11,6 +11,7 @@ import ( func resourceGithubOrganizationCustomProperties() *schema.Resource { return &schema.Resource{ + DeprecationMessage: "This resource is deprecated and will be removed in a future release. Use github_organization_repository_custom_property instead.", Create: resourceGithubCustomPropertiesCreate, Read: resourceGithubCustomPropertiesRead, Update: resourceGithubCustomPropertiesUpdate, diff --git a/templates/data-sources/organization_custom_properties.md.tmpl b/templates/data-sources/organization_custom_properties.md.tmpl index 45a0ddfe97..5bb53261ea 100644 --- a/templates/data-sources/organization_custom_properties.md.tmpl +++ b/templates/data-sources/organization_custom_properties.md.tmpl @@ -6,6 +6,8 @@ description: |- # {{.Name}} ({{.Type}}) +~> **Deprecated:** Use the singular [`github_organization_repository_custom_property`](organization_repository_custom_property) data source instead. This data source will be removed in a future major release. + Use this data source to retrieve information about a GitHub organization custom property. ## Example Usage diff --git a/templates/resources/organization_custom_properties.md.tmpl b/templates/resources/organization_custom_properties.md.tmpl index a65ecca029..2c4c30bd99 100644 --- a/templates/resources/organization_custom_properties.md.tmpl +++ b/templates/resources/organization_custom_properties.md.tmpl @@ -6,6 +6,8 @@ description: |- # {{.Name}} ({{.Type}}) +~> **Deprecated:** Use the singular [`github_organization_repository_custom_property`](organization_repository_custom_property) resource instead. This resource will be removed in a future major release. + This resource allows you to create and manage custom properties for a GitHub organization. Custom properties enable you to add metadata to repositories within your organization. You can use custom properties to add context about repositories, such as who owns them, when they expire, or compliance requirements. From 0a60ab4be65739676eaa1593e9efbe7a2a30de3c Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Tue, 11 Aug 2026 18:20:13 +0200 Subject: [PATCH 04/17] style: fix gofmt struct-field alignment in deprecated org custom properties resource Adding the DeprecationMessage field left the remaining Create/Read/Update/Delete fields misaligned. --- github/resource_github_organization_custom_properties.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index 7830f802cd..27ff2e7605 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -12,10 +12,10 @@ import ( func resourceGithubOrganizationCustomProperties() *schema.Resource { return &schema.Resource{ DeprecationMessage: "This resource is deprecated and will be removed in a future release. Use github_organization_repository_custom_property instead.", - Create: resourceGithubCustomPropertiesCreate, - Read: resourceGithubCustomPropertiesRead, - Update: resourceGithubCustomPropertiesUpdate, - Delete: resourceGithubCustomPropertiesDelete, + Create: resourceGithubCustomPropertiesCreate, + Read: resourceGithubCustomPropertiesRead, + Update: resourceGithubCustomPropertiesUpdate, + Delete: resourceGithubCustomPropertiesDelete, Importer: &schema.ResourceImporter{ State: resourceGithubCustomPropertiesImport, }, From 23d243b0f796709dcf63f5bca73c7ff5e441b1ee Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Tue, 11 Aug 2026 18:20:36 +0200 Subject: [PATCH 05/17] refactor: align CRUD signatures with ARCHITECTURE.md and inline state setting Addresses review feedback from #3234: - Use m any + meta, _ := m.(*Owner) in every CRUD/read function, matching the convention documented in ARCHITECTURE.md and used by the #3476 refactor of github_repository_custom_property. Keep checkOrganizationOK (used by ~15 other data sources) rather than an inline org check. - Replace slices.Contains with a switch statement for the select-type check. - Drop the shared setOrganizationRepositoryCustomPropertyState helper and inline the d.Set calls in each CRUD method body, per stevehipwell's request to keep a single documented pattern until state-setting logic is unified repo-wide. --- ...organization_repository_custom_property.go | 47 +++-- ...organization_repository_custom_property.go | 161 +++++++++++------- 2 files changed, 131 insertions(+), 77 deletions(-) diff --git a/github/data_source_github_organization_repository_custom_property.go b/github/data_source_github_organization_repository_custom_property.go index 3b35f0e2fe..4cf42023df 100644 --- a/github/data_source_github_organization_repository_custom_property.go +++ b/github/data_source_github_organization_repository_custom_property.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "slices" "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -58,33 +57,53 @@ func dataSourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { } } -func dataSourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - if err := checkOrganization(meta); err != nil { - return diag.FromErr(err) +func dataSourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + client := meta.v3client + owner := meta.name propertyName := d.Get("property_name").(string) - tflog.Debug(ctx, "Reading organization custom property", map[string]any{"org": orgName, "property": propertyName}) + tflog.Debug(ctx, "Reading organization custom property", map[string]any{"org": owner, "property": propertyName}) - cp, _, err := client.Organizations.GetCustomProperty(ctx, orgName, propertyName) + cp, _, err := client.Organizations.GetCustomProperty(ctx, owner, propertyName) if err != nil { if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { - return diag.FromErr(fmt.Errorf("organization custom property %q not found in %q", propertyName, orgName)) + return diag.FromErr(fmt.Errorf("organization custom property %q not found in %q", propertyName, owner)) } return diag.FromErr(fmt.Errorf("error reading organization custom property %q: %w", propertyName, err)) } - if !slices.Contains([]github.PropertyValueType{ - github.PropertyValueTypeSingleSelect, - github.PropertyValueTypeMultiSelect, - }, cp.ValueType) { + switch cp.ValueType { + case github.PropertyValueTypeSingleSelect, github.PropertyValueTypeMultiSelect: + default: cp.AllowedValues = nil } - if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + defaultValue, _ := cp.DefaultValueString() + d.SetId(cp.GetPropertyName()) + if err := d.Set("property_name", cp.GetPropertyName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("value_type", string(cp.ValueType)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("required", cp.GetRequired()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("default_value", defaultValue); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", cp.GetDescription()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("allowed_values", cp.AllowedValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("values_editable_by", cp.GetValuesEditableBy()); err != nil { return diag.FromErr(err) } diff --git a/github/resource_github_organization_repository_custom_property.go b/github/resource_github_organization_repository_custom_property.go index 574e839e6d..9c5aa5eef3 100644 --- a/github/resource_github_organization_repository_custom_property.go +++ b/github/resource_github_organization_repository_custom_property.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "slices" "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -137,124 +136,160 @@ func buildOrganizationRepositoryCustomProperty(d *schema.ResourceData) *github.C return cp } -func setOrganizationRepositoryCustomPropertyState(d *schema.ResourceData, cp *github.CustomProperty) error { - defaultValue, _ := cp.DefaultValueString() - - d.SetId(cp.GetPropertyName()) - - for _, set := range []struct { - key string - value any - }{ - {"property_name", cp.GetPropertyName()}, - {"value_type", string(cp.ValueType)}, - {"required", cp.GetRequired()}, - {"description", cp.GetDescription()}, - {"default_value", defaultValue}, - {"allowed_values", cp.AllowedValues}, - {"values_editable_by", cp.GetValuesEditableBy()}, - } { - if err := d.Set(set.key, set.value); err != nil { - return err - } - } - - return nil -} - -func resourceGithubOrganizationRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - if err := checkOrganization(meta); err != nil { - return diag.FromErr(err) +func resourceGithubOrganizationRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + client := meta.v3client + owner := meta.name propertyName := d.Get("property_name").(string) - tflog.Debug(ctx, "Creating organization custom property", map[string]any{"org": orgName, "property": propertyName}) + tflog.Debug(ctx, "Creating organization custom property", map[string]any{"org": owner, "property": propertyName}) - cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, orgName, propertyName, buildOrganizationRepositoryCustomProperty(d)) + cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, owner, propertyName, buildOrganizationRepositoryCustomProperty(d)) if err != nil { return diag.FromErr(fmt.Errorf("error creating organization custom property %q: %w", propertyName, err)) } - if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + defaultValue, _ := cp.DefaultValueString() + d.SetId(cp.GetPropertyName()) + if err := d.Set("property_name", cp.GetPropertyName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("value_type", string(cp.ValueType)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("required", cp.GetRequired()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("default_value", defaultValue); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", cp.GetDescription()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("allowed_values", cp.AllowedValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("values_editable_by", cp.GetValuesEditableBy()); err != nil { return diag.FromErr(err) } return nil } -func resourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - if err := checkOrganization(meta); err != nil { - return diag.FromErr(err) +func resourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + client := meta.v3client + owner := meta.name propertyName := d.Id() - cp, _, err := client.Organizations.GetCustomProperty(ctx, orgName, propertyName) + cp, _, err := client.Organizations.GetCustomProperty(ctx, owner, propertyName) if err != nil { if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { - tflog.Info(ctx, "Removing organization custom property from state because it no longer exists", map[string]any{"org": orgName, "property": propertyName}) + tflog.Info(ctx, "Removing organization custom property from state because it no longer exists", map[string]any{"org": owner, "property": propertyName}) d.SetId("") return nil } return diag.FromErr(fmt.Errorf("error reading organization custom property %q: %w", propertyName, err)) } - // Sentinel: the API ignores allowed_values for non-select types, so don't - // import a phantom empty list into state. Only persist it when meaningful. - if !slices.Contains([]github.PropertyValueType{ - github.PropertyValueTypeSingleSelect, - github.PropertyValueTypeMultiSelect, - }, cp.ValueType) { + switch cp.ValueType { + case github.PropertyValueTypeSingleSelect, github.PropertyValueTypeMultiSelect: + default: cp.AllowedValues = nil } - if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + defaultValue, _ := cp.DefaultValueString() + d.SetId(cp.GetPropertyName()) + if err := d.Set("property_name", cp.GetPropertyName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("value_type", string(cp.ValueType)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("required", cp.GetRequired()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("default_value", defaultValue); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", cp.GetDescription()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("allowed_values", cp.AllowedValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("values_editable_by", cp.GetValuesEditableBy()); err != nil { return diag.FromErr(err) } return nil } -func resourceGithubOrganizationRepositoryCustomPropertyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - if err := checkOrganization(meta); err != nil { - return diag.FromErr(err) +func resourceGithubOrganizationRepositoryCustomPropertyUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + client := meta.v3client + owner := meta.name propertyName := d.Get("property_name").(string) - tflog.Debug(ctx, "Updating organization custom property", map[string]any{"org": orgName, "property": propertyName}) + tflog.Debug(ctx, "Updating organization custom property", map[string]any{"org": owner, "property": propertyName}) - cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, orgName, propertyName, buildOrganizationRepositoryCustomProperty(d)) + cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, owner, propertyName, buildOrganizationRepositoryCustomProperty(d)) if err != nil { return diag.FromErr(fmt.Errorf("error updating organization custom property %q: %w", propertyName, err)) } - if err := setOrganizationRepositoryCustomPropertyState(d, cp); err != nil { + defaultValue, _ := cp.DefaultValueString() + d.SetId(cp.GetPropertyName()) + if err := d.Set("property_name", cp.GetPropertyName()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("value_type", string(cp.ValueType)); err != nil { + return diag.FromErr(err) + } + if err := d.Set("required", cp.GetRequired()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("default_value", defaultValue); err != nil { + return diag.FromErr(err) + } + if err := d.Set("description", cp.GetDescription()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("allowed_values", cp.AllowedValues); err != nil { + return diag.FromErr(err) + } + if err := d.Set("values_editable_by", cp.GetValuesEditableBy()); err != nil { return diag.FromErr(err) } return nil } -func resourceGithubOrganizationRepositoryCustomPropertyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - if err := checkOrganization(meta); err != nil { - return diag.FromErr(err) +func resourceGithubOrganizationRepositoryCustomPropertyDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + client := meta.v3client + owner := meta.name propertyName := d.Get("property_name").(string) - tflog.Debug(ctx, "Deleting organization custom property", map[string]any{"org": orgName, "property": propertyName}) + tflog.Debug(ctx, "Deleting organization custom property", map[string]any{"org": owner, "property": propertyName}) - if _, err := client.Organizations.RemoveCustomProperty(ctx, orgName, propertyName); err != nil { + if _, err := client.Organizations.RemoveCustomProperty(ctx, owner, propertyName); err != nil { if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { return nil } From 7ad83f84d87f1c0a8d3b37b553a5aeeb1d126cdd Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Tue, 11 Aug 2026 18:20:56 +0200 Subject: [PATCH 06/17] test: randomize property names and parallelize acceptance tests Addresses stevehipwell's feedback that the data-source test needs to work in an organization that already has custom properties, and applies the same fix to the resource acceptance test, which had the identical problem: every subtest used a hardcoded tf-acc-test-* property name with no t.Parallel(), so concurrent runs against one org (or a leftover from a failed run) would collide. Also switches the data-source test to create its fixture via mustCreateTestOrganizationRepositoryCustomProperty instead of a resource block in the TF config, per the pattern in #3476. --- ...ization_repository_custom_property_test.go | 23 ++-- ...ization_repository_custom_property_test.go | 115 +++++++++++------- 2 files changed, 85 insertions(+), 53 deletions(-) diff --git a/github/data_source_github_organization_repository_custom_property_test.go b/github/data_source_github_organization_repository_custom_property_test.go index 1279c2376b..b8207b4c8f 100644 --- a/github/data_source_github_organization_repository_custom_property_test.go +++ b/github/data_source_github_organization_repository_custom_property_test.go @@ -1,6 +1,7 @@ package github import ( + "fmt" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -11,19 +12,17 @@ import ( func TestAccGithubOrganizationRepositoryCustomPropertyDataSource(t *testing.T) { const dataAddr = "data.github_organization_repository_custom_property.test" + t.Parallel() - t.Run("reads a property created by the resource", func(t *testing.T) { - config := ` - resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-ds" - value_type = "single_select" - description = "tf-acc-test data source" - allowed_values = ["a", "b"] - } + t.Run("reads a property created by the fixture", func(t *testing.T) { + t.Parallel() - data "github_organization_repository_custom_property" "test" { - property_name = github_organization_repository_custom_property.test.property_name - }` + property := mustCreateTestOrganizationRepositoryCustomProperty(t, "single_select", []string{"a", "b"}) + config := fmt.Sprintf(` +data "github_organization_repository_custom_property" "test" { + property_name = %q +} +`, property.GetPropertyName()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -32,8 +31,8 @@ func TestAccGithubOrganizationRepositoryCustomPropertyDataSource(t *testing.T) { { Config: config, ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("property_name"), knownvalue.StringExact(property.GetPropertyName())), statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("value_type"), knownvalue.StringExact("single_select")), - statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("description"), knownvalue.StringExact("tf-acc-test data source")), statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("allowed_values"), knownvalue.ListExact([]knownvalue.Check{ knownvalue.StringExact("a"), knownvalue.StringExact("b"), diff --git a/github/resource_github_organization_repository_custom_property_test.go b/github/resource_github_organization_repository_custom_property_test.go index eb9fd77300..9665c1e043 100644 --- a/github/resource_github_organization_repository_custom_property_test.go +++ b/github/resource_github_organization_repository_custom_property_test.go @@ -5,6 +5,7 @@ import ( "regexp" "testing" + "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/plancheck" @@ -15,13 +16,18 @@ import ( func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { const resourceAddr = "github_organization_repository_custom_property.test" + t.Parallel() + t.Run("creates a string property without error", func(t *testing.T) { - config := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-string" + property_name = %[1]q value_type = "string" description = "tf-acc-test string property" - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -30,7 +36,7 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { { Config: config, ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("property_name"), knownvalue.StringExact("tf-acc-test-string")), + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("property_name"), knownvalue.StringExact(name)), statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("value_type"), knownvalue.StringExact("string")), statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_actors")), }, @@ -40,20 +46,23 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("creates a single_select property and grows allowed_values", func(t *testing.T) { - configBefore := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + configBefore := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-single-select" + property_name = %[1]q value_type = "single_select" description = "tf-acc-test single_select property" allowed_values = ["one"] - }` - configAfter := ` + }`, name) + configAfter := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-single-select" + property_name = %[1]q value_type = "single_select" description = "tf-acc-test single_select property updated" allowed_values = ["one", "two"] - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -82,12 +91,15 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("imports without error", func(t *testing.T) { - config := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-import" + property_name = %[1]q value_type = "string" description = "tf-acc-test import" - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -104,16 +116,20 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("forces new when property_name changes", func(t *testing.T) { - before := ` + t.Parallel() + + nameBefore := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + nameAfter := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + before := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-rename-a" + property_name = %[1]q value_type = "string" - }` - after := ` + }`, nameBefore) + after := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-rename-b" + property_name = %[1]q value_type = "string" - }` + }`, nameAfter) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -133,17 +149,20 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("forces new when value_type changes", func(t *testing.T) { - before := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + before := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-retype" + property_name = %[1]q value_type = "string" - }` - after := ` + }`, name) + after := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-retype" + property_name = %[1]q value_type = "single_select" allowed_values = ["x"] - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -163,12 +182,15 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("rejects allowed_values on string type", func(t *testing.T) { - config := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-invalid" + property_name = %[1]q value_type = "string" allowed_values = ["nope"] - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -183,11 +205,14 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("requires allowed_values on single_select type", func(t *testing.T) { - config := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-missing-allowed" + property_name = %[1]q value_type = "single_select" - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -202,12 +227,15 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("rejects invalid values_editable_by", func(t *testing.T) { - config := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-bad-editable" + property_name = %[1]q value_type = "string" values_editable_by = "nope" - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -222,18 +250,21 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("updates values_editable_by from org_actors to org_and_repo_actors", func(t *testing.T) { - before := ` + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + before := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-editable" + property_name = %[1]q value_type = "string" values_editable_by = "org_actors" - }` - after := ` + }`, name) + after := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { - property_name = "tf-acc-test-editable" + property_name = %[1]q value_type = "string" values_editable_by = "org_and_repo_actors" - }` + }`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -256,10 +287,12 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) t.Run("retains values_editable_by set out-of-band when omitted from config", func(t *testing.T) { + t.Parallel() + // Mirrors the upstream behaviour where a value set via the UI before // Terraform managed the property is reflected back into state via the // Computed attribute even when the config omits it. - propertyName := "tf-acc-test-ui-set" + propertyName := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) configWithField := fmt.Sprintf(` resource "github_organization_repository_custom_property" "test" { property_name = %[1]q From 5dd29f4f35e2340991223974716bb94a0e1a92c0 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Tue, 11 Aug 2026 18:21:27 +0200 Subject: [PATCH 07/17] docs: align templates and examples with the repository_custom_property pattern Copies the pattern from #3476 (github_repository_custom_property): - Rewrite both templates to be schema-driven ({{ .Description }}, subcategory, HasExamples/ExampleFiles, HasImport/HasImportIDConfig/HasImportIdentityConfig) instead of hardcoded front-matter prose, so the rendered docs can't drift from the resource/data-source Description fields again. - Move examples to the auto-discovered examples/{resources,data-sources}/ github_organization_repository_custom_property/ layout. Fold the three previously separately-headed usage examples (default, editable-by, boolean) into a single resource_1.tf with comment headers, per the single-example-file convention in examples.instructions.md. - Add import-by-string-id.tf for the modern `import` block syntax. - Regenerate docs/resources and docs/data-sources for this resource. --- ...organization_repository_custom_property.md | 3 +- ...organization_repository_custom_property.md | 36 ++++++++------ .../data-source_1.tf} | 0 .../import-by-string-id.tf | 4 ++ .../import.sh | 0 .../resource_1.tf | 29 ++++++++++++ .../example_1.tf | 12 ----- .../example_2.tf | 6 --- .../example_3.tf | 6 --- ...ization_repository_custom_property.md.tmpl | 12 +++-- ...ization_repository_custom_property.md.tmpl | 47 +++++++++++++------ 11 files changed, 99 insertions(+), 56 deletions(-) rename examples/data-sources/{organization_repository_custom_property/example_1.tf => github_organization_repository_custom_property/data-source_1.tf} (100%) create mode 100644 examples/resources/github_organization_repository_custom_property/import-by-string-id.tf rename examples/resources/{organization_repository_custom_property => github_organization_repository_custom_property}/import.sh (100%) create mode 100644 examples/resources/github_organization_repository_custom_property/resource_1.tf delete mode 100644 examples/resources/organization_repository_custom_property/example_1.tf delete mode 100644 examples/resources/organization_repository_custom_property/example_2.tf delete mode 100644 examples/resources/organization_repository_custom_property/example_3.tf diff --git a/docs/data-sources/organization_repository_custom_property.md b/docs/data-sources/organization_repository_custom_property.md index 61dceee182..9d2f251f6c 100644 --- a/docs/data-sources/organization_repository_custom_property.md +++ b/docs/data-sources/organization_repository_custom_property.md @@ -1,7 +1,8 @@ --- page_title: "github_organization_repository_custom_property (Data Source) - GitHub" +subcategory: "" description: |- - Looks up a GitHub organization custom property definition. + Looks up a single GitHub organization custom property definition by name. --- # github_organization_repository_custom_property (Data Source) diff --git a/docs/resources/organization_repository_custom_property.md b/docs/resources/organization_repository_custom_property.md index 4fbc78e0ab..2e780e5335 100644 --- a/docs/resources/organization_repository_custom_property.md +++ b/docs/resources/organization_repository_custom_property.md @@ -1,19 +1,22 @@ --- page_title: "github_organization_repository_custom_property (Resource) - GitHub" +subcategory: "" description: |- - Manages a GitHub organization custom property definition. + Manages a GitHub organization custom property definition. Custom properties defined here can later be assigned values on individual repositories. --- # github_organization_repository_custom_property (Resource) -Manages a single GitHub organization custom property definition. Repositories -in the organization can subsequently be tagged with values for this property -via the `github_repository_custom_property` resource or directly through the -GitHub UI / API. +Manages a GitHub organization custom property definition. Custom properties defined here can later be assigned values on individual repositories. +Repositories in the organization can subsequently be tagged with values for +this property via the [`github_repository_custom_property`](repository_custom_property) +resource or directly through the GitHub UI / API. For more information, see +the [GitHub API documentation](https://docs.github.com/rest/orgs/custom-properties). ## Example Usage ```terraform +# single_select property with a default value resource "github_organization_repository_custom_property" "environment" { property_name = "environment" value_type = "single_select" @@ -26,22 +29,16 @@ resource "github_organization_repository_custom_property" "environment" { "production", ] } -``` - -## Example Usage - Allow Repository Actors to Edit -```terraform +# string property that repository actors (not just org owners) can edit resource "github_organization_repository_custom_property" "team_contact" { property_name = "team_contact" value_type = "string" description = "Contact information for the team managing this repository" values_editable_by = "org_and_repo_actors" } -``` - -## Example Usage - Boolean Property -```terraform +# true_false property resource "github_organization_repository_custom_property" "archived" { property_name = "archived" value_type = "true_false" @@ -72,7 +69,18 @@ resource "github_organization_repository_custom_property" "archived" { ## Import -Organization custom properties can be imported using the property name: +Import is supported using the following syntax: + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +```terraform +import { + to = github_organization_repository_custom_property.environment + id = "environment" +} +``` + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: ```shell terraform import github_organization_repository_custom_property.environment environment diff --git a/examples/data-sources/organization_repository_custom_property/example_1.tf b/examples/data-sources/github_organization_repository_custom_property/data-source_1.tf similarity index 100% rename from examples/data-sources/organization_repository_custom_property/example_1.tf rename to examples/data-sources/github_organization_repository_custom_property/data-source_1.tf diff --git a/examples/resources/github_organization_repository_custom_property/import-by-string-id.tf b/examples/resources/github_organization_repository_custom_property/import-by-string-id.tf new file mode 100644 index 0000000000..06c001dd08 --- /dev/null +++ b/examples/resources/github_organization_repository_custom_property/import-by-string-id.tf @@ -0,0 +1,4 @@ +import { + to = github_organization_repository_custom_property.environment + id = "environment" +} diff --git a/examples/resources/organization_repository_custom_property/import.sh b/examples/resources/github_organization_repository_custom_property/import.sh similarity index 100% rename from examples/resources/organization_repository_custom_property/import.sh rename to examples/resources/github_organization_repository_custom_property/import.sh diff --git a/examples/resources/github_organization_repository_custom_property/resource_1.tf b/examples/resources/github_organization_repository_custom_property/resource_1.tf new file mode 100644 index 0000000000..29ef9955ca --- /dev/null +++ b/examples/resources/github_organization_repository_custom_property/resource_1.tf @@ -0,0 +1,29 @@ +# single_select property with a default value +resource "github_organization_repository_custom_property" "environment" { + property_name = "environment" + value_type = "single_select" + required = true + description = "The deployment environment for this repository" + default_value = "development" + allowed_values = [ + "development", + "staging", + "production", + ] +} + +# string property that repository actors (not just org owners) can edit +resource "github_organization_repository_custom_property" "team_contact" { + property_name = "team_contact" + value_type = "string" + description = "Contact information for the team managing this repository" + values_editable_by = "org_and_repo_actors" +} + +# true_false property +resource "github_organization_repository_custom_property" "archived" { + property_name = "archived" + value_type = "true_false" + description = "Whether this repository is archived" + default_value = "false" +} diff --git a/examples/resources/organization_repository_custom_property/example_1.tf b/examples/resources/organization_repository_custom_property/example_1.tf deleted file mode 100644 index 7ee5abdf01..0000000000 --- a/examples/resources/organization_repository_custom_property/example_1.tf +++ /dev/null @@ -1,12 +0,0 @@ -resource "github_organization_repository_custom_property" "environment" { - property_name = "environment" - value_type = "single_select" - required = true - description = "The deployment environment for this repository" - default_value = "development" - allowed_values = [ - "development", - "staging", - "production", - ] -} diff --git a/examples/resources/organization_repository_custom_property/example_2.tf b/examples/resources/organization_repository_custom_property/example_2.tf deleted file mode 100644 index 6543a9b57d..0000000000 --- a/examples/resources/organization_repository_custom_property/example_2.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "github_organization_repository_custom_property" "team_contact" { - property_name = "team_contact" - value_type = "string" - description = "Contact information for the team managing this repository" - values_editable_by = "org_and_repo_actors" -} diff --git a/examples/resources/organization_repository_custom_property/example_3.tf b/examples/resources/organization_repository_custom_property/example_3.tf deleted file mode 100644 index 4e2246162e..0000000000 --- a/examples/resources/organization_repository_custom_property/example_3.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "github_organization_repository_custom_property" "archived" { - property_name = "archived" - value_type = "true_false" - description = "Whether this repository is archived" - default_value = "false" -} diff --git a/templates/data-sources/organization_repository_custom_property.md.tmpl b/templates/data-sources/organization_repository_custom_property.md.tmpl index 3825d2a462..a725089cbe 100644 --- a/templates/data-sources/organization_repository_custom_property.md.tmpl +++ b/templates/data-sources/organization_repository_custom_property.md.tmpl @@ -1,15 +1,21 @@ --- page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +subcategory: "" description: |- - Looks up a GitHub organization custom property definition. +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) -Looks up a single GitHub organization custom property definition by name. +{{ .Description | trimspace }} +{{ if .HasExamples -}} ## Example Usage -{{ tffile "examples/data-sources/organization_repository_custom_property/example_1.tf" }} +{{- range .ExampleFiles }} + +{{ tffile . }} +{{- end }} +{{- end }} {{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/organization_repository_custom_property.md.tmpl b/templates/resources/organization_repository_custom_property.md.tmpl index 9be0841085..34a51368ae 100644 --- a/templates/resources/organization_repository_custom_property.md.tmpl +++ b/templates/resources/organization_repository_custom_property.md.tmpl @@ -1,32 +1,51 @@ --- page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +subcategory: "" description: |- - Manages a GitHub organization custom property definition. +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) -Manages a single GitHub organization custom property definition. Repositories -in the organization can subsequently be tagged with values for this property -via the `github_repository_custom_property` resource or directly through the -GitHub UI / API. +{{ .Description | trimspace }} +Repositories in the organization can subsequently be tagged with values for +this property via the [`github_repository_custom_property`](repository_custom_property) +resource or directly through the GitHub UI / API. For more information, see +the [GitHub API documentation](https://docs.github.com/rest/orgs/custom-properties). +{{ if .HasExamples -}} ## Example Usage -{{ tffile "examples/resources/organization_repository_custom_property/example_1.tf" }} +{{- range .ExampleFiles }} -## Example Usage - Allow Repository Actors to Edit +{{ tffile . }} +{{- end }} +{{- end }} -{{ tffile "examples/resources/organization_repository_custom_property/example_2.tf" }} +{{ .SchemaMarkdown | trimspace }} +{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} -## Example Usage - Boolean Property +## Import -{{ tffile "examples/resources/organization_repository_custom_property/example_3.tf" }} +Import is supported using the following syntax: +{{- end }} +{{- if .HasImportIdentityConfig }} -{{ .SchemaMarkdown | trimspace }} +In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute, for example: -## Import +{{tffile .ImportIdentityConfigFile }} + +{{ .IdentitySchemaMarkdown | trimspace }} +{{- end }} +{{- if .HasImportIDConfig }} + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +{{tffile .ImportIDConfigFile }} +{{- end }} +{{- if .HasImport }} -Organization custom properties can be imported using the property name: +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: -{{ codefile "shell" "examples/resources/organization_repository_custom_property/import.sh" }} +{{codefile "shell" .ImportFile }} +{{- end }} From b004e5b6b437e307b77c6906a32fe6c4a1ce55e6 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Tue, 11 Aug 2026 18:21:45 +0200 Subject: [PATCH 08/17] docs: track the new organization repository custom property in maintainer indexes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RESOURCES.md and ARCHITECTURE.md list every resource/data source and its deprecation status; both were missing the new github_organization_repository_custom_property resource and data source, and the (🚫) deprecation marker on github_organization_custom_properties. --- ARCHITECTURE.md | 2 ++ RESOURCES.md | 6 ++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f3684faa32..4af99015d9 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -580,6 +580,7 @@ The following resources are deprecated and will be removed in future versions: | `github_project_card` | None (Classic Projects API removed) | | `github_project_column` | None (Classic Projects API removed) | | `github_repository_project` | None (Classic Projects API removed) | +| `github_organization_custom_properties` | `github_organization_repository_custom_property` | ### Deprecated Data Sources @@ -587,6 +588,7 @@ The following resources are deprecated and will be removed in future versions: | ---------------------------------------------- | --------------------------------------------------- | | `github_organization_custom_role` | `github_organization_repository_role` | | `github_organization_security_managers` | `github_organization_role_teams` | +| `github_organization_custom_properties` | `github_organization_repository_custom_property` | | `github_repository_deployment_branch_policies` | `github_repository_environment_deployment_policies` | ### Known Limitations diff --git a/RESOURCES.md b/RESOURCES.md index b1b1c3e2b0..15696c6a1a 100644 --- a/RESOURCES.md +++ b/RESOURCES.md @@ -75,12 +75,13 @@ The overall status of each resource or data source is captured in this document | `github_membership` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | | `github_organization` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | | `github_organization_app_installations` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | -| `github_organization_custom_properties` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | +| `github_organization_custom_properties` (🚫) | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | | `github_organization_custom_role` (🚫) | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | | `github_organization_external_identities` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | | `github_organization_ip_allow_list` | ⚠️ | ✅ | ❓ | ❓ | ❓ | ❓ | | `github_organization_members` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_organization_repositories` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `github_organization_repository_custom_property` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_organization_repository_role` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_organization_repository_roles` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_organization_role` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | @@ -168,9 +169,10 @@ The overall status of each resource or data source is captured in this document | `github_issue_labels` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_membership` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_organization_block` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | -| `github_organization_custom_properties` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | +| `github_organization_custom_properties` (🚫) | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_organization_custom_role` (🚫) | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_organization_project` (🚫) | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | +| `github_organization_repository_custom_property` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_organization_repository_role` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_organization_role` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_organization_role_team` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | From f674439dae3e6950ec4c3f172ea166489ee319c8 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Wed, 12 Aug 2026 11:31:10 +0200 Subject: [PATCH 09/17] fix: support scalar and list default_value for all custom property types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit github.CustomProperty.DefaultValue is polymorphic: multi_select carries an array, true_false a stringified bool, and the rest plain strings. The code called only DefaultValueString() and discarded its ok bool, which returns ("", false) for both multi_select AND true_false, so: - true_false properties with a default_value failed apply outright with "Provider produced inconsistent result after apply" — the committed resource_1.tf example hit this. - multi_select list defaults could not be represented at all (#2806). default_value is now a TypeList of strings in both the resource and the data source, mirroring how #3476 models property_value on github_repository_custom_property. Requests send an array for multi_select and a bare string otherwise; responses are normalised by flattenOrganizationRepositoryCustomPropertyDefaultValue, which selects the matching go-github accessor per value_type and returns an error rather than silently yielding "" when a value cannot be parsed. CustomizeDiff rejects multi-element default_value for the four scalar types. Also addresses review feedback: - Timeouts block so users can configure per-operation timeouts. - Real import function seeding property_name from the ID, letting Read use the attribute getter instead of d.Id(). - diag.Errorf instead of diag.FromErr(fmt.Errorf(...)), the dominant idiom in this package. - Guard against the API returning an empty property name. - Soften the `required` description: GitHub's REST docs do not actually document that default_value is mandatory when required is true, so this is no longer stated as a hard rule. --- ...organization_repository_custom_property.go | 20 ++- ...organization_repository_custom_property.go | 133 +++++++++++++++--- 2 files changed, 126 insertions(+), 27 deletions(-) diff --git a/github/data_source_github_organization_repository_custom_property.go b/github/data_source_github_organization_repository_custom_property.go index 4cf42023df..f30f470937 100644 --- a/github/data_source_github_organization_repository_custom_property.go +++ b/github/data_source_github_organization_repository_custom_property.go @@ -3,7 +3,6 @@ package github import ( "context" "errors" - "fmt" "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -33,9 +32,10 @@ func dataSourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { Description: "Whether the custom property must be set on every repository.", }, "default_value": { - Type: schema.TypeString, + Type: schema.TypeList, Computed: true, - Description: "Default value applied to repositories that do not explicitly set the property.", + Description: "Default value applied to repositories that do not explicitly set the property. Holds multiple elements only when `value_type` is `multi_select`.", + Elem: &schema.Schema{Type: schema.TypeString}, }, "description": { Type: schema.TypeString, @@ -72,9 +72,13 @@ func dataSourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Contex cp, _, err := client.Organizations.GetCustomProperty(ctx, owner, propertyName) if err != nil { if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { - return diag.FromErr(fmt.Errorf("organization custom property %q not found in %q", propertyName, owner)) + return diag.Errorf("organization custom property %q not found in %q", propertyName, owner) } - return diag.FromErr(fmt.Errorf("error reading organization custom property %q: %w", propertyName, err)) + return diag.Errorf("error reading organization custom property %q: %v", propertyName, err) + } + + if cp.GetPropertyName() == "" { + return diag.Errorf("organization %q returned a custom property with an empty name when reading %q", owner, propertyName) } switch cp.ValueType { @@ -83,7 +87,11 @@ func dataSourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Contex cp.AllowedValues = nil } - defaultValue, _ := cp.DefaultValueString() + defaultValue, err := flattenOrganizationRepositoryCustomPropertyDefaultValue(cp) + if err != nil { + return diag.Errorf("error reading organization custom property %q: %v", propertyName, err) + } + d.SetId(cp.GetPropertyName()) if err := d.Set("property_name", cp.GetPropertyName()); err != nil { return diag.FromErr(err) diff --git a/github/resource_github_organization_repository_custom_property.go b/github/resource_github_organization_repository_custom_property.go index 9c5aa5eef3..f48741a0e8 100644 --- a/github/resource_github_organization_repository_custom_property.go +++ b/github/resource_github_organization_repository_custom_property.go @@ -4,6 +4,8 @@ import ( "context" "errors" "fmt" + "strconv" + "time" "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -32,11 +34,18 @@ func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { UpdateContext: resourceGithubOrganizationRepositoryCustomPropertyUpdate, DeleteContext: resourceGithubOrganizationRepositoryCustomPropertyDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubOrganizationRepositoryCustomPropertyImport, }, CustomizeDiff: customdiff.All(resourceGithubOrganizationRepositoryCustomPropertyDiff), + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + Schema: map[string]*schema.Schema{ "property_name": { Type: schema.TypeString, @@ -54,13 +63,14 @@ func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { "required": { Type: schema.TypeBool, Optional: true, - Description: "Whether the custom property must be set on every repository. When true, `default_value` must be provided.", + Description: "Whether the custom property must be set on every repository. GitHub may reject `required = true` unless a `default_value` is also provided.", }, "default_value": { - Type: schema.TypeString, + Type: schema.TypeList, Optional: true, Computed: true, - Description: "Default value applied to repositories that do not explicitly set the property.", + Description: "Default value applied to repositories that do not explicitly set the property. Exactly one element for the `string`, `single_select`, `true_false` and `url` types; one or more for `multi_select`.", + Elem: &schema.Schema{Type: schema.TypeString}, }, "description": { Type: schema.TypeString, @@ -87,20 +97,29 @@ func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { } func resourceGithubOrganizationRepositoryCustomPropertyDiff(ctx context.Context, d *schema.ResourceDiff, _ any) error { - if !d.NewValueKnown("value_type") || !d.NewValueKnown("allowed_values") { + if !d.NewValueKnown("value_type") { return nil } valueType := github.PropertyValueType(d.Get("value_type").(string)) - allowedValues, _ := d.Get("allowed_values").([]any) - selectType := valueType == github.PropertyValueTypeSingleSelect || valueType == github.PropertyValueTypeMultiSelect - if selectType && len(allowedValues) == 0 { - return fmt.Errorf("allowed_values is required when value_type is %q", valueType) + if d.NewValueKnown("allowed_values") { + allowedValues, _ := d.Get("allowed_values").([]any) + + if selectType && len(allowedValues) == 0 { + return fmt.Errorf("allowed_values is required when value_type is %q", valueType) + } + if !selectType && len(allowedValues) > 0 { + return fmt.Errorf("allowed_values must not be set when value_type is %q", valueType) + } } - if !selectType && len(allowedValues) > 0 { - return fmt.Errorf("allowed_values must not be set when value_type is %q", valueType) + + // Only multi_select accepts a list-valued default; every other type is scalar. + if d.NewValueKnown("default_value") && valueType != github.PropertyValueTypeMultiSelect { + if defaultValue, _ := d.Get("default_value").([]any); len(defaultValue) > 1 { + return fmt.Errorf("default_value must contain at most one element when value_type is %q, got %d", valueType, len(defaultValue)) + } } return nil @@ -120,8 +139,15 @@ func buildOrganizationRepositoryCustomProperty(d *schema.ResourceData) *github.C } if v, ok := d.GetOk("default_value"); ok { - s := v.(string) - cp.DefaultValue = &s + if defaultValue := expandStringList(v.([]any)); len(defaultValue) > 0 { + // Only multi_select sends an array; the other types send a bare string. + switch valueType { + case github.PropertyValueTypeMultiSelect: + cp.DefaultValue = defaultValue + default: + cp.DefaultValue = defaultValue[0] + } + } } if v, ok := d.GetOk("allowed_values"); ok { @@ -136,6 +162,33 @@ func buildOrganizationRepositoryCustomProperty(d *schema.ResourceData) *github.C return cp } +// flattenOrganizationRepositoryCustomPropertyDefaultValue normalises the +// polymorphic default_value returned by the API into a list of strings. The +// wire type depends on value_type: multi_select is an array, true_false is a +// stringified bool and the rest are plain strings. +func flattenOrganizationRepositoryCustomPropertyDefaultValue(cp *github.CustomProperty) ([]string, error) { + if cp.DefaultValue == nil { + return nil, nil + } + + switch cp.ValueType { + case github.PropertyValueTypeMultiSelect: + if v, ok := cp.DefaultValueStrings(); ok { + return v, nil + } + case github.PropertyValueTypeTrueFalse: + if v, ok := cp.DefaultValueBool(); ok { + return []string{strconv.FormatBool(v)}, nil + } + default: + if v, ok := cp.DefaultValueString(); ok { + return []string{v}, nil + } + } + + return nil, fmt.Errorf("default_value %#v could not be parsed for value_type %q", cp.DefaultValue, cp.ValueType) +} + func resourceGithubOrganizationRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { meta, _ := m.(*Owner) if ok, diags := checkOrganizationOK(meta); !ok { @@ -150,10 +203,18 @@ func resourceGithubOrganizationRepositoryCustomPropertyCreate(ctx context.Contex cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, owner, propertyName, buildOrganizationRepositoryCustomProperty(d)) if err != nil { - return diag.FromErr(fmt.Errorf("error creating organization custom property %q: %w", propertyName, err)) + return diag.Errorf("error creating organization custom property %q: %v", propertyName, err) + } + + if cp.GetPropertyName() == "" { + return diag.Errorf("organization %q returned a custom property with an empty name when creating %q", owner, propertyName) + } + + defaultValue, err := flattenOrganizationRepositoryCustomPropertyDefaultValue(cp) + if err != nil { + return diag.Errorf("error reading organization custom property %q: %v", propertyName, err) } - defaultValue, _ := cp.DefaultValueString() d.SetId(cp.GetPropertyName()) if err := d.Set("property_name", cp.GetPropertyName()); err != nil { return diag.FromErr(err) @@ -188,7 +249,7 @@ func resourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, client := meta.v3client owner := meta.name - propertyName := d.Id() + propertyName := d.Get("property_name").(string) cp, _, err := client.Organizations.GetCustomProperty(ctx, owner, propertyName) if err != nil { @@ -197,7 +258,11 @@ func resourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, d.SetId("") return nil } - return diag.FromErr(fmt.Errorf("error reading organization custom property %q: %w", propertyName, err)) + return diag.Errorf("error reading organization custom property %q: %v", propertyName, err) + } + + if cp.GetPropertyName() == "" { + return diag.Errorf("organization %q returned a custom property with an empty name when reading %q", owner, propertyName) } switch cp.ValueType { @@ -206,7 +271,11 @@ func resourceGithubOrganizationRepositoryCustomPropertyRead(ctx context.Context, cp.AllowedValues = nil } - defaultValue, _ := cp.DefaultValueString() + defaultValue, err := flattenOrganizationRepositoryCustomPropertyDefaultValue(cp) + if err != nil { + return diag.Errorf("error reading organization custom property %q: %v", propertyName, err) + } + d.SetId(cp.GetPropertyName()) if err := d.Set("property_name", cp.GetPropertyName()); err != nil { return diag.FromErr(err) @@ -247,10 +316,18 @@ func resourceGithubOrganizationRepositoryCustomPropertyUpdate(ctx context.Contex cp, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, owner, propertyName, buildOrganizationRepositoryCustomProperty(d)) if err != nil { - return diag.FromErr(fmt.Errorf("error updating organization custom property %q: %w", propertyName, err)) + return diag.Errorf("error updating organization custom property %q: %v", propertyName, err) + } + + if cp.GetPropertyName() == "" { + return diag.Errorf("organization %q returned a custom property with an empty name when updating %q", owner, propertyName) + } + + defaultValue, err := flattenOrganizationRepositoryCustomPropertyDefaultValue(cp) + if err != nil { + return diag.Errorf("error reading organization custom property %q: %v", propertyName, err) } - defaultValue, _ := cp.DefaultValueString() d.SetId(cp.GetPropertyName()) if err := d.Set("property_name", cp.GetPropertyName()); err != nil { return diag.FromErr(err) @@ -293,8 +370,22 @@ func resourceGithubOrganizationRepositoryCustomPropertyDelete(ctx context.Contex if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == 404 { return nil } - return diag.FromErr(fmt.Errorf("error deleting organization custom property %q: %w", propertyName, err)) + return diag.Errorf("error deleting organization custom property %q: %v", propertyName, err) } return nil } + +func resourceGithubOrganizationRepositoryCustomPropertyImport(ctx context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) { + propertyName := d.Id() + if propertyName == "" { + return nil, errors.New("custom property name must not be empty") + } + + // Read looks the property up by attribute, so seed it from the import ID. + if err := d.Set("property_name", propertyName); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} From dcaf0b69f0fb7e71d63db42c474387432f2b8dc6 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Wed, 12 Aug 2026 11:34:01 +0200 Subject: [PATCH 10/17] test: cover default_value round-trips, out-of-band deletion and update plans New subtests, closing gaps that let the default_value bug ship green: - true_false with a default_value, which previously failed apply with "Provider produced inconsistent result after apply". - multi_select with multiple default values, the #2806 scenario. - a scalar type rejecting a multi-element default_value. - out-of-band deletion via the API, asserting the next plan is a Create so the graceful-404 handling in Read cannot regress. - default_value is an empty list (not [""]) when the property has no default. Addresses deiga's review feedback: - Collapse every before/after config pair into one parameterised template string instead of duplicated configBefore/configAfter blocks. - Assert ResourceActionUpdate on the in-place update steps. --- ...ization_repository_custom_property_test.go | 3 + ...ization_repository_custom_property_test.go | 338 +++++++++++++----- 2 files changed, 248 insertions(+), 93 deletions(-) diff --git a/github/data_source_github_organization_repository_custom_property_test.go b/github/data_source_github_organization_repository_custom_property_test.go index b8207b4c8f..9783015149 100644 --- a/github/data_source_github_organization_repository_custom_property_test.go +++ b/github/data_source_github_organization_repository_custom_property_test.go @@ -37,6 +37,9 @@ data "github_organization_repository_custom_property" "test" { knownvalue.StringExact("a"), knownvalue.StringExact("b"), })), + // The fixture sets no default, so the list must come back empty + // rather than as a phantom single empty string. + statecheck.ExpectKnownValue(dataAddr, tfjsonpath.New("default_value"), knownvalue.ListSizeExact(0)), }, }, }, diff --git a/github/resource_github_organization_repository_custom_property_test.go b/github/resource_github_organization_repository_custom_property_test.go index 9665c1e043..75524f900d 100644 --- a/github/resource_github_organization_repository_custom_property_test.go +++ b/github/resource_github_organization_repository_custom_property_test.go @@ -23,11 +23,13 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) config := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - description = "tf-acc-test string property" - }`, name) +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + description = "tf-acc-test string property" + default_value = ["dev"] +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -39,7 +41,106 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("property_name"), knownvalue.StringExact(name)), statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("value_type"), knownvalue.StringExact("string")), statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_actors")), + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("default_value"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("dev"), + })), + }, + }, + }, + }) + }) + + t.Run("creates a true_false property with a default value", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "true_false" + description = "tf-acc-test true_false property" + default_value = [%%q] +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, "false"), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("default_value"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("false"), + })), + }, + }, + { + Config: fmt.Sprintf(config, "true"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("default_value"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("true"), + })), + }, + }, + { + ResourceName: resourceAddr, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("creates a multi_select property with multiple default values", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "multi_select" + description = "tf-acc-test multi_select property" + allowed_values = ["one", "two", "three"] + default_value = %%s +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, `["one", "two"]`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("default_value"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("one"), + knownvalue.StringExact("two"), + })), + }, + }, + { + Config: fmt.Sprintf(config, `["three"]`), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionUpdate), + }, }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("default_value"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("three"), + })), + }, + }, + { + ResourceName: resourceAddr, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -49,27 +150,21 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { t.Parallel() name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) - configBefore := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "single_select" - description = "tf-acc-test single_select property" - allowed_values = ["one"] - }`, name) - configAfter := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "single_select" - description = "tf-acc-test single_select property updated" - allowed_values = ["one", "two"] - }`, name) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "single_select" + description = "tf-acc-test single_select property %%[1]s" + allowed_values = %%[2]s +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: configBefore, + Config: fmt.Sprintf(config, "initial", `["one"]`), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("allowed_values"), knownvalue.ListExact([]knownvalue.Check{ knownvalue.StringExact("one"), @@ -77,7 +172,12 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }, }, { - Config: configAfter, + Config: fmt.Sprintf(config, "updated", `["one", "two"]`), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionUpdate), + }, + }, ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("allowed_values"), knownvalue.ListExact([]knownvalue.Check{ knownvalue.StringExact("one"), @@ -95,11 +195,12 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) config := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - description = "tf-acc-test import" - }`, name) +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + description = "tf-acc-test import" +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -115,29 +216,62 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) }) + t.Run("recreates a property deleted outside of terraform", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + description = "tf-acc-test out-of-band delete" +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + {Config: config}, + { + // Read must classify the resulting 404 as "gone" and drop the + // resource from state, so the next plan recreates it rather + // than erroring. + PreConfig: func() { + if _, err := testAccConf.meta.v3client.Organizations.RemoveCustomProperty(t.Context(), testAccConf.meta.name, name); err != nil { + t.Fatalf("failed to delete organization custom property %s out of band: %v", name, err) + } + }, + Config: config, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionCreate), + }, + }, + }, + }, + }) + }) + t.Run("forces new when property_name changes", func(t *testing.T) { t.Parallel() nameBefore := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) nameAfter := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) - before := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - }`, nameBefore) - after := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - }`, nameAfter) + config := ` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" +} +` resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ - {Config: before}, + {Config: fmt.Sprintf(config, nameBefore)}, { - Config: after, + Config: fmt.Sprintf(config, nameAfter), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionDestroyBeforeCreate), @@ -152,25 +286,20 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { t.Parallel() name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) - before := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - }`, name) - after := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "single_select" - allowed_values = ["x"] - }`, name) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + %%s +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ - {Config: before}, + {Config: fmt.Sprintf(config, `value_type = "string"`)}, { - Config: after, + Config: fmt.Sprintf(config, "value_type = \"single_select\"\n allowed_values = [\"x\"]"), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionDestroyBeforeCreate), @@ -186,11 +315,12 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) config := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - allowed_values = ["nope"] - }`, name) +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + allowed_values = ["nope"] +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -209,10 +339,11 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) config := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "single_select" - }`, name) +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "single_select" +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -226,16 +357,41 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { }) }) + t.Run("rejects multiple default_value entries on a scalar type", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + default_value = ["one", "two"] +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("default_value must contain at most one element"), + }, + }, + }) + }) + t.Run("rejects invalid values_editable_by", func(t *testing.T) { t.Parallel() name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) config := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - values_editable_by = "nope" - }`, name) +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + values_editable_by = "nope" +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, @@ -253,31 +409,31 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { t.Parallel() name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) - before := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - values_editable_by = "org_actors" - }`, name) - after := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - values_editable_by = "org_and_repo_actors" - }`, name) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + values_editable_by = %%q +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: before, + Config: fmt.Sprintf(config, "org_actors"), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_actors")), }, }, { - Config: after, + Config: fmt.Sprintf(config, "org_and_repo_actors"), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceAddr, plancheck.ResourceActionUpdate), + }, + }, ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), }, @@ -292,31 +448,27 @@ func TestAccGithubOrganizationRepositoryCustomProperty(t *testing.T) { // Mirrors the upstream behaviour where a value set via the UI before // Terraform managed the property is reflected back into state via the // Computed attribute even when the config omits it. - propertyName := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) - configWithField := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - values_editable_by = "org_and_repo_actors" - }`, propertyName) - configWithoutField := fmt.Sprintf(` - resource "github_organization_repository_custom_property" "test" { - property_name = %[1]q - value_type = "string" - }`, propertyName) + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + %%s +} +`, name) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: configWithField, + Config: fmt.Sprintf(config, `values_editable_by = "org_and_repo_actors"`), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), }, }, { - Config: configWithoutField, + Config: fmt.Sprintf(config, ""), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("values_editable_by"), knownvalue.StringExact("org_and_repo_actors")), }, From ea63aa1a757a998c2454e0053cd52f97bad94547 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Wed, 12 Aug 2026 11:35:06 +0200 Subject: [PATCH 11/17] docs: regenerate for list-shaped default_value and the timeouts block Updates the resource example to the new default_value list syntax, adds a multi_select example demonstrating multiple default values (#2806), and regenerates both pages so the schema tables and the new timeouts block match the code. --- ...organization_repository_custom_property.md | 2 +- ...organization_repository_custom_property.md | 28 ++++++++++++++++--- .../resource_1.tf | 13 +++++++-- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/docs/data-sources/organization_repository_custom_property.md b/docs/data-sources/organization_repository_custom_property.md index 9d2f251f6c..d1a2494c89 100644 --- a/docs/data-sources/organization_repository_custom_property.md +++ b/docs/data-sources/organization_repository_custom_property.md @@ -27,7 +27,7 @@ data "github_organization_repository_custom_property" "environment" { ### Read-Only - `allowed_values` (List of String) Allowed values when `value_type` is `single_select` or `multi_select`. -- `default_value` (String) Default value applied to repositories that do not explicitly set the property. +- `default_value` (List of String) Default value applied to repositories that do not explicitly set the property. Holds multiple elements only when `value_type` is `multi_select`. - `description` (String) Short description of the custom property. - `id` (String) The ID of this resource. - `required` (Boolean) Whether the custom property must be set on every repository. diff --git a/docs/resources/organization_repository_custom_property.md b/docs/resources/organization_repository_custom_property.md index 2e780e5335..ec89a1fb4a 100644 --- a/docs/resources/organization_repository_custom_property.md +++ b/docs/resources/organization_repository_custom_property.md @@ -22,7 +22,7 @@ resource "github_organization_repository_custom_property" "environment" { value_type = "single_select" required = true description = "The deployment environment for this repository" - default_value = "development" + default_value = ["development"] allowed_values = [ "development", "staging", @@ -43,7 +43,16 @@ resource "github_organization_repository_custom_property" "archived" { property_name = "archived" value_type = "true_false" description = "Whether this repository is archived" - default_value = "false" + default_value = ["false"] +} + +# multi_select property; only this type accepts more than one default value +resource "github_organization_repository_custom_property" "compliance" { + property_name = "compliance" + value_type = "multi_select" + description = "Compliance regimes this repository is in scope for" + allowed_values = ["pci", "sox", "hipaa"] + default_value = ["pci", "sox"] } ``` @@ -58,15 +67,26 @@ resource "github_organization_repository_custom_property" "archived" { ### Optional - `allowed_values` (List of String) Allowed values for `single_select` and `multi_select` property types. Must be omitted for other types. -- `default_value` (String) Default value applied to repositories that do not explicitly set the property. +- `default_value` (List of String) Default value applied to repositories that do not explicitly set the property. Exactly one element for the `string`, `single_select`, `true_false` and `url` types; one or more for `multi_select`. - `description` (String) Short description of the custom property. -- `required` (Boolean) Whether the custom property must be set on every repository. When true, `default_value` must be provided. +- `required` (Boolean) Whether the custom property must be set on every repository. GitHub may reject `required = true` unless a `default_value` is also provided. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) - `values_editable_by` (String) Who can edit values of this property on repositories. One of: [org_actors org_and_repo_actors]. Defaults to `org_actors` server-side. ### Read-Only - `id` (String) The ID of this resource. + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) +- `read` (String) +- `update` (String) + ## Import Import is supported using the following syntax: diff --git a/examples/resources/github_organization_repository_custom_property/resource_1.tf b/examples/resources/github_organization_repository_custom_property/resource_1.tf index 29ef9955ca..cac1b12f9a 100644 --- a/examples/resources/github_organization_repository_custom_property/resource_1.tf +++ b/examples/resources/github_organization_repository_custom_property/resource_1.tf @@ -4,7 +4,7 @@ resource "github_organization_repository_custom_property" "environment" { value_type = "single_select" required = true description = "The deployment environment for this repository" - default_value = "development" + default_value = ["development"] allowed_values = [ "development", "staging", @@ -25,5 +25,14 @@ resource "github_organization_repository_custom_property" "archived" { property_name = "archived" value_type = "true_false" description = "Whether this repository is archived" - default_value = "false" + default_value = ["false"] +} + +# multi_select property; only this type accepts more than one default value +resource "github_organization_repository_custom_property" "compliance" { + property_name = "compliance" + value_type = "multi_select" + description = "Compliance regimes this repository is in scope for" + allowed_values = ["pci", "sox", "hipaa"] + default_value = ["pci", "sox"] } From e98800edad566b98688ed1e65cc13513db1e4860 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Wed, 12 Aug 2026 12:19:08 +0200 Subject: [PATCH 12/17] refactor: move the custom property flatten helper to a domain util file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flattenOrganizationRepositoryCustomPropertyDefaultValue is used by both the resource and the data source, so per review feedback it moves out of the resource file into github/util_custom_property.go, following the util_.go convention in ARCHITECTURE.md. buildOrganizationRepositoryCustomProperty and the two value-type var blocks stay put — they are only used by the resource. --- ...organization_repository_custom_property.go | 28 --------------- github/util_custom_property.go | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 28 deletions(-) create mode 100644 github/util_custom_property.go diff --git a/github/resource_github_organization_repository_custom_property.go b/github/resource_github_organization_repository_custom_property.go index f48741a0e8..91d88ec192 100644 --- a/github/resource_github_organization_repository_custom_property.go +++ b/github/resource_github_organization_repository_custom_property.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "strconv" "time" "github.com/google/go-github/v89/github" @@ -162,33 +161,6 @@ func buildOrganizationRepositoryCustomProperty(d *schema.ResourceData) *github.C return cp } -// flattenOrganizationRepositoryCustomPropertyDefaultValue normalises the -// polymorphic default_value returned by the API into a list of strings. The -// wire type depends on value_type: multi_select is an array, true_false is a -// stringified bool and the rest are plain strings. -func flattenOrganizationRepositoryCustomPropertyDefaultValue(cp *github.CustomProperty) ([]string, error) { - if cp.DefaultValue == nil { - return nil, nil - } - - switch cp.ValueType { - case github.PropertyValueTypeMultiSelect: - if v, ok := cp.DefaultValueStrings(); ok { - return v, nil - } - case github.PropertyValueTypeTrueFalse: - if v, ok := cp.DefaultValueBool(); ok { - return []string{strconv.FormatBool(v)}, nil - } - default: - if v, ok := cp.DefaultValueString(); ok { - return []string{v}, nil - } - } - - return nil, fmt.Errorf("default_value %#v could not be parsed for value_type %q", cp.DefaultValue, cp.ValueType) -} - func resourceGithubOrganizationRepositoryCustomPropertyCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { meta, _ := m.(*Owner) if ok, diags := checkOrganizationOK(meta); !ok { diff --git a/github/util_custom_property.go b/github/util_custom_property.go new file mode 100644 index 0000000000..3c2a73bb92 --- /dev/null +++ b/github/util_custom_property.go @@ -0,0 +1,35 @@ +package github + +import ( + "fmt" + "strconv" + + "github.com/google/go-github/v89/github" +) + +// flattenOrganizationRepositoryCustomPropertyDefaultValue normalises the +// polymorphic default_value returned by the API into a list of strings. The +// wire type depends on value_type: multi_select is an array, true_false is a +// stringified bool and the rest are plain strings. +func flattenOrganizationRepositoryCustomPropertyDefaultValue(cp *github.CustomProperty) ([]string, error) { + if cp.DefaultValue == nil { + return nil, nil + } + + switch cp.ValueType { + case github.PropertyValueTypeMultiSelect: + if v, ok := cp.DefaultValueStrings(); ok { + return v, nil + } + case github.PropertyValueTypeTrueFalse: + if v, ok := cp.DefaultValueBool(); ok { + return []string{strconv.FormatBool(v)}, nil + } + default: + if v, ok := cp.DefaultValueString(); ok { + return []string{v}, nil + } + } + + return nil, fmt.Errorf("default_value %#v could not be parsed for value_type %q", cp.DefaultValue, cp.ValueType) +} From 6a978bab3b2ff1625362d555331165d68881fa90 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Wed, 12 Aug 2026 12:20:26 +0200 Subject: [PATCH 13/17] docs: move the cross-reference note from the template into the example Per review feedback the narrative note doesn't belong in the doc template. The pointer to github_repository_custom_property and the REST API link move into a top-of-file comment in resource_1.tf, which examples.instructions.md names as the place for extra context. The template now holds nothing beyond the repo-standard front matter. --- docs/resources/organization_repository_custom_property.md | 8 ++++---- .../resource_1.tf | 4 ++++ .../organization_repository_custom_property.md.tmpl | 4 ---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/resources/organization_repository_custom_property.md b/docs/resources/organization_repository_custom_property.md index ec89a1fb4a..b4143651a7 100644 --- a/docs/resources/organization_repository_custom_property.md +++ b/docs/resources/organization_repository_custom_property.md @@ -8,14 +8,14 @@ description: |- # github_organization_repository_custom_property (Resource) Manages a GitHub organization custom property definition. Custom properties defined here can later be assigned values on individual repositories. -Repositories in the organization can subsequently be tagged with values for -this property via the [`github_repository_custom_property`](repository_custom_property) -resource or directly through the GitHub UI / API. For more information, see -the [GitHub API documentation](https://docs.github.com/rest/orgs/custom-properties). ## Example Usage ```terraform +# This resource defines the property itself at the organization level. To set a +# value for it on an individual repository, use `github_repository_custom_property`. +# See https://docs.github.com/rest/orgs/custom-properties for the underlying API. + # single_select property with a default value resource "github_organization_repository_custom_property" "environment" { property_name = "environment" diff --git a/examples/resources/github_organization_repository_custom_property/resource_1.tf b/examples/resources/github_organization_repository_custom_property/resource_1.tf index cac1b12f9a..da38bc68ba 100644 --- a/examples/resources/github_organization_repository_custom_property/resource_1.tf +++ b/examples/resources/github_organization_repository_custom_property/resource_1.tf @@ -1,3 +1,7 @@ +# This resource defines the property itself at the organization level. To set a +# value for it on an individual repository, use `github_repository_custom_property`. +# See https://docs.github.com/rest/orgs/custom-properties for the underlying API. + # single_select property with a default value resource "github_organization_repository_custom_property" "environment" { property_name = "environment" diff --git a/templates/resources/organization_repository_custom_property.md.tmpl b/templates/resources/organization_repository_custom_property.md.tmpl index 34a51368ae..759f56a64d 100644 --- a/templates/resources/organization_repository_custom_property.md.tmpl +++ b/templates/resources/organization_repository_custom_property.md.tmpl @@ -8,10 +8,6 @@ description: |- # {{.Name}} ({{.Type}}) {{ .Description | trimspace }} -Repositories in the organization can subsequently be tagged with values for -this property via the [`github_repository_custom_property`](repository_custom_property) -resource or directly through the GitHub UI / API. For more information, see -the [GitHub API documentation](https://docs.github.com/rest/orgs/custom-properties). {{ if .HasExamples -}} ## Example Usage From 0c05a029ff4ac9a5f27c3ad1edd01aa686537c46 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Thu, 13 Aug 2026 11:14:59 +0200 Subject: [PATCH 14/17] refactor: move the repository custom property value parser to the util file parseRepositoryCustomPropertyValueToStringSlice is shared by data_source_github_repository_custom_properties.go and resource_github_repository_custom_property.go, so it joins the other custom property helper in util_custom_property.go rather than living in a data source file. Requested in review. --- ...ta_source_github_repository_custom_properties.go | 12 ------------ github/util_custom_property.go | 13 +++++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/github/data_source_github_repository_custom_properties.go b/github/data_source_github_repository_custom_properties.go index 42c69403ca..e0a5d2e15d 100644 --- a/github/data_source_github_repository_custom_properties.go +++ b/github/data_source_github_repository_custom_properties.go @@ -2,7 +2,6 @@ package github import ( "context" - "fmt" "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" @@ -88,14 +87,3 @@ func flattenRepositoryCustomProperties(customProperties []*github.CustomProperty return results, nil } - -func parseRepositoryCustomPropertyValueToStringSlice(prop *github.CustomPropertyValue) ([]string, error) { - switch value := prop.Value.(type) { - case string: - return []string{value}, nil - case []string: - return value, nil - default: - return nil, fmt.Errorf("custom property value couldn't be parsed as a string or a list of strings: %s", value) - } -} diff --git a/github/util_custom_property.go b/github/util_custom_property.go index 3c2a73bb92..b7ede11ced 100644 --- a/github/util_custom_property.go +++ b/github/util_custom_property.go @@ -33,3 +33,16 @@ func flattenOrganizationRepositoryCustomPropertyDefaultValue(cp *github.CustomPr return nil, fmt.Errorf("default_value %#v could not be parsed for value_type %q", cp.DefaultValue, cp.ValueType) } + +// parseRepositoryCustomPropertyValueToStringSlice normalises the polymorphic +// value of a custom property set on a repository into a list of strings. +func parseRepositoryCustomPropertyValueToStringSlice(prop *github.CustomPropertyValue) ([]string, error) { + switch value := prop.Value.(type) { + case string: + return []string{value}, nil + case []string: + return value, nil + default: + return nil, fmt.Errorf("custom property value couldn't be parsed as a string or a list of strings: %s", value) + } +} From 468d8e83b1d7c947041d3196b18de2dae13a8509 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Thu, 13 Aug 2026 11:17:05 +0200 Subject: [PATCH 15/17] fix: make custom property plan-time validation actually reject bad input Four schema/diff correctness fixes from review: - allowed_values is no longer Computed. An omitted Optional+Computed list is unknown during plan (schemaMap.diffList marks the count NewComputed when both old and new lengths are 0), so d.NewValueKnown returned false and the cross-field validation skipped itself -- meaning "allowed_values is required when value_type is single_select" could never fire and the request reached the API without values. Nothing needed the Computed behaviour: select types always set the field in config, and Read clears it for the other types. - Reject empty strings in allowed_values and default_value elements. expandStringList silently drops "", so allowed_values = [""] passed the length check and became an empty API list, and default_value = [""] became no default at all while the config said otherwise. - Constrain true_false defaults to exactly "true"/"false". strconv.ParseBool accepts "True" and "1", which the read path would normalise back to "true" and fail the apply with an inconsistent-result error. - Reject an empty property_name on both the resource and the data source rather than building an invalid API path. Also documents that a default_value cannot be removed once set: the API field is omitted when nil rather than sent as JSON null, so GitHub keeps the old value. --- ...organization_repository_custom_property.go | 8 ++-- ...organization_repository_custom_property.go | 47 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/github/data_source_github_organization_repository_custom_property.go b/github/data_source_github_organization_repository_custom_property.go index f30f470937..defc05d6e3 100644 --- a/github/data_source_github_organization_repository_custom_property.go +++ b/github/data_source_github_organization_repository_custom_property.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func dataSourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { @@ -17,9 +18,10 @@ func dataSourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { Schema: map[string]*schema.Schema{ "property_name": { - Type: schema.TypeString, - Required: true, - Description: "Name of the custom property to look up.", + Type: schema.TypeString, + Required: true, + Description: "Name of the custom property to look up.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, "value_type": { Type: schema.TypeString, diff --git a/github/resource_github_organization_repository_custom_property.go b/github/resource_github_organization_repository_custom_property.go index 91d88ec192..5594838d2a 100644 --- a/github/resource_github_organization_repository_custom_property.go +++ b/github/resource_github_organization_repository_custom_property.go @@ -47,10 +47,11 @@ func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { Schema: map[string]*schema.Schema{ "property_name": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - Description: "Name of the custom property.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the custom property.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), }, "value_type": { Type: schema.TypeString, @@ -68,8 +69,11 @@ func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { Type: schema.TypeList, Optional: true, Computed: true, - Description: "Default value applied to repositories that do not explicitly set the property. Exactly one element for the `string`, `single_select`, `true_false` and `url` types; one or more for `multi_select`.", - Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Default value applied to repositories that do not explicitly set the property. Exactly one element for the `string`, `single_select`, `true_false` and `url` types; one or more for `multi_select`. Once set, a default cannot be removed via the API, only changed.", + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), + }, }, "description": { Type: schema.TypeString, @@ -77,12 +81,19 @@ func resourceGithubOrganizationRepositoryCustomProperty() *schema.Resource { Computed: true, Description: "Short description of the custom property.", }, + // Deliberately not Computed: an omitted Optional+Computed list is + // unknown at plan time, which would make the cross-field validation + // in CustomizeDiff silently skip itself. Nothing needs to be read + // back here either -- select types always set it in config, and Read + // clears it for the other types. "allowed_values": { Type: schema.TypeList, Optional: true, - Computed: true, Description: "Allowed values for `single_select` and `multi_select` property types. Must be omitted for other types.", - Elem: &schema.Schema{Type: schema.TypeString}, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotEmpty), + }, }, "values_editable_by": { Type: schema.TypeString, @@ -114,11 +125,25 @@ func resourceGithubOrganizationRepositoryCustomPropertyDiff(ctx context.Context, } } - // Only multi_select accepts a list-valued default; every other type is scalar. - if d.NewValueKnown("default_value") && valueType != github.PropertyValueTypeMultiSelect { - if defaultValue, _ := d.Get("default_value").([]any); len(defaultValue) > 1 { + if d.NewValueKnown("default_value") { + defaultValue, _ := d.Get("default_value").([]any) + + // Only multi_select accepts a list-valued default; every other type is scalar. + if valueType != github.PropertyValueTypeMultiSelect && len(defaultValue) > 1 { return fmt.Errorf("default_value must contain at most one element when value_type is %q, got %d", valueType, len(defaultValue)) } + + // GitHub stores true_false defaults as the strings "true"/"false". Reject + // anything else here: strconv.ParseBool would accept "True" or "1" and the + // read path would then normalise it to a different string than the config, + // failing the apply with an inconsistent-result error. + if valueType == github.PropertyValueTypeTrueFalse { + for _, v := range defaultValue { + if s, _ := v.(string); s != "true" && s != "false" { + return fmt.Errorf("default_value must be %q or %q when value_type is %q, got %q", "true", "false", valueType, s) + } + } + } } return nil From 255b43e77deb2eaf2fa1c299638b4c1b472011ac Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Thu, 13 Aug 2026 11:18:16 +0200 Subject: [PATCH 16/17] test: cover url defaults, delete-of-missing-property and the new validations - url subtest with a scalar default and import, so all five value_types are genuinely exercised as the PR claims. - Destroy step whose PreConfig removes the property out of band, covering the 404-as-success branch in Delete. The existing recreate test always recreates the property first, so that branch was never reached. - Plan-only subtests for the two new rules: a non-boolean true_false default ("True", which strconv.ParseBool would otherwise accept and silently normalise) and an empty string in allowed_values. --- ...ization_repository_custom_property_test.go | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/github/resource_github_organization_repository_custom_property_test.go b/github/resource_github_organization_repository_custom_property_test.go index 75524f900d..478ffe89be 100644 --- a/github/resource_github_organization_repository_custom_property_test.go +++ b/github/resource_github_organization_repository_custom_property_test.go @@ -253,6 +253,122 @@ resource "github_organization_repository_custom_property" "test" { }) }) + t.Run("destroys cleanly when the property is already gone", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "string" + description = "tf-acc-test delete of a missing property" +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + {Config: config}, + { + // Delete must treat a 404 as success. Removing the property + // out of band immediately before destroy exercises that branch, + // which the recreate test above never reaches. + PreConfig: func() { + if _, err := testAccConf.meta.v3client.Organizations.RemoveCustomProperty(t.Context(), testAccConf.meta.name, name); err != nil { + t.Fatalf("failed to delete organization custom property %s out of band: %v", name, err) + } + }, + Config: config, + Destroy: true, + }, + }, + }) + }) + + t.Run("creates a url property with a default value", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "url" + description = "tf-acc-test url property" + default_value = ["https://example.com/runbook"] +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("value_type"), knownvalue.StringExact("url")), + statecheck.ExpectKnownValue(resourceAddr, tfjsonpath.New("default_value"), knownvalue.ListExact([]knownvalue.Check{ + knownvalue.StringExact("https://example.com/runbook"), + })), + }, + }, + { + ResourceName: resourceAddr, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) + + t.Run("rejects a non-boolean default_value on true_false", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "true_false" + default_value = ["True"] +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile(`default_value must be "true" or "false"`), + }, + }, + }) + }) + + t.Run("rejects an empty string in allowed_values", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(testRandomIDLength)) + config := fmt.Sprintf(` +resource "github_organization_repository_custom_property" "test" { + property_name = %[1]q + value_type = "single_select" + allowed_values = [""] +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("expected .* to not be an empty string"), + }, + }, + }) + }) + t.Run("forces new when property_name changes", func(t *testing.T) { t.Parallel() From d4427c07c95717b044eba7e8363bbb3e78027662 Mon Sep 17 00:00:00 2001 From: Misha Kushakov Date: Thu, 13 Aug 2026 11:19:31 +0200 Subject: [PATCH 17/17] docs: drop the redundant per-resource templates and regenerate Both templates were byte-for-byte identical to the repo-level fallbacks at templates/resources.md.tmpl and templates/data-sources.md.tmpl, which tfplugindocs uses when no per-resource template exists. Verified the generated pages are unchanged with the templates removed. Also picks up the default_value description note from the previous commit. --- ...organization_repository_custom_property.md | 2 +- ...ization_repository_custom_property.md.tmpl | 21 --------- ...ization_repository_custom_property.md.tmpl | 47 ------------------- 3 files changed, 1 insertion(+), 69 deletions(-) delete mode 100644 templates/data-sources/organization_repository_custom_property.md.tmpl delete mode 100644 templates/resources/organization_repository_custom_property.md.tmpl diff --git a/docs/resources/organization_repository_custom_property.md b/docs/resources/organization_repository_custom_property.md index b4143651a7..70611c19be 100644 --- a/docs/resources/organization_repository_custom_property.md +++ b/docs/resources/organization_repository_custom_property.md @@ -67,7 +67,7 @@ resource "github_organization_repository_custom_property" "compliance" { ### Optional - `allowed_values` (List of String) Allowed values for `single_select` and `multi_select` property types. Must be omitted for other types. -- `default_value` (List of String) Default value applied to repositories that do not explicitly set the property. Exactly one element for the `string`, `single_select`, `true_false` and `url` types; one or more for `multi_select`. +- `default_value` (List of String) Default value applied to repositories that do not explicitly set the property. Exactly one element for the `string`, `single_select`, `true_false` and `url` types; one or more for `multi_select`. Once set, a default cannot be removed via the API, only changed. - `description` (String) Short description of the custom property. - `required` (Boolean) Whether the custom property must be set on every repository. GitHub may reject `required = true` unless a `default_value` is also provided. - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) diff --git a/templates/data-sources/organization_repository_custom_property.md.tmpl b/templates/data-sources/organization_repository_custom_property.md.tmpl deleted file mode 100644 index a725089cbe..0000000000 --- a/templates/data-sources/organization_repository_custom_property.md.tmpl +++ /dev/null @@ -1,21 +0,0 @@ ---- -page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" -subcategory: "" -description: |- -{{ .Description | plainmarkdown | trimspace | prefixlines " " }} ---- - -# {{.Name}} ({{.Type}}) - -{{ .Description | trimspace }} - -{{ if .HasExamples -}} -## Example Usage - -{{- range .ExampleFiles }} - -{{ tffile . }} -{{- end }} -{{- end }} - -{{ .SchemaMarkdown | trimspace }} diff --git a/templates/resources/organization_repository_custom_property.md.tmpl b/templates/resources/organization_repository_custom_property.md.tmpl deleted file mode 100644 index 759f56a64d..0000000000 --- a/templates/resources/organization_repository_custom_property.md.tmpl +++ /dev/null @@ -1,47 +0,0 @@ ---- -page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" -subcategory: "" -description: |- -{{ .Description | plainmarkdown | trimspace | prefixlines " " }} ---- - -# {{.Name}} ({{.Type}}) - -{{ .Description | trimspace }} - -{{ if .HasExamples -}} -## Example Usage - -{{- range .ExampleFiles }} - -{{ tffile . }} -{{- end }} -{{- end }} - -{{ .SchemaMarkdown | trimspace }} -{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} - -## Import - -Import is supported using the following syntax: -{{- end }} -{{- if .HasImportIdentityConfig }} - -In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute, for example: - -{{tffile .ImportIdentityConfigFile }} - -{{ .IdentitySchemaMarkdown | trimspace }} -{{- end }} -{{- if .HasImportIDConfig }} - -In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: - -{{tffile .ImportIDConfigFile }} -{{- end }} -{{- if .HasImport }} - -The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: - -{{codefile "shell" .ImportFile }} -{{- end }}