Repository navigation
Add github_repository_environment_deployment_protection_rule resource #3566
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
142296e
7f34c33
c6d66b0
26614a3
2b5bc5e
7e0bef3
a84af47
fbaaba1
278f1c3
f2b5c5a
c732a38
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| --- | ||
| 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" | ||
| } |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing tests |
| 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{ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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": { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| } | ||
There was a problem hiding this comment.
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:
Look into
templates/resources/repository_environment_deployment_policy.md.tmplfor example