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
3 changes: 2 additions & 1 deletion github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,9 @@ func Provider() *schema.Provider {
"github_release": resourceGithubRelease(),
"github_repository": resourceGithubRepository(),
"github_repository_autolink_reference": resourceGithubRepositoryAutolinkReference(),
"github_repository_dependabot_security_updates": resourceGithubRepositoryDependabotSecurityUpdates(),
"github_repository_code_scanning_default_setup": resourceGithubRepositoryCodeScanningDefaultSetup(),
"github_repository_collaborator": resourceGithubRepositoryCollaborator(),
"github_repository_dependabot_security_updates": resourceGithubRepositoryDependabotSecurityUpdates(),
"github_repository_collaborators": resourceGithubRepositoryCollaborators(),
"github_repository_custom_property": resourceGithubRepositoryCustomProperty(),
"github_repository_deploy_key": resourceGithubRepositoryDeployKey(),
Expand Down
246 changes: 246 additions & 0 deletions github/resource_github_repository_code_scanning_default_setup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package github

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

"github.com/google/go-github/v84/github"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

func resourceGithubRepositoryCodeScanningDefaultSetup() *schema.Resource {
return &schema.Resource{
CreateContext: resourceGithubRepositoryCodeScanningDefaultSetupCreateOrUpdate,
ReadContext: resourceGithubRepositoryCodeScanningDefaultSetupRead,
UpdateContext: resourceGithubRepositoryCodeScanningDefaultSetupCreateOrUpdate,
DeleteContext: resourceGithubRepositoryCodeScanningDefaultSetupDelete,
Importer: &schema.ResourceImporter{
StateContext: resourceGithubRepositoryCodeScanningDefaultSetupImport,
},

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(5 * time.Minute),
Delete: schema.DefaultTimeout(5 * time.Minute),
},

CustomizeDiff: diffRepository,

Schema: map[string]*schema.Schema{
"repository": {
Type: schema.TypeString,
Required: true,
Description: "The GitHub repository name.",
},
Comment on lines +37 to +41
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.

Remove ForceNew and add the diffRepo structure used in other similar resources. This allows support for renaming repositories

"repository_id": {
Type: schema.TypeInt,
Computed: true,
Description: "The ID of the GitHub repository.",
},
"state": {
Type: schema.TypeString,
Required: true,
Description: "The desired state of code scanning default setup. Must be `configured` or `not-configured`.",
ValidateDiagFunc: validation.ToDiagFunc(
validation.StringInSlice([]string{"configured", "not-configured"}, false),
),
},
"query_suite": {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "The query suite to use. Must be `default` or `extended`.",
ValidateDiagFunc: validation.ToDiagFunc(
validation.StringInSlice([]string{"default", "extended"}, false),
),
},
"languages": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Description: "The languages to enable for code scanning. Supported values include `actions`, `c-cpp`, `csharp`, `go`, `java-kotlin`, `javascript-typescript`, `python`, `ruby`, `swift`.",
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

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

repo, _, err := client.Repositories.Get(ctx, owner, repoName)
if err != nil {
return diag.Errorf("error reading repository %s/%s: %s", owner, repoName, err)
}
if repo.GetArchived() {
return diag.Errorf("repository %s/%s is archived", owner, repoName)
}

options := &github.UpdateDefaultSetupConfigurationOptions{
State: state,
}

if v, ok := d.GetOk("query_suite"); ok {
qs := v.(string)
options.QuerySuite = &qs
}

if v, ok := d.GetOk("languages"); ok {
options.Languages = expandStringList(v.(*schema.Set).List())
}

_, _, err = client.CodeScanning.UpdateDefaultSetupConfiguration(ctx, owner, repoName, options)
if err != nil {
// 202 Accepted is expected — go-github surfaces it as AcceptedError
var acceptedErr *github.AcceptedError
if !errors.As(err, &acceptedErr) {
return diag.Errorf("error updating code scanning default setup for %s/%s: %s", owner, repoName, err)
}
}

d.SetId(repoName)

var timeout time.Duration
if d.IsNewResource() {
timeout = d.Timeout(schema.TimeoutCreate)
} else {
timeout = d.Timeout(schema.TimeoutUpdate)
}

config, err := waitForCodeScanningState(ctx, client, owner, repoName, state, timeout)
if err != nil {
return diag.Errorf("error waiting for code scanning default setup state for %s/%s: %s", owner, repoName, err)
}

return setCodeScanningDefaultSetupState(d, config)
}

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

repo, _, err := client.Repositories.Get(ctx, owner, repoName)
if err != nil {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound {
tflog.Info(ctx, "Repository not found, removing from state", map[string]any{
"owner": owner,
"repository": repoName,
})
d.SetId("")
return nil
}
return diag.Errorf("error reading repository %s/%s: %s", owner, repoName, err)
}
if err := d.Set("repository_id", int(repo.GetID())); err != nil {
return diag.Errorf("error setting repository_id: %s", err)
}

config, _, err := client.CodeScanning.GetDefaultSetupConfiguration(ctx, owner, repoName)
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.

question: does this return err or success if the repo has been archived?
If success, then we need to also check if the repo is archived and return an error

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.

The API returns 403 on archived repos, but with a misleading message ("Code scanning is not enabled"). Added an explicit archived check in Create/Update to provide a clear error message, along with a test case. Read does not check (consistent with other resources).

# configured before archiving
$ gh api repos/oda251/actrun-mcp/code-scanning/default-setup
{"state":"configured","languages":[],"query_suite":"default",...}

# after archiving
$ gh api repos/oda251/actrun-mcp/code-scanning/default-setup
403: "Code scanning is not enabled for this repository."

if err != nil {
return diag.Errorf("error reading code scanning default setup for %s/%s: %s", owner, repoName, err)
}

return setCodeScanningDefaultSetupState(d, config)
}

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

options := &github.UpdateDefaultSetupConfigurationOptions{
State: "not-configured",
}

_, _, err := client.CodeScanning.UpdateDefaultSetupConfiguration(ctx, owner, repoName, options)
if err != nil {
var acceptedErr *github.AcceptedError
var ghErr *github.ErrorResponse
switch {
case errors.As(err, &acceptedErr):
// 202 Accepted is expected
case errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound:
// repository already gone
return nil
default:
return diag.Errorf("error disabling code scanning default setup for %s/%s: %s", owner, repoName, err)
}
}

tflog.Info(ctx, "Code scanning default setup disabled", map[string]any{
"owner": owner,
"repository": repoName,
})
return nil
}

func resourceGithubRepositoryCodeScanningDefaultSetupImport(_ context.Context, d *schema.ResourceData, _ any) ([]*schema.ResourceData, error) {
repoName := d.Id()
if repoName == "" {
return nil, fmt.Errorf("repository name must not be empty")
}

if err := d.Set("repository", repoName); err != nil {
return nil, err
}

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

func setCodeScanningDefaultSetupState(d *schema.ResourceData, config *github.DefaultSetupConfiguration) diag.Diagnostics {
if err := d.Set("state", config.GetState()); err != nil {
return diag.Errorf("error setting state: %s", err)
}
if err := d.Set("query_suite", config.GetQuerySuite()); err != nil {
return diag.Errorf("error setting query_suite: %s", err)
}
if err := d.Set("languages", config.Languages); err != nil {
return diag.Errorf("error setting languages: %s", err)
}
return nil
}

func waitForCodeScanningState(ctx context.Context, client *github.Client, owner, repo, targetState string, timeout time.Duration) (*github.DefaultSetupConfiguration, error) {
conf := &retry.StateChangeConf{
Pending: []string{"pending"},
Target: []string{targetState},
Timeout: timeout,
Delay: 1 * time.Second,
MinTimeout: 1 * time.Second,
Refresh: func() (any, string, error) {
config, _, err := client.CodeScanning.GetDefaultSetupConfiguration(ctx, owner, repo)
if err != nil {
return nil, "", err
}
state := config.GetState()
if state == targetState {
return config, state, nil
}
return config, "pending", nil
},
}

result, err := conf.WaitForStateContext(ctx)
if err != nil {
return nil, err
}
if result == nil {
return nil, fmt.Errorf("code scanning default setup returned nil result for %s/%s", owner, repo)
}

return result.(*github.DefaultSetupConfiguration), nil
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.

Wouldn't this panic if the wait returns nil?

}
Loading