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
1 change: 1 addition & 0 deletions RESOURCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ The overall status of each resource or data source is captured in this document
| `github_repository_deployment_branch_policy` (🚫) | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
| `github_repository_environment` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
| `github_repository_environment_deployment_policy` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
| `github_repository_environment_deployment_protection_rule` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
| `github_repository_file` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
| `github_repository_milestone` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
| `github_repository_pages` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The documentation should be generated through templates after running make generatedocs.

I've tested locally and deleting it is actually not working.
I would expect the following templates:

  • templates/resources/repository_environment_deployment_protection_rule.md.tmpl
  • examples/resources/repository_environment_deployment_protection_rule/example_1.tf (usage)
  • examples/resources/repository_environment_deployment_protection_rule/example_2.tf (import block)

Look into templates/resources/repository_environment_deployment_policy.md.tmpl for example

page_title: "github_repository_environment_deployment_protection_rule (Resource) - GitHub"
description: |-
Creates and manages custom deployment protection rules for GitHub repository environments
---

# github_repository_environment_deployment_protection_rule (Resource)

Manages a custom deployment protection rule for a GitHub repository environment. A custom deployment protection rule is a GitHub App that approves or rejects deployments to the environment.

When a workflow job targets the environment, GitHub sends the App a `deployment_protection_rule` webhook and waits for the App to approve or reject the deployment. The App must be installed on the repository and available as a custom deployment protection rule for the environment before it can be enabled.

## Example Usage

```terraform
resource "github_repository" "example" {
name = "example"
}

resource "github_repository_environment" "example" {
repository = github_repository.example.name
environment = "production"
}

resource "github_repository_environment_deployment_protection_rule" "example" {
repository = github_repository.example.name
environment = github_repository_environment.example.environment
integration_id = 123456 # The ID of the GitHub App
}
```

## Argument Reference

The following arguments are supported:

- `repository` - (Required) The name of the GitHub repository.

- `environment` - (Required) The name of the environment. Changing this forces a new resource.

- `integration_id` - (Required) The ID of the GitHub App to enable as a custom deployment protection rule. Changing this forces a new resource.

## Attributes Reference

- `repository_id` - The ID of the repository.
- `protection_rule_id` - The ID of the custom deployment protection rule.

## Import

This resource can be imported using an ID made of the repository name, environment name (any `:` in the environment name needs to be escaped as `??`), and custom deployment protection rule ID, all separated by a `:`.

### Import Block

The following import block imports a custom deployment protection rule with the ID `123456` for the repo `myrepo` and environment `myenv` to a `github_repository_environment_deployment_protection_rule` resource named `example`.

```terraform
import {
to = github_repository_environment_deployment_protection_rule.example
id = "myrepo:myenv:123456"
}
```

### Import Command

The following command imports a custom deployment protection rule with the ID `123456` for the repo `myrepo` and environment `myenv` to a `github_repository_environment_deployment_protection_rule` resource named `example`.

```shell
terraform import github_repository_environment_deployment_protection_rule.example myrepo:myenv:123456
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
resource "github_repository" "example" {
name = "example"
}

resource "github_repository_environment" "example" {
repository = github_repository.example.name
environment = "production"
}

resource "github_repository_environment_deployment_protection_rule" "example" {
repository = github_repository.example.name
environment = github_repository_environment.example.environment
integration_id = 123456 # The ID of the GitHub App
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {
to = github_repository_environment_deployment_protection_rule.example
id = "myrepo:myenv:123456"
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ func NewProvider(version, commit string) func() *schema.Provider {
"github_repository_deployment_branch_policy": resourceGithubRepositoryDeploymentBranchPolicy(),
"github_repository_environment": resourceGithubRepositoryEnvironment(),
"github_repository_environment_deployment_policy": resourceGithubRepositoryEnvironmentDeploymentPolicy(),
"github_repository_environment_deployment_protection_rule": resourceGithubRepositoryEnvironmentDeploymentProtectionRule(),
"github_repository_file": resourceGithubRepositoryFile(),
"github_repository_milestone": resourceGithubRepositoryMilestone(),
"github_repository_pages": resourceGithubRepositoryPages(),
Expand Down

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing tests resource_github_repository_environment_deployment_protection_rule_test.go with TestAccGithubRepositoryEnvironmentDeploymentProtectionRule

Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package github

import (
"context"
"errors"
"fmt"
"net/http"
"strconv"

"github.com/google/go-github/v92/github"
"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 resourceGithubRepositoryEnvironmentDeploymentProtectionRule() *schema.Resource {
return &schema.Resource{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This resource is missing a Description field used for the documentation generation

Description: "Manages a custom deployment protection rule for a GitHub repository environment. A custom deployment protection rule is a GitHub App that approves or rejects deployments to the environment.",

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

You should have a computed repository_id to be resilient to repository name changes

Description: "The name of the GitHub repository.",
Type: schema.TypeString,
Required: true,
},
"repository_id": {
Description: "The ID of the GitHub repository.",
Type: schema.TypeInt,
Computed: true,
},
"environment": {
Description: "The name of the environment.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"integration_id": {
Description: "The ID of the GitHub App to enable as a custom deployment protection rule. The App must be installed on the repository and available as a custom deployment protection rule for the environment.",
Type: schema.TypeInt,
Required: true,
ForceNew: true,
ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)),
},
"protection_rule_id": {
Description: "The ID of the custom deployment protection rule.",
Type: schema.TypeInt,
Computed: true,
},
},

CustomizeDiff: diffRepository,

CreateContext: resourceGithubRepositoryEnvironmentDeploymentProtectionRuleCreate,
ReadContext: resourceGithubRepositoryEnvironmentDeploymentProtectionRuleRead,
UpdateContext: resourceGithubRepositoryEnvironmentDeploymentProtectionRuleUpdate,
DeleteContext: resourceGithubRepositoryEnvironmentDeploymentProtectionRuleDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceGithubRepositoryEnvironmentDeploymentProtectionRuleImport,
},
}
}

func resourceGithubRepositoryEnvironmentDeploymentProtectionRuleCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta, _ := m.(*Owner)
client := meta.v3client
owner := meta.name

repoName, _ := d.Get("repository").(string)
envName, _ := d.Get("environment").(string)
integrationID, _ := d.Get("integration_id").(int)

repo, _, err := client.Repositories.Get(ctx, owner, repoName)
if err != nil {
return diag.FromErr(err)
}

rule, _, err := client.Repositories.CreateCustomDeploymentProtectionRule(ctx, owner, repoName, envName, &github.CustomDeploymentProtectionRuleRequest{
IntegrationID: new(int64(integrationID)),
})
if err != nil {
return diag.FromErr(err)
}

id, err := buildID(repoName, escapeIDPart(envName), strconv.FormatInt(rule.GetID(), 10))
if err != nil {
return diag.FromErr(err)
}
d.SetId(id)

if err := d.Set("repository_id", int(repo.GetID())); err != nil {
return diag.FromErr(err)
}
if err := d.Set("protection_rule_id", int(rule.GetID())); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceGithubRepositoryEnvironmentDeploymentProtectionRuleRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
ctx = tflog.SetField(ctx, "id", d.Id())

meta, _ := m.(*Owner)
client := meta.v3client
owner := meta.name

repoName, _ := d.Get("repository").(string)
envName, _ := d.Get("environment").(string)
ruleID, _ := d.Get("protection_rule_id").(int)

rule, _, err := client.Repositories.GetCustomDeploymentProtectionRule(ctx, owner, repoName, envName, int64(ruleID))
if err != nil {
if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound {
tflog.Info(ctx, "Custom deployment protection rule not found, removing from state.", map[string]any{"repository": repoName, "environment": envName, "protection_rule_id": ruleID})
d.SetId("")
return nil
}
return diag.FromErr(err)
}

if err := d.Set("integration_id", int(rule.GetApp().GetID())); err != nil {
return diag.FromErr(err)
}

return nil
}

// Update only runs when repository changes without diffRepository forcing a new resource. That is a rename, or a
// repository diffRepository could not look up at plan time, so check the new name is still the same repository.
func resourceGithubRepositoryEnvironmentDeploymentProtectionRuleUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
meta, _ := m.(*Owner)
client := meta.v3client
owner := meta.name

repoName, _ := d.Get("repository").(string)
envName, _ := d.Get("environment").(string)
ruleID, _ := d.Get("protection_rule_id").(int)
repoID, _ := d.Get("repository_id").(int)

repo, _, err := client.Repositories.Get(ctx, owner, repoName)
if err != nil {
return diag.FromErr(err)
}
if repo.GetID() != int64(repoID) {
return diag.Errorf("repository %q (ID %d) is not the repository this protection rule was created in (ID %d); replace the resource to move the rule to another repository", repoName, repo.GetID(), repoID)
}

id, err := buildID(repoName, escapeIDPart(envName), strconv.Itoa(ruleID))
if err != nil {
return diag.FromErr(err)
}
d.SetId(id)

return nil
}

func resourceGithubRepositoryEnvironmentDeploymentProtectionRuleDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics {
ctx = tflog.SetField(ctx, "id", d.Id())

meta, _ := m.(*Owner)
client := meta.v3client
owner := meta.name

repoName, _ := d.Get("repository").(string)
envName, _ := d.Get("environment").(string)
ruleID, _ := d.Get("protection_rule_id").(int)

_, err := client.Repositories.DisableCustomDeploymentProtectionRule(ctx, owner, repoName, envName, int64(ruleID))
if err != nil {
if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok && ghErr.Response.StatusCode == http.StatusNotFound {
tflog.Info(ctx, "Custom deployment protection rule not found, treating it as deleted.", map[string]any{"repository": repoName, "environment": envName, "protection_rule_id": ruleID})
return nil
}
return diag.FromErr(err)
}

return nil
}

func resourceGithubRepositoryEnvironmentDeploymentProtectionRuleImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) {
meta, _ := m.(*Owner)
client := meta.v3client
owner := meta.name

repoName, envNamePart, ruleIDStr, err := parseID3(d.Id())
if err != nil {
return nil, fmt.Errorf("invalid id (%s), expected format <repository>:<environment>:<protection_rule_id>", d.Id())
}

ruleID, err := strconv.Atoi(ruleIDStr)
if err != nil {
return nil, fmt.Errorf("invalid protection rule id: %s", ruleIDStr)
}

repo, _, err := client.Repositories.Get(ctx, owner, repoName)
if err != nil {
return nil, fmt.Errorf("failed to retrieve repository %s: %w", repoName, err)
}

if err := d.Set("repository", repoName); err != nil {
return nil, err
}
if err := d.Set("repository_id", int(repo.GetID())); err != nil {
return nil, err
}
if err := d.Set("environment", unescapeIDPart(envNamePart)); err != nil {
return nil, err
}
if err := d.Set("protection_rule_id", ruleID); err != nil {
return nil, err
}

return []*schema.ResourceData{d}, nil
}
Loading
Loading