From 7230ec4ef91bd302b8f5dce187dacdc731d70c98 Mon Sep 17 00:00:00 2001 From: Andrew Sweet Date: Fri, 1 May 2026 13:55:34 +0100 Subject: [PATCH] fix: add Computed and use GetOkExists for internal repos field Add Computed: true to members_can_create_internal_repositories schema so Terraform accepts API values when users omit the field, preventing phantom diffs that trigger 422 errors under enterprise policy. Replace shouldInclude with d.GetOkExists for this field since GetOk cannot distinguish "set to false" from "not set" on Computed+Optional bools. Follows allow_forking pattern in resource_github_repository.go. Add unit tests for buildOrganizationSettings and acceptance test proving no phantom diff after apply. Resolves #3385 Co-Authored-By: Claude Opus 4.6 (1M context) --- .../resource_github_organization_settings.go | 9 +- ...ource_github_organization_settings_test.go | 32 ++++++ ..._github_organization_settings_unit_test.go | 101 ++++++++++++++++++ .../r/organization_settings.html.markdown | 2 +- 4 files changed, 141 insertions(+), 3 deletions(-) create mode 100644 github/resource_github_organization_settings_unit_test.go diff --git a/github/resource_github_organization_settings.go b/github/resource_github_organization_settings.go index 5b8d0b7062..652bb502e0 100644 --- a/github/resource_github_organization_settings.go +++ b/github/resource_github_organization_settings.go @@ -84,6 +84,7 @@ func resourceGithubOrganizationSettings() *schema.Resource { "members_can_create_internal_repositories": { Type: schema.TypeBool, Optional: true, + Computed: true, Description: "Whether or not organization members can create new internal repositories. For Enterprise Organizations only.", }, "members_can_create_private_repositories": { @@ -291,9 +292,13 @@ func buildOrganizationSettings(d *schema.ResourceData, isEnterprise bool) *githu settings.SecretScanningPushProtectionEnabledForNewRepos = new(d.Get("secret_scanning_push_protection_enabled_for_new_repositories").(bool)) } - // Enterprise-specific field + // Enterprise-specific field: use GetOkExists to correctly detect explicit false if isEnterprise { - if shouldInclude("members_can_create_internal_repositories") { + if !isUpdate { + if _, ok := d.GetOkExists("members_can_create_internal_repositories"); ok { //nolint:staticcheck // SA1019 // GetOkExists needed for Computed+Optional bool fields + settings.MembersCanCreateInternalRepos = new(d.Get("members_can_create_internal_repositories").(bool)) + } + } else if d.HasChange("members_can_create_internal_repositories") { settings.MembersCanCreateInternalRepos = new(d.Get("members_can_create_internal_repositories").(bool)) } } diff --git a/github/resource_github_organization_settings_test.go b/github/resource_github_organization_settings_test.go index 742f779b3e..25eaa22c6a 100644 --- a/github/resource_github_organization_settings_test.go +++ b/github/resource_github_organization_settings_test.go @@ -611,3 +611,35 @@ func TestAccGithubOrganizationSettings(t *testing.T) { }) }) } + +func TestAccGithubOrganizationSettings_omittedInternalReposFieldProducesCleanPlan(t *testing.T) { + config := ` + resource "github_organization_settings" "test" { + billing_email = "test@example.com" + }` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_organization_settings.test", + "billing_email", "test@example.com", + ), + resource.TestCheckResourceAttrSet( + "github_organization_settings.test", + "members_can_create_internal_repositories", + ), + ), + }, + { + Config: config, + PlanOnly: true, + ExpectNonEmptyPlan: false, + }, + }, + }) +} diff --git a/github/resource_github_organization_settings_unit_test.go b/github/resource_github_organization_settings_unit_test.go new file mode 100644 index 0000000000..8d11afc3bc --- /dev/null +++ b/github/resource_github_organization_settings_unit_test.go @@ -0,0 +1,101 @@ +package github + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestBuildOrganizationSettings_OmittedInternalReposNotSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + }) + + settings := buildOrganizationSettings(d, true) + + if settings.MembersCanCreateInternalRepos != nil { + t.Error("MembersCanCreateInternalRepos should be nil when not configured") + } +} + +func TestBuildOrganizationSettings_ExplicitTrueInternalReposSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + "members_can_create_internal_repositories": true, + }) + + settings := buildOrganizationSettings(d, true) + + if settings.MembersCanCreateInternalRepos == nil { + t.Fatal("MembersCanCreateInternalRepos should not be nil when explicitly set to true") + } + if *settings.MembersCanCreateInternalRepos != true { + t.Errorf("MembersCanCreateInternalRepos = %v, want true", *settings.MembersCanCreateInternalRepos) + } +} + +func TestBuildOrganizationSettings_ExplicitFalseInternalReposSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + "members_can_create_internal_repositories": false, + }) + + settings := buildOrganizationSettings(d, true) + + if settings.MembersCanCreateInternalRepos == nil { + t.Fatal("MembersCanCreateInternalRepos should not be nil when explicitly set to false") + } + if *settings.MembersCanCreateInternalRepos != false { + t.Errorf("MembersCanCreateInternalRepos = %v, want false", *settings.MembersCanCreateInternalRepos) + } +} + +func TestBuildOrganizationSettings_UpdateOmittedInternalReposNotSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + }) + d.SetId("test-org") + + settings := buildOrganizationSettings(d, true) + + if settings.MembersCanCreateInternalRepos != nil { + t.Error("MembersCanCreateInternalRepos should be nil on update when field has not changed") + } +} + +func TestBuildOrganizationSettings_NonEnterpriseExcludesInternalRepos(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + "members_can_create_internal_repositories": true, + }) + + settings := buildOrganizationSettings(d, false) + + if settings.MembersCanCreateInternalRepos != nil { + t.Error("MembersCanCreateInternalRepos should be nil when not enterprise") + } +} + +func TestOrganizationSettingsSchemaInternalRepos(t *testing.T) { + resource := resourceGithubOrganizationSettings() + + field := resource.Schema["members_can_create_internal_repositories"] + if field == nil { + t.Fatal("members_can_create_internal_repositories not found in schema") + } + + if !field.Optional { + t.Error("members_can_create_internal_repositories should be Optional") + } + if !field.Computed { + t.Error("members_can_create_internal_repositories should be Computed") + } + if field.Default != nil { + t.Errorf("members_can_create_internal_repositories should have no Default, got %v", field.Default) + } +} diff --git a/website/docs/r/organization_settings.html.markdown b/website/docs/r/organization_settings.html.markdown index c8a4db54a6..270a914ba0 100644 --- a/website/docs/r/organization_settings.html.markdown +++ b/website/docs/r/organization_settings.html.markdown @@ -60,7 +60,7 @@ The following arguments are supported: * `members_can_create_repositories` - (Optional) Whether or not organization members can create new repositories. Defaults to `true`. * `members_can_create_public_repositories` - (Optional) Whether or not organization members can create new public repositories. Defaults to `true`. * `members_can_create_private_repositories` - (Optional) Whether or not organization members can create new private repositories. Defaults to `true`. -* `members_can_create_internal_repositories` - (Optional) Whether or not organization members can create new internal repositories. For Enterprise Organizations only. +* `members_can_create_internal_repositories` - (Optional) Whether or not organization members can create new internal repositories. For Enterprise Organizations only. When an enterprise policy controls this setting, omit this attribute to avoid API validation errors. * `members_can_create_pages` - (Optional) Whether or not organization members can create new pages. Defaults to `true`. * `members_can_create_public_pages` - (Optional) Whether or not organization members can create new public pages. Defaults to `true`. * `members_can_create_private_pages` - (Optional) Whether or not organization members can create new private pages. Defaults to `true`.