Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6e0afdf
feat: Add GitHub Actions hosted runner resource and examples
austenstone Nov 13, 2025
b1b4498
feat: Add documentation for GitHub Actions hosted runner resource
austenstone Nov 13, 2025
8cabf1f
feat: Update GitHub Actions hosted runner resource to use numeric ima…
austenstone Nov 13, 2025
c734c06
feat: Update GitHub Actions hosted runner resource to allow size fiel…
austenstone Nov 13, 2025
7d5319a
feat: Enhance GitHub Actions hosted runner resource to handle image I…
austenstone Nov 13, 2025
ec3d531
Add link to GitHub Actions hosted runner documentation in website
austenstone Nov 14, 2025
3663e38
feat: Improve error handling and type assertion in resourceGithubActi…
austenstone Nov 14, 2025
677d95a
Merge branch 'main' into feature/github-actions-hosted-runner
austenstone Nov 14, 2025
4debb80
Merge branch 'main' into feature/github-actions-hosted-runner
nickfloyd Nov 20, 2025
ebb7398
lint: Replace interface{} with any in resource_github_actions_hosted_…
austenstone Nov 22, 2025
0f09eec
Merge branch 'main' into feature/github-actions-hosted-runner
austenstone Nov 22, 2025
cb234bd
Merge branch 'main' into feature/github-actions-hosted-runner
nickfloyd Nov 25, 2025
c6c36b8
Merge branch 'main' into feature/github-actions-hosted-runner
nickfloyd Nov 25, 2025
8db66a1
Merge main into feature/github-actions-hosted-runner
austenstone Jan 7, 2026
be16a48
Merge remote-tracking branch 'origin/main' into feature/github-action…
austenstone Mar 15, 2026
eebea06
feat: add organization network configuration resource
austenstone Mar 15, 2026
f562c9b
refactor: clean up network configuration resource
austenstone Mar 15, 2026
290b604
test: align network configuration resource with repo patterns
austenstone Mar 15, 2026
6d6b598
docs: polish organization network configuration resource
austenstone Mar 15, 2026
4f00048
refactor: use RFC3339 constant for network configuration timestamps
austenstone Mar 15, 2026
e487e35
refactor: address network configuration review comments
austenstone Mar 15, 2026
e8b240b
refactor: align network configuration with project patterns
austenstone Mar 15, 2026
4d3ceb3
feat: add link for github_organization_network_configuration in docum…
austenstone Mar 15, 2026
7d1303d
refactor: enhance error handling and documentation for organization n…
austenstone Mar 15, 2026
44bdafc
feat: add support for network configurations in GitHub Actions runner…
austenstone Mar 15, 2026
3aebd44
feat: add network configuration validation for GitHub Actions runner …
austenstone Mar 15, 2026
6127464
feat: update advanced hosted runner configuration and enhance documen…
austenstone Mar 15, 2026
70a1a81
Merge remote-tracking branch 'origin/feature/github-organization-netw…
austenstone Mar 15, 2026
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
8 changes: 4 additions & 4 deletions examples/hosted_runner/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ resource "github_actions_hosted_runner" "advanced" {
source = "github"
}

size = "8-core"
runner_group_id = github_actions_runner_group.example.id
maximum_runners = 10
enable_static_ip = true
size = "8-core"
runner_group_id = github_actions_runner_group.example.id
maximum_runners = 10
public_ip_enabled = true
}
1 change: 1 addition & 0 deletions github/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ func Provider() *schema.Provider {
"github_organization_block": resourceOrganizationBlock(),
"github_organization_custom_role": resourceGithubOrganizationCustomRole(),
"github_organization_custom_properties": resourceGithubOrganizationCustomProperties(),
"github_organization_network_configuration": resourceGithubOrganizationNetworkConfiguration(),
"github_organization_project": resourceGithubOrganizationProject(),
"github_organization_repository_role": resourceGithubOrganizationRepositoryRole(),
"github_organization_role": resourceGithubOrganizationRole(),
Expand Down
105 changes: 94 additions & 11 deletions github/resource_github_actions_runner_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

type organizationRunnerGroup struct {
NetworkConfigurationID *string `json:"network_configuration_id,omitempty"`
}

func resourceGithubActionsRunnerGroup() *schema.Resource {
return &schema.Resource{
Create: resourceGithubActionsRunnerGroupCreate,
Expand Down Expand Up @@ -92,10 +96,74 @@ func resourceGithubActionsRunnerGroup() *schema.Resource {
Optional: true,
Description: "List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to 'true'.",
},
"network_configuration_id": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringLenBetween(1, 255),
Description: "The identifier of the hosted compute network configuration to associate with this runner group for GitHub-hosted private networking.",
},
},
}
}

func getOrganizationRunnerGroupNetworking(client *github.Client, ctx context.Context, org string, groupID int64) (*organizationRunnerGroup, *github.Response, error) {
req, err := client.NewRequest("GET", fmt.Sprintf("orgs/%s/actions/runner-groups/%d", org, groupID), nil)
if err != nil {
return nil, nil, err
}

var runnerGroup organizationRunnerGroup
resp, err := client.Do(ctx, req, &runnerGroup)
if err != nil {
return nil, resp, err
}

return &runnerGroup, resp, nil
}

func getOrganizationRunnerGroup(client *github.Client, ctx context.Context, org string, groupID int64) (*github.RunnerGroup, *github.Response, error) {
runnerGroup, resp, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, groupID)
if err != nil {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) {
// ignore error StatusNotModified
return runnerGroup, resp, nil
}
}
return runnerGroup, resp, err
}

func updateOrganizationRunnerGroupNetworking(client *github.Client, ctx context.Context, org string, groupID int64, networkConfigurationID *string) (*github.Response, error) {
payload := map[string]any{
"network_configuration_id": networkConfigurationID,
}

req, err := client.NewRequest("PATCH", fmt.Sprintf("orgs/%s/actions/runner-groups/%d", org, groupID), payload)
if err != nil {
return nil, err
}

resp, err := client.Do(ctx, req, nil)
if err != nil {
return resp, err
}

return resp, nil
}

func setGithubActionsRunnerGroupNetworkingState(d *schema.ResourceData, runnerGroup *organizationRunnerGroup) error {
if runnerGroup != nil && runnerGroup.NetworkConfigurationID != nil && *runnerGroup.NetworkConfigurationID != "" {
if err := d.Set("network_configuration_id", *runnerGroup.NetworkConfigurationID); err != nil {
return err
}
} else {
if err := d.Set("network_configuration_id", nil); err != nil {
return err
}
}
return nil
}

func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) error {
err := checkOrganization(meta)
if err != nil {
Expand Down Expand Up @@ -186,19 +254,14 @@ func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) er
return err
}

return resourceGithubActionsRunnerGroupRead(d, meta)
}

func getOrganizationRunnerGroup(client *github.Client, ctx context.Context, org string, groupID int64) (*github.RunnerGroup, *github.Response, error) {
runnerGroup, resp, err := client.Actions.GetOrganizationRunnerGroup(ctx, org, groupID)
if err != nil {
var ghErr *github.ErrorResponse
if errors.As(err, &ghErr) {
// ignore error StatusNotModified
return runnerGroup, resp, nil
if networkConfigurationID, ok := d.GetOk("network_configuration_id"); ok {
networkConfigurationIDValue := networkConfigurationID.(string)
if _, err = updateOrganizationRunnerGroupNetworking(client, ctx, orgName, runnerGroup.GetID(), &networkConfigurationIDValue); err != nil {
return err
}
}
return runnerGroup, resp, err

return resourceGithubActionsRunnerGroupRead(d, meta)
}

func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) error {
Expand Down Expand Up @@ -272,6 +335,14 @@ func resourceGithubActionsRunnerGroupRead(d *schema.ResourceData, meta any) erro
return err
}

runnerGroupNetworking, _, err := getOrganizationRunnerGroupNetworking(client, context.WithValue(context.Background(), ctxId, d.Id()), orgName, runnerGroupID)
if err != nil {
return err
}
if err = setGithubActionsRunnerGroupNetworkingState(d, runnerGroupNetworking); err != nil {
return err
}

selectedRepositoryIDs := []int64{}
options := github.ListOptions{
PerPage: maxPerPage,
Expand Down Expand Up @@ -339,6 +410,18 @@ func resourceGithubActionsRunnerGroupUpdate(d *schema.ResourceData, meta any) er
return err
}

if d.HasChange("network_configuration_id") {
var networkConfigurationIDValue *string
if networkConfigurationID, ok := d.GetOk("network_configuration_id"); ok {
value := networkConfigurationID.(string)
networkConfigurationIDValue = &value
}

if _, err := updateOrganizationRunnerGroupNetworking(client, ctx, orgName, runnerGroupID, networkConfigurationIDValue); err != nil {
return err
}
}

selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids")
selectedRepositoryIDs := []int64{}

Expand Down
132 changes: 132 additions & 0 deletions github/resource_github_actions_runner_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@ import (
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName string) resource.TestCheckFunc {
return func(state *terraform.State) error {
runnerGroup, ok := state.RootModule().Resources[resourceName]
if !ok {
return fmt.Errorf("runner group resource %s not found in state", resourceName)
}

networkConfiguration, ok := state.RootModule().Resources[networkConfigurationResourceName]
if !ok {
return fmt.Errorf("network configuration resource %s not found in state", networkConfigurationResourceName)
}

actual := runnerGroup.Primary.Attributes["network_configuration_id"]
expected := networkConfiguration.Primary.ID

if actual != expected {
return fmt.Errorf("actual network_configuration_id %q does not match expected %q", actual, expected)
}

return nil
}
}

func TestAccGithubActionsRunnerGroup(t *testing.T) {
t.Run("creates runner groups without error", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
Expand Down Expand Up @@ -97,6 +120,115 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) {
})
})

t.Run("manages private networking association for hosted runners", func(t *testing.T) {
networkSettingsID := testAccOrganizationNetworkConfigurationID(t)
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "github_actions_runner_group.test"
networkConfigurationResourceName := "github_organization_network_configuration.test"
networkConfigurationName := fmt.Sprintf("%snetwork-config-%s", testResourcePrefix, randomID)
runnerGroupName := fmt.Sprintf("%srunner-group-%s", testResourcePrefix, randomID)

configWithoutNetworkConfiguration := fmt.Sprintf(`
resource "github_organization_network_configuration" "test" {
name = %q
compute_service = "actions"
network_settings_ids = [%q]
}

resource "github_actions_runner_group" "test" {
name = %q
visibility = "all"
}
`, networkConfigurationName, networkSettingsID, runnerGroupName)

configWithNetworkConfiguration := fmt.Sprintf(`
resource "github_organization_network_configuration" "test" {
name = %q
compute_service = "actions"
network_settings_ids = [%q]
}

resource "github_actions_runner_group" "test" {
name = %q
visibility = "all"
network_configuration_id = github_organization_network_configuration.test.id
}
`, networkConfigurationName, networkSettingsID, runnerGroupName)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: configWithoutNetworkConfiguration,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"),
),
},
{
Config: configWithNetworkConfiguration,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"),
testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
{
Config: configWithoutNetworkConfiguration,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckNoResourceAttr(resourceName, "network_configuration_id"),
),
},
},
})
})

t.Run("creates private networking association for hosted runners on create", func(t *testing.T) {
networkSettingsID := testAccOrganizationNetworkConfigurationID(t)
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
resourceName := "github_actions_runner_group.test"
networkConfigurationResourceName := "github_organization_network_configuration.test"
networkConfigurationName := fmt.Sprintf("%snetwork-config-create-%s", testResourcePrefix, randomID)
runnerGroupName := fmt.Sprintf("%srunner-group-create-%s", testResourcePrefix, randomID)

config := fmt.Sprintf(`
resource "github_organization_network_configuration" "test" {
name = %q
compute_service = "actions"
network_settings_ids = [%q]
}

resource "github_actions_runner_group" "test" {
name = %q
visibility = "all"
network_configuration_id = github_organization_network_configuration.test.id
}
`, networkConfigurationName, networkSettingsID, runnerGroupName)

resource.Test(t, resource.TestCase{
PreCheck: func() { skipUnlessHasPaidOrgs(t) },
ProviderFactories: providerFactories,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet(resourceName, "network_configuration_id"),
testCheckRunnerGroupNetworkConfigurationMatches(resourceName, networkConfigurationResourceName),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
})

t.Run("manages runner visibility", func(t *testing.T) {
randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)
repoName := fmt.Sprintf("%srepo-act-runner-%s", testResourcePrefix, randomID)
Expand Down
Loading