From 282657299281c07a70147abf7473e55fbbed56e3 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 26 Dec 2025 23:03:25 +0200 Subject: [PATCH 1/5] `github_repository`: Convert to use Context-aware functions Signed-off-by: Timo Sand --- github/resource_github_repository.go | 92 ++++++++++++++-------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 2be5f8717f..a403a2e23d 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/google/go-github/v67/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -17,12 +18,12 @@ import ( func resourceGithubRepository() *schema.Resource { return &schema.Resource{ - Create: resourceGithubRepositoryCreate, - Read: resourceGithubRepositoryRead, - Update: resourceGithubRepositoryUpdate, - Delete: resourceGithubRepositoryDelete, + CreateContext: resourceGithubRepositoryCreate, + ReadContext: resourceGithubRepositoryRead, + UpdateContext: resourceGithubRepositoryUpdate, + DeleteContext: resourceGithubRepositoryDelete, Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { if err := d.Set("auto_init", false); err != nil { return nil, err } @@ -620,18 +621,17 @@ func resourceGithubRepositoryObject(d *schema.ResourceData) *github.Repository { return repository } -func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client if branchName, hasDefaultBranch := d.GetOk("default_branch"); hasDefaultBranch && (branchName != "main") { - return fmt.Errorf("cannot set the default branch on a new repository to something other than 'main'") + return diag.Errorf("cannot set the default branch on a new repository to something other than 'main'") } repoReq := resourceGithubRepositoryObject(d) owner := meta.(*Owner).name repoName := repoReq.GetName() - ctx := context.Background() // determine if repository should be private. assume public to start isPrivate := false @@ -657,7 +657,7 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { for _, templateConfigBlock := range templateConfigBlocks { templateConfigMap, ok := templateConfigBlock.(map[string]any) if !ok { - return errors.New("failed to unpack template configuration block") + return diag.FromErr(errors.New("failed to unpack template configuration block")) } templateRepo := templateConfigMap["repository"].(string) @@ -678,7 +678,7 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { &templateRepoReq, ) if err != nil { - return err + return diag.FromErr(err) } d.SetId(*repo.Name) @@ -691,7 +691,7 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { log.Printf("[INFO] Creating fork of %s/%s in %s", sourceOwner, sourceRepo, owner) if sourceOwner == "" || sourceRepo == "" { - return fmt.Errorf("source_owner and source_repo must be provided when forking a repository") + return diag.Errorf("source_owner and source_repo must be provided when forking a repository") } // Create the fork using the GitHub client library @@ -712,18 +712,18 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { log.Printf("[INFO] Fork is being created asynchronously") // Despite the 202 status, the API should still return preliminary fork information if fork == nil { - return fmt.Errorf("fork information not available after accepted status") + return diag.Errorf("fork information not available after accepted status") } log.Printf("[DEBUG] Fork name: %s", fork.GetName()) } else { - return fmt.Errorf("failed to create fork: %w", err) + return diag.Errorf("failed to create fork: %s", err.Error()) } } else if resp != nil { log.Printf("[DEBUG] Fork response status: %d", resp.StatusCode) } if fork == nil { - return fmt.Errorf("fork creation failed - no repository returned") + return diag.Errorf("fork creation failed - no repository returned") } log.Printf("[INFO] Fork created with name: %s", fork.GetName()) @@ -747,7 +747,7 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { repo, _, err = client.Repositories.Create(ctx, "", repoReq) } if err != nil { - return err + return diag.FromErr(err) } d.SetId(repo.GetName()) } @@ -756,7 +756,7 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { if len(topics) > 0 { _, _, err := client.Repositories.ReplaceAllTopics(ctx, owner, repoName, topics) if err != nil { - return err + return diag.FromErr(err) } } @@ -764,19 +764,19 @@ func resourceGithubRepositoryCreate(d *schema.ResourceData, meta any) error { if pages != nil { _, _, err := client.Repositories.EnablePages(ctx, owner, repoName, pages) if err != nil { - return err + return diag.FromErr(err) } } err := updateVulnerabilityAlerts(d, client, ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } - return resourceGithubRepositoryUpdate(d, meta) + return resourceGithubRepositoryUpdate(ctx, d, meta) } -func resourceGithubRepositoryRead(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name @@ -788,7 +788,7 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta any) error { owner = explicitOwner } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -807,7 +807,7 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta any) error { return nil } } - return err + return diag.FromErr(err) } _ = d.Set("etag", resp.Header.Get("ETag")) @@ -853,10 +853,10 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta any) error { if repo.GetHasPages() { pages, _, err := client.Repositories.GetPagesInfo(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } if err := d.Set("pages", flattenPages(pages)); err != nil { - return fmt.Errorf("error setting pages: %w", err) + return diag.Errorf("error setting pages: %s", err.Error()) } } @@ -882,32 +882,32 @@ func resourceGithubRepositoryRead(d *schema.ResourceData, meta any) error { "repository": repo.TemplateRepository.Name, }, }); err != nil { - return err + return diag.FromErr(err) } } else { if err = d.Set("template", []any{}); err != nil { - return err + return diag.FromErr(err) } } if !d.Get("ignore_vulnerability_alerts_during_read").(bool) { vulnerabilityAlerts, _, err := client.Repositories.GetVulnerabilityAlerts(ctx, owner, repoName) if err != nil { - return fmt.Errorf("error reading repository vulnerability alerts: %w", err) + return diag.Errorf("error reading repository vulnerability alerts: %s", err.Error()) } if err = d.Set("vulnerability_alerts", vulnerabilityAlerts); err != nil { - return err + return diag.FromErr(err) } } if err = d.Set("security_and_analysis", flattenSecurityAndAnalysis(repo.GetSecurityAndAnalysis())); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { // Can only update a repository if it is not archived or the update is to // archive the repository (unarchiving is not supported by the GitHub API) if d.Get("archived").(bool) && !d.HasChange("archived") { @@ -937,7 +937,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { repoName := d.Id() owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) // When the organization has "Require sign off on web-based commits" enabled, // the API doesn't allow you to send `web_commit_signoff_required` in order to @@ -955,7 +955,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { repo, _, err := client.Repositories.Edit(ctx, owner, repoName, repoReq) if err != nil { - return err + return diag.FromErr(err) } d.SetId(*repo.Name) @@ -964,7 +964,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { if opts != nil { pages, res, err := client.Repositories.GetPagesInfo(ctx, owner, repoName) if res.StatusCode != http.StatusNotFound && err != nil { - return err + return diag.FromErr(err) } if pages == nil { @@ -973,12 +973,12 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { _, err = client.Repositories.UpdatePages(ctx, owner, repoName, opts) } if err != nil { - return err + return diag.FromErr(err) } } else { _, err := client.Repositories.DisablePages(ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } } } @@ -987,7 +987,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { topics := repoReq.Topics _, _, err = client.Repositories.ReplaceAllTopics(ctx, owner, *repo.Name, topics) if err != nil { - return err + return diag.FromErr(err) } d.SetId(*repo.Name) @@ -995,7 +995,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { topics := repoReq.Topics _, _, err = client.Repositories.ReplaceAllTopics(ctx, owner, *repo.Name, topics) if err != nil { - return err + return diag.FromErr(err) } } } @@ -1003,7 +1003,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { if d.HasChange("vulnerability_alerts") { err = updateVulnerabilityAlerts(d, client, ctx, owner, repoName) if err != nil { - return err + return diag.FromErr(err) } } @@ -1014,7 +1014,7 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { _, resp, err := client.Repositories.Edit(ctx, owner, repoName, repoReq) if err != nil { if resp.StatusCode != 422 || !strings.Contains(err.Error(), fmt.Sprintf("Visibility is already %s", n.(string))) { - return err + return diag.FromErr(err) } } } else { @@ -1028,21 +1028,21 @@ func resourceGithubRepositoryUpdate(d *schema.ResourceData, meta any) error { _, _, err = client.Repositories.Edit(ctx, owner, repoName, repoReq) if err != nil { if !strings.Contains(err.Error(), "422 Privacy is already set") { - return err + return diag.FromErr(err) } } } else { log.Printf("[DEBUG] No privacy update required. private: %v", d.Get("private")) } - return resourceGithubRepositoryRead(d, meta) + return resourceGithubRepositoryRead(ctx, d, meta) } -func resourceGithubRepositoryDelete(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client repoName := d.Id() owner := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) archiveOnDestroy := d.Get("archive_on_destroy").(bool) if archiveOnDestroy { @@ -1051,20 +1051,20 @@ func resourceGithubRepositoryDelete(d *schema.ResourceData, meta any) error { return nil } else { if err := d.Set("archived", true); err != nil { - return err + return diag.FromErr(err) } repoReq := resourceGithubRepositoryObject(d) // Always remove `web_commit_signoff_required` when archiving, to avoid 422 error repoReq.WebCommitSignoffRequired = nil log.Printf("[DEBUG] Archiving repository on delete: %s/%s", owner, repoName) _, _, err := client.Repositories.Edit(ctx, owner, repoName, repoReq) - return err + return diag.FromErr(err) } } log.Printf("[DEBUG] Deleting repository: %s/%s", owner, repoName) _, err := client.Repositories.Delete(ctx, owner, repoName) - return err + return diag.FromErr(err) } func expandPages(input []any) *github.Pages { From 4cd8d1c239a6843af009324c6e82ee8ec115151c Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 26 Dec 2025 23:06:07 +0200 Subject: [PATCH 2/5] Add tests to ensure `vulnerability_alerts` behaviour keeps working Signed-off-by: Timo Sand --- github/resource_github_repository_test.go | 166 ++++++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/github/resource_github_repository_test.go b/github/resource_github_repository_test.go index 403935e061..07957b3e7d 100644 --- a/github/resource_github_repository_test.go +++ b/github/resource_github_repository_test.go @@ -2026,6 +2026,172 @@ func TestAccGithubRepository_fork(t *testing.T) { }) } +func TestAccRepository_VulnerabilityAlerts(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + + t.Run("can enable vulnerability alerts", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-vuln-%s" + description = "Terraform acceptance test - repository %[1]s" + vulnerability_alerts = true + } + `, randomID) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "vulnerability_alerts", + "true", + ), + ), + }, + }, + }) + } + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + }) + t.Run("sets vulnerability alerts to false when not set in config", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-vuln-%s" + description = "Terraform acceptance test - repository %[1]s" + } + `, randomID) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "vulnerability_alerts", "false", + ), + ), + }, + }, + }) + } + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + }) + t.Run("can disable vulnerability alerts", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-vuln-%s" + description = "Terraform acceptance test - repository %[1]s" + vulnerability_alerts = false + } + `, randomID) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "vulnerability_alerts", + "false", + ), + ), + }, + }, + }) + } + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + }) + t.Run("can modify vulnerability alerts", func(t *testing.T) { + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "tf-acc-test-vuln-%s" + description = "Terraform acceptance test - repository %[1]s" + vulnerability_alerts = false + } + `, randomID) + + testCase := func(t *testing.T, mode string) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessMode(t, mode) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "vulnerability_alerts", "false", + ), + ), + }, + { + Config: strings.Replace(config, "vulnerability_alerts = false", "vulnerability_alerts = true", 1), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr( + "github_repository.test", "vulnerability_alerts", "true", + ), + ), + }, + }, + }) + } + + t.Run("with an individual account", func(t *testing.T) { + testCase(t, individual) + }) + + t.Run("with an organization account", func(t *testing.T) { + testCase(t, organization) + }) + + t.Run("with an anonymous account", func(t *testing.T) { + t.Skip("anonymous account not supported for this operation") + }) + }) +} + func createForkedRepository(repositoryName string) error { baseURL, isGHES, err := getBaseURL(os.Getenv("GITHUB_BASE_URL")) if err != nil { From 81a9234a3876431e6f342fb485155c3d8418e345 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 26 Dec 2025 23:09:51 +0200 Subject: [PATCH 3/5] Add error handling to ignore 422 response In the case of `vulnerability_alerts = false` and the message indicates control from parent Org/Enterprise Signed-off-by: Timo Sand --- github/resource_github_repository.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index a403a2e23d..a1c2a5a19c 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -1243,11 +1243,17 @@ func resourceGithubParseFullName(resourceDataLike interface { } func updateVulnerabilityAlerts(d *schema.ResourceData, client *github.Client, ctx context.Context, owner, repoName string) error { - updateVulnerabilityAlerts := client.Repositories.DisableVulnerabilityAlerts - if vulnerabilityAlerts, ok := d.GetOk("vulnerability_alerts"); ok && vulnerabilityAlerts.(bool) { - updateVulnerabilityAlerts = client.Repositories.EnableVulnerabilityAlerts + updateVulnerabilityAlertsSDK := client.Repositories.DisableVulnerabilityAlerts + vulnerabilityAlerts, ok := d.GetOk("vulnerability_alerts") + if ok && vulnerabilityAlerts.(bool) { + updateVulnerabilityAlertsSDK = client.Repositories.EnableVulnerabilityAlerts } - _, err := updateVulnerabilityAlerts(ctx, owner, repoName) + resp, err := updateVulnerabilityAlertsSDK(ctx, owner, repoName) + if err != nil { + if resp.StatusCode == http.StatusUnprocessableEntity && strings.Contains(err.Error(), "An enforced security configuration prevented modifying") && !ok { + return nil // An Organization or Enterprise policy is preventing the change + } + } return err } From 68e65ac8eaffb3617ca2efda9460a1b904f085fd Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Wed, 31 Dec 2025 23:13:04 +0200 Subject: [PATCH 4/5] Improve descriptions to make interactions slightly clearer Signed-off-by: Timo Sand --- github/resource_github_repository.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index a1c2a5a19c..a872042c51 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -391,7 +391,7 @@ func resourceGithubRepository() *schema.Resource { Type: schema.TypeBool, Optional: true, Computed: true, - Description: "Set to 'true' to enable security alerts for vulnerable dependencies. Enabling requires alerts to be enabled on the owner level. (Note for importing: GitHub enables the alerts on public repos but disables them on private repos by default). Note that vulnerability alerts have not been successfully tested on any GitHub Enterprise instance and may be unavailable in those settings.", + Description: "Set to 'true' to enable security alerts for vulnerable dependencies. Enabling requires alerts to be enabled on the owner level. (Note for importing: GitHub enables the alerts on all repos by default). Note that vulnerability alerts have not been successfully tested on any GitHub Enterprise instance and may be unavailable in those settings.", }, "ignore_vulnerability_alerts_during_read": { Type: schema.TypeBool, @@ -1245,6 +1245,9 @@ func resourceGithubParseFullName(resourceDataLike interface { func updateVulnerabilityAlerts(d *schema.ResourceData, client *github.Client, ctx context.Context, owner, repoName string) error { updateVulnerabilityAlertsSDK := client.Repositories.DisableVulnerabilityAlerts vulnerabilityAlerts, ok := d.GetOk("vulnerability_alerts") + + // Only if the vulnerability alerts are specifically set to true, enable them. + // Otherwise, disable them as GitHub defaults to enabled and we have not wanted to introduce a breaking change for this yet. if ok && vulnerabilityAlerts.(bool) { updateVulnerabilityAlertsSDK = client.Repositories.EnableVulnerabilityAlerts } From 2d652e137e8c766a6d3e347f150ce2f03267d1ee Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 4 Jan 2026 21:19:23 +0200 Subject: [PATCH 5/5] Add comment to describe this as a temporary workaround Signed-off-by: Timo Sand --- github/resource_github_repository.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index a872042c51..6b24925882 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -1254,8 +1254,10 @@ func updateVulnerabilityAlerts(d *schema.ResourceData, client *github.Client, ct resp, err := updateVulnerabilityAlertsSDK(ctx, owner, repoName) if err != nil { + // Check if the error is because an Organization or Enterprise policy is preventing the change + // This is a temporary workaround while we extract Vulnerability Alerts into a separate resource. if resp.StatusCode == http.StatusUnprocessableEntity && strings.Contains(err.Error(), "An enforced security configuration prevented modifying") && !ok { - return nil // An Organization or Enterprise policy is preventing the change + return nil } } return err