From a75de62f901347ee470ca01b29178767f792d56d Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer Date: Thu, 23 Jul 2026 14:24:37 +0200 Subject: [PATCH 1/5] fix(resource_github_organization_custom_properties): support bool default values --- .../organization_custom_properties.md | 2 +- ...e_github_organization_custom_properties.go | 16 +++++++-- ...hub_organization_custom_properties_test.go | 36 +++++++++++++++++++ .../organization_custom_properties.md.tmpl | 2 +- 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/docs/resources/organization_custom_properties.md b/docs/resources/organization_custom_properties.md index f27d8af532..ee21ea4444 100644 --- a/docs/resources/organization_custom_properties.md +++ b/docs/resources/organization_custom_properties.md @@ -76,7 +76,7 @@ The following arguments are supported: - `description` - (Optional) The description of the custom property. -- `default_value` - (Optional) The default value of the custom property. +- `default_value` - (Optional) The default value of the custom property. Not supported for `multi_select` properties. - `allowed_values` - (Optional) List of allowed values for the custom property. Only applicable when `value_type` is `single_select` or `multi_select`. diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index 84916ab879..f8fae879d0 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -2,6 +2,7 @@ package github import ( "context" + "strconv" "github.com/google/go-github/v89/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" @@ -121,8 +122,19 @@ func resourceGithubCustomPropertiesRead(d *schema.ResourceData, meta any) error return err } - // TODO: Add support for other types of default values - defaultValue, _ := customProperty.DefaultValueString() + // multi_select is not supported: its default value is a []string, which + // cannot round-trip through the TypeString default_value attribute. + var defaultValue string + switch customProperty.ValueType { + case github.PropertyValueTypeTrueFalse: + if b, ok := customProperty.DefaultValueBool(); ok { + defaultValue = strconv.FormatBool(b) + } + default: + if s, ok := customProperty.DefaultValueString(); ok { + defaultValue = s + } + } d.SetId(*customProperty.PropertyName) _ = d.Set("allowed_values", customProperty.AllowedValues) diff --git a/github/resource_github_organization_custom_properties_test.go b/github/resource_github_organization_custom_properties_test.go index 1e7ee1024c..02e523d0c1 100644 --- a/github/resource_github_organization_custom_properties_test.go +++ b/github/resource_github_organization_custom_properties_test.go @@ -265,6 +265,42 @@ resource "github_organization_custom_properties" "test" { }) }) + t.Run("true_false property with default_value produces no drift", func(t *testing.T) { + t.Parallel() + + name := fmt.Sprintf("%s%s", testResourcePrefix, acctest.RandString(5)) + + config := fmt.Sprintf(` +resource "github_organization_custom_properties" "test" { + property_name = "%s" + value_type = "true_false" + required = false + description = "Test true_false default_value" + default_value = "true" +} +`, name) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_organization_custom_properties.test", "default_value", "true"), + ), + }, + { + // Read must round-trip the true_false default_value so the + // second plan is empty (regression guard for perpetual drift). + Config: config, + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) + }) + t.Run("imports existing property with values_editable_by set via UI", func(t *testing.T) { t.Parallel() diff --git a/templates/resources/organization_custom_properties.md.tmpl b/templates/resources/organization_custom_properties.md.tmpl index a65ecca029..7171ab7166 100644 --- a/templates/resources/organization_custom_properties.md.tmpl +++ b/templates/resources/organization_custom_properties.md.tmpl @@ -40,7 +40,7 @@ The following arguments are supported: - `description` - (Optional) The description of the custom property. -- `default_value` - (Optional) The default value of the custom property. +- `default_value` - (Optional) The default value of the custom property. Not supported for `multi_select` properties. - `allowed_values` - (Optional) List of allowed values for the custom property. Only applicable when `value_type` is `single_select` or `multi_select`. From 31134d9852ea3319ba5209c32a644d63bcec5bb2 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer Date: Wed, 29 Jul 2026 15:08:42 +0200 Subject: [PATCH 2/5] test/docs: address review feedback Use ConfigStateChecks/ExpectKnownValue for the true_false default_value assertion instead of the legacy TestCheckResourceAttr Check, and note the multi_select limitation in the default_value schema Description. Co-Authored-By: Claude Opus 4.8 --- github/resource_github_organization_custom_properties.go | 2 +- ...esource_github_organization_custom_properties_test.go | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index f8fae879d0..9ce8de6a84 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -45,7 +45,7 @@ func resourceGithubOrganizationCustomProperties() *schema.Resource { }, "default_value": { Type: schema.TypeString, - Description: "The default value of the custom property", + Description: "The default value of the custom property. Not supported for multi_select properties.", Optional: true, Computed: true, }, diff --git a/github/resource_github_organization_custom_properties_test.go b/github/resource_github_organization_custom_properties_test.go index 02e523d0c1..1aaa0e6c72 100644 --- a/github/resource_github_organization_custom_properties_test.go +++ b/github/resource_github_organization_custom_properties_test.go @@ -7,6 +7,9 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubOrganizationCustomProperties(t *testing.T) { @@ -286,9 +289,9 @@ resource "github_organization_custom_properties" "test" { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_organization_custom_properties.test", "default_value", "true"), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_organization_custom_properties.test", tfjsonpath.New("default_value"), knownvalue.StringExact("true")), + }, }, { // Read must round-trip the true_false default_value so the From adea79e30ab7fdfdf61f2f80a087ce8204a22ea5 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer Date: Wed, 5 Aug 2026 11:45:02 +0200 Subject: [PATCH 3/5] test: assert empty post-refresh plan instead of a second plan-only step Drop the redundant default_value state check (Terraform core already enforces consistency with config) and fold the drift regression guard into a PostApplyPostRefresh plan check on the apply step. --- ...hub_organization_custom_properties_test.go | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/github/resource_github_organization_custom_properties_test.go b/github/resource_github_organization_custom_properties_test.go index 1aaa0e6c72..6bf351124b 100644 --- a/github/resource_github_organization_custom_properties_test.go +++ b/github/resource_github_organization_custom_properties_test.go @@ -7,9 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/knownvalue" - "github.com/hashicorp/terraform-plugin-testing/statecheck" - "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/plancheck" ) func TestAccGithubOrganizationCustomProperties(t *testing.T) { @@ -289,17 +287,15 @@ resource "github_organization_custom_properties" "test" { Steps: []resource.TestStep{ { Config: config, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_organization_custom_properties.test", tfjsonpath.New("default_value"), knownvalue.StringExact("true")), + ConfigPlanChecks: resource.ConfigPlanChecks{ + // Read must round-trip the true_false default_value so the + // plan after refresh is empty (regression guard for + // perpetual drift). + PostApplyPostRefresh: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, }, }, - { - // Read must round-trip the true_false default_value so the - // second plan is empty (regression guard for perpetual drift). - Config: config, - PlanOnly: true, - ExpectNonEmptyPlan: false, - }, }, }) }) From 3b2aac54d161d202bad3f283f3192a30e75463d1 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer Date: Mon, 10 Aug 2026 12:50:44 +0200 Subject: [PATCH 4/5] refactor(resource_github_organization_custom_properties): migrate to context-aware CRUD Switches the resource to the CreateContext/ReadContext/UpdateContext/ DeleteContext and StateContext signatures used by most resources in the provider, so the CRUD functions receive a context and can return diagnostics instead of a bare error. Update now delegates to Create, which already upserts via PUT and reads the property back, removing a redundant second read. --- ...e_github_organization_custom_properties.go | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index 9ce8de6a84..ed3d98b808 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/google/go-github/v89/github" + "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" @@ -12,12 +13,12 @@ import ( func resourceGithubOrganizationCustomProperties() *schema.Resource { return &schema.Resource{ - Create: resourceGithubCustomPropertiesCreate, - Read: resourceGithubCustomPropertiesRead, - Update: resourceGithubCustomPropertiesUpdate, - Delete: resourceGithubCustomPropertiesDelete, + CreateContext: resourceGithubCustomPropertiesCreate, + ReadContext: resourceGithubCustomPropertiesRead, + UpdateContext: resourceGithubCustomPropertiesUpdate, + DeleteContext: resourceGithubCustomPropertiesDelete, Importer: &schema.ResourceImporter{ - State: resourceGithubCustomPropertiesImport, + StateContext: resourceGithubCustomPropertiesImport, }, CustomizeDiff: customdiff.Sequence( @@ -73,8 +74,7 @@ func resourceGithubOrganizationCustomProperties() *schema.Resource { } } -func resourceGithubCustomPropertiesCreate(d *schema.ResourceData, meta any) error { - ctx := context.Background() +func resourceGithubCustomPropertiesCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client ownerName := meta.(*Owner).name @@ -105,21 +105,20 @@ func resourceGithubCustomPropertiesCreate(d *schema.ResourceData, meta any) erro customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, d.Get("property_name").(string), customProperty) if err != nil { - return err + return diag.FromErr(err) } d.SetId(*customProperty.PropertyName) - return resourceGithubCustomPropertiesRead(d, meta) + return resourceGithubCustomPropertiesRead(ctx, d, meta) } -func resourceGithubCustomPropertiesRead(d *schema.ResourceData, meta any) error { - ctx := context.Background() +func resourceGithubCustomPropertiesRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client ownerName := meta.(*Owner).name customProperty, _, err := client.Organizations.GetCustomProperty(ctx, ownerName, d.Get("property_name").(string)) if err != nil { - return err + return diag.FromErr(err) } // multi_select is not supported: its default value is a []string, which @@ -148,26 +147,25 @@ func resourceGithubCustomPropertiesRead(d *schema.ResourceData, meta any) error return nil } -func resourceGithubCustomPropertiesUpdate(d *schema.ResourceData, meta any) error { - if err := resourceGithubCustomPropertiesCreate(d, meta); err != nil { - return err - } - return resourceGithubCustomPropertiesRead(d, meta) +func resourceGithubCustomPropertiesUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + // Create issues a PUT, which the API treats as an upsert, and reads the + // property back afterwards. + return resourceGithubCustomPropertiesCreate(ctx, d, meta) } -func resourceGithubCustomPropertiesDelete(d *schema.ResourceData, meta any) error { +func resourceGithubCustomPropertiesDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client ownerName := meta.(*Owner).name - _, err := client.Organizations.RemoveCustomProperty(context.Background(), ownerName, d.Get("property_name").(string)) + _, err := client.Organizations.RemoveCustomProperty(ctx, ownerName, d.Get("property_name").(string)) if err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubCustomPropertiesImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func resourceGithubCustomPropertiesImport(_ context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { if err := d.Set("property_name", d.Id()); err != nil { return nil, err } From c958643e5a292a26b1927957bcb23e8966472108 Mon Sep 17 00:00:00 2001 From: Sebastian Poxhofer Date: Mon, 10 Aug 2026 12:52:23 +0200 Subject: [PATCH 5/5] feat(resource_github_organization_custom_properties): warn on multi_select default_value GitHub returns the default value of a multi_select property as a list of strings, which cannot be represented by the string default_value attribute. The value is therefore not stored in state and every plan shows a change for default_value. Emit a warning diagnostic on create and update instead of rejecting the combination, so existing configurations relying on the ignore_changes workaround keep working. --- ...e_github_organization_custom_properties.go | 27 ++++++++- ...hub_organization_custom_properties_test.go | 57 +++++++++++++++++++ 2 files changed, 82 insertions(+), 2 deletions(-) diff --git a/github/resource_github_organization_custom_properties.go b/github/resource_github_organization_custom_properties.go index ed3d98b808..99094ea1af 100644 --- a/github/resource_github_organization_custom_properties.go +++ b/github/resource_github_organization_custom_properties.go @@ -5,6 +5,7 @@ import ( "strconv" "github.com/google/go-github/v89/github" + "github.com/hashicorp/go-cty/cty" "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" @@ -103,13 +104,35 @@ func resourceGithubCustomPropertiesCreate(ctx context.Context, d *schema.Resourc customProperty.ValuesEditableBy = &str } + diags := multiSelectDefaultValueWarning(valueType, defaultValue) + customProperty, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, ownerName, d.Get("property_name").(string), customProperty) if err != nil { - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } d.SetId(*customProperty.PropertyName) - return resourceGithubCustomPropertiesRead(ctx, d, meta) + return append(diags, resourceGithubCustomPropertiesRead(ctx, d, meta)...) +} + +// multiSelectDefaultValueWarning warns when a default value is configured for a +// multi_select property. GitHub returns those defaults as a list of strings, +// which cannot be represented by the string default_value attribute, so the +// configured value is not reflected in state and shows up as a change on every +// plan. +func multiSelectDefaultValueWarning(valueType github.PropertyValueType, defaultValue string) diag.Diagnostics { + if valueType != github.PropertyValueTypeMultiSelect || defaultValue == "" { + return nil + } + + return diag.Diagnostics{ + { + Severity: diag.Warning, + Summary: "default_value is not supported for multi_select properties", + Detail: "The default value of a multi_select property cannot be read back by this provider, so it is not stored in state and every plan will show a change for default_value. Remove default_value to avoid this.", + AttributePath: cty.GetAttrPath("default_value"), + }, + } } func resourceGithubCustomPropertiesRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { diff --git a/github/resource_github_organization_custom_properties_test.go b/github/resource_github_organization_custom_properties_test.go index 6bf351124b..fe7ef423f9 100644 --- a/github/resource_github_organization_custom_properties_test.go +++ b/github/resource_github_organization_custom_properties_test.go @@ -5,11 +5,68 @@ import ( "regexp" "testing" + "github.com/google/go-github/v89/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/plancheck" ) +func Test_multiSelectDefaultValueWarning(t *testing.T) { + t.Parallel() + + for _, d := range []struct { + testName string + valueType github.PropertyValueType + defaultValue string + expectWarn bool + }{ + { + testName: "multi_select_with_default_value", + valueType: github.PropertyValueTypeMultiSelect, + defaultValue: "Test", + expectWarn: true, + }, + { + testName: "multi_select_without_default_value", + valueType: github.PropertyValueTypeMultiSelect, + }, + { + testName: "single_select_with_default_value", + valueType: github.PropertyValueTypeSingleSelect, + defaultValue: "Test", + }, + { + testName: "true_false_with_default_value", + valueType: github.PropertyValueTypeTrueFalse, + defaultValue: "true", + }, + } { + t.Run(d.testName, func(t *testing.T) { + t.Parallel() + + got := multiSelectDefaultValueWarning(d.valueType, d.defaultValue) + + if !d.expectWarn { + if len(got) != 0 { + t.Fatalf("expected no diagnostics but got %v", got) + } + return + } + + if len(got) != 1 { + t.Fatalf("expected a single diagnostic but got %v", got) + } + if got[0].Severity != diag.Warning { + t.Errorf("expected a warning but got severity %v", got[0].Severity) + } + if got.HasError() { + t.Error("expected the diagnostics to not contain an error") + } + }) + } +} + func TestAccGithubOrganizationCustomProperties(t *testing.T) { t.Parallel()