Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions docs/resources/repository_pull_request_creation_policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
page_title: "github_repository_pull_request_creation_policy (Resource) - GitHub"
description: |-
Manages the pull request creation policy for a repository
---

# github_repository_pull_request_creation_policy (Resource)

This resource allows you to manage the pull request creation policy for a repository. The policy controls who is allowed to create pull requests in the repository.

Destroying this resource does not delete anything on GitHub; it resets the repository's pull request creation policy to `all`.

## Example Usage

```terraform
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. Renaming the repository is supported without recreating this resource.

- `policy` - (Required) Controls who can create pull requests in the repository. Can be `all` or `collaborators_only`.

## Attribute Reference

In addition to the above arguments, the following attributes are exported:

- `repository_id` - The numeric ID of the GitHub repository.

## Import

The pull request creation policy can be imported using the repository name.

```shell
terraform import github_repository_pull_request_creation_policy.example my-repo
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
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"
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ func NewProvider() func() *schema.Provider {
"github_repository_pages": resourceGithubRepositoryPages(),
"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(),
Expand Down
138 changes: 138 additions & 0 deletions github/resource_github_repository_pull_request_creation_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
package github

import (
"context"

"github.com/hashicorp/terraform-plugin-log/tflog"
"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: schema.ImportStatePassthroughContext,
},
CustomizeDiff: diffRepository,

Schema: map[string]*schema.Schema{
"repository": {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add also repository_id and the diffRepo CustomizeDiff configuration to support renaming repositories

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 97d8104: removed ForceNew from repository, added a computed repository_id, and set CustomizeDiff: diffRepository, per the "Repository as a Required Argument" convention. An acceptance subtest renames the repository and asserts the resource is updated in place rather than replaced, with the policy preserved.

Type: schema.TypeString,
Required: true,
Description: "The name of the GitHub repository.",
},
"repository_id": {
Type: schema.TypeInt,
Computed: true,
Description: "The numeric ID 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)),
},
},
}
Comment on lines +24 to +42

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in 97d8104. Added the computed repository_id, CustomizeDiff: diffRepository, and removed ForceNew, with an acceptance test covering the rename-in-place behavior.

}

func resourceGithubRepositoryPullRequestCreationPolicyCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
owner := meta.(*Owner).name
repoName := d.Get("repository").(string)
policy := d.Get("policy").(string)

nodeID, databaseID, err := getRepositoryNodeAndDatabaseID(ctx, owner, repoName, meta)
if err != nil {
return diag.Errorf("error resolving repository 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)
if err := d.Set("repository_id", databaseID); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubRepositoryPullRequestCreationPolicyRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
owner := meta.(*Owner).name
repoName := d.Id()

policy, databaseID, err := getRepositoryPullRequestCreationPolicy(ctx, owner, repoName, meta)
if err != nil {
if isRepositoryNotFoundError(err) {
tflog.Info(ctx, "Repository no longer exists, removing pull request creation policy from state", map[string]any{
"owner": owner,
"repository": repoName,
})
d.SetId("")
return nil
}
return diag.Errorf("error reading pull request creation policy for %s/%s: %s", owner, repoName, err)
}
Comment on lines +72 to +82

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please differentiate between an actual error and for example the "repo doesn't exist anymore" case where this resource can be removed from state directly

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in b2f15e8: Read now detects the repository-not-found GraphQL error and removes the resource from state instead of failing. The detection lives in an isRepositoryNotFoundError helper with a unit test that pins GitHub's actual not-found payload, and the log uses structured tflog.


if err := d.Set("policy", policy); err != nil {
return diag.FromErr(err)
}
if err := d.Set("repository", repoName); err != nil {
return diag.FromErr(err)
}
if err := d.Set("repository_id", databaseID); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubRepositoryPullRequestCreationPolicyUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
owner := meta.(*Owner).name
repoName := d.Get("repository").(string)
policy := d.Get("policy").(string)

nodeID, databaseID, err := getRepositoryNodeAndDatabaseID(ctx, owner, repoName, meta)
if err != nil {
return diag.Errorf("error resolving repository 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)
}

d.SetId(repoName)
if err := d.Set("repository_id", databaseID); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubRepositoryPullRequestCreationPolicyDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
repoName := d.Id()

nodeID, err := getRepositoryID(repoName, meta)
if err != nil {
if isRepositoryNotFoundError(err) {
tflog.Info(ctx, "Repository no longer exists, nothing to reset for the pull request creation policy", map[string]any{
"repository": repoName,
})
return 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)
}
Comment on lines +123 to +135

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check if it's an actual error before returning an error. Is the repo gone? Not an actual error, delete resource from state and be happy.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 890cab5: Delete treats a missing repository as success. The guard sits on the by-name repository resolution, since that is where a missing repo surfaces with the error string the helper verifies. I did not guard the subsequent node-ID mutation: GitHub returns a different error shape there ("Could not resolve to a node with the global id"), it is only reachable in a delete-between-calls race, and a retry converges through the by-name guard anyway.


return nil
}
171 changes: 171 additions & 0 deletions github/resource_github_repository_pull_request_creation_policy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package github

import (
"fmt"
"testing"

"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/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
)

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 := "collaborators_only"
updated := "all"

config := `
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 = "%s"
}
`

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnauthenticated(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(config, repoName, initial),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("policy"),
knownvalue.StringExact(initial),
),
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("repository_id"),
knownvalue.NotNull(),
),
},
},
{
Config: fmt.Sprintf(config, repoName, updated),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("policy"),
knownvalue.StringExact(updated),
),
},
},
},
})
})

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)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnauthenticated(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("repository"),
knownvalue.StringExact(repoName),
),
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("policy"),
knownvalue.StringExact("collaborators_only"),
),
},
},
{
ResourceName: "github_repository_pull_request_creation_policy.test",
ImportState: true,
ImportStateVerify: true,
},
},
})
})

t.Run("survives repository rename without replacement", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
repoName := fmt.Sprintf("%srepo-pr-policy-%s", testResourcePrefix, randomID)
renamedRepoName := fmt.Sprintf("%srepo-pr-policy-renamed-%s", testResourcePrefix, randomID)

config := `
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"
}
`

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnauthenticated(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: fmt.Sprintf(config, repoName),
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("repository"),
knownvalue.StringExact(repoName),
),
},
},
{
Config: fmt.Sprintf(config, renamedRepoName),
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction(
"github_repository_pull_request_creation_policy.test",
plancheck.ResourceActionUpdate,
),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("repository"),
knownvalue.StringExact(renamedRepoName),
),
statecheck.ExpectKnownValue(
"github_repository_pull_request_creation_policy.test",
tfjsonpath.New("policy"),
knownvalue.StringExact("collaborators_only"),
),
},
},
},
})
})
}
Loading