From 21524a1c83c439b6417bfd0c5d5e4a06b227b6aa Mon Sep 17 00:00:00 2001 From: Andrew Sweet Date: Wed, 22 Apr 2026 14:54:57 +0100 Subject: [PATCH 1/2] fix: avoid 422 when enterprise policy controls org fork setting Remove Default: false from members_can_fork_private_repositories schema to prevent the field from being sent to the API when not explicitly set by the user. When an enterprise-level policy locks this setting, sending any value causes a 422 validation error. Without a default, the field is only included in API calls when the user explicitly configures it, matching the behavior of members_can_create_internal_repositories (enterprise-only field). Fixes #2689 Relates to #1333 Co-Authored-By: Claude Opus 4.7 (1M context) --- github/resource_github_organization_settings.go | 3 +-- website/docs/r/organization_settings.html.markdown | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/github/resource_github_organization_settings.go b/github/resource_github_organization_settings.go index 5b8d0b7062..643493e5ac 100644 --- a/github/resource_github_organization_settings.go +++ b/github/resource_github_organization_settings.go @@ -119,8 +119,7 @@ func resourceGithubOrganizationSettings() *schema.Resource { "members_can_fork_private_repositories": { Type: schema.TypeBool, Optional: true, - Default: false, - Description: "Whether or not organization members can fork private repositories.", + Description: "Whether or not organization members can fork private repositories. When an enterprise policy controls this setting, omit this attribute to avoid API validation errors.", }, "web_commit_signoff_required": { Type: schema.TypeBool, diff --git a/website/docs/r/organization_settings.html.markdown b/website/docs/r/organization_settings.html.markdown index c8a4db54a6..639f5801cf 100644 --- a/website/docs/r/organization_settings.html.markdown +++ b/website/docs/r/organization_settings.html.markdown @@ -64,7 +64,7 @@ The following arguments are supported: * `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`. -* `members_can_fork_private_repositories` - (Optional) Whether or not organization members can fork private repositories. Defaults to `false`. +* `members_can_fork_private_repositories` - (Optional) Whether or not organization members can fork private repositories. When an enterprise policy controls this setting, omit this attribute to avoid API validation errors. * `web_commit_signoff_required` - (Optional) Whether or not commit signatures are required for commits to the organization. Defaults to `false`. * `advanced_security_enabled_for_new_repositories` - (Optional) Whether or not advanced security is enabled for new repositories. Defaults to `false`. * `dependabot_alerts_enabled_for_new_repositories` - (Optional) Whether or not dependabot alerts are enabled for new repositories. Defaults to `false`. From 73dfb5d39df58715416985a9987d0cc8b1addb4b Mon Sep 17 00:00:00 2001 From: Andrew Sweet Date: Fri, 1 May 2026 13:34:52 +0100 Subject: [PATCH 2/2] fix: add Computed and use GetOkExists for fork field Add Computed: true to members_can_fork_private_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. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../resource_github_organization_settings.go | 7 +- ...ource_github_organization_settings_test.go | 32 ++++++ ..._github_organization_settings_unit_test.go | 104 ++++++++++++++++++ 3 files changed, 142 insertions(+), 1 deletion(-) 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 643493e5ac..ad8e248a5d 100644 --- a/github/resource_github_organization_settings.go +++ b/github/resource_github_organization_settings.go @@ -119,6 +119,7 @@ func resourceGithubOrganizationSettings() *schema.Resource { "members_can_fork_private_repositories": { Type: schema.TypeBool, Optional: true, + Computed: true, Description: "Whether or not organization members can fork private repositories. When an enterprise policy controls this setting, omit this attribute to avoid API validation errors.", }, "web_commit_signoff_required": { @@ -265,7 +266,11 @@ func buildOrganizationSettings(d *schema.ResourceData, isEnterprise bool) *githu if shouldInclude("members_can_create_private_pages") { settings.MembersCanCreatePrivatePages = new(d.Get("members_can_create_private_pages").(bool)) } - if shouldInclude("members_can_fork_private_repositories") { + if !isUpdate { + if _, ok := d.GetOkExists("members_can_fork_private_repositories"); ok { //nolint:staticcheck // SA1019 // GetOkExists needed for Computed+Optional bool fields + settings.MembersCanForkPrivateRepos = new(d.Get("members_can_fork_private_repositories").(bool)) + } + } else if d.HasChange("members_can_fork_private_repositories") { settings.MembersCanForkPrivateRepos = new(d.Get("members_can_fork_private_repositories").(bool)) } if shouldInclude("web_commit_signoff_required") { diff --git a/github/resource_github_organization_settings_test.go b/github/resource_github_organization_settings_test.go index 742f779b3e..2f9f96305c 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_omittedForkFieldProducesCleanPlan(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_fork_private_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..e431f52047 --- /dev/null +++ b/github/resource_github_organization_settings_unit_test.go @@ -0,0 +1,104 @@ +package github + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestBuildOrganizationSettings_OmittedFieldsNotSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + }) + + settings := buildOrganizationSettings(d, false) + + if settings.MembersCanForkPrivateRepos != nil { + t.Error("MembersCanForkPrivateRepos should be nil when not configured") + } + if settings.BillingEmail == nil { + t.Error("BillingEmail should be set when configured") + } +} + +func TestBuildOrganizationSettings_ExplicitTrueFieldsSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + "members_can_fork_private_repositories": true, + }) + + settings := buildOrganizationSettings(d, false) + + if settings.MembersCanForkPrivateRepos == nil { + t.Fatal("MembersCanForkPrivateRepos should not be nil when explicitly set to true") + } + if *settings.MembersCanForkPrivateRepos != true { + t.Errorf("MembersCanForkPrivateRepos = %v, want true", *settings.MembersCanForkPrivateRepos) + } +} + +func TestBuildOrganizationSettings_ExplicitFalseFieldsSent(t *testing.T) { + resource := resourceGithubOrganizationSettings() + d := schema.TestResourceDataRaw(t, resource.Schema, map[string]any{ + "billing_email": "test@example.com", + "members_can_fork_private_repositories": false, + }) + + settings := buildOrganizationSettings(d, false) + + if settings.MembersCanForkPrivateRepos == nil { + t.Fatal("MembersCanForkPrivateRepos should not be nil when explicitly set to false") + } + if *settings.MembersCanForkPrivateRepos != false { + t.Errorf("MembersCanForkPrivateRepos = %v, want false", *settings.MembersCanForkPrivateRepos) + } +} + +func TestBuildOrganizationSettings_UpdateOmittedFieldsNotSent(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, false) + + if settings.MembersCanForkPrivateRepos != nil { + t.Error("MembersCanForkPrivateRepos 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 TestOrganizationSettingsSchemaProperties(t *testing.T) { + resource := resourceGithubOrganizationSettings() + + field := resource.Schema["members_can_fork_private_repositories"] + if field == nil { + t.Fatal("members_can_fork_private_repositories not found in schema") + } + + if !field.Optional { + t.Error("members_can_fork_private_repositories should be Optional") + } + if !field.Computed { + t.Error("members_can_fork_private_repositories should be Computed") + } + if field.Default != nil { + t.Errorf("members_can_fork_private_repositories should have no Default, got %v", field.Default) + } +}