From ac7049b2ebd36effad052de03cddec7cb82f9766 Mon Sep 17 00:00:00 2001 From: Joel Pepper Date: Mon, 27 Apr 2026 23:41:50 +0200 Subject: [PATCH] feat: add github_actions_runner_group_repository_access resource Resolves #3375 Currently the only way to control which repositories have access to a specific runner group is via the central runner group resource `github_actions_runner_group`, which manages the list of all repositories that have access to the runner group. In many cases, especially surrounding building Internal Development Platforms which create managed repositories as needed it is not desirable to centralize the list of repositories in a central state and rather allow independent, potentially concurrent deployment processes to add/remove a specific repository. This commit adds a new resource which allows to configure this access on a single repo basis without needing to track the state of all authorized repositories for a runner group --- github/provider.go | 1 + ..._actions_runner_group_repository_access.go | 127 ++++++++++++++++++ ...ons_runner_group_repository_access_test.go | 53 ++++++++ ...nner_group_repository_access.html.markdown | 51 +++++++ 4 files changed, 232 insertions(+) create mode 100644 github/resource_github_actions_runner_group_repository_access.go create mode 100644 github/resource_github_actions_runner_group_repository_access_test.go create mode 100644 website/docs/r/actions_runner_group_repository_access.html.markdown diff --git a/github/provider.go b/github/provider.go index cf044eee66..b73255f285 100644 --- a/github/provider.go +++ b/github/provider.go @@ -148,6 +148,7 @@ func Provider() *schema.Provider { "github_actions_repository_oidc_subject_claim_customization_template": resourceGithubActionsRepositoryOIDCSubjectClaimCustomizationTemplate(), "github_actions_repository_permissions": resourceGithubActionsRepositoryPermissions(), "github_actions_runner_group": resourceGithubActionsRunnerGroup(), + "github_actions_runner_group_repository_access": resourceGithubActionsRunnerGroupRepositoryAccess(), "github_actions_hosted_runner": resourceGithubActionsHostedRunner(), "github_actions_secret": resourceGithubActionsSecret(), "github_actions_variable": resourceGithubActionsVariable(), diff --git a/github/resource_github_actions_runner_group_repository_access.go b/github/resource_github_actions_runner_group_repository_access.go new file mode 100644 index 0000000000..6377a682ff --- /dev/null +++ b/github/resource_github_actions_runner_group_repository_access.go @@ -0,0 +1,127 @@ +package github + +import ( + "context" + "fmt" + "log" + "strconv" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubActionsRunnerGroupRepositoryAccess() *schema.Resource { + return &schema.Resource{ + Description: "Manages the access of a repository to an organization runner group.", + CreateContext: resourceGithubActionsRunnerGroupRepositoryAccessCreate, + ReadContext: resourceGithubActionsRunnerGroupRepositoryAccessRead, + // Omitting update function since this resource expresses a simple membership in the set of repositories with access to the runner group, it either exists or doesn't + // UpdateContext: resourceGithubActionsRunnerGroupRepositoryAccessUpdate, + DeleteContext: resourceGithubActionsRunnerGroupRepositoryAccessDelete, + Importer: &schema.ResourceImporter{ + StateContext: resourceGithubActionsRunnerGroupRepositoryAccessImport, + }, + + Schema: map[string]*schema.Schema{ + "runner_group_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "The ID of the runner group to grant the repository access on", + }, + "repository_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "The ID of the repository to grant access to the runner group", + }, + }, + } +} + +func resourceGithubActionsRunnerGroupRepositoryAccessCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + + runnerGroupId := d.Get("runner_group_id").(int) + repositoryId := d.Get("repository_id").(int) + + _, err := client.Actions.AddRepositoryAccessRunnerGroup(ctx, orgName, int64(runnerGroupId), int64(repositoryId)) + + if err != nil { + return diag.FromErr(err) + } + + d.SetId(fmt.Sprintf("%d/%d", runnerGroupId, repositoryId)) + + return resourceGithubActionsRunnerGroupRepositoryAccessRead(ctx, d, meta) +} + +func resourceGithubActionsRunnerGroupRepositoryAccessRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + + runnerGroupId := d.Get("runner_group_id").(int) + repositoryId := d.Get("repository_id").(int) + + for repo, err := range client.Actions.ListRepositoryAccessRunnerGroupIter(ctx, orgName, int64(runnerGroupId), nil) { + if err != nil { + return diag.FromErr(err) + } + + if *repo.ID == int64(repositoryId) { + // Resource matches the state in github exactly (repo has access), no need for further modifications + return nil + } + } + // We reached the end of the list without a match for our desired repository access + log.Printf("[INFO] Removing access of repository with id %d to runner group %s/%d from state because access no longer exists in GitHub", repositoryId, orgName, runnerGroupId) + d.SetId("") + return nil + +} + +func resourceGithubActionsRunnerGroupRepositoryAccessDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { + client := meta.(*Owner).v3client + orgName := meta.(*Owner).name + + runnerGroupId := d.Get("runner_group_id").(int) + repositoryId := d.Get("repository_id").(int) + + _, err := client.Actions.RemoveRepositoryAccessRunnerGroup(ctx, orgName, int64(runnerGroupId), int64(repositoryId)) + return diag.FromErr(err) + +} + +func resourceGithubActionsRunnerGroupRepositoryAccessImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + id := d.Id() + parts := strings.Split(id, "/") + if len(parts) != 2 { + return nil, fmt.Errorf("invalid import specified: supplied import must be written as /") + } + + runnerGroupID, err := strconv.Atoi(parts[0]) + + if err != nil { + return nil, fmt.Errorf("runner_group_id in id must be convertible to an int: %w", err) + } + + err = d.Set("runner_group_id", runnerGroupID) + if err != nil { + return nil, err + } + + repositoryId, err := strconv.Atoi(parts[1]) + + if err != nil { + return nil, fmt.Errorf("runner_id in id must be convertible to an int: %w", err) + } + + err = d.Set("repository_id", repositoryId) + if err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +} diff --git a/github/resource_github_actions_runner_group_repository_access_test.go b/github/resource_github_actions_runner_group_repository_access_test.go new file mode 100644 index 0000000000..77f1668603 --- /dev/null +++ b/github/resource_github_actions_runner_group_repository_access_test.go @@ -0,0 +1,53 @@ +package github + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGithubActionsRunnerGroupRepositoryAccess(t *testing.T) { + t.Run("set repo access directly and verify import", func(t *testing.T) { + randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + repoName := fmt.Sprintf("%srepo-act-runner-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` + resource "github_repository" "test" { + name = "%s" + vulnerability_alerts = false + auto_init = true + } + + resource "github_actions_runner_group" "test" { + name = github_repository.test.name + visibility = "selected" + allows_public_repositories = true + } + + resource "github_actions_runner_group_repository_access" "test" { + runner_group_id = github_actions_runner_group.test.id + repository_id = github_repository.test.repo_id + } + `, repoName) + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("github_actions_runner_group_repository_access.test", "id"), + resource.TestCheckResourceAttrSet("github_actions_runner_group_repository_access.test", "runner_group_id"), + resource.TestCheckResourceAttrSet("github_actions_runner_group_repository_access.test", "repository_id"), + ), + }, + { + ResourceName: "github_actions_runner_group_repository_access.test", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + }) +} diff --git a/website/docs/r/actions_runner_group_repository_access.html.markdown b/website/docs/r/actions_runner_group_repository_access.html.markdown new file mode 100644 index 0000000000..22897bc326 --- /dev/null +++ b/website/docs/r/actions_runner_group_repository_access.html.markdown @@ -0,0 +1,51 @@ +--- +layout: "github" +page_title: "GitHub: github_actions_runner_group_repository_access" +description: |- + Manages a repository's access to an Actions Runner Group within a GitHub organization +--- + +# github_actions_runner_group_repository_access + +This resource allows you to manage repository access to GitHub Actions runner groups within your GitHub (enterprise) organizations independently for each repository. +You must have runner group admin access to an organization to use this resource. + +~> **Note:** The action runners group's `visibility` must be `selected` and if also managing the runner group via terraform: `selected_repository_ids` must **not** be set. + +## Example Usage + +```hcl +resource "github_repository" "example" { + name = "my-repository" +} + +resource "github_actions_runner_group" "example" { + name = github_repository.example.name + visibility = "selected" +} + +resource "github_actions_runner_group_repository_access" "example" { + runner_group_id = github_actions_runner_group.id + repository_id = github_repository.example.repo_id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `runner_group_id` - (Required) Id of the runner group +* `repository_id` - (Required) Id of the repository to give access to the runner group + + +## Attributes Reference + +* `id` - id of this resource, formed as `/` + +## Import + +This resource can be imported using the ID of the runner group and the repository ID: + +``` +$ terraform import github_actions_runner_group_repository_access.test / +```