-
Notifications
You must be signed in to change notification settings - Fork 958
[FEAT] Add Organization workflow permission resource #3015
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
Merged
stevehipwell
merged 8 commits into
integrations:main
from
F-Secure-web:add-org-workflow-permission-resource
Jan 13, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
18da1bb
Add resource for Organization Actions Workflow Permissions
deiga 6264f7a
Update documentation
deiga 282282c
Upgrade to use v81 of `go-github`
deiga 27dc317
Align tests with new test structure
deiga 26109ef
Add further tests
deiga 9b4f634
Refactor to use `tflog`
deiga 9c01d9d
Add further Trace, Debug and Info logs
deiga ac6dead
Address review comments
deiga File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
221 changes: 221 additions & 0 deletions
221
github/resource_github_actions_organization_workflow_permissions.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| package github | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| "github.com/google/go-github/v81/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" | ||
| ) | ||
|
|
||
| type GithubActionsOrganizationWorkflowPermissionsErrorResponse struct { | ||
| Message string `json:"message"` | ||
| Errors string `json:"errors"` | ||
| DocumentationURL string `json:"documentation_url"` | ||
| Status string `json:"status"` | ||
| } | ||
|
|
||
| func resourceGithubActionsOrganizationWorkflowPermissions() *schema.Resource { | ||
| return &schema.Resource{ | ||
| Description: "This resource allows you to manage GitHub Actions workflow permissions for a GitHub Organization account. This controls the default permissions granted to the GITHUB_TOKEN when running workflows and whether GitHub Actions can approve pull request reviews.\n\nYou must have organization admin access to use this resource.", | ||
| CreateContext: resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate, | ||
| ReadContext: resourceGithubActionsOrganizationWorkflowPermissionsRead, | ||
| UpdateContext: resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate, | ||
| DeleteContext: resourceGithubActionsOrganizationWorkflowPermissionsDelete, | ||
| Importer: &schema.ResourceImporter{ | ||
| StateContext: schema.ImportStatePassthroughContext, | ||
| }, | ||
|
|
||
| Schema: map[string]*schema.Schema{ | ||
| "organization_slug": { | ||
| Type: schema.TypeString, | ||
| Required: true, | ||
| ForceNew: true, | ||
| Description: "The slug of the Organization.", | ||
| }, | ||
| "default_workflow_permissions": { | ||
| Type: schema.TypeString, | ||
| Optional: true, | ||
| Default: "read", | ||
| Description: "The default workflow permissions granted to the GITHUB_TOKEN when running workflows in any repository in the organization. Can be 'read' or 'write'.", | ||
| ValidateFunc: validation.StringInSlice([]string{"read", "write"}, false), | ||
| }, | ||
| "can_approve_pull_request_reviews": { | ||
| Type: schema.TypeBool, | ||
| Optional: true, | ||
| Default: false, | ||
| Description: "Whether GitHub Actions can approve pull request reviews in any repository in the organization.", | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| func handleEditWorkflowPermissionsError(ctx context.Context, err error, resp *github.Response) diag.Diagnostics { | ||
| var ghErr *github.ErrorResponse | ||
| if errors.As(err, &ghErr) { | ||
| if ghErr.Response.StatusCode == http.StatusConflict { | ||
| tflog.Info(ctx, "Detected conflict with workflow permissions", map[string]any{ | ||
| "status_code": ghErr.Response.StatusCode, | ||
| }) | ||
|
|
||
| errorResponse := &GithubActionsOrganizationWorkflowPermissionsErrorResponse{} | ||
| data, readError := io.ReadAll(resp.Body) | ||
| if readError == nil && data != nil { | ||
| unmarshalError := json.Unmarshal(data, errorResponse) | ||
| if unmarshalError != nil { | ||
| tflog.Error(ctx, "Failed to unmarshal error response", map[string]any{ | ||
| "error": unmarshalError.Error(), | ||
| }) | ||
| return diag.FromErr(unmarshalError) | ||
| } | ||
|
|
||
| tflog.Debug(ctx, "Parsed workflow permissions conflict error", map[string]any{ | ||
| "message": errorResponse.Message, | ||
| "errors": errorResponse.Errors, | ||
| "documentation_url": errorResponse.DocumentationURL, | ||
| "status": errorResponse.Status, | ||
| }) | ||
| } | ||
| return diag.FromErr(fmt.Errorf("you are trying to modify a value restricted by the Enterprise's settings.\n Message: %s\n Errors: %s\n Documentation URL: %s\n Status: %s\nerr: %w", errorResponse.Message, errorResponse.Errors, errorResponse.DocumentationURL, errorResponse.Status, err)) | ||
| } | ||
| } | ||
|
|
||
| tflog.Trace(ctx, "Returning generic error", map[string]any{ | ||
| "error": err.Error(), | ||
| }) | ||
|
|
||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| func resourceGithubActionsOrganizationWorkflowPermissionsCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { | ||
| tflog.Trace(ctx, "Entering Create/Update workflow permissions", map[string]any{ | ||
| "organization_slug": d.Get("organization_slug").(string), | ||
| }) | ||
|
|
||
| client := meta.(*Owner).v3client | ||
|
|
||
| organizationSlug := d.Get("organization_slug").(string) | ||
| d.SetId(organizationSlug) | ||
|
|
||
| if d.IsNewResource() { | ||
| tflog.Info(ctx, "Creating organization workflow permissions", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
| } else { | ||
| tflog.Info(ctx, "Updating organization workflow permissions", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
| } | ||
|
|
||
| workflowPerms := github.DefaultWorkflowPermissionOrganization{} | ||
|
|
||
| if v, ok := d.GetOk("default_workflow_permissions"); ok { | ||
| workflowPerms.DefaultWorkflowPermissions = github.Ptr(v.(string)) | ||
| } | ||
|
|
||
| if v, ok := d.GetOk("can_approve_pull_request_reviews"); ok { | ||
| workflowPerms.CanApprovePullRequestReviews = github.Ptr(v.(bool)) | ||
| } | ||
|
|
||
| tflog.Debug(ctx, "Calling GitHub API to update workflow permissions", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| "default_workflow_permissions": workflowPerms.DefaultWorkflowPermissions, | ||
| "can_approve_pull_request_reviews": workflowPerms.CanApprovePullRequestReviews, | ||
| }) | ||
| _, resp, err := client.Actions.UpdateDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms) | ||
| if err != nil { | ||
| return handleEditWorkflowPermissionsError(ctx, err, resp) | ||
| } | ||
|
|
||
| tflog.Trace(ctx, "Exiting Create/Update workflow permissions successfully", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
| return nil | ||
| } | ||
|
|
||
| func resourceGithubActionsOrganizationWorkflowPermissionsRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { | ||
| tflog.Trace(ctx, "Entering Read workflow permissions", map[string]any{ | ||
| "organization_slug": d.Id(), | ||
| }) | ||
|
|
||
| client := meta.(*Owner).v3client | ||
|
|
||
| organizationSlug := d.Id() | ||
| tflog.Debug(ctx, "Calling GitHub API to read workflow permissions", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
|
|
||
| workflowPerms, _, err := client.Actions.GetDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug) | ||
| if err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| tflog.Debug(ctx, "Retrieved workflow permissions from API", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| "default_workflow_permissions": workflowPerms.DefaultWorkflowPermissions, | ||
| "can_approve_pull_request_reviews": workflowPerms.CanApprovePullRequestReviews, | ||
| }) | ||
|
|
||
| if err := d.Set("organization_slug", organizationSlug); err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
| if err := d.Set("default_workflow_permissions", workflowPerms.DefaultWorkflowPermissions); err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
| if err := d.Set("can_approve_pull_request_reviews", workflowPerms.CanApprovePullRequestReviews); err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| tflog.Trace(ctx, "Exiting Read workflow permissions successfully", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func resourceGithubActionsOrganizationWorkflowPermissionsDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { | ||
| tflog.Trace(ctx, "Entering Delete workflow permissions", map[string]any{ | ||
| "organization_slug": d.Id(), | ||
| }) | ||
|
|
||
| client := meta.(*Owner).v3client | ||
|
|
||
| organizationSlug := d.Id() | ||
| tflog.Info(ctx, "Deleting organization workflow permissions (resetting to defaults)", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
|
|
||
| // Reset to safe defaults | ||
| workflowPerms := github.DefaultWorkflowPermissionOrganization{ | ||
| DefaultWorkflowPermissions: github.Ptr("read"), | ||
| CanApprovePullRequestReviews: github.Ptr(false), | ||
| } | ||
|
|
||
| tflog.Debug(ctx, "Using safe default values", map[string]any{ | ||
| "default_workflow_permissions": "read", | ||
| "can_approve_pull_request_reviews": false, | ||
| }) | ||
|
|
||
| tflog.Debug(ctx, "Calling GitHub API to reset workflow permissions", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| "workflow_permissions": workflowPerms, | ||
| }) | ||
|
|
||
| _, resp, err := client.Actions.UpdateDefaultWorkflowPermissionsInOrganization(ctx, organizationSlug, workflowPerms) | ||
| if err != nil { | ||
| return handleEditWorkflowPermissionsError(ctx, err, resp) | ||
| } | ||
|
|
||
| tflog.Trace(ctx, "Exiting Delete workflow permissions successfully", map[string]any{ | ||
| "organization_slug": organizationSlug, | ||
| }) | ||
|
|
||
| return nil | ||
| } | ||
166 changes: 166 additions & 0 deletions
166
github/resource_github_actions_organization_workflow_permissions_test.go
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| package github | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" | ||
| ) | ||
|
|
||
| func TestAccGithubActionsOrganizationWorkflowPermissions(t *testing.T) { | ||
| t.Run("creates organization workflow permissions without error", func(t *testing.T) { | ||
| config := fmt.Sprintf(` | ||
| resource "github_actions_organization_workflow_permissions" "test" { | ||
| organization_slug = "%s" | ||
|
|
||
| default_workflow_permissions = "read" | ||
| can_approve_pull_request_reviews = false | ||
| } | ||
| `, testAccConf.owner) | ||
|
|
||
| check := resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"), | ||
| ) | ||
|
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { skipUnlessHasOrgs(t) }, | ||
| ProviderFactories: providerFactories, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: config, | ||
| Check: check, | ||
| }, | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| t.Run("updates organization workflow permissions without error", func(t *testing.T) { | ||
| configs := map[string]string{ | ||
| "before": fmt.Sprintf(` | ||
| resource "github_actions_organization_workflow_permissions" "test" { | ||
| organization_slug = "%s" | ||
|
|
||
| default_workflow_permissions = "read" | ||
| can_approve_pull_request_reviews = false | ||
| } | ||
| `, testAccConf.owner), | ||
|
|
||
| "after": fmt.Sprintf(` | ||
| resource "github_actions_organization_workflow_permissions" "test" { | ||
| organization_slug = "%s" | ||
|
|
||
| default_workflow_permissions = "write" // This change might be restricted by the Enterprise's settings | ||
| can_approve_pull_request_reviews = true | ||
| } | ||
| `, testAccConf.owner), | ||
| } | ||
|
|
||
| checks := map[string]resource.TestCheckFunc{ | ||
| "before": resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"), | ||
| ), | ||
| "after": resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "write"), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "true"), | ||
| ), | ||
| } | ||
|
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { skipUnlessHasOrgs(t) }, | ||
| ProviderFactories: providerFactories, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: configs["before"], | ||
| Check: checks["before"], | ||
| }, | ||
| { | ||
| Config: configs["after"], | ||
| Check: checks["after"], | ||
| }, | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| t.Run("imports organization workflow permissions without error", func(t *testing.T) { | ||
| config := fmt.Sprintf(` | ||
| resource "github_actions_organization_workflow_permissions" "test" { | ||
| organization_slug = "%s" | ||
|
|
||
| default_workflow_permissions = "read" | ||
| can_approve_pull_request_reviews = false | ||
| } | ||
| `, testAccConf.owner) | ||
|
|
||
| check := resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"), | ||
| ) | ||
|
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { skipUnlessHasOrgs(t) }, | ||
| ProviderFactories: providerFactories, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: config, | ||
| Check: check, | ||
| }, | ||
| { | ||
| ResourceName: "github_actions_organization_workflow_permissions.test", | ||
| ImportState: true, | ||
| ImportStateVerify: true, | ||
| }, | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| t.Run("deletes organization workflow permissions without error", func(t *testing.T) { | ||
| config := fmt.Sprintf(` | ||
| resource "github_actions_organization_workflow_permissions" "test" { | ||
| organization_slug = "%s" | ||
|
|
||
| default_workflow_permissions = "write" | ||
| can_approve_pull_request_reviews = true | ||
| } | ||
| `, testAccConf.owner) | ||
|
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { skipUnlessHasOrgs(t) }, | ||
| ProviderFactories: providerFactories, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: config, | ||
| Destroy: true, | ||
| }, | ||
| }, | ||
| }) | ||
| }) | ||
|
|
||
| t.Run("creates with minimal config using defaults", func(t *testing.T) { | ||
| config := fmt.Sprintf(` | ||
| resource "github_actions_organization_workflow_permissions" "test" { | ||
| organization_slug = "%s" | ||
| } | ||
| `, testAccConf.owner) | ||
|
|
||
| check := resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "organization_slug", testAccConf.owner), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "default_workflow_permissions", "read"), | ||
| resource.TestCheckResourceAttr("github_actions_organization_workflow_permissions.test", "can_approve_pull_request_reviews", "false"), | ||
| ) | ||
|
|
||
| resource.Test(t, resource.TestCase{ | ||
| PreCheck: func() { skipUnlessHasOrgs(t) }, | ||
| ProviderFactories: providerFactories, | ||
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: config, | ||
| Check: check, | ||
| }, | ||
| }, | ||
| }) | ||
| }) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.