From 3004840b1fd46bc98f1672b301de51485d5e19c2 Mon Sep 17 00:00:00 2001 From: RoFz <4117458+RoFz@users.noreply.github.com> Date: Sun, 19 Apr 2026 12:23:33 +0100 Subject: [PATCH 1/2] feat: Add pull_request_creation_policy to github_repository --- github/resource_github_repository.go | 33 ++++++ github/resource_github_repository_test.go | 37 ++++++ github/util_v4_repository.go | 103 ++++++++++++++++ github/util_v4_repository_test.go | 138 ++++++++++++++++++++++ website/docs/r/repository.html.markdown | 2 + 5 files changed, 313 insertions(+) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 71688c102c..bcc7c57c9d 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -284,6 +284,13 @@ func resourceGithubRepository() *schema.Resource { Default: false, Description: "Automatically delete head branch after a pull request is merged. Defaults to 'false'.", }, + "pull_request_creation_policy": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "collaborators_only"}, false)), + Description: "Controls who can create pull requests in the repository. Can be `all` or `collaborators_only`.", + }, "web_commit_signoff_required": { Type: schema.TypeBool, Optional: true, @@ -870,6 +877,19 @@ func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, m } } + pullRequestCreationPolicy, err := getRepositoryPullRequestCreationPolicy(ctx, owner, repoName, meta) + if err != nil { + if isUnsupportedPullRequestCreationPolicyError(err) { + log.Printf("[DEBUG] Skipping pull_request_creation_policy read for %s/%s: %s", owner, repoName, err) + } else { + return diag.Errorf("error reading repository pull request creation policy: %s", err.Error()) + } + } else { + if err = d.Set("pull_request_creation_policy", pullRequestCreationPolicy); err != nil { + return diag.FromErr(err) + } + } + // Set fork information if this is a fork if repo.GetFork() { _ = d.Set("fork", "true") @@ -995,6 +1015,19 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, } } + if d.IsNewResource() || d.HasChange("pull_request_creation_policy") { + if v, ok := d.GetOk("pull_request_creation_policy"); ok { + repositoryID, err := getRepositoryID(repo.GetName(), meta) + if err != nil { + return diag.Errorf("error resolving repository id for pull request creation policy update: %s", err.Error()) + } + + if err := updateRepositoryPullRequestCreationPolicy(ctx, repositoryID, v.(string), meta); err != nil { + return diag.Errorf("error updating repository pull request creation policy: %s", err.Error()) + } + } + } + if d.IsNewResource() || d.HasChange("vulnerability_alerts") { if v, ok := d.GetOkExists("vulnerability_alerts"); ok { //nolint:staticcheck // SA1019 // We sometimes need to use GetOkExists for booleans if val, ok := v.(bool); ok { diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index cc6df3c64d..b5abfb82b5 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -1334,6 +1334,43 @@ resource "github_repository" "test" { }) }) + t.Run("check_pull_request_creation_policy", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + testRepoName := fmt.Sprintf("%spr-policy-%s", testResourcePrefix, randomID) + config := ` + resource "github_repository" "test" { + name = "%s" + visibility = "%s" + pull_request_creation_policy = "%s" + } + ` + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, testRepoName, testAccConf.testRepositoryVisibility, "collaborators_only"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "pull_request_creation_policy", "collaborators_only"), + ), + }, + { + Config: fmt.Sprintf(config, testRepoName, testAccConf.testRepositoryVisibility, "all"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("github_repository.test", "pull_request_creation_policy", "all"), + ), + }, + { + ResourceName: "github_repository.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"auto_init", "vulnerability_alerts", "ignore_vulnerability_alerts_during_read"}, + }, + }, + }) + }) + t.Run("check_web_commit_signoff_required_organization_enabled_but_not_set", func(t *testing.T) { t.Skip("This test should be run manually after confirming that the test organization has 'Require contributors to sign off on web-based commits' enabled under Organizations -> Settings -> Repository -> Repository defaults.") diff --git a/github/util_v4_repository.go b/github/util_v4_repository.go index 4e4fa837f6..b02afadc76 100644 --- a/github/util_v4_repository.go +++ b/github/util_v4_repository.go @@ -4,10 +4,31 @@ import ( "context" "encoding/base64" "errors" + "fmt" + "strings" "github.com/shurcooL/githubv4" ) +// PullRequestCreationPolicy mirrors the GitHub GraphQL enum type of the same +// name so we can query and mutate the field even when the vendored client +// model lags behind the live schema. +type PullRequestCreationPolicy string + +const ( + PullRequestCreationPolicyAll PullRequestCreationPolicy = "ALL" + PullRequestCreationPolicyCollaboratorsOnly PullRequestCreationPolicy = "COLLABORATORS_ONLY" +) + +// UpdateRepositoryInput intentionally mirrors the GitHub GraphQL input type +// name so the graphql client emits the correct variable type in mutations. +// We only model the fields needed for pullRequestCreationPolicy updates. +type UpdateRepositoryInput struct { + RepositoryID githubv4.ID `json:"repositoryId"` + PullRequestCreationPolicy *PullRequestCreationPolicy `json:"pullRequestCreationPolicy,omitempty"` + ClientMutationID *githubv4.String `json:"clientMutationId,omitempty"` +} + func getRepositoryID(name string, meta any) (githubv4.ID, error) { // Interpret `name` as a node ID exists, nodeIDerr := repositoryNodeIDExists(name, meta) @@ -65,6 +86,88 @@ func repositoryNodeIDExists(name string, meta any) (bool, error) { return query.Node.ID.(string) == name, nil } +func flattenPullRequestCreationPolicy(policy PullRequestCreationPolicy) (string, error) { + switch policy { + case PullRequestCreationPolicyAll: + return "all", nil + case PullRequestCreationPolicyCollaboratorsOnly: + return "collaborators_only", nil + case "": + return "", nil + default: + return "", fmt.Errorf("unsupported GraphQL pull request creation policy %q", policy) + } +} + +func expandPullRequestCreationPolicy(policy string) (PullRequestCreationPolicy, error) { + switch policy { + case "all": + return PullRequestCreationPolicyAll, nil + case "collaborators_only": + return PullRequestCreationPolicyCollaboratorsOnly, nil + default: + return "", fmt.Errorf("unsupported Terraform pull request creation policy %q", policy) + } +} + +func isUnsupportedPullRequestCreationPolicyError(err error) bool { + if err == nil { + return false + } + + message := strings.ToLower(err.Error()) + + return strings.Contains(message, "pullrequestcreationpolicy") && + (strings.Contains(message, "doesn't exist") || + strings.Contains(message, "does not exist") || + strings.Contains(message, "undefined field") || + strings.Contains(message, "unknown argument") || + strings.Contains(message, "cannot query field")) +} + +func getRepositoryPullRequestCreationPolicy(ctx context.Context, owner, name string, meta any) (string, error) { + var query struct { + Repository struct { + PullRequestCreationPolicy PullRequestCreationPolicy + } `graphql:"repository(owner:$owner, name:$name)"` + } + + variables := map[string]any{ + "owner": githubv4.String(owner), + "name": githubv4.String(name), + } + + client := meta.(*Owner).v4client + if err := client.Query(ctx, &query, variables); err != nil { + return "", err + } + + return flattenPullRequestCreationPolicy(query.Repository.PullRequestCreationPolicy) +} + +func updateRepositoryPullRequestCreationPolicy(ctx context.Context, repositoryID githubv4.ID, policy string, meta any) error { + expandedPolicy, err := expandPullRequestCreationPolicy(policy) + if err != nil { + return err + } + + input := UpdateRepositoryInput{ + RepositoryID: repositoryID, + PullRequestCreationPolicy: &expandedPolicy, + } + + var mutation struct { + UpdateRepository struct { + Repository struct { + ID githubv4.ID + } + } `graphql:"updateRepository(input:$input)"` + } + + client := meta.(*Owner).v4client + return client.Mutate(ctx, &mutation, input, nil) +} + // Maintain compatibility with deprecated Global ID format // https://github.blog/2021-02-10-new-global-id-format-coming-to-graphql/ func repositoryLegacyNodeIDExists(name string, meta any) (bool, error) { diff --git a/github/util_v4_repository_test.go b/github/util_v4_repository_test.go index 7e0a1daf40..c88a0cae3c 100644 --- a/github/util_v4_repository_test.go +++ b/github/util_v4_repository_test.go @@ -2,6 +2,8 @@ package github import ( "bytes" + "context" + "errors" "io" "net/http" "net/http/httptest" @@ -191,6 +193,142 @@ func TestGetRepositoryIDPositiveMatches(t *testing.T) { } } +func TestPullRequestCreationPolicyMapping(t *testing.T) { + t.Run("flatten GraphQL values", func(t *testing.T) { + cases := []struct { + name string + input PullRequestCreationPolicy + want string + wantErr bool + }{ + {name: "all", input: PullRequestCreationPolicyAll, want: "all"}, + {name: "collaborators_only", input: PullRequestCreationPolicyCollaboratorsOnly, want: "collaborators_only"}, + {name: "empty", input: "", want: ""}, + {name: "invalid", input: PullRequestCreationPolicy("NOPE"), wantErr: true}, + } + + for _, tc := range cases { + got, err := flattenPullRequestCreationPolicy(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("%s: expected error, got nil", tc.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", tc.name, err) + } + if got != tc.want { + t.Fatalf("%s: got %q want %q", tc.name, got, tc.want) + } + } + }) + + t.Run("expand Terraform values", func(t *testing.T) { + cases := []struct { + name string + input string + want PullRequestCreationPolicy + wantErr bool + }{ + {name: "all", input: "all", want: PullRequestCreationPolicyAll}, + {name: "collaborators_only", input: "collaborators_only", want: PullRequestCreationPolicyCollaboratorsOnly}, + {name: "invalid", input: "everyone", wantErr: true}, + } + + for _, tc := range cases { + got, err := expandPullRequestCreationPolicy(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("%s: expected error, got nil", tc.name) + } + continue + } + if err != nil { + t.Fatalf("%s: unexpected error: %v", tc.name, err) + } + if got != tc.want { + t.Fatalf("%s: got %q want %q", tc.name, got, tc.want) + } + } + }) +} + +func TestRepositoryPullRequestCreationPolicyGraphQL(t *testing.T) { + mux := http.NewServeMux() + mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) { + body := mustRead(req.Body) + w.Header().Set("Content-Type", "application/json") + + switch { + case strings.Contains(body, "repository(owner:$owner, name:$name){pullRequestCreationPolicy}"): + if !strings.Contains(body, `"owner":"integrations"`) { + t.Fatalf("expected resolved owner in GraphQL body, got %s", body) + } + mustWrite(w, `{"data":{"repository":{"pullRequestCreationPolicy":"COLLABORATORS_ONLY"}}}`) + case strings.Contains(body, "mutation($input:UpdateRepositoryInput!){updateRepository(input:$input){repository{id}}}"): + mustWrite(w, `{"data":{"updateRepository":{"repository":{"id":"R_kgDOGGmaaw"}}}}`) + default: + t.Fatalf("unexpected GraphQL body: %s", body) + } + }) + + meta := Owner{ + v4client: githubv4.NewClient(&http.Client{Transport: localRoundTripper{handler: mux}}), + name: "integrations", + } + + ctx := context.Background() + + got, err := getRepositoryPullRequestCreationPolicy(ctx, "integrations", "terraform-provider-github", &meta) + if err != nil { + t.Fatalf("unexpected read error: %v", err) + } + if got != "collaborators_only" { + t.Fatalf("got %q want %q", got, "collaborators_only") + } + + if err := updateRepositoryPullRequestCreationPolicy(ctx, githubv4.ID("R_kgDOGGmaaw"), "all", &meta); err != nil { + t.Fatalf("unexpected update error: %v", err) + } +} + +func TestIsUnsupportedPullRequestCreationPolicyError(t *testing.T) { + cases := []struct { + name string + err error + want bool + }{ + { + name: "missing field", + err: errors.New(`Field 'pullRequestCreationPolicy' doesn't exist on type 'Repository'`), + want: true, + }, + { + name: "cannot query field", + err: errors.New(`Cannot query field "pullRequestCreationPolicy" on type "Repository".`), + want: true, + }, + { + name: "unrelated graphql error", + err: errors.New(`Could not resolve to a Repository with the name 'integrations/terraform-provider-github'.`), + want: false, + }, + { + name: "nil", + err: nil, + want: false, + }, + } + + for _, tc := range cases { + got := isUnsupportedPullRequestCreationPolicyError(tc.err) + if got != tc.want { + t.Fatalf("%s: got %t want %t", tc.name, got, tc.want) + } + } +} + // localRoundTripper is an http.RoundTripper that executes HTTP transactions // by using handler directly, instead of going over an HTTP connection. type localRoundTripper struct { diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 182e98125d..954e08a6a6 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -112,6 +112,8 @@ The following arguments are supported: - `delete_branch_on_merge` - (Optional) Automatically delete head branch after a pull request is merged. Defaults to `false`. +- `pull_request_creation_policy` - (Optional) Controls who can create pull requests in the repository. Can be `all` or `collaborators_only`. If omitted, the provider reads the current remote value and does not impose a provider-side default. + - `web_commit_signoff_required` - (Optional) Require contributors to sign off on web-based commits. See more in the [GitHub documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-commit-signoff-policy-for-your-repository). - `has_downloads` - (**DEPRECATED**) (Optional) Set to `true` to enable the (deprecated) downloads features on the repository. This attribute is no longer in use, but it hasn't been removed yet. It will be removed in a future version. See [this discussion](https://github.com/orgs/community/discussions/102145#discussioncomment-8351756). From efeb9d41169f5801986d323af733cdcd74a096d6 Mon Sep 17 00:00:00 2001 From: RoFz <4117458+RoFz@users.noreply.github.com> Date: Tue, 21 Apr 2026 19:48:41 +0100 Subject: [PATCH 2/2] refactor: move pull_request_creation_policy to a dedicated resource - refactor: extract pull_request_creation_policy into github_repository_pull_request_creation_policy resource - refactor: revert pull_request_creation_policy field and GraphQL calls from github_repository - refactor: remove isUnsupportedPullRequestCreationPolicyError; dedicated resource fails loudly on missing API field - feat: add github_repository_pull_request_creation_policy resource with GraphQL CRUD and import - test: add acceptance tests for github_repository_pull_request_creation_policy - docs: add documentation for github_repository_pull_request_creation_policy --- github/provider.go | 1 + github/resource_github_repository.go | 33 ----- ...repository_pull_request_creation_policy.go | 120 ++++++++++++++++++ ...itory_pull_request_creation_policy_test.go | 100 +++++++++++++++ github/resource_github_repository_test.go | 37 ------ github/util_v4_repository.go | 16 --- github/util_v4_repository_test.go | 37 ------ website/docs/r/repository.html.markdown | 2 - ...pull_request_creation_policy.html.markdown | 40 ++++++ 9 files changed, 261 insertions(+), 125 deletions(-) create mode 100644 github/resource_github_repository_pull_request_creation_policy.go create mode 100644 github/resource_github_repository_pull_request_creation_policy_test.go create mode 100644 website/docs/r/repository_pull_request_creation_policy.html.markdown diff --git a/github/provider.go b/github/provider.go index 1baf0263ad..e562bc451e 100644 --- a/github/provider.go +++ b/github/provider.go @@ -200,6 +200,7 @@ func Provider() *schema.Provider { "github_repository_milestone": resourceGithubRepositoryMilestone(), "github_repository_project": resourceGithubRepositoryProject(), "github_repository_pull_request": resourceGithubRepositoryPullRequest(), + "github_repository_pull_request_creation_policy": resourceGithubRepositoryPullRequestCreationPolicy(), "github_repository_ruleset": resourceGithubRepositoryRuleset(), "github_repository_topics": resourceGithubRepositoryTopics(), "github_repository_webhook": resourceGithubRepositoryWebhook(), diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index bcc7c57c9d..71688c102c 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -284,13 +284,6 @@ func resourceGithubRepository() *schema.Resource { Default: false, Description: "Automatically delete head branch after a pull request is merged. Defaults to 'false'.", }, - "pull_request_creation_policy": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "collaborators_only"}, false)), - Description: "Controls who can create pull requests in the repository. Can be `all` or `collaborators_only`.", - }, "web_commit_signoff_required": { Type: schema.TypeBool, Optional: true, @@ -877,19 +870,6 @@ func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, m } } - pullRequestCreationPolicy, err := getRepositoryPullRequestCreationPolicy(ctx, owner, repoName, meta) - if err != nil { - if isUnsupportedPullRequestCreationPolicyError(err) { - log.Printf("[DEBUG] Skipping pull_request_creation_policy read for %s/%s: %s", owner, repoName, err) - } else { - return diag.Errorf("error reading repository pull request creation policy: %s", err.Error()) - } - } else { - if err = d.Set("pull_request_creation_policy", pullRequestCreationPolicy); err != nil { - return diag.FromErr(err) - } - } - // Set fork information if this is a fork if repo.GetFork() { _ = d.Set("fork", "true") @@ -1015,19 +995,6 @@ func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, } } - if d.IsNewResource() || d.HasChange("pull_request_creation_policy") { - if v, ok := d.GetOk("pull_request_creation_policy"); ok { - repositoryID, err := getRepositoryID(repo.GetName(), meta) - if err != nil { - return diag.Errorf("error resolving repository id for pull request creation policy update: %s", err.Error()) - } - - if err := updateRepositoryPullRequestCreationPolicy(ctx, repositoryID, v.(string), meta); err != nil { - return diag.Errorf("error updating repository pull request creation policy: %s", err.Error()) - } - } - } - if d.IsNewResource() || d.HasChange("vulnerability_alerts") { if v, ok := d.GetOkExists("vulnerability_alerts"); ok { //nolint:staticcheck // SA1019 // We sometimes need to use GetOkExists for booleans if val, ok := v.(bool); ok { diff --git a/github/resource_github_repository_pull_request_creation_policy.go b/github/resource_github_repository_pull_request_creation_policy.go new file mode 100644 index 0000000000..3b37e0cfc1 --- /dev/null +++ b/github/resource_github_repository_pull_request_creation_policy.go @@ -0,0 +1,120 @@ +package github + +import ( + "context" + "fmt" + + "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 resourceGithubRepositoryPullRequestCreationPolicy() *schema.Resource { + return &schema.Resource{ + Description: "Manages the pull request creation policy for a repository.", + CreateContext: resourceGithubRepositoryPullRequestCreationPolicyCreate, + ReadContext: resourceGithubRepositoryPullRequestCreationPolicyRead, + UpdateContext: resourceGithubRepositoryPullRequestCreationPolicyUpdate, + DeleteContext: resourceGithubRepositoryPullRequestCreationPolicyDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubRepositoryPullRequestCreationPolicyImport, + }, + + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the GitHub repository.", + }, + "policy": { + Type: schema.TypeString, + Required: true, + Description: "Controls who can create pull requests for the repository. Can be `all` or `collaborators_only`.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "collaborators_only"}, false)), + }, + }, + } +} + +func resourceGithubRepositoryPullRequestCreationPolicyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + repoName := d.Get("repository").(string) + policy := d.Get("policy").(string) + + nodeID, err := getRepositoryID(repoName, meta) + if err != nil { + return diag.Errorf("error resolving repository node ID for %s: %s", repoName, err) + } + + if err := updateRepositoryPullRequestCreationPolicy(ctx, nodeID, policy, meta); err != nil { + return diag.Errorf("error setting pull request creation policy for %s: %s", repoName, err) + } + + d.SetId(repoName) + return resourceGithubRepositoryPullRequestCreationPolicyRead(ctx, d, meta) +} + +func resourceGithubRepositoryPullRequestCreationPolicyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + owner := meta.(*Owner).name + repoName := d.Id() + + policy, err := getRepositoryPullRequestCreationPolicy(ctx, owner, repoName, meta) + if err != nil { + return diag.Errorf("error reading pull request creation policy for %s/%s: %s", owner, repoName, err) + } + + if err := d.Set("policy", policy); err != nil { + return diag.FromErr(err) + } + if err := d.Set("repository", repoName); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceGithubRepositoryPullRequestCreationPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + repoName := d.Id() + policy := d.Get("policy").(string) + + nodeID, err := getRepositoryID(repoName, meta) + if err != nil { + return diag.Errorf("error resolving repository node ID for %s: %s", repoName, err) + } + + if err := updateRepositoryPullRequestCreationPolicy(ctx, nodeID, policy, meta); err != nil { + return diag.Errorf("error updating pull request creation policy for %s: %s", repoName, err) + } + + return resourceGithubRepositoryPullRequestCreationPolicyRead(ctx, d, meta) +} + +func resourceGithubRepositoryPullRequestCreationPolicyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + repoName := d.Id() + + nodeID, err := getRepositoryID(repoName, meta) + if err != nil { + return diag.Errorf("error resolving repository node ID for %s: %s", repoName, err) + } + + if err := updateRepositoryPullRequestCreationPolicy(ctx, nodeID, "all", meta); err != nil { + return diag.Errorf("error resetting pull request creation policy for %s: %s", repoName, err) + } + + return nil +} + +func resourceGithubRepositoryPullRequestCreationPolicyImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + repoName := d.Id() + + if err := d.Set("repository", repoName); err != nil { + return nil, err + } + + diags := resourceGithubRepositoryPullRequestCreationPolicyRead(ctx, d, meta) + if diags.HasError() { + return nil, fmt.Errorf("%s", diags[0].Summary) + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_repository_pull_request_creation_policy_test.go b/github/resource_github_repository_pull_request_creation_policy_test.go new file mode 100644 index 0000000000..ed86001018 --- /dev/null +++ b/github/resource_github_repository_pull_request_creation_policy_test.go @@ -0,0 +1,100 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGithubRepositoryPullRequestCreationPolicy(t *testing.T) { + t.Run("sets policy without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%srepo-pr-policy-%s", testResourcePrefix, randomID) + initial := `policy = "collaborators_only"` + updated := `policy = "all"` + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + visibility = "private" + auto_init = true + } + + resource "github_repository_pull_request_creation_policy" "test" { + repository = github_repository.test.name + %%s + } + `, repoName) + + checks := map[string]resource.TestCheckFunc{ + "before": resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_pull_request_creation_policy.test", "policy", + "collaborators_only", + ), + ), + "after": resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository_pull_request_creation_policy.test", "policy", + "all", + ), + ), + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(config, initial), + Check: checks["before"], + }, + { + Config: fmt.Sprintf(config, updated), + Check: checks["after"], + }, + }, + }) + }) + + t.Run("imports without error", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%srepo-pr-policy-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + visibility = "private" + auto_init = true + } + + resource "github_repository_pull_request_creation_policy" "test" { + repository = github_repository.test.name + policy = "collaborators_only" + } + `, repoName) + + check := resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_repository_pull_request_creation_policy.test", "repository"), + resource.TestCheckResourceAttr("github_repository_pull_request_creation_policy.test", "policy", "collaborators_only"), + ) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnauthenticated(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + ResourceName: "github_repository_pull_request_creation_policy.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index b5abfb82b5..cc6df3c64d 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -1334,43 +1334,6 @@ resource "github_repository" "test" { }) }) - t.Run("check_pull_request_creation_policy", func(t *testing.T) { - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - testRepoName := fmt.Sprintf("%spr-policy-%s", testResourcePrefix, randomID) - config := ` - resource "github_repository" "test" { - name = "%s" - visibility = "%s" - pull_request_creation_policy = "%s" - } - ` - - resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnauthenticated(t) }, - ProviderFactories: providerFactories, - Steps: []resource.TestStep{ - { - Config: fmt.Sprintf(config, testRepoName, testAccConf.testRepositoryVisibility, "collaborators_only"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository.test", "pull_request_creation_policy", "collaborators_only"), - ), - }, - { - Config: fmt.Sprintf(config, testRepoName, testAccConf.testRepositoryVisibility, "all"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("github_repository.test", "pull_request_creation_policy", "all"), - ), - }, - { - ResourceName: "github_repository.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"auto_init", "vulnerability_alerts", "ignore_vulnerability_alerts_during_read"}, - }, - }, - }) - }) - t.Run("check_web_commit_signoff_required_organization_enabled_but_not_set", func(t *testing.T) { t.Skip("This test should be run manually after confirming that the test organization has 'Require contributors to sign off on web-based commits' enabled under Organizations -> Settings -> Repository -> Repository defaults.") diff --git a/github/util_v4_repository.go b/github/util_v4_repository.go index b02afadc76..b083008391 100644 --- a/github/util_v4_repository.go +++ b/github/util_v4_repository.go @@ -5,7 +5,6 @@ import ( "encoding/base64" "errors" "fmt" - "strings" "github.com/shurcooL/githubv4" ) @@ -110,21 +109,6 @@ func expandPullRequestCreationPolicy(policy string) (PullRequestCreationPolicy, } } -func isUnsupportedPullRequestCreationPolicyError(err error) bool { - if err == nil { - return false - } - - message := strings.ToLower(err.Error()) - - return strings.Contains(message, "pullrequestcreationpolicy") && - (strings.Contains(message, "doesn't exist") || - strings.Contains(message, "does not exist") || - strings.Contains(message, "undefined field") || - strings.Contains(message, "unknown argument") || - strings.Contains(message, "cannot query field")) -} - func getRepositoryPullRequestCreationPolicy(ctx context.Context, owner, name string, meta any) (string, error) { var query struct { Repository struct { diff --git a/github/util_v4_repository_test.go b/github/util_v4_repository_test.go index c88a0cae3c..2c04e6132f 100644 --- a/github/util_v4_repository_test.go +++ b/github/util_v4_repository_test.go @@ -3,7 +3,6 @@ package github import ( "bytes" "context" - "errors" "io" "net/http" "net/http/httptest" @@ -293,42 +292,6 @@ func TestRepositoryPullRequestCreationPolicyGraphQL(t *testing.T) { } } -func TestIsUnsupportedPullRequestCreationPolicyError(t *testing.T) { - cases := []struct { - name string - err error - want bool - }{ - { - name: "missing field", - err: errors.New(`Field 'pullRequestCreationPolicy' doesn't exist on type 'Repository'`), - want: true, - }, - { - name: "cannot query field", - err: errors.New(`Cannot query field "pullRequestCreationPolicy" on type "Repository".`), - want: true, - }, - { - name: "unrelated graphql error", - err: errors.New(`Could not resolve to a Repository with the name 'integrations/terraform-provider-github'.`), - want: false, - }, - { - name: "nil", - err: nil, - want: false, - }, - } - - for _, tc := range cases { - got := isUnsupportedPullRequestCreationPolicyError(tc.err) - if got != tc.want { - t.Fatalf("%s: got %t want %t", tc.name, got, tc.want) - } - } -} - // localRoundTripper is an http.RoundTripper that executes HTTP transactions // by using handler directly, instead of going over an HTTP connection. type localRoundTripper struct { diff --git a/website/docs/r/repository.html.markdown b/website/docs/r/repository.html.markdown index 954e08a6a6..182e98125d 100644 --- a/website/docs/r/repository.html.markdown +++ b/website/docs/r/repository.html.markdown @@ -112,8 +112,6 @@ The following arguments are supported: - `delete_branch_on_merge` - (Optional) Automatically delete head branch after a pull request is merged. Defaults to `false`. -- `pull_request_creation_policy` - (Optional) Controls who can create pull requests in the repository. Can be `all` or `collaborators_only`. If omitted, the provider reads the current remote value and does not impose a provider-side default. - - `web_commit_signoff_required` - (Optional) Require contributors to sign off on web-based commits. See more in the [GitHub documentation](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/managing-repository-settings/managing-the-commit-signoff-policy-for-your-repository). - `has_downloads` - (**DEPRECATED**) (Optional) Set to `true` to enable the (deprecated) downloads features on the repository. This attribute is no longer in use, but it hasn't been removed yet. It will be removed in a future version. See [this discussion](https://github.com/orgs/community/discussions/102145#discussioncomment-8351756). diff --git a/website/docs/r/repository_pull_request_creation_policy.html.markdown b/website/docs/r/repository_pull_request_creation_policy.html.markdown new file mode 100644 index 0000000000..4c2856fa64 --- /dev/null +++ b/website/docs/r/repository_pull_request_creation_policy.html.markdown @@ -0,0 +1,40 @@ +--- +layout: "github" +page_title: "GitHub: github_repository_pull_request_creation_policy" +description: |- + Manages the pull request creation policy for a repository +--- + +# github_repository_pull_request_creation_policy + +This resource allows you to manage the pull request creation policy for a repository. The policy controls who is allowed to create pull requests. + +## Example Usage + +```hcl +resource "github_repository" "example" { + name = "example-repo" + visibility = "private" +} + +resource "github_repository_pull_request_creation_policy" "example" { + repository = github_repository.example.name + policy = "collaborators_only" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `repository` - (Required) The name of the GitHub repository. Changing this forces a new resource. + +* `policy` - (Required) Controls who can create pull requests in the repository. Supported values are `all` and `collaborators_only`. + +## Import + +The pull request creation policy can be imported using the repository name. + +```sh +terraform import github_repository_pull_request_creation_policy.example my-repo +```