From 6fdc79b2ae133c3e57b4afefcf0078d4d6ae6348 Mon Sep 17 00:00:00 2001 From: Steve Hipwell Date: Thu, 15 Oct 2026 09:35:43 +0100 Subject: [PATCH] chore: Refactor github_actions_organization_workflow_permissions Signed-off-by: Steve Hipwell --- RESOURCES.md | 2 +- .../actions_organization_permissions.md | 79 ++- .../import-by-string-id.tf | 4 + .../import.sh | 1 + .../resource_1.tf} | 15 +- ...github_actions_organization_permissions.go | 559 +++++++++++------- ...b_actions_organization_permissions_test.go | 216 ++++++- .../actions_organization_permissions.md.tmpl | 50 +- 8 files changed, 654 insertions(+), 272 deletions(-) create mode 100644 examples/resources/github_actions_organization_permissions/import-by-string-id.tf create mode 100644 examples/resources/github_actions_organization_permissions/import.sh rename examples/resources/{actions_organization_permissions/example_1.tf => github_actions_organization_permissions/resource_1.tf} (73%) diff --git a/RESOURCES.md b/RESOURCES.md index b1b1c3e2b0..3390c36254 100644 --- a/RESOURCES.md +++ b/RESOURCES.md @@ -128,7 +128,7 @@ The overall status of each resource or data source is captured in this document | `github_actions_environment_variable` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_actions_hosted_runner` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_organization_oidc_subject_claim_customization_template` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | -| `github_actions_organization_permissions` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | +| `github_actions_organization_permissions` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_actions_organization_secret` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_actions_organization_secret_repositories` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_organization_secret_repository` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | diff --git a/docs/resources/actions_organization_permissions.md b/docs/resources/actions_organization_permissions.md index 14ab6c6857..429a176b3e 100644 --- a/docs/resources/actions_organization_permissions.md +++ b/docs/resources/actions_organization_permissions.md @@ -1,12 +1,13 @@ --- page_title: "github_actions_organization_permissions (Resource) - GitHub" +subcategory: "" description: |- - Creates and manages Actions permissions within a GitHub organization + Resource to manage the GitHub Actions organization permissions. --- # github_actions_organization_permissions (Resource) -This resource allows you to create and manage GitHub Actions permissions within your GitHub enterprise organizations. You must have admin access to an organization to use this resource. +Resource to manage the GitHub Actions organization permissions. ## Example Usage @@ -15,48 +16,78 @@ resource "github_repository" "example" { name = "my-repository" } -resource "github_actions_organization_permissions" "test" { - allowed_actions = "selected" +resource "github_actions_organization_permissions" "example" { enabled_repositories = "selected" + + enabled_repositories_config { + repository_ids = [github_repository.example.repo_id] + } + + allowed_actions = "selected" + allowed_actions_config { github_owned_allowed = true patterns_allowed = ["actions/cache@*", "actions/checkout@*"] verified_allowed = true } - enabled_repositories_config { - repository_ids = [github_repository.example.repo_id] - } + + sha_pinning_required = true } ``` -## Argument Reference + +## Schema + +### Required + +- `enabled_repositories` (String) Policy that controls the repositories in the organization that are allowed to run GitHub Actions; this can be one of `all`, `none`, or `selected`. -The following arguments are supported: +### Optional -- `allowed_actions` - (Optional) The permissions policy that controls the actions that are allowed to run. Can be one of: `all`, `local_only`, or `selected`. -- `enabled_repositories` - (Required) The policy that controls the repositories in the organization that are allowed to run GitHub Actions. Can be one of: `all`, `none`, or `selected`. -- `allowed_actions_config` - (Optional) Sets the actions that are allowed in an organization. Only available when `allowed_actions` = `selected`. See [Allowed Actions Config](#allowed-actions-config) below for details. -- `enabled_repositories_config` - (Optional) Sets the list of selected repositories that are enabled for GitHub Actions in an organization. Only available when `enabled_repositories` = `selected`. See [Enabled Repositories Config](#enabled-repositories-config) below for details. -- `sha_pinning_required` - (Optional) Whether pinning to a specific SHA is required for all actions and reusable workflows in the organization. +- `allowed_actions` (String) Policy that controls the actions that are allowed to run; this can be one of `all`, `local_only`, or `selected`. +- `allowed_actions_config` (Block List, Max: 1) Configure the actions that are allowed in an organization; this is only supported when `allowed_actions` is set to `selected`. (see [below for nested schema](#nestedblock--allowed_actions_config)) +- `enabled_repositories_config` (Block List, Max: 1) Configure the repositories that are enabled for GitHub Actions in an organization; this is only supported when `enabled_repositories` is set to `selected`. (see [below for nested schema](#nestedblock--enabled_repositories_config)) +- `sha_pinning_required` (Boolean) If all actions and reusable workflows in an organization require pinning to a specific SHA. -### Allowed Actions Config +### Read-Only -The `allowed_actions_config` block supports the following: +- `id` (String) The ID of this resource. -- `github_owned_allowed` - (Required) Whether GitHub-owned actions are allowed in the organization. -- `patterns_allowed` - (Optional) Specifies a list of string-matching patterns to allow specific action(s). Wildcards, tags, and SHAs are allowed. For example, monalisa/octocat@*, monalisa/octocat@v2, monalisa/*." -- `verified_allowed` - (Optional) Whether actions in GitHub Marketplace from verified creators are allowed. Set to true to allow all GitHub Marketplace actions by verified creators. + +### Nested Schema for `allowed_actions_config` -### Enabled Repositories Config +Required: -The `enabled_repositories_config` block supports the following: +- `github_owned_allowed` (Boolean) If GitHub-owned actions are allowed. -- `repository_ids` - (Required) List of repository IDs to enable for GitHub Actions. +Optional: + +- `patterns_allowed` (Set of String) List of string-matching patterns to allow specific actions; wildcards, tags, and SHAs are allowed. For example, `monalisa/octocat@*`, `monalisa/octocat@v2`, `monalisa/*`. +- `verified_allowed` (Boolean) If actions in GitHub Marketplace from verified creators are allowed. + + + +### Nested Schema for `enabled_repositories_config` + +Required: + +- `repository_ids` (Set of Number) List of repository IDs to enable for GitHub Actions. ## Import -This resource can be imported using the name of the GitHub organization: +Import is supported using the following syntax: + +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: + +```terraform +import { + to = github_actions_organization_permissions.example + id = "org-name" +} +``` + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: ```shell -terraform import github_actions_organization_permissions.test github_organization_name +terraform import github_actions_organization_permissions.example org-name ``` diff --git a/examples/resources/github_actions_organization_permissions/import-by-string-id.tf b/examples/resources/github_actions_organization_permissions/import-by-string-id.tf new file mode 100644 index 0000000000..ccf1cd74c0 --- /dev/null +++ b/examples/resources/github_actions_organization_permissions/import-by-string-id.tf @@ -0,0 +1,4 @@ +import { + to = github_actions_organization_permissions.example + id = "org-name" +} diff --git a/examples/resources/github_actions_organization_permissions/import.sh b/examples/resources/github_actions_organization_permissions/import.sh new file mode 100644 index 0000000000..9ae72faf0c --- /dev/null +++ b/examples/resources/github_actions_organization_permissions/import.sh @@ -0,0 +1 @@ +terraform import github_actions_organization_permissions.example org-name diff --git a/examples/resources/actions_organization_permissions/example_1.tf b/examples/resources/github_actions_organization_permissions/resource_1.tf similarity index 73% rename from examples/resources/actions_organization_permissions/example_1.tf rename to examples/resources/github_actions_organization_permissions/resource_1.tf index fa97dab7c7..c72caf08c5 100644 --- a/examples/resources/actions_organization_permissions/example_1.tf +++ b/examples/resources/github_actions_organization_permissions/resource_1.tf @@ -2,15 +2,20 @@ resource "github_repository" "example" { name = "my-repository" } -resource "github_actions_organization_permissions" "test" { - allowed_actions = "selected" +resource "github_actions_organization_permissions" "example" { enabled_repositories = "selected" + + enabled_repositories_config { + repository_ids = [github_repository.example.repo_id] + } + + allowed_actions = "selected" + allowed_actions_config { github_owned_allowed = true patterns_allowed = ["actions/cache@*", "actions/checkout@*"] verified_allowed = true } - enabled_repositories_config { - repository_ids = [github_repository.example.repo_id] - } + + sha_pinning_required = true } diff --git a/github/resource_github_actions_organization_permissions.go b/github/resource_github_actions_organization_permissions.go index 9692b5ccc8..7374e202e4 100644 --- a/github/resource_github_actions_organization_permissions.go +++ b/github/resource_github_actions_organization_permissions.go @@ -3,75 +3,81 @@ package github import ( "context" "errors" - "log" + "fmt" "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 resourceGithubActionsOrganizationPermissions() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsOrganizationPermissionsCreateOrUpdate, - Read: resourceGithubActionsOrganizationPermissionsRead, - Update: resourceGithubActionsOrganizationPermissionsCreateOrUpdate, - Delete: resourceGithubActionsOrganizationPermissionsDelete, + CreateContext: resourceGithubActionsOrganizationPermissionsCreate, + ReadContext: resourceGithubActionsOrganizationPermissionsRead, + UpdateContext: resourceGithubActionsOrganizationPermissionsUpdate, + DeleteContext: resourceGithubActionsOrganizationPermissionsDelete, Importer: &schema.ResourceImporter{ - StateContext: schema.ImportStatePassthroughContext, + StateContext: resourceGithubActionsOrganizationPermissionsImport, }, + CustomizeDiff: diffGithubActionsOrganizationPermissions, + + Description: "Resource to manage the GitHub Actions organization permissions.", + Schema: map[string]*schema.Schema{ - "allowed_actions": { - Type: schema.TypeString, - Optional: true, - Description: "The permissions policy that controls the actions that are allowed to run. Can be one of: 'all', 'local_only', or 'selected'.", - ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "local_only", "selected"}, false)), - }, "enabled_repositories": { Type: schema.TypeString, Required: true, - Description: "The policy that controls the repositories in the organization that are allowed to run GitHub Actions. Can be one of: 'all', 'none', or 'selected'.", + Description: "Policy that controls the repositories in the organization that are allowed to run GitHub Actions; this can be one of `all`, `none`, or `selected`.", ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "none", "selected"}, false)), }, - "allowed_actions_config": { + "enabled_repositories_config": { Type: schema.TypeList, Optional: true, MaxItems: 1, - Description: "Sets the actions that are allowed in an organization. Only available when 'allowed_actions' = 'selected'", + Description: "Configure the repositories that are enabled for GitHub Actions in an organization; this is only supported when `enabled_repositories` is set to `selected`.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "github_owned_allowed": { - Type: schema.TypeBool, - Required: true, - Description: "Whether GitHub-owned actions are allowed in the organization.", - }, - "patterns_allowed": { + "repository_ids": { Type: schema.TypeSet, - Optional: true, - Description: "Specifies a list of string-matching patterns to allow specific action(s). Wildcards, tags, and SHAs are allowed. For example, 'monalisa/octocat@', 'monalisa/octocat@v2', 'monalisa/'.", - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, - }, - "verified_allowed": { - Type: schema.TypeBool, - Optional: true, - Description: "Whether actions in GitHub Marketplace from verified creators are allowed. Set to 'true' to allow all GitHub Marketplace actions by verified creators.", + Description: "List of repository IDs to enable for GitHub Actions.", + Elem: &schema.Schema{Type: schema.TypeInt}, + Required: true, }, }, }, }, - "enabled_repositories_config": { + "allowed_actions": { + Type: schema.TypeString, + Optional: true, + Description: "Policy that controls the actions that are allowed to run; this can be one of `all`, `local_only`, or `selected`.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "local_only", "selected"}, false)), + }, + "allowed_actions_config": { Type: schema.TypeList, Optional: true, MaxItems: 1, - Description: "Sets the list of selected repositories that are enabled for GitHub Actions in an organization. Only available when 'enabled_repositories' = 'selected'.", + Description: "Configure the actions that are allowed in an organization; this is only supported when `allowed_actions` is set to `selected`.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "repository_ids": { + "github_owned_allowed": { + Type: schema.TypeBool, + Required: true, // TODO: Make optional in v7 + Description: "If GitHub-owned actions are allowed.", + }, + "verified_allowed": { + Type: schema.TypeBool, + Optional: true, + Description: "If actions in GitHub Marketplace from verified creators are allowed.", + }, + "patterns_allowed": { Type: schema.TypeSet, - Description: "List of repository IDs to enable for GitHub Actions.", - Elem: &schema.Schema{Type: schema.TypeInt}, - Required: true, + Optional: true, + Description: "List of string-matching patterns to allow specific actions; wildcards, tags, and SHAs are allowed. For example, `monalisa/octocat@*`, `monalisa/octocat@v2`, `monalisa/*`.", + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, }, }, }, @@ -79,253 +85,394 @@ func resourceGithubActionsOrganizationPermissions() *schema.Resource { "sha_pinning_required": { Type: schema.TypeBool, Optional: true, - Computed: true, - Description: "Whether pinning to a specific SHA is required for all actions and reusable workflows in an organization.", + Computed: true, // TODO: Remove computed in v7 + Description: "If all actions and reusable workflows in an organization require pinning to a specific SHA.", }, }, } } -func resourceGithubActionsOrganizationAllowedObject(d *schema.ResourceData) *github.ActionsAllowed { - allowed := &github.ActionsAllowed{} +func diffGithubActionsOrganizationPermissions(ctx context.Context, diff *schema.ResourceDiff, m any) error { + if !diff.NewValueKnown("enabled_repositories") || !diff.NewValueKnown("enabled_repositories_config") || !diff.NewValueKnown("allowed_actions") || !diff.NewValueKnown("allowed_actions_config") { + return nil + } - config := d.Get("allowed_actions_config").([]any) - if len(config) > 0 { - data := config[0].(map[string]any) - switch x := data["github_owned_allowed"].(type) { - case bool: - allowed.GithubOwnedAllowed = &x - } + enabledRepos, _ := diff.Get("enabled_repositories").(string) + enabledReposConfig := false - switch x := data["verified_allowed"].(type) { - case bool: - allowed.VerifiedAllowed = &x + if v, ok := diff.GetOk("enabled_repositories_config"); ok { + if p, ok := v.([]any); ok && len(p) > 0 { + enabledReposConfig = true } + } - patternsAllowed := []string{} + if enabledRepos != "selected" && enabledReposConfig { + return fmt.Errorf("enabled_repositories_config is only valid when enabled_repositories is set to selected") + } - switch t := data["patterns_allowed"].(type) { - case *schema.Set: - for _, value := range t.List() { - patternsAllowed = append(patternsAllowed, value.(string)) - } - } + if enabledRepos == "selected" && !enabledReposConfig { + return fmt.Errorf("enabled_repositories_config is required when enabled_repositories is set to selected") + } - allowed.PatternsAllowed = patternsAllowed - } else { - return nil + allowedActions, _ := diff.Get("allowed_actions").(string) + allowedActionsConfig := false + + if v, ok := diff.GetOk("allowed_actions_config"); ok { + if p, ok := v.([]any); ok && len(p) > 0 { + allowedActionsConfig = true + } } - return allowed -} + if allowedActions != "" && enabledRepos == "none" { + return fmt.Errorf("allowed_actions is only valid when enabled_repositories is set to all or selected") + } -func resourceGithubActionsEnabledRepositoriesObject(d *schema.ResourceData) ([]int64, error) { - var enabled []int64 + if allowedActions != "selected" && allowedActionsConfig { + return fmt.Errorf("allowed_actions_config is only valid when allowed_actions is set to selected") + } - config := d.Get("enabled_repositories_config").([]any) - if len(config) > 0 { - data := config[0].(map[string]any) - switch x := data["repository_ids"].(type) { - case *schema.Set: - for _, value := range x.List() { - enabled = append(enabled, int64(value.(int))) - } - } - } else { - return nil, errors.New("the enabled_repositories_config {} block must be specified if enabled_repositories == 'selected'") + if allowedActions == "selected" && !allowedActionsConfig { + return fmt.Errorf("allowed_actions_config is required when allowed_actions is set to selected") } - return enabled, nil + + return nil } -func resourceGithubActionsOrganizationPermissionsCreateOrUpdate(d *schema.ResourceData, m any) error { +func resourceGithubActionsOrganizationPermissionsCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { meta, _ := m.(*Owner) - client := meta.v3client - orgName := meta.name - ctx := context.Background() - if !d.IsNewResource() { - ctx = context.WithValue(ctx, ctxId, d.Id()) - } - err := checkOrganization(meta) - if err != nil { - return err + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - allowedActions := d.Get("allowed_actions").(string) - enabledRepositories := d.Get("enabled_repositories").(string) + enabledRepositories, _ := d.Get("enabled_repositories").(string) - actionsPermissions := github.ActionsPermissions{ - AllowedActions: &allowedActions, + req := github.ActionsPermissions{ EnabledRepositories: &enabledRepositories, } - if v, ok := d.GetOk("sha_pinning_required"); ok { - actionsPermissions.SHAPinningRequired = new(v.(bool)) + if v, ok := d.GetOk("allowed_actions"); ok { + s, _ := v.(string) + req.AllowedActions = &s } - _, _, err = client.Actions.UpdateActionsPermissions(ctx, - orgName, - actionsPermissions) - if err != nil { - return err + if v, ok := d.GetOkExists("sha_pinning_required"); ok { //nolint:staticcheck // SA1019: d.GetOkExists is deprecated but necessary for bool fields + b, _ := v.(bool) + req.SHAPinningRequired = &b } - if allowedActions == "selected" { - actionsAllowedData := resourceGithubActionsOrganizationAllowedObject(d) - if actionsAllowedData != nil { - log.Printf("[DEBUG] Allowed actions config is set") - _, _, err = client.Actions.UpdateActionsAllowed(ctx, - orgName, - *actionsAllowedData) - if err != nil { - return err - } - } else { - log.Printf("[DEBUG] Allowed actions config not set, skipping") - } + if _, _, err := meta.v3client.Actions.UpdateActionsPermissions(ctx, meta.name, req); err != nil { + return diag.FromErr(err) } - if enabledRepositories == "selected" { - enabledReposData, err := resourceGithubActionsEnabledRepositoriesObject(d) + if req.SHAPinningRequired == nil { + perm, _, err := meta.v3client.Actions.GetActionsPermissions(ctx, meta.name) if err != nil { - return err + return diag.FromErr(err) } - _, err = client.Actions.SetEnabledReposInOrg(ctx, - orgName, - enabledReposData) - if err != nil { - return err + + if err := d.Set("sha_pinning_required", perm.GetSHAPinningRequired()); err != nil { + return diag.FromErr(err) } } - d.SetId(orgName) - return resourceGithubActionsOrganizationPermissionsRead(d, meta) + if *req.EnabledRepositories == "selected" { + if v, ok := d.GetOk("enabled_repositories_config"); ok { + if p, ok := v.([]any); ok && len(p) > 0 && p[0] != nil { + block, _ := p[0].(map[string]any) + col, _ := block["repository_ids"].(*schema.Set) + + repoIDs := make([]int64, col.Len()) + for i, a := range col.List() { + id, _ := a.(int) + repoIDs[i] = int64(id) + } + + if _, err := meta.v3client.Actions.SetEnabledReposInOrg(ctx, meta.name, repoIDs); err != nil { + return diag.FromErr(err) + } + } + } + } + + if req.AllowedActions != nil && *req.AllowedActions == "selected" { + if v, ok := d.GetOk("allowed_actions_config"); ok { + if p, ok := v.([]any); ok && len(p) > 0 && p[0] != nil { + block, _ := p[0].(map[string]any) + + ownerAllowed, _ := block["github_owned_allowed"].(bool) + + req := github.ActionsAllowed{ + GithubOwnedAllowed: &ownerAllowed, + } + + if v, ok := block["verified_allowed"]; ok { + if b, ok := v.(bool); ok { + req.VerifiedAllowed = &b + } + } + + if v, ok := block["patterns_allowed"]; ok { + if p, ok := v.(*schema.Set); ok { + patterns := make([]string, p.Len()) + for i, a := range p.List() { + patterns[i], _ = a.(string) + } + req.PatternsAllowed = patterns + } + } + + if _, _, err := meta.v3client.Actions.UpdateActionsAllowed(ctx, meta.name, req); err != nil { + return diag.FromErr(err) + } + } + } + } + + d.SetId(meta.name) + + return nil } -func resourceGithubActionsOrganizationPermissionsRead(d *schema.ResourceData, m any) error { +func resourceGithubActionsOrganizationPermissionsRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { meta, _ := m.(*Owner) - client := meta.v3client - ctx := context.Background() - err := checkOrganization(meta) - if err != nil { - return err + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - actionsPermissions, _, err := client.Actions.GetActionsPermissions(ctx, d.Id()) + perm, _, err := meta.v3client.Actions.GetActionsPermissions(ctx, meta.name) if err != nil { - return err + return diag.FromErr(err) } - // only load and fill allowed_actions_config if allowed_actions_config is also set - // in the TF code. (see #2105) - // on initial import there might not be any value in the state, then we have to import the data - // -> but we can only load an existing state if the current config is set to "selected" (see #2182) - allowedActions := d.Get("allowed_actions").(string) - allowedActionsConfig := d.Get("allowed_actions_config").([]any) + enabledRepositories := perm.GetEnabledRepositories() + if err := d.Set("enabled_repositories", enabledRepositories); err != nil { + return diag.FromErr(err) + } - serverHasAllowedActionsConfig := actionsPermissions.GetAllowedActions() == "selected" - userWantsAllowedActionsConfig := (allowedActions == "selected" && len(allowedActionsConfig) > 0) || allowedActions == "" + var reposCfg []any + if enabledRepositories == "selected" { + repoIDs := []int{} - if serverHasAllowedActionsConfig && userWantsAllowedActionsConfig { - actionsAllowed, _, err := client.Actions.GetActionsAllowed(ctx, d.Id()) - if err != nil { - return err + for repo, err := range meta.v3client.Actions.ListEnabledReposInOrgIter(ctx, meta.name, &github.ListOptions{PerPage: meta.maxPerPage}) { + if err != nil { + return diag.FromErr(err) + } + + repoIDs = append(repoIDs, int(repo.GetID())) } - // If actionsAllowed set to local/all by removing all actions config settings, the response will be empty - if actionsAllowed != nil { - if err = d.Set("allowed_actions_config", []any{ - map[string]any{ - "github_owned_allowed": actionsAllowed.GetGithubOwnedAllowed(), - "patterns_allowed": actionsAllowed.PatternsAllowed, - "verified_allowed": actionsAllowed.GetVerifiedAllowed(), - }, - }); err != nil { - return err + reposCfg = []any{map[string]any{ + "repository_ids": repoIDs, + }} + } + + if err := d.Set("enabled_repositories_config", reposCfg); err != nil { + return diag.FromErr(err) + } + + if _, ok := d.GetOk("allowed_actions"); ok { + allowedActions := perm.GetAllowedActions() + if err := d.Set("allowed_actions", allowedActions); err != nil { + return diag.FromErr(err) + } + + var actionsCfg []any + if allowedActions == "selected" { + act, _, err := meta.v3client.Actions.GetActionsAllowed(ctx, meta.name) + if err != nil { + return diag.FromErr(err) } + + actionsCfg = []any{map[string]any{ + "github_owned_allowed": act.GetGithubOwnedAllowed(), + "patterns_allowed": act.GetPatternsAllowed(), + "verified_allowed": act.GetVerifiedAllowed(), + }} } - } else { - if err = d.Set("allowed_actions_config", []any{}); err != nil { - return err + + if err = d.Set("allowed_actions_config", actionsCfg); err != nil { + return diag.FromErr(err) } } - if actionsPermissions.GetEnabledRepositories() == "selected" { - opts := github.ListOptions{PerPage: meta.maxPerPage} - var repoList []int64 - var allRepos []*github.Repository + if err := d.Set("sha_pinning_required", perm.GetSHAPinningRequired()); err != nil { + return diag.FromErr(err) + } - for { - enabledRepos, resp, err := client.Actions.ListEnabledReposInOrg(ctx, d.Id(), &opts) - if err != nil { - return err - } - allRepos = append(allRepos, enabledRepos.Repositories...) + return nil +} + +func resourceGithubActionsOrganizationPermissionsUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + + if ok, diags := checkOrganizationOK(meta); !ok { + return diags + } + + enabledRepositories, _ := d.Get("enabled_repositories").(string) + shaPinningRequired, _ := d.Get("sha_pinning_required").(bool) // TODO: Update for v7 when no longer computed + + req := github.ActionsPermissions{ + EnabledRepositories: &enabledRepositories, + SHAPinningRequired: &shaPinningRequired, + } - opts.Page = resp.NextPage + if v, ok := d.GetOk("allowed_actions"); ok { + s, _ := v.(string) + req.AllowedActions = &s + } + + if _, _, err := meta.v3client.Actions.UpdateActionsPermissions(ctx, meta.name, req); err != nil { + return diag.FromErr(err) + } - if resp.NextPage == 0 { - break + if *req.EnabledRepositories == "selected" { + if v, ok := d.GetOk("enabled_repositories_config"); ok { + if p, ok := v.([]any); ok && len(p) > 0 && p[0] != nil { + block, _ := p[0].(map[string]any) + col, _ := block["repository_ids"].(*schema.Set) + + repoIDs := make([]int64, col.Len()) + for i, a := range col.List() { + id, _ := a.(int) + repoIDs[i] = int64(id) + } + + if _, err := meta.v3client.Actions.SetEnabledReposInOrg(ctx, meta.name, repoIDs); err != nil { + return diag.FromErr(err) + } } } - for index := range allRepos { - repoList = append(repoList, *allRepos[index].ID) - } - if allRepos != nil { - if err = d.Set("enabled_repositories_config", []any{ - map[string]any{ - "repository_ids": repoList, - }, - }); err != nil { - return err - } - } else { - if err = d.Set("enabled_repositories_config", []any{}); err != nil { - return err + } + + if req.AllowedActions != nil && *req.AllowedActions == "selected" { + if v, ok := d.GetOk("allowed_actions_config"); ok { + if p, ok := v.([]any); ok && len(p) > 0 && p[0] != nil { + block, _ := p[0].(map[string]any) + + ownerAllowed, _ := block["github_owned_allowed"].(bool) + + req := github.ActionsAllowed{ + GithubOwnedAllowed: &ownerAllowed, + } + + if v, ok := block["verified_allowed"]; ok { + if b, ok := v.(bool); ok { + req.VerifiedAllowed = &b + } + } + + if v, ok := block["patterns_allowed"]; ok { + if p, ok := v.(*schema.Set); ok { + patterns := make([]string, p.Len()) + for i, a := range p.List() { + patterns[i], _ = a.(string) + } + req.PatternsAllowed = patterns + } + } + + if _, _, err := meta.v3client.Actions.UpdateActionsAllowed(ctx, meta.name, req); err != nil { + return diag.FromErr(err) + } } } - } else { - if err = d.Set("enabled_repositories_config", []any{}); err != nil { - return err - } } - if err = d.Set("allowed_actions", actionsPermissions.GetAllowedActions()); err != nil { - return err + return nil +} + +func resourceGithubActionsOrganizationPermissionsDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + meta, _ := m.(*Owner) + + if ok, diags := checkOrganizationOK(meta); !ok { + return diags } - if err = d.Set("enabled_repositories", actionsPermissions.GetEnabledRepositories()); err != nil { - return err + + req := github.ActionsPermissions{ + AllowedActions: new("all"), + EnabledRepositories: new("all"), } - if err = d.Set("sha_pinning_required", actionsPermissions.GetSHAPinningRequired()); err != nil { - return err + if _, _, err := meta.v3client.Actions.UpdateActionsPermissions(ctx, meta.name, req); err != nil { + if ghErr, ok := errors.AsType[*github.ErrorResponse](err); ok { + if ghErr.Response.StatusCode == 409 { + tflog.Info(ctx, "Organization actions permissions couldn't be unset as they're already configured at enterprise level.") + return nil + } + } + return diag.FromErr(err) } return nil } -func resourceGithubActionsOrganizationPermissionsDelete(d *schema.ResourceData, m any) error { +func resourceGithubActionsOrganizationPermissionsImport(ctx context.Context, d *schema.ResourceData, m any) ([]*schema.ResourceData, error) { meta, _ := m.(*Owner) - client := meta.v3client - orgName := meta.name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) - err := checkOrganization(meta) - if err != nil { - return err + if d.Id() != meta.name { + return nil, fmt.Errorf("invalid id %q, expected %q", d.Id(), meta.name) } - // This will nullify any allowedActions elements - _, _, err = client.Actions.UpdateActionsPermissions(ctx, - orgName, - github.ActionsPermissions{ - AllowedActions: new("all"), - EnabledRepositories: new("all"), - }) + perm, _, err := meta.v3client.Actions.GetActionsPermissions(ctx, meta.name) if err != nil { - return err + return nil, err } - return nil + enabledRepositories := perm.GetEnabledRepositories() + if err := d.Set("enabled_repositories", enabledRepositories); err != nil { + return nil, err + } + + var reposCfg []any + if enabledRepositories == "selected" { + repoIDs := []int{} + + for repo, err := range meta.v3client.Actions.ListEnabledReposInOrgIter(ctx, meta.name, &github.ListOptions{PerPage: meta.maxPerPage}) { + if err != nil { + return nil, err + } + + repoIDs = append(repoIDs, int(repo.GetID())) + } + + reposCfg = []any{map[string]any{ + "repository_ids": repoIDs, + }} + } + + if err := d.Set("enabled_repositories_config", reposCfg); err != nil { + return nil, err + } + + allowedActions := perm.GetAllowedActions() + if err := d.Set("allowed_actions", allowedActions); err != nil { + return nil, err + } + + var actionsCfg []any + if allowedActions == "selected" { + act, _, err := meta.v3client.Actions.GetActionsAllowed(ctx, meta.name) + if err != nil { + return nil, err + } + + actionsCfg = []any{map[string]any{ + "github_owned_allowed": act.GetGithubOwnedAllowed(), + "patterns_allowed": act.GetPatternsAllowed(), + "verified_allowed": act.GetVerifiedAllowed(), + }} + } + + if err = d.Set("allowed_actions_config", actionsCfg); err != nil { + return nil, err + } + + if err := d.Set("sha_pinning_required", perm.GetSHAPinningRequired()); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil } diff --git a/github/resource_github_actions_organization_permissions_test.go b/github/resource_github_actions_organization_permissions_test.go index 2b88a58da2..a0ebb3340f 100644 --- a/github/resource_github_actions_organization_permissions_test.go +++ b/github/resource_github_actions_organization_permissions_test.go @@ -2,6 +2,7 @@ package github import ( "fmt" + "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" @@ -13,41 +14,50 @@ import ( func TestAccGithubActionsOrganizationPermissions(t *testing.T) { // IMPORTANT: Do not run these tests in parallel as they modify the organization state. + skipUnlessHasOrgs(t) + t.Run("full_lifecycle", func(t *testing.T) { repo := mustCreateTestRepository(t) + configNone := ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "none" +} +` + configMinimal := ` resource "github_actions_organization_permissions" "test" { - allowed_actions = "all" - enabled_repositories = "all" + enabled_repositories = "%s" + + allowed_actions = "%s" } ` - configFull := fmt.Sprintf(` + configFull := ` resource "github_actions_organization_permissions" "test" { - allowed_actions = "selected" enabled_repositories = "selected" + enabled_repositories_config { + repository_ids = [%v] + } + + allowed_actions = "selected" + allowed_actions_config { github_owned_allowed = true patterns_allowed = ["actions/cache@*", "actions/checkout@*"] verified_allowed = true } - enabled_repositories_config { - repository_ids = [%d] - } - sha_pinning_required = true } -`, repo.GetID()) +` resource.Test(t, resource.TestCase{ - PreCheck: func() { skipUnlessHasOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: configMinimal, + Config: fmt.Sprintf(configMinimal, "all", "all"), ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_actions_organization_permissions.test", tfjsonpath.New("allowed_actions_config"), knownvalue.ListSizeExact(0)), statecheck.ExpectKnownValue("github_actions_organization_permissions.test", tfjsonpath.New("enabled_repositories_config"), knownvalue.ListSizeExact(0)), @@ -55,7 +65,16 @@ resource "github_actions_organization_permissions" "test" { }, }, { - Config: configFull, + Config: configNone, + }, + { + Config: fmt.Sprintf(configMinimal, "all", "local_only"), + }, + { + Config: fmt.Sprintf(configFull, ""), + }, + { + Config: fmt.Sprintf(configFull, repo.GetID()), }, { ResourceName: "github_actions_organization_permissions.test", @@ -65,4 +84,177 @@ resource "github_actions_organization_permissions" "test" { }, }) }) + + t.Run("disabled_actions", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "none" + + allowed_actions = "all" +}`, + ExpectError: regexp.MustCompile(regexp.QuoteMeta("allowed_actions is only valid when enabled_repositories is set to all or selected")), + }, + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "none" +}`, + }, + }, + }) + }) + + t.Run("sha_pinning_can_be_disabled", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" + + sha_pinning_required = true +}`, + }, + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" + + sha_pinning_required = false +}`, + }, + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" + + sha_pinning_required = true +}`, + }, + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" +}`, + }, + }, + }) + }) + + t.Run("sha_pinning_can_be_enabled", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" +}`, + }, + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" + + sha_pinning_required = true +}`, + }, + }, + }) + }) + + t.Run("invalid_repository_config_set", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` + resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + enabled_repositories_config { + repository_ids = [] + } + + allowed_actions = "all" +}`, + ExpectError: regexp.MustCompile(regexp.QuoteMeta("enabled_repositories_config is only valid when enabled_repositories is set to selected")), + }, + }, + }) + }) + + t.Run("invalid_repository_config_missing", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "selected" + + allowed_actions = "all" +}`, + ExpectError: regexp.MustCompile(regexp.QuoteMeta("enabled_repositories_config is required when enabled_repositories is set to selected")), + }, + }, + }) + }) + + t.Run("invalid_actions_config_set", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` + resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "all" + + allowed_actions_config { + github_owned_allowed = true + patterns_allowed = ["actions/cache@*", "actions/checkout@*"] + verified_allowed = true + } +}`, + ExpectError: regexp.MustCompile(regexp.QuoteMeta("allowed_actions_config is only valid when allowed_actions is set to selected")), + }, + }, + }) + }) + + t.Run("invalid_actions_config_missing", func(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: ` +resource "github_actions_organization_permissions" "test" { + enabled_repositories = "all" + + allowed_actions = "selected" +}`, + ExpectError: regexp.MustCompile(regexp.QuoteMeta("allowed_actions_config is required when allowed_actions is set to selected")), + }, + }, + }) + }) } diff --git a/templates/resources/actions_organization_permissions.md.tmpl b/templates/resources/actions_organization_permissions.md.tmpl index d15662e05a..759f56a64d 100644 --- a/templates/resources/actions_organization_permissions.md.tmpl +++ b/templates/resources/actions_organization_permissions.md.tmpl @@ -1,45 +1,47 @@ --- page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +subcategory: "" description: |- - Creates and manages Actions permissions within a GitHub organization +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) -This resource allows you to create and manage GitHub Actions permissions within your GitHub enterprise organizations. You must have admin access to an organization to use this resource. +{{ .Description | trimspace }} +{{ if .HasExamples -}} ## Example Usage -{{ tffile "examples/resources/actions_organization_permissions/example_1.tf" }} +{{- range .ExampleFiles }} -## Argument Reference +{{ tffile . }} +{{- end }} +{{- end }} -The following arguments are supported: +{{ .SchemaMarkdown | trimspace }} +{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} -- `allowed_actions` - (Optional) The permissions policy that controls the actions that are allowed to run. Can be one of: `all`, `local_only`, or `selected`. -- `enabled_repositories` - (Required) The policy that controls the repositories in the organization that are allowed to run GitHub Actions. Can be one of: `all`, `none`, or `selected`. -- `allowed_actions_config` - (Optional) Sets the actions that are allowed in an organization. Only available when `allowed_actions` = `selected`. See [Allowed Actions Config](#allowed-actions-config) below for details. -- `enabled_repositories_config` - (Optional) Sets the list of selected repositories that are enabled for GitHub Actions in an organization. Only available when `enabled_repositories` = `selected`. See [Enabled Repositories Config](#enabled-repositories-config) below for details. -- `sha_pinning_required` - (Optional) Whether pinning to a specific SHA is required for all actions and reusable workflows in the organization. - -### Allowed Actions Config +## Import -The `allowed_actions_config` block supports the following: +Import is supported using the following syntax: +{{- end }} +{{- if .HasImportIdentityConfig }} -- `github_owned_allowed` - (Required) Whether GitHub-owned actions are allowed in the organization. -- `patterns_allowed` - (Optional) Specifies a list of string-matching patterns to allow specific action(s). Wildcards, tags, and SHAs are allowed. For example, monalisa/octocat@*, monalisa/octocat@v2, monalisa/*." -- `verified_allowed` - (Optional) Whether actions in GitHub Marketplace from verified creators are allowed. Set to true to allow all GitHub Marketplace actions by verified creators. +In Terraform v1.12.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `identity` attribute, for example: -### Enabled Repositories Config +{{tffile .ImportIdentityConfigFile }} -The `enabled_repositories_config` block supports the following: +{{ .IdentitySchemaMarkdown | trimspace }} +{{- end }} +{{- if .HasImportIDConfig }} -- `repository_ids` - (Required) List of repository IDs to enable for GitHub Actions. +In Terraform v1.5.0 and later, the [`import` block](https://developer.hashicorp.com/terraform/language/import) can be used with the `id` attribute, for example: -## Import +{{tffile .ImportIDConfigFile }} +{{- end }} +{{- if .HasImport }} -This resource can be imported using the name of the GitHub organization: +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: -```shell -terraform import github_actions_organization_permissions.test github_organization_name -``` +{{codefile "shell" .ImportFile }} +{{- end }}