From 735fe866fb7bca9a7a0d572497e9aa8c23e9a9b6 Mon Sep 17 00:00:00 2001 From: Jaden Lee Date: Fri, 16 Jan 2026 18:14:22 -0500 Subject: [PATCH 1/4] fix: have an option to set org level forking policy --- github/resource_github_repository.go | 22 ++++++++-- github/resource_github_repository_test.go | 49 +++++++++++++++++++++++ 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 9540f83eff..3e1b0fb525 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -259,6 +259,12 @@ func resourceGithubRepository() *schema.Resource { Computed: true, Description: "Set to 'true' to allow private forking on the repository; this is only relevant if the repository is owned by an organization and is private or internal.", }, + "org_allow_forking": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Set to 'true' if the org allows forking.", + }, "squash_merge_commit_title": { Type: schema.TypeString, Optional: true, @@ -634,9 +640,13 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { } // only configure allow forking if repository is not public - allowForking, ok := d.Get("allow_forking").(bool) - if ok && visibility != "public" { - repository.AllowForking = github.Ptr(allowForking) + // skip this when the org doesn't allow forking + orgAllowForking, ok := d.Get("org_allow_forking").(bool) + if ok && orgAllowForking { + allowForking, forkingOk := d.Get("allow_forking").(bool) + if forkingOk && visibility != "public" { + repository.AllowForking = github.Ptr(allowForking) + } } return repository @@ -845,7 +855,11 @@ func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, m _ = d.Set("allow_rebase_merge", repo.GetAllowRebaseMerge()) _ = d.Set("allow_squash_merge", repo.GetAllowSquashMerge()) _ = d.Set("allow_update_branch", repo.GetAllowUpdateBranch()) - _ = d.Set("allow_forking", repo.GetAllowForking()) + if orgAllowForking, ok := d.Get("org_allow_forking").(bool); ok && orgAllowForking { + _ = d.Set("allow_forking", repo.GetAllowForking()) + } else { + _ = d.Set("allow_forking", false) + } _ = d.Set("delete_branch_on_merge", repo.GetDeleteBranchOnMerge()) _ = d.Set("web_commit_signoff_required", repo.GetWebCommitSignoffRequired()) _ = d.Set("has_downloads", repo.GetHasDownloads()) diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index 9bfde4fd9a..ab04b95181 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -597,6 +597,55 @@ func TestAccGithubRepository(t *testing.T) { }) }) + t.Run("create_private_with_org_allow_forking", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%sorg-forking-%s", testResourcePrefix, randomID) + + // Test with org_allow_forking = true, allow_forking should be applied + configOrgAllowsForking := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + visibility = "private" + org_allow_forking = true + allow_forking = true + } + `, repoName) + + // Test with org_allow_forking = false, allow_forking should not cause errors + configOrgDisallowsForking := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + visibility = "private" + org_allow_forking = false + allow_forking = true + } + `, repoName) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: configOrgAllowsForking, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "visibility", "private"), + resource.TestCheckResourceAttr("github_repository.test", "org_allow_forking", "true"), + resource.TestCheckResourceAttr("github_repository.test", "allow_forking", "true"), + ), + }, + { + Config: configOrgDisallowsForking, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "visibility", "private"), + resource.TestCheckResourceAttr("github_repository.test", "org_allow_forking", "false"), + // allow_forking should be false in state when org doesn't allow forking + resource.TestCheckResourceAttr("github_repository.test", "allow_forking", "false"), + ), + }, + }, + }) + }) + t.Run("configures vulnerability alerts for a private repository", func(t *testing.T) { randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%sprv-vuln-%s", testResourcePrefix, randomID) From 8bce3acf51761a7acb7ee7096742782891332b10 Mon Sep 17 00:00:00 2001 From: Jaden Lee Date: Sat, 17 Jan 2026 03:16:01 -0500 Subject: [PATCH 2/4] fix: move membersCanForkPrivateRepos to config instead of adding new input --- github/config.go | 14 ++++++++------ github/resource_github_repository.go | 28 +++++++++++++++------------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/github/config.go b/github/config.go index de232c1766..a6d000491d 100644 --- a/github/config.go +++ b/github/config.go @@ -30,12 +30,13 @@ type Config struct { } type Owner struct { - name string - id int64 - v3client *github.Client - v4client *githubv4.Client - StopContext context.Context - IsOrganization bool + name string + id int64 + v3client *github.Client + v4client *githubv4.Client + StopContext context.Context + IsOrganization bool + MembersCanForkPrivateRepos bool } const ( @@ -133,6 +134,7 @@ func (c *Config) ConfigureOwner(owner *Owner) (*Owner, error) { if remoteOrg != nil { owner.id = remoteOrg.GetID() owner.IsOrganization = true + owner.MembersCanForkPrivateRepos = remoteOrg.MembersCanForkPrivateRepos() } } } diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 3e1b0fb525..a0396cafaa 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -259,12 +259,6 @@ func resourceGithubRepository() *schema.Resource { Computed: true, Description: "Set to 'true' to allow private forking on the repository; this is only relevant if the repository is owned by an organization and is private or internal.", }, - "org_allow_forking": { - Type: schema.TypeBool, - Optional: true, - Default: true, - Description: "Set to 'true' if the org allows forking.", - }, "squash_merge_commit_title": { Type: schema.TypeString, Optional: true, @@ -640,13 +634,9 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { } // only configure allow forking if repository is not public - // skip this when the org doesn't allow forking - orgAllowForking, ok := d.Get("org_allow_forking").(bool) - if ok && orgAllowForking { - allowForking, forkingOk := d.Get("allow_forking").(bool) - if forkingOk && visibility != "public" { - repository.AllowForking = github.Ptr(allowForking) - } + allowForking, ok := d.Get("allow_forking").(bool) + if ok && visibility != "public" { + repository.AllowForking = github.Ptr(allowForking) } return repository @@ -665,6 +655,11 @@ func resourceGithubRepositoryCreate(ctx context.Context, d *schema.ResourceData, isPrivate := repoReq.GetVisibility() == "private" repoReq.Private = github.Ptr(isPrivate) + + if !meta.(*Owner).MembersCanForkPrivateRepos { + repoReq.AllowForking = nil + } + if template, ok := d.GetOk("template"); ok { templateConfigBlocks := template.([]any) @@ -948,6 +943,10 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, repoReq.AllowForking = nil } + if !meta.(*Owner).MembersCanForkPrivateRepos { + repoReq.AllowForking = nil + } + if !d.HasChange("security_and_analysis") { repoReq.SecurityAndAnalysis = nil log.Print("[DEBUG] No security_and_analysis update required. Removing this field from the payload.") @@ -1036,6 +1035,9 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, if d.HasChanges("visibility", "private") { repoReq.Visibility = github.Ptr(visibility) repoReq.AllowForking = allowForking + if !meta.(*Owner).MembersCanForkPrivateRepos { + repoReq.AllowForking = nil + } log.Printf("[DEBUG] Updating repository visibility from %s to %s", repo.GetVisibility(), visibility) _, resp, err := client.Repositories.Edit(ctx, owner, repoName, repoReq) From c603fa8db5395599cba6acdf4cbd2c0fc2217f9e Mon Sep 17 00:00:00 2001 From: Jaden Lee Date: Sat, 17 Jan 2026 03:50:03 -0500 Subject: [PATCH 3/4] fix: make it inverse so it doesn't affect nonorg owner --- github/config.go | 16 +-- github/resource_github_repository.go | 12 +- github/resource_github_repository_test.go | 130 +++++++++++++--------- 3 files changed, 92 insertions(+), 66 deletions(-) diff --git a/github/config.go b/github/config.go index a6d000491d..f56ce3116f 100644 --- a/github/config.go +++ b/github/config.go @@ -30,13 +30,13 @@ type Config struct { } type Owner struct { - name string - id int64 - v3client *github.Client - v4client *githubv4.Client - StopContext context.Context - IsOrganization bool - MembersCanForkPrivateRepos bool + name string + id int64 + v3client *github.Client + v4client *githubv4.Client + StopContext context.Context + IsOrganization bool + MembersCannotForkPrivateRepos bool } const ( @@ -134,7 +134,7 @@ func (c *Config) ConfigureOwner(owner *Owner) (*Owner, error) { if remoteOrg != nil { owner.id = remoteOrg.GetID() owner.IsOrganization = true - owner.MembersCanForkPrivateRepos = remoteOrg.MembersCanForkPrivateRepos() + owner.MembersCannotForkPrivateRepos = !remoteOrg.GetMembersCanForkPrivateRepos() } } } diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index a0396cafaa..70b673318e 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -656,7 +656,7 @@ func resourceGithubRepositoryCreate(ctx context.Context, d *schema.ResourceData, isPrivate := repoReq.GetVisibility() == "private" repoReq.Private = github.Ptr(isPrivate) - if !meta.(*Owner).MembersCanForkPrivateRepos { + if meta.(*Owner).MembersCannotForkPrivateRepos { repoReq.AllowForking = nil } @@ -850,11 +850,7 @@ func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, m _ = d.Set("allow_rebase_merge", repo.GetAllowRebaseMerge()) _ = d.Set("allow_squash_merge", repo.GetAllowSquashMerge()) _ = d.Set("allow_update_branch", repo.GetAllowUpdateBranch()) - if orgAllowForking, ok := d.Get("org_allow_forking").(bool); ok && orgAllowForking { - _ = d.Set("allow_forking", repo.GetAllowForking()) - } else { - _ = d.Set("allow_forking", false) - } + _ = d.Set("allow_forking", repo.GetAllowForking()) _ = d.Set("delete_branch_on_merge", repo.GetDeleteBranchOnMerge()) _ = d.Set("web_commit_signoff_required", repo.GetWebCommitSignoffRequired()) _ = d.Set("has_downloads", repo.GetHasDownloads()) @@ -943,7 +939,7 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, repoReq.AllowForking = nil } - if !meta.(*Owner).MembersCanForkPrivateRepos { + if meta.(*Owner).MembersCannotForkPrivateRepos { repoReq.AllowForking = nil } @@ -1035,7 +1031,7 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, if d.HasChanges("visibility", "private") { repoReq.Visibility = github.Ptr(visibility) repoReq.AllowForking = allowForking - if !meta.(*Owner).MembersCanForkPrivateRepos { + if meta.(*Owner).MembersCannotForkPrivateRepos { repoReq.AllowForking = nil } diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index ab04b95181..ca2e6104cd 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -570,6 +570,10 @@ func TestAccGithubRepository(t *testing.T) { }) t.Run("create_private_with_forking", func(t *testing.T) { + // This test verifies that creating a private repo with allow_forking = true + // succeeds without error, regardless of whether the organization allows + // members to fork private repositories. The actual value of allow_forking + // in state will depend on the org's MembersCanForkPrivateRepos setting. randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) repoName := fmt.Sprintf("%svisibility-%s", testResourcePrefix, randomID) @@ -590,56 +594,8 @@ func TestAccGithubRepository(t *testing.T) { Config: config, Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("github_repository.test", "visibility", "private"), - resource.TestCheckResourceAttr("github_repository.test", "allow_forking", "true"), - ), - }, - }, - }) - }) - - t.Run("create_private_with_org_allow_forking", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - repoName := fmt.Sprintf("%sorg-forking-%s", testResourcePrefix, randomID) - - // Test with org_allow_forking = true, allow_forking should be applied - configOrgAllowsForking := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - visibility = "private" - org_allow_forking = true - allow_forking = true - } - `, repoName) - - // Test with org_allow_forking = false, allow_forking should not cause errors - configOrgDisallowsForking := fmt.Sprintf(` - resource "github_repository" "test" { - name = "%s" - visibility = "private" - org_allow_forking = false - allow_forking = true - } - `, repoName) - - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessHasOrgs(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: configOrgAllowsForking, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository.test", "visibility", "private"), - resource.TestCheckResourceAttr("github_repository.test", "org_allow_forking", "true"), - resource.TestCheckResourceAttr("github_repository.test", "allow_forking", "true"), - ), - }, - { - Config: configOrgDisallowsForking, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository.test", "visibility", "private"), - resource.TestCheckResourceAttr("github_repository.test", "org_allow_forking", "false"), - // allow_forking should be false in state when org doesn't allow forking - resource.TestCheckResourceAttr("github_repository.test", "allow_forking", "false"), + // Note: allow_forking value depends on org's MembersCanForkPrivateRepos setting + // The key test is that creation succeeds without a 422 error ), }, }, @@ -1806,3 +1762,77 @@ func TestAccRepository_VulnerabilityAlerts(t *testing.T) { }) }) } + +// TestMembersCannotForkPrivateReposLogic verifies that when MembersCannotForkPrivateRepos is true, +// the AllowForking field should be set to nil to avoid 422 errors from the GitHub API. +func TestMembersCannotForkPrivateReposLogic(t *testing.T) { + t.Run("AllowForking is nil when org disallows forking", func(t *testing.T) { + // Create a mock Owner where org disallows forking + owner := &Owner{ + name: "test-org", + IsOrganization: true, + MembersCannotForkPrivateRepos: true, // org restricts forking + } + + // Simulate the logic from resourceGithubRepositoryCreate + var allowForking *bool + val := true + allowForking = &val + + if owner.MembersCannotForkPrivateRepos { + allowForking = nil + } + + if allowForking != nil { + t.Error("AllowForking should be nil when MembersCannotForkPrivateRepos is true") + } + }) + + t.Run("AllowForking is set when org allows forking", func(t *testing.T) { + // Create a mock Owner where org allows forking + owner := &Owner{ + name: "test-org", + IsOrganization: true, + MembersCannotForkPrivateRepos: false, // org allows forking + } + + // Simulate the logic from resourceGithubRepositoryCreate + var allowForking *bool + val := true + allowForking = &val + + if owner.MembersCannotForkPrivateRepos { + allowForking = nil + } + + if allowForking == nil { + t.Error("AllowForking should be set when MembersCannotForkPrivateRepos is false") + } + if *allowForking != true { + t.Error("AllowForking should be true") + } + }) + + t.Run("personal account has no forking restriction by default", func(t *testing.T) { + // Personal accounts (non-org) default to MembersCannotForkPrivateRepos = false + // This means no restriction on forking + owner := &Owner{ + name: "personal-user", + IsOrganization: false, + MembersCannotForkPrivateRepos: false, // default zero value + } + + // Simulate the logic - should allow forking for personal accounts + var allowForking *bool + val := true + allowForking = &val + + if owner.MembersCannotForkPrivateRepos { + allowForking = nil + } + + if allowForking == nil { + t.Error("AllowForking should be set for personal accounts (default false = no restriction)") + } + }) +} From 9ddb9d9cb4f90e43da89ed15309d133ec511587e Mon Sep 17 00:00:00 2001 From: Jaden Lee Date: Sat, 17 Jan 2026 03:57:11 -0500 Subject: [PATCH 4/4] chore: comments --- github/config.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/github/config.go b/github/config.go index f56ce3116f..d984476e47 100644 --- a/github/config.go +++ b/github/config.go @@ -134,6 +134,14 @@ func (c *Config) ConfigureOwner(owner *Owner) (*Owner, error) { if remoteOrg != nil { owner.id = remoteOrg.GetID() owner.IsOrganization = true + // MembersCannotForkPrivateRepos is intentionally inverted from the API's + // MembersCanForkPrivateRepos field. This ensures the default zero value (false) + // means "no restriction" which is correct for: + // - Personal accounts (non-org): forking is always allowed, no org policy applies + // - Organizations that allow forking: MembersCanForkPrivateRepos=true → inverted to false + // Only when an org explicitly disallows forking (MembersCanForkPrivateRepos=false) + // does this become true, triggering the guard that skips sending AllowForking + // to the API to avoid HTTP 422 errors. owner.MembersCannotForkPrivateRepos = !remoteOrg.GetMembersCanForkPrivateRepos() } }