From a7ac90ac7ee254acc228b11275bcd3b751c112a1 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 3 Jul 2026 12:59:25 +0300 Subject: [PATCH 01/22] Ensure we don't re-use the same names for runner groups Signed-off-by: Timo Sand --- ...ource_github_actions_hosted_runner_test.go | 88 ++++++++++++------- 1 file changed, 55 insertions(+), 33 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 7b6a7f83fc..96812b6c11 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -11,19 +11,21 @@ import ( func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() - randomID := acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) - t.Run("creates hosted runners without error", func(t *testing.T) { t.Parallel() + randomID := acctest.RandString(5) + runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) + hostedRunnerName := fmt.Sprintf("%srunner-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-%s" + name = "%s" image { id = "2306" @@ -33,12 +35,12 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = github_actions_runner_group.test.id } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) check := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "name", - fmt.Sprintf("tf-acc-test-%s", randomID), + hostedRunnerName, ), resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "size", @@ -93,14 +95,18 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("creates hosted runner with optional parameters", func(t *testing.T) { t.Parallel() + randomID := acctest.RandString(5) + runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) + hostedRunnerName := fmt.Sprintf("%srunner-optional-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-optional-%s" + name = "%s" image { id = "2306" @@ -112,12 +118,12 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { maximum_runners = 5 public_ip_enabled = true } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) check := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "name", - fmt.Sprintf("tf-acc-test-optional-%s", randomID), + hostedRunnerName, ), resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "size", @@ -148,14 +154,18 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("updates hosted runner configuration", func(t *testing.T) { t.Parallel() + randomID := acctest.RandString(5) + runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) + hostedRunnerName := fmt.Sprintf("%srunner-update-%s", testResourcePrefix, randomID) + configBefore := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-update-%s" + name = "%s" image { id = "2306" @@ -166,16 +176,16 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { runner_group_id = github_actions_runner_group.test.id maximum_runners = 3 } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) configAfter := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-update-%s-updated" + name = "%s-updated" image { id = "2306" @@ -186,12 +196,12 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { runner_group_id = github_actions_runner_group.test.id maximum_runners = 5 } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) checkBefore := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "name", - fmt.Sprintf("tf-acc-test-update-%s", randomID), + hostedRunnerName, ), resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "size", @@ -206,7 +216,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { checkAfter := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "name", - fmt.Sprintf("tf-acc-test-update-%s-updated", randomID), + fmt.Sprintf("%s-updated", hostedRunnerName), ), resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "size", @@ -237,14 +247,18 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("updates size field", func(t *testing.T) { t.Parallel() + randomID := acctest.RandString(5) + runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) + hostedRunnerName := fmt.Sprintf("%srunner-size-%s", testResourcePrefix, randomID) + configBefore := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-size-%s" + name = "%s" image { id = "2306" @@ -254,16 +268,16 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = github_actions_runner_group.test.id } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) configAfter := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-size-%s" + name = "%s" image { id = "2306" @@ -273,7 +287,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "8-core" runner_group_id = github_actions_runner_group.test.id } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) checkBefore := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr( @@ -316,14 +330,18 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("imports hosted runner", func(t *testing.T) { t.Parallel() + randomID := acctest.RandString(5) + runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) + hostedRunnerName := fmt.Sprintf("%srunner-import-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-import-%s" + name = "%s" image { id = "2306" @@ -333,7 +351,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = github_actions_runner_group.test.id } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) check := resource.ComposeTestCheckFunc( resource.TestCheckResourceAttrSet( @@ -341,7 +359,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ), resource.TestCheckResourceAttr( "github_actions_hosted_runner.test", "name", - fmt.Sprintf("tf-acc-test-import-%s", randomID), + hostedRunnerName, ), ) @@ -366,14 +384,18 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("deletes hosted runner", func(t *testing.T) { t.Parallel() + randomID := acctest.RandString(5) + runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) + hostedRunnerName := fmt.Sprintf("%srunner-delete-%s", testResourcePrefix, randomID) + config := fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } resource "github_actions_hosted_runner" "test" { - name = "tf-acc-test-delete-%s" + name = "%s" image { id = "2306" @@ -383,7 +405,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = github_actions_runner_group.test.id } - `, randomID, randomID) + `, runnerGroupName, hostedRunnerName) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -401,10 +423,10 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { { Config: fmt.Sprintf(` resource "github_actions_runner_group" "test" { - name = "tf-acc-test-group-%s" + name = "%s" visibility = "all" } - `, randomID), + `, runnerGroupName), }, }, }) From f11c0f1657100fb1a3b9fb9b4193fd7cf8a81d54 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 4 Jul 2026 17:40:07 +0300 Subject: [PATCH 02/22] Refactor to us `ConfigStateChecks` in `creates_hosted_runners_without_error` Signed-off-by: Timo Sand --- ...ource_github_actions_hosted_runner_test.go | 64 ++++++------------- 1 file changed, 19 insertions(+), 45 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 96812b6c11..e843333f55 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -6,12 +6,15 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() - t.Run("creates hosted runners without error", func(t *testing.T) { + t.Run("creates_hosted_runners_without_error", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) @@ -37,56 +40,27 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, runnerGroupName, hostedRunnerName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "name", - hostedRunnerName, - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "size", - "4-core", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "image.0.id", - "2306", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "image.0.source", - "github", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "id", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "status", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "platform", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "image.0.size_gb", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "machine_size_details.0.id", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "machine_size_details.0.cpu_cores", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "machine_size_details.0.memory_gb", - ), - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "machine_size_details.0.storage_gb", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("runner_group_id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("status"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("platform"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("image").AtSliceIndex(0).AtMapKey("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("image").AtSliceIndex(0).AtMapKey("source"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("image").AtSliceIndex(0).AtMapKey("size_gb"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("memory_gb"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("storage_gb"), knownvalue.NotNull()), + }, }, }, }) From c43fab203bbaae7c5d39223ea56e3986c5bc140e Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 12:46:13 +0300 Subject: [PATCH 03/22] Refactor `creates_hosted_runner_with_optional_parameters` to use `ConfigStateChecks` Signed-off-by: Timo Sand --- ...ource_github_actions_hosted_runner_test.go | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index e843333f55..7b64a10619 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -66,7 +66,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }) }) - t.Run("creates hosted runner with optional parameters", func(t *testing.T) { + t.Run("creates_hosted_runner_with_optional_parameters", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) @@ -94,32 +94,18 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, runnerGroupName, hostedRunnerName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "name", - hostedRunnerName, - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "size", - "2-core", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "maximum_runners", - "5", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "public_ip_enabled", - "true", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("2-core")), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(5)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("public_ip_enabled"), knownvalue.Bool(true)), + }, }, }, }) From e04667cb395416b6c8815382d6cdc71330ea729f Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 12:47:13 +0300 Subject: [PATCH 04/22] Refactor `updates_hosted_runner_configuration` to use `ConfigStateChecks` and `ConfigPlanChecks` Signed-off-by: Timo Sand --- ...ource_github_actions_hosted_runner_test.go | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 7b64a10619..69077f8288 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/plancheck" "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) @@ -111,7 +112,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }) }) - t.Run("updates hosted runner configuration", func(t *testing.T) { + t.Run("updates_hosted_runner_configuration", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) @@ -158,47 +159,30 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, runnerGroupName, hostedRunnerName) - checkBefore := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "name", - hostedRunnerName, - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "size", - "4-core", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "maximum_runners", - "3", - ), - ) - - checkAfter := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "name", - fmt.Sprintf("%s-updated", hostedRunnerName), - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "size", - "4-core", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "maximum_runners", - "5", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: configBefore, - Check: checkBefore, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(3)), + }, }, { Config: configAfter, - Check: checkAfter, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_actions_hosted_runner.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(fmt.Sprintf("%s-updated", hostedRunnerName))), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(5)), + }, }, }, }) From 614d7d42f6441e4b4aa3b7e384e2a42aef3e2745 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 12:48:22 +0300 Subject: [PATCH 05/22] Refactor `updates_size_field` to use `ConfigStateChecks` and `ConfigPlanChecks` Signed-off-by: Timo Sand --- ...ource_github_actions_hosted_runner_test.go | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 69077f8288..d8bf48ffcb 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -188,7 +188,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }) }) - t.Run("updates size field", func(t *testing.T) { + t.Run("updates_size_field", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) @@ -233,39 +233,28 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, runnerGroupName, hostedRunnerName) - checkBefore := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "size", - "4-core", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "machine_size_details.0.cpu_cores", - "4", - ), - ) - - checkAfter := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "size", - "8-core", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "machine_size_details.0.cpu_cores", - "8", - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: configBefore, - Check: checkBefore, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.Int64Exact(4)), + }, }, { Config: configAfter, - Check: checkAfter, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("github_actions_hosted_runner.test", plancheck.ResourceActionUpdate), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("8-core")), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.Int64Exact(8)), + }, }, }, }) From 4ca1685f38868079d7eaf71c68ae61f4e2c73127 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 12:49:26 +0300 Subject: [PATCH 06/22] Refactor `imports_hosted_runner` to use `ConfigStateChecks` Signed-off-by: Timo Sand --- ...esource_github_actions_hosted_runner_test.go | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index d8bf48ffcb..48ba03c9e3 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -260,7 +260,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }) }) - t.Run("imports hosted runner", func(t *testing.T) { + t.Run("imports_hosted_runner", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) @@ -286,23 +286,16 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, runnerGroupName, hostedRunnerName) - check := resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "id", - ), - resource.TestCheckResourceAttr( - "github_actions_hosted_runner.test", "name", - hostedRunnerName, - ), - ) - resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { Config: config, - Check: check, + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), + }, }, { ResourceName: "github_actions_hosted_runner.test", From ce15f157d2106d0854d2a7da03d2d8a65c266a66 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 12:50:24 +0300 Subject: [PATCH 07/22] Refactor `deletes_hosted_runner` to use `ConfigStateChecks` Signed-off-by: Timo Sand --- github/resource_github_actions_hosted_runner_test.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 48ba03c9e3..062d68812c 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -307,7 +307,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }) }) - t.Run("deletes hosted runner", func(t *testing.T) { + t.Run("deletes_hosted_runner", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) @@ -339,11 +339,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttrSet( - "github_actions_hosted_runner.test", "id", - ), - ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), + }, }, // This step should successfully delete the runner { From ef0e95fd39fc7a015474d788bc23ecd866415f85 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 15:17:00 +0300 Subject: [PATCH 08/22] Ensure `make sweep` clears all resources by default Signed-off-by: Timo Sand --- GNUmakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index c23c4b8b55..04824add4a 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,4 +1,4 @@ -SWEEP?=repositories,teams +SWEEP?=all PKG_NAME=github TESTACC?=./$(PKG_NAME)/... @@ -81,7 +81,7 @@ testacc: sweep: @echo "WARNING: This will destroy infrastructure. Use only in development accounts." - go test $(TESTACC) -v -sweep=$(SWEEP) $(SWEEPARGS) + go test ./github -v -sweep=$(SWEEP) $(SWEEPARGS) generatedocs: @cd tools; go generate ./... From ef20a4ce14e941c5e29d8332ad3b85683a3ddca7 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 15:17:39 +0300 Subject: [PATCH 09/22] Add sweeper for Org Actions Runner Groups Signed-off-by: Timo Sand --- github/acc_test.go | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/github/acc_test.go b/github/acc_test.go index 4c9f72a4be..b30a0a870e 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -2,7 +2,9 @@ package github import ( "context" + "errors" "fmt" + "net/http" "net/url" "os" "slices" @@ -267,6 +269,11 @@ func configureSweepers() { Name: "teams", F: sweepTeams, }) + + resource.AddTestSweepers("organization_runner_groups", &resource.Sweeper{ + Name: "organization_runner_groups", + F: sweepOrganizationRunnerGroups, + }) } func sweepTeams(_ string) error { @@ -329,6 +336,72 @@ func sweepRepositories(_ string) error { return nil } +func sweepOrganizationRunnerGroups(_ string) error { + if !slices.Contains(orgTestModes, testMode(os.Getenv("GH_TEST_AUTH_MODE"))) { + return nil + } + + client := testAccConf.meta.v3client + owner := testAccConf.meta.name + ctx := context.Background() + + fmt.Println("sweeping organization runner groups") + runnerGroups, _, err := client.Actions.ListOrganizationRunnerGroups(ctx, owner, &github.ListOrgRunnerGroupOptions{}) + if err != nil { + return err + } + + for _, rg := range runnerGroups.GetRunnerGroups() { + if name := rg.GetName(); strings.HasPrefix(name, testResourcePrefix) { + fmt.Printf("destroying organization runner group %s\n", name) + + if err := sweepRunnerGroupRunners(rg); err != nil { + return err + } + + if _, err := client.Actions.DeleteOrganizationRunnerGroup(ctx, owner, rg.GetID()); err != nil { + return err + } + } + } + + return nil +} + +func sweepRunnerGroupRunners(rg *github.RunnerGroup) error { + client := testAccConf.meta.v3client + owner := testAccConf.meta.name + ctx := context.Background() + + fmt.Printf("sweeping organization hosted runners of runner group: %s\n", rg.GetName()) + runners, _, err := client.Actions.ListRunnerGroupHostedRunners(ctx, owner, rg.GetID(), &github.ListOptions{}) + if err != nil { + return err + } + + for _, r := range runners.GetRunners() { + fmt.Printf("destroying organization hosted runner %s\n", r.GetName()) + + if _, resp, err := client.Actions.DeleteHostedRunner(ctx, owner, r.GetID()); err != nil { + if err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + return nil + } + if _, ok := errors.AsType[*github.ErrorResponse](err); ok { + return waitForRunnerDeletion(ctx, client, owner, strconv.FormatInt(r.GetID(), 10), 0) + } + return err + } + + if resp != nil && resp.StatusCode == http.StatusAccepted { + return waitForRunnerDeletion(ctx, client, owner, strconv.FormatInt(r.GetID(), 10), 0) + } + } + } + + return nil +} + func skipUnauthenticated(t *testing.T) { if testAccConf.authMode == anonymous { t.Skip("Skipping as test mode not authenticated") From 29234d0e0af151ef0a07a03646f83a017cb2d0b7 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 15:38:08 +0300 Subject: [PATCH 10/22] Refactor to use a single test runner group for all actions runner tests Signed-off-by: Timo Sand --- github/acc_helpers_test.go | 28 ++++++ ...ource_github_actions_hosted_runner_test.go | 90 +++++-------------- 2 files changed, 49 insertions(+), 69 deletions(-) diff --git a/github/acc_helpers_test.go b/github/acc_helpers_test.go index ebd2daf3af..5683bc9d7d 100644 --- a/github/acc_helpers_test.go +++ b/github/acc_helpers_test.go @@ -161,6 +161,34 @@ func mustCreateTestRepository(t *testing.T) *github.Repository { return repo } +func mustCreateTestOrganizationActionsRunnerGroup(t *testing.T) *github.RunnerGroup { + t.Helper() + + randomID := acctest.RandString(testRandomIDLength) + name := fmt.Sprintf("%s%s", testResourcePrefix, randomID) + + req := github.CreateRunnerGroupRequest{ + Name: &name, + Visibility: new("all"), + } + + runnerGroup, _, err := testAccConf.meta.v3client.Actions.CreateOrganizationRunnerGroup(t.Context(), testAccConf.meta.name, req) + if err != nil { + t.Fatalf("failed to create test organization runner group: %v", err) + } + + t.Cleanup(func() { + if _, err := testAccConf.meta.v3client.Actions.DeleteOrganizationRunnerGroup(context.WithoutCancel(t.Context()), testAccConf.meta.name, runnerGroup.GetID()); err != nil { + if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == 404 { + return + } + t.Logf("failed to delete test organization runner group %s: %v", name, err) + } + }) + + return runnerGroup +} + func mustAddRepositoryCollaborator(t *testing.T, repo *github.Repository, username string) { t.Helper() diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 062d68812c..fca971e4f1 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -14,20 +14,17 @@ import ( func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() + skipUnlessHasPaidOrgs(t) + + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) t.Run("creates_hosted_runners_without_error", func(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) - runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) hostedRunnerName := fmt.Sprintf("%srunner-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -37,9 +34,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "4-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -71,15 +68,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) - runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) hostedRunnerName := fmt.Sprintf("%srunner-optional-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -89,11 +80,11 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "2-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" maximum_runners = 5 public_ip_enabled = true } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -116,15 +107,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) - runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) hostedRunnerName := fmt.Sprintf("%srunner-update-%s", testResourcePrefix, randomID) configBefore := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -134,17 +119,12 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "4-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" maximum_runners = 3 } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) configAfter := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s-updated" @@ -154,10 +134,10 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "4-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" maximum_runners = 5 } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -192,15 +172,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) - runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) hostedRunnerName := fmt.Sprintf("%srunner-size-%s", testResourcePrefix, randomID) configBefore := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -210,16 +184,11 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "4-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) configAfter := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -229,9 +198,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "8-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -264,15 +233,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) - runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) hostedRunnerName := fmt.Sprintf("%srunner-import-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -282,9 +245,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "4-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -311,15 +274,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() randomID := acctest.RandString(5) - runnerGroupName := fmt.Sprintf("%sgroup-%s", testResourcePrefix, randomID) hostedRunnerName := fmt.Sprintf("%srunner-delete-%s", testResourcePrefix, randomID) config := fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - resource "github_actions_hosted_runner" "test" { name = "%s" @@ -329,9 +286,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } size = "4-core" - runner_group_id = github_actions_runner_group.test.id + runner_group_id = "%d" } - `, runnerGroupName, hostedRunnerName) + `, hostedRunnerName, runnerGroup.GetID()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, @@ -343,14 +300,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), }, }, - // This step should successfully delete the runner { - Config: fmt.Sprintf(` - resource "github_actions_runner_group" "test" { - name = "%s" - visibility = "all" - } - `, runnerGroupName), + Config: config, + Destroy: true, }, }, }) From 976cb5e6275d88f8c519cd8cb65fe65ea04d55ab Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 18:17:55 +0300 Subject: [PATCH 11/22] Ensure sweeper waits correctly for runner group runner deletions --- github/acc_test.go | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/github/acc_test.go b/github/acc_test.go index b30a0a870e..d9ea726036 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -11,6 +11,7 @@ import ( "strconv" "strings" "testing" + "time" "github.com/google/go-github/v88/github" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -355,11 +356,14 @@ func sweepOrganizationRunnerGroups(_ string) error { if name := rg.GetName(); strings.HasPrefix(name, testResourcePrefix) { fmt.Printf("destroying organization runner group %s\n", name) - if err := sweepRunnerGroupRunners(rg); err != nil { - return err + if err := sweepRunnerGroupRunners(ctx, rg); err != nil { + return fmt.Errorf("failed to sweep runner group runners for %s: %w", rg.GetName(), err) } - if _, err := client.Actions.DeleteOrganizationRunnerGroup(ctx, owner, rg.GetID()); err != nil { + if resp, err := client.Actions.DeleteOrganizationRunnerGroup(ctx, owner, rg.GetID()); err != nil { + if resp != nil && resp.StatusCode == http.StatusNotFound { + continue + } return err } } @@ -368,34 +372,33 @@ func sweepOrganizationRunnerGroups(_ string) error { return nil } -func sweepRunnerGroupRunners(rg *github.RunnerGroup) error { +func sweepRunnerGroupRunners(ctx context.Context, rg *github.RunnerGroup) error { client := testAccConf.meta.v3client owner := testAccConf.meta.name - ctx := context.Background() - fmt.Printf("sweeping organization hosted runners of runner group: %s\n", rg.GetName()) + fmt.Printf("removing organization hosted runners of runner group: %s\n", rg.GetName()) runners, _, err := client.Actions.ListRunnerGroupHostedRunners(ctx, owner, rg.GetID(), &github.ListOptions{}) if err != nil { return err } for _, r := range runners.GetRunners() { - fmt.Printf("destroying organization hosted runner %s\n", r.GetName()) - - if _, resp, err := client.Actions.DeleteHostedRunner(ctx, owner, r.GetID()); err != nil { + _, resp, err := client.Actions.DeleteHostedRunner(ctx, owner, r.GetID()) + if err != nil { if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { - return nil + continue } - if _, ok := errors.AsType[*github.ErrorResponse](err); ok { - return waitForRunnerDeletion(ctx, client, owner, strconv.FormatInt(r.GetID(), 10), 0) + if _, ok := errors.AsType[*github.AcceptedError](err); ok { + waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute) + continue } - return err + return fmt.Errorf("failed to delete hosted runner %s: %w", r.GetName(), err) } + } - if resp != nil && resp.StatusCode == http.StatusAccepted { - return waitForRunnerDeletion(ctx, client, owner, strconv.FormatInt(r.GetID(), 10), 0) - } + if resp != nil && resp.StatusCode == http.StatusAccepted { + waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute) } } From 692816ff4ea9ff2bfa6769035cc7c5f8c11409db Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 18:20:52 +0300 Subject: [PATCH 12/22] Refactor `resource_github_actions_hosted_runner.go` to use `go-github` API methods Signed-off-by: Timo Sand --- github/acc_helpers_test.go | 6 +- github/acc_test.go | 8 +- .../resource_github_actions_hosted_runner.go | 308 ++++++++---------- ...ource_github_actions_hosted_runner_test.go | 2 +- 4 files changed, 146 insertions(+), 178 deletions(-) diff --git a/github/acc_helpers_test.go b/github/acc_helpers_test.go index 5683bc9d7d..fe3ed5c781 100644 --- a/github/acc_helpers_test.go +++ b/github/acc_helpers_test.go @@ -178,7 +178,11 @@ func mustCreateTestOrganizationActionsRunnerGroup(t *testing.T) *github.RunnerGr } t.Cleanup(func() { - if _, err := testAccConf.meta.v3client.Actions.DeleteOrganizationRunnerGroup(context.WithoutCancel(t.Context()), testAccConf.meta.name, runnerGroup.GetID()); err != nil { + ctx := context.WithoutCancel(t.Context()) + if err := sweepRunnerGroupRunners(ctx, runnerGroup); err != nil { + t.Logf("failed to delete runner group runners for %s: %v", runnerGroup.GetName(), err) + } + if _, err := testAccConf.meta.v3client.Actions.DeleteOrganizationRunnerGroup(ctx, testAccConf.meta.name, runnerGroup.GetID()); err != nil { if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == 404 { return } diff --git a/github/acc_test.go b/github/acc_test.go index d9ea726036..d789821fff 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -390,7 +390,9 @@ func sweepRunnerGroupRunners(ctx context.Context, rg *github.RunnerGroup) error continue } if _, ok := errors.AsType[*github.AcceptedError](err); ok { - waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute) + if err := waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute); err != nil { + return fmt.Errorf("failed to wait for runner deletion %s: %w", r.GetName(), err) + } continue } return fmt.Errorf("failed to delete hosted runner %s: %w", r.GetName(), err) @@ -398,7 +400,9 @@ func sweepRunnerGroupRunners(ctx context.Context, rg *github.RunnerGroup) error } if resp != nil && resp.StatusCode == http.StatusAccepted { - waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute) + if err := waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute); err != nil { + return fmt.Errorf("failed to wait for runner deletion %s: %w", r.GetName(), err) + } } } diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index 027fb648a2..a87f67af2a 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -3,7 +3,6 @@ package github import ( "context" "errors" - "fmt" "log" "net/http" "regexp" @@ -183,92 +182,63 @@ func resourceGithubActionsHostedRunner() *schema.Resource { } } -func expandImage(imageList []any) map[string]any { +func expandImage(imageList []any) github.HostedRunnerImage { if len(imageList) == 0 { - return nil + return github.HostedRunnerImage{} } + runnerImage := github.HostedRunnerImage{} imageMap := imageList[0].(map[string]any) - result := make(map[string]any) if id, ok := imageMap["id"].(string); ok { - result["id"] = id + runnerImage.ID = id } if source, ok := imageMap["source"].(string); ok { - result["source"] = source + runnerImage.Source = source } - return result + return runnerImage } -func flattenImage(image map[string]any) []any { +func flattenImage(image *github.HostedRunnerImageDetail) []any { if image == nil { return []any{} } result := make(map[string]any) - // Handle id as either string or number - if id, ok := image["id"].(string); ok { - result["id"] = id - } else if id, ok := image["id"].(float64); ok { - result["id"] = fmt.Sprintf("%.0f", id) - } - - if source, ok := image["source"].(string); ok { - result["source"] = source - } - if size, ok := image["size"].(float64); ok { - result["size_gb"] = int(size) - } + result["id"] = image.GetID() + result["source"] = image.GetSource() + result["size_gb"] = image.GetSizeGB() return []any{result} } -func flattenMachineSizeDetails(details map[string]any) []any { +func flattenMachineSizeDetails(details *github.HostedRunnerMachineSpec) []any { if details == nil { return []any{} } result := make(map[string]any) - if id, ok := details["id"].(string); ok { - result["id"] = id - } - if cpuCores, ok := details["cpu_cores"].(float64); ok { - result["cpu_cores"] = int(cpuCores) - } - if memoryGB, ok := details["memory_gb"].(float64); ok { - result["memory_gb"] = int(memoryGB) - } - if storageGB, ok := details["storage_gb"].(float64); ok { - result["storage_gb"] = int(storageGB) - } + result["id"] = details.GetID() + result["cpu_cores"] = details.GetCPUCores() + result["memory_gb"] = details.GetMemoryGB() + result["storage_gb"] = details.GetStorageGB() return []any{result} } -func flattenPublicIPs(ips []any) []any { +func flattenPublicIPs(ips []*github.HostedRunnerPublicIP) []any { if ips == nil { return []any{} } result := make([]any, 0, len(ips)) for _, ip := range ips { - ipMap, ok := ip.(map[string]any) - if !ok { - continue - } - ipResult := make(map[string]any) - if enabled, ok := ipMap["enabled"].(bool); ok { - ipResult["enabled"] = enabled - } - if prefix, ok := ipMap["prefix"].(string); ok { - ipResult["prefix"] = prefix - } - if length, ok := ipMap["length"].(float64); ok { - ipResult["length"] = int(length) - } + ipResult["enabled"] = ip.GetEnabled() + ipResult["prefix"] = ip.GetPrefix() + ipResult["length"] = ip.GetLength() result = append(result, ipResult) } @@ -285,57 +255,63 @@ func resourceGithubActionsHostedRunnerCreate(d *schema.ResourceData, meta any) e orgName := meta.(*Owner).name ctx := context.Background() - // Build request payload - payload := map[string]any{ - "name": d.Get("name").(string), - "image": expandImage(d.Get("image").([]any)), - "size": d.Get("size").(string), - "runner_group_id": d.Get("runner_group_id").(int), + req := github.CreateHostedRunnerRequest{ + Name: d.Get("name").(string), + Image: expandImage(d.Get("image").([]any)), + Size: d.Get("size").(string), + RunnerGroupID: int64(d.Get("runner_group_id").(int)), } if v, ok := d.GetOk("maximum_runners"); ok { - payload["maximum_runners"] = v.(int) + req.MaximumRunners = new(int64(v.(int))) } - if v, ok := d.GetOk("public_ip_enabled"); ok { - payload["enable_static_ip"] = v.(bool) + if v, okExists := d.GetOkExists("public_ip_enabled"); okExists { //nolint:staticcheck // SA1019: d.GetOkExists is deprecated but necessary for bool fields + req.EnableStaticIP = new(v.(bool)) } if v, ok := d.GetOk("image_version"); ok { - payload["image_version"] = v.(string) + req.Image.Version = new(v.(string)) } if v, ok := d.GetOk("image_gen"); ok { - payload["image_gen"] = v.(bool) + req.ImageGen = new(v.(bool)) } - // Create HTTP request - req, err := client.NewRequest(ctx, "POST", fmt.Sprintf("orgs/%s/actions/hosted-runners", orgName), payload) + runner, _, err := client.Actions.CreateHostedRunner(ctx, orgName, req) if err != nil { + if _, ok := errors.AsType[*github.AcceptedError](err); !ok { + return err + } + } + + d.SetId(strconv.Itoa(int(runner.GetID()))) + + if err := d.Set("status", runner.GetStatus()); err != nil { + return err + } + if err := d.Set("platform", runner.GetPlatform()); err != nil { + return err + } + if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { return err } - var runner map[string]any - _, err = client.Do(req, &runner) - if err != nil { - var acceptedErr *github.AcceptedError - if !errors.As(err, &acceptedErr) { + if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { + if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { return err } } - if runner == nil { - return fmt.Errorf("no runner data returned from API") + if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { + return err } - // Set the ID - if id, ok := runner["id"].(float64); ok { - d.SetId(strconv.Itoa(int(id))) - } else { - return fmt.Errorf("failed to get runner ID from response: %+v", runner) + if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { + return err } - return resourceGithubActionsHostedRunnerRead(d, meta) + return nil } func resourceGithubActionsHostedRunnerRead(d *schema.ResourceData, meta any) error { @@ -346,67 +322,46 @@ func resourceGithubActionsHostedRunnerRead(d *schema.ResourceData, meta any) err client := meta.(*Owner).v3client orgName := meta.(*Owner).name - runnerID := d.Id() - ctx := context.WithValue(context.Background(), ctxId, runnerID) - - // Create GET request - req, err := client.NewRequest(ctx, "GET", fmt.Sprintf("orgs/%s/actions/hosted-runners/%s", orgName, runnerID), nil) + runnerIDStr := d.Id() + runnerID, err := strconv.ParseInt(runnerIDStr, 10, 64) if err != nil { return err } - var runner map[string]any - _, err = client.Do(req, &runner) + ctx := context.WithValue(context.Background(), ctxId, runnerIDStr) + + runner, _, err := client.Actions.GetHostedRunner(ctx, orgName, runnerID) if err != nil { - var ghErr *github.ErrorResponse - if errors.As(err, &ghErr) { - if ghErr.Response.StatusCode == http.StatusNotFound { - log.Printf("[WARN] Removing hosted runner %s from state because it no longer exists in GitHub", runnerID) - d.SetId("") - return nil - } + if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == http.StatusNotFound { + log.Printf("[WARN] Removing hosted runner %s from state because it no longer exists in GitHub", runnerIDStr) + d.SetId("") + return nil } return err } - if runner == nil { - return fmt.Errorf("no runner data returned from API") - } - - if name, ok := runner["name"].(string); ok { - if err := d.Set("name", name); err != nil { - return err - } + if err := d.Set("name", runner.GetName()); err != nil { + return err } - if status, ok := runner["status"].(string); ok { - if err := d.Set("status", status); err != nil { - return err - } + if err := d.Set("status", runner.GetStatus()); err != nil { + return err } - if platform, ok := runner["platform"].(string); ok { - if err := d.Set("platform", platform); err != nil { - return err - } + if err := d.Set("platform", runner.GetPlatform()); err != nil { + return err } - if lastActiveOn, ok := runner["last_active_on"].(string); ok { - if err := d.Set("last_active_on", lastActiveOn); err != nil { - return err - } + if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { + return err } - if publicIPEnabled, ok := runner["public_ip_enabled"].(bool); ok { - if err := d.Set("public_ip_enabled", publicIPEnabled); err != nil { - return err - } + if err := d.Set("public_ip_enabled", runner.GetPublicIPEnabled()); err != nil { + return err } - if image, ok := runner["image"].(map[string]any); ok { - if err := d.Set("image", flattenImage(image)); err != nil { - return err - } + if err := d.Set("image", flattenImage(runner.GetImageDetails())); err != nil { + return err } - if machineSizeDetails, ok := runner["machine_size_details"].(map[string]any); ok { - if err := d.Set("size", machineSizeDetails["id"]); err != nil { + if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { + if err := d.Set("size", machineSizeDetails.GetID()); err != nil { return err } if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { @@ -414,29 +369,22 @@ func resourceGithubActionsHostedRunnerRead(d *schema.ResourceData, meta any) err } } - if runnerGroupID, ok := runner["runner_group_id"].(float64); ok { - if err := d.Set("runner_group_id", int(runnerGroupID)); err != nil { - return err - } + if err := d.Set("runner_group_id", runner.GetRunnerGroupID()); err != nil { + return err } - if maxRunners, ok := runner["maximum_runners"].(float64); ok { - if err := d.Set("maximum_runners", int(maxRunners)); err != nil { - return err - } + if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { + return err } - if publicIPs, ok := runner["public_ips"].([]any); ok { - if err := d.Set("public_ips", flattenPublicIPs(publicIPs)); err != nil { - return err - } + if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { + return err } - if imageGen, ok := runner["image_gen"].(bool); ok { - if err := d.Set("image_gen", imageGen); err != nil { - return err - } - } + // TODO: Uncomment when go-github supports image_gen field in the HostedRunner struct + // if err := d.Set("image_gen", runner.GetImageGen()); err != nil { + // return err + // } return nil } @@ -449,50 +397,70 @@ func resourceGithubActionsHostedRunnerUpdate(d *schema.ResourceData, meta any) e client := meta.(*Owner).v3client orgName := meta.(*Owner).name - runnerID := d.Id() + + runnerID, err := strconv.ParseInt(d.Id(), 10, 64) + if err != nil { + return err + } ctx := context.WithValue(context.Background(), ctxId, runnerID) - payload := make(map[string]any) + req := github.UpdateHostedRunnerRequest{} if d.HasChange("name") { - payload["name"] = d.Get("name").(string) + req.Name = new(d.Get("name").(string)) } if d.HasChange("size") { - payload["size"] = d.Get("size").(string) + req.Size = new(d.Get("size").(string)) } if d.HasChange("runner_group_id") { - payload["runner_group_id"] = d.Get("runner_group_id").(int) + req.RunnerGroupID = new(int64(d.Get("runner_group_id").(int))) } if d.HasChange("maximum_runners") { - payload["maximum_runners"] = d.Get("maximum_runners").(int) + req.MaximumRunners = new(int64(d.Get("maximum_runners").(int))) } if d.HasChange("public_ip_enabled") { - payload["enable_static_ip"] = d.Get("public_ip_enabled").(bool) + req.EnableStaticIP = new(d.Get("public_ip_enabled").(bool)) } if d.HasChange("image_version") { - payload["image_version"] = d.Get("image_version").(string) + req.ImageVersion = new(d.Get("image_version").(string)) } - if len(payload) == 0 { - return resourceGithubActionsHostedRunnerRead(d, meta) - } + // Only update the runner if any of the configured fields have changed + if d.HasChanges("name", "size", "runner_group_id", "maximum_runners", "public_ip_enabled", "image_version") { - // Create PATCH request - req, err := client.NewRequest(ctx, "PATCH", fmt.Sprintf("orgs/%s/actions/hosted-runners/%s", orgName, runnerID), payload) - if err != nil { - return err - } + runner, _, err := client.Actions.UpdateHostedRunner(ctx, orgName, runnerID, req) + if err != nil { + if _, ok := errors.AsType[*github.AcceptedError](err); !ok { + return err + } + } - var runner map[string]any - _, err = client.Do(req, &runner) - if err != nil { - var acceptedErr *github.AcceptedError - if !errors.As(err, &acceptedErr) { + if err := d.Set("status", runner.GetStatus()); err != nil { + return err + } + if err := d.Set("platform", runner.GetPlatform()); err != nil { + return err + } + if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { + return err + } + + if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { + if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { + return err + } + } + + if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { + return err + } + + if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { return err } } - return resourceGithubActionsHostedRunnerRead(d, meta) + return nil } func resourceGithubActionsHostedRunnerDelete(d *schema.ResourceData, meta any) error { @@ -505,44 +473,36 @@ func resourceGithubActionsHostedRunnerDelete(d *schema.ResourceData, meta any) e orgName := meta.(*Owner).name ctx := context.Background() - runnerID := d.Id() - - // Send DELETE request - req, err := client.NewRequest(ctx, "DELETE", fmt.Sprintf("orgs/%s/actions/hosted-runners/%s", orgName, runnerID), nil) + runnerID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { return err } - resp, err := client.Do(req, nil) + runner, resp, err := client.Actions.DeleteHostedRunner(ctx, orgName, runnerID) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { return nil } - var acceptedErr *github.AcceptedError - if errors.As(err, &acceptedErr) { - return waitForRunnerDeletion(ctx, client, orgName, runnerID, d.Timeout(schema.TimeoutDelete)) + if _, ok := errors.AsType[*github.AcceptedError](err); ok { + return waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete)) } return err } if resp != nil && resp.StatusCode == http.StatusAccepted { - return waitForRunnerDeletion(ctx, client, orgName, runnerID, d.Timeout(schema.TimeoutDelete)) + return waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete)) } return nil } -func waitForRunnerDeletion(ctx context.Context, client *github.Client, orgName, runnerID string, timeout time.Duration) error { +func waitForRunnerDeletion(ctx context.Context, client *github.Client, orgName string, runnerID int64, timeout time.Duration) error { conf := &retry.StateChangeConf{ - Pending: []string{"deleting", "active"}, + Pending: []string{"deleting", "active", "stuck"}, Target: []string{"deleted"}, Refresh: func() (any, string, error) { - req, err := client.NewRequest(ctx, "GET", fmt.Sprintf("orgs/%s/actions/hosted-runners/%s", orgName, runnerID), nil) - if err != nil { - return nil, "", err - } + _, resp, err := client.Actions.GetHostedRunner(ctx, orgName, runnerID) - resp, err := client.Do(req, nil) if resp != nil && resp.StatusCode == http.StatusNotFound { return "deleted", "deleted", nil } diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index fca971e4f1..84ece47066 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -264,7 +264,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ResourceName: "github_actions_hosted_runner.test", ImportState: true, ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"image", "image_gen"}, + ImportStateVerifyIgnore: []string{"status", "image.0.size_gb", "image_gen"}, }, }, }) From 7ddf645a7d145b91067cd5b711ae7cfc3d37853e Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 20:42:26 +0300 Subject: [PATCH 13/22] Refactor `resource_github_actions_hosted_runner.go` to use Context-aware CRUD functions Signed-off-by: Timo Sand --- .../resource_github_actions_hosted_runner.go | 96 +++++++++---------- 1 file changed, 46 insertions(+), 50 deletions(-) diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index a87f67af2a..535e6565d0 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -10,6 +10,7 @@ import ( "time" "github.com/google/go-github/v88/github" + "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" @@ -17,10 +18,10 @@ import ( func resourceGithubActionsHostedRunner() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsHostedRunnerCreate, - Read: resourceGithubActionsHostedRunnerRead, - Update: resourceGithubActionsHostedRunnerUpdate, - Delete: resourceGithubActionsHostedRunnerDelete, + CreateContext: resourceGithubActionsHostedRunnerCreate, + ReadContext: resourceGithubActionsHostedRunnerRead, + UpdateContext: resourceGithubActionsHostedRunnerUpdate, + DeleteContext: resourceGithubActionsHostedRunnerDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -245,15 +246,14 @@ func flattenPublicIPs(ips []*github.HostedRunnerPublicIP) []any { return result } -func resourceGithubActionsHostedRunnerCreate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.Background() req := github.CreateHostedRunnerRequest{ Name: d.Get("name").(string), @@ -281,43 +281,43 @@ func resourceGithubActionsHostedRunnerCreate(d *schema.ResourceData, meta any) e runner, _, err := client.Actions.CreateHostedRunner(ctx, orgName, req) if err != nil { if _, ok := errors.AsType[*github.AcceptedError](err); !ok { - return err + return diag.FromErr(err) } } d.SetId(strconv.Itoa(int(runner.GetID()))) if err := d.Set("status", runner.GetStatus()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("platform", runner.GetPlatform()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { - return err + return diag.FromErr(err) } if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { - return err + return diag.FromErr(err) } } if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubActionsHostedRunnerRead(d *schema.ResourceData, meta any) error { +func resourceGithubActionsHostedRunnerRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client @@ -325,11 +325,9 @@ func resourceGithubActionsHostedRunnerRead(d *schema.ResourceData, meta any) err runnerIDStr := d.Id() runnerID, err := strconv.ParseInt(runnerIDStr, 10, 64) if err != nil { - return err + return diag.FromErr(err) } - ctx := context.WithValue(context.Background(), ctxId, runnerIDStr) - runner, _, err := client.Actions.GetHostedRunner(ctx, orgName, runnerID) if err != nil { if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == http.StatusNotFound { @@ -337,62 +335,62 @@ func resourceGithubActionsHostedRunnerRead(d *schema.ResourceData, meta any) err d.SetId("") return nil } - return err + return diag.FromErr(err) } if err := d.Set("name", runner.GetName()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("status", runner.GetStatus()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("platform", runner.GetPlatform()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("public_ip_enabled", runner.GetPublicIPEnabled()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("image", flattenImage(runner.GetImageDetails())); err != nil { - return err + return diag.FromErr(err) } if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { if err := d.Set("size", machineSizeDetails.GetID()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { - return err + return diag.FromErr(err) } } if err := d.Set("runner_group_id", runner.GetRunnerGroupID()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { - return err + return diag.FromErr(err) } // TODO: Uncomment when go-github supports image_gen field in the HostedRunner struct // if err := d.Set("image_gen", runner.GetImageGen()); err != nil { - // return err + // return diag.FromErr(err) // } return nil } -func resourceGithubActionsHostedRunnerUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client @@ -400,9 +398,8 @@ func resourceGithubActionsHostedRunnerUpdate(d *schema.ResourceData, meta any) e runnerID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return err + return diag.FromErr(err) } - ctx := context.WithValue(context.Background(), ctxId, runnerID) req := github.UpdateHostedRunnerRequest{} @@ -431,51 +428,50 @@ func resourceGithubActionsHostedRunnerUpdate(d *schema.ResourceData, meta any) e runner, _, err := client.Actions.UpdateHostedRunner(ctx, orgName, runnerID, req) if err != nil { if _, ok := errors.AsType[*github.AcceptedError](err); !ok { - return err + return diag.FromErr(err) } } if err := d.Set("status", runner.GetStatus()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("platform", runner.GetPlatform()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { - return err + return diag.FromErr(err) } if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { - return err + return diag.FromErr(err) } } if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { - return err + return diag.FromErr(err) } if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { - return err + return diag.FromErr(err) } } return nil } -func resourceGithubActionsHostedRunnerDelete(d *schema.ResourceData, meta any) error { +func resourceGithubActionsHostedRunnerDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { err := checkOrganization(meta) if err != nil { - return err + return diag.FromErr(err) } client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.Background() runnerID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return err + return diag.FromErr(err) } runner, resp, err := client.Actions.DeleteHostedRunner(ctx, orgName, runnerID) @@ -484,13 +480,13 @@ func resourceGithubActionsHostedRunnerDelete(d *schema.ResourceData, meta any) e return nil } if _, ok := errors.AsType[*github.AcceptedError](err); ok { - return waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete)) + return diag.FromErr(waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete))) } - return err + return diag.FromErr(err) } if resp != nil && resp.StatusCode == http.StatusAccepted { - return waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete)) + return diag.FromErr(waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete))) } return nil From 19201268409df6fd636bf23dc765a0c95d3eea4a Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 20:45:32 +0300 Subject: [PATCH 14/22] Refactor to use `tflog` Signed-off-by: Timo Sand --- github/resource_github_actions_hosted_runner.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index 535e6565d0..c714d0d31a 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -3,13 +3,13 @@ package github import ( "context" "errors" - "log" "net/http" "regexp" "strconv" "time" "github.com/google/go-github/v88/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" @@ -331,7 +331,7 @@ func resourceGithubActionsHostedRunnerRead(ctx context.Context, d *schema.Resour runner, _, err := client.Actions.GetHostedRunner(ctx, orgName, runnerID) if err != nil { if err, ok := errors.AsType[*github.ErrorResponse](err); ok && err.Response.StatusCode == http.StatusNotFound { - log.Printf("[WARN] Removing hosted runner %s from state because it no longer exists in GitHub", runnerIDStr) + tflog.Warn(ctx, "Removing hosted runner from state because it no longer exists in GitHub", map[string]any{"runner_id": runnerIDStr}) d.SetId("") return nil } @@ -494,7 +494,7 @@ func resourceGithubActionsHostedRunnerDelete(ctx context.Context, d *schema.Reso func waitForRunnerDeletion(ctx context.Context, client *github.Client, orgName string, runnerID int64, timeout time.Duration) error { conf := &retry.StateChangeConf{ - Pending: []string{"deleting", "active", "stuck"}, + Pending: []string{"deleting", "active", "ready", "provisioning"}, Target: []string{"deleted"}, Refresh: func() (any, string, error) { _, resp, err := client.Actions.GetHostedRunner(ctx, orgName, runnerID) From 4d0442259a0126d07d6b2f0e16f39384b5c6936a Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 21:35:28 +0300 Subject: [PATCH 15/22] Address linter issues Signed-off-by: Timo Sand --- .../resource_github_actions_hosted_runner.go | 43 +++++++++++++------ 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index c714d0d31a..ba9cd82541 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -255,27 +255,36 @@ func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.Reso client := meta.(*Owner).v3client orgName := meta.(*Owner).name + runnerName, _ := d.Get("name").(string) + runnerImageDetails, _ := d.Get("image").([]any) + runnerSize, _ := d.Get("size").(string) + runnerGroupID, _ := d.Get("runner_group_id").(int) + req := github.CreateHostedRunnerRequest{ - Name: d.Get("name").(string), - Image: expandImage(d.Get("image").([]any)), - Size: d.Get("size").(string), - RunnerGroupID: int64(d.Get("runner_group_id").(int)), + Name: runnerName, + Image: expandImage(runnerImageDetails), + Size: runnerSize, + RunnerGroupID: int64(runnerGroupID), } if v, ok := d.GetOk("maximum_runners"); ok { - req.MaximumRunners = new(int64(v.(int))) + maxRunners, _ := v.(int) + req.MaximumRunners = new(int64(maxRunners)) } if v, okExists := d.GetOkExists("public_ip_enabled"); okExists { //nolint:staticcheck // SA1019: d.GetOkExists is deprecated but necessary for bool fields - req.EnableStaticIP = new(v.(bool)) + publicIPEnabled, _ := v.(bool) + req.EnableStaticIP = new(publicIPEnabled) } if v, ok := d.GetOk("image_version"); ok { - req.Image.Version = new(v.(string)) + runnerImageVersion, _ := v.(string) + req.Image.Version = new(runnerImageVersion) } if v, ok := d.GetOk("image_gen"); ok { - req.ImageGen = new(v.(bool)) + useRunnerForImageGen, _ := v.(bool) + req.ImageGen = new(useRunnerForImageGen) } runner, _, err := client.Actions.CreateHostedRunner(ctx, orgName, req) @@ -404,22 +413,28 @@ func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.Reso req := github.UpdateHostedRunnerRequest{} if d.HasChange("name") { - req.Name = new(d.Get("name").(string)) + name, _ := d.Get("name").(string) + req.Name = new(name) } if d.HasChange("size") { - req.Size = new(d.Get("size").(string)) + size, _ := d.Get("size").(string) + req.Size = new(size) } if d.HasChange("runner_group_id") { - req.RunnerGroupID = new(int64(d.Get("runner_group_id").(int))) + runnerGroupID, _ := d.Get("runner_group_id").(int) + req.RunnerGroupID = new(int64(runnerGroupID)) } if d.HasChange("maximum_runners") { - req.MaximumRunners = new(int64(d.Get("maximum_runners").(int))) + maximumRunners, _ := d.Get("maximum_runners").(int) + req.MaximumRunners = new(int64(maximumRunners)) } if d.HasChange("public_ip_enabled") { - req.EnableStaticIP = new(d.Get("public_ip_enabled").(bool)) + publicIPEnabled, _ := d.Get("public_ip_enabled").(bool) + req.EnableStaticIP = new(publicIPEnabled) } if d.HasChange("image_version") { - req.ImageVersion = new(d.Get("image_version").(string)) + imageVersion, _ := d.Get("image_version").(string) + req.ImageVersion = new(imageVersion) } // Only update the runner if any of the configured fields have changed From 6570a3aece8ae19a69c833acec1ecc2cde4b0b40 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sun, 5 Jul 2026 21:36:26 +0300 Subject: [PATCH 16/22] Reduce test cleanup time by having less runners to delete Signed-off-by: Timo Sand --- github/acc_test.go | 2 +- github/resource_github_actions_hosted_runner_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/github/acc_test.go b/github/acc_test.go index d789821fff..dad9fd83b4 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -376,7 +376,7 @@ func sweepRunnerGroupRunners(ctx context.Context, rg *github.RunnerGroup) error client := testAccConf.meta.v3client owner := testAccConf.meta.name - fmt.Printf("removing organization hosted runners of runner group: %s\n", rg.GetName()) + fmt.Printf("cleanup: removing organization hosted runners of runner group '%s'\n", rg.GetName()) runners, _, err := client.Actions.ListRunnerGroupHostedRunners(ctx, owner, rg.GetID(), &github.ListOptions{}) if err != nil { return err diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 84ece47066..4bb4f12470 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -81,7 +81,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "2-core" runner_group_id = "%d" - maximum_runners = 5 + maximum_runners = 2 public_ip_enabled = true } `, hostedRunnerName, runnerGroup.GetID()) @@ -95,7 +95,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("2-core")), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(5)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(2)), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("public_ip_enabled"), knownvalue.Bool(true)), }, }, @@ -120,7 +120,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = "%d" - maximum_runners = 3 + maximum_runners = 2 } `, hostedRunnerName, runnerGroup.GetID()) @@ -135,7 +135,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = "%d" - maximum_runners = 5 + maximum_runners = 3 } `, hostedRunnerName, runnerGroup.GetID()) @@ -148,7 +148,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(3)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(2)), }, }, { @@ -161,7 +161,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(fmt.Sprintf("%s-updated", hostedRunnerName))), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(5)), + statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(3)), }, }, }, From 2abbbd481e1d2dd5c9569f4dcf61db7fcd1b8b7f Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Wed, 8 Jul 2026 21:51:26 +0300 Subject: [PATCH 17/22] Address review comments Signed-off-by: Timo Sand --- .../resource_github_actions_hosted_runner.go | 185 +++++++++--------- 1 file changed, 97 insertions(+), 88 deletions(-) diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index ba9cd82541..13d13c1b8e 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -3,6 +3,7 @@ package github import ( "context" "errors" + "fmt" "net/http" "regexp" "strconv" @@ -183,86 +184,28 @@ func resourceGithubActionsHostedRunner() *schema.Resource { } } -func expandImage(imageList []any) github.HostedRunnerImage { - if len(imageList) == 0 { - return github.HostedRunnerImage{} - } - runnerImage := github.HostedRunnerImage{} - - imageMap := imageList[0].(map[string]any) - - if id, ok := imageMap["id"].(string); ok { - runnerImage.ID = id - } - if source, ok := imageMap["source"].(string); ok { - runnerImage.Source = source - } - - return runnerImage -} - -func flattenImage(image *github.HostedRunnerImageDetail) []any { - if image == nil { - return []any{} - } - - result := make(map[string]any) - - result["id"] = image.GetID() - result["source"] = image.GetSource() - result["size_gb"] = image.GetSizeGB() - - return []any{result} -} - -func flattenMachineSizeDetails(details *github.HostedRunnerMachineSpec) []any { - if details == nil { - return []any{} - } - - result := make(map[string]any) - result["id"] = details.GetID() - result["cpu_cores"] = details.GetCPUCores() - result["memory_gb"] = details.GetMemoryGB() - result["storage_gb"] = details.GetStorageGB() - - return []any{result} -} - -func flattenPublicIPs(ips []*github.HostedRunnerPublicIP) []any { - if ips == nil { - return []any{} - } - - result := make([]any, 0, len(ips)) - for _, ip := range ips { - ipResult := make(map[string]any) - ipResult["enabled"] = ip.GetEnabled() - ipResult["prefix"] = ip.GetPrefix() - ipResult["length"] = ip.GetLength() - result = append(result, ipResult) - } - - return result -} - -func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - err := checkOrganization(meta) +func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + err := checkOrganization(m) if err != nil { return diag.FromErr(err) } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + meta, _ := m.(*Owner) + client := meta.v3client + orgName := meta.name runnerName, _ := d.Get("name").(string) runnerImageDetails, _ := d.Get("image").([]any) runnerSize, _ := d.Get("size").(string) runnerGroupID, _ := d.Get("runner_group_id").(int) + expandedImageDetails, err := expandImage(runnerImageDetails) + if err != nil { + return diag.FromErr(err) + } req := github.CreateHostedRunnerRequest{ Name: runnerName, - Image: expandImage(runnerImageDetails), + Image: *expandedImageDetails, Size: runnerSize, RunnerGroupID: int64(runnerGroupID), } @@ -272,20 +215,16 @@ func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.Reso req.MaximumRunners = new(int64(maxRunners)) } - if v, okExists := d.GetOkExists("public_ip_enabled"); okExists { //nolint:staticcheck // SA1019: d.GetOkExists is deprecated but necessary for bool fields - publicIPEnabled, _ := v.(bool) - req.EnableStaticIP = new(publicIPEnabled) - } + publicIPEnabled, _ := d.Get("public_ip_enabled").(bool) + req.EnableStaticIP = new(publicIPEnabled) if v, ok := d.GetOk("image_version"); ok { runnerImageVersion, _ := v.(string) req.Image.Version = new(runnerImageVersion) } - if v, ok := d.GetOk("image_gen"); ok { - useRunnerForImageGen, _ := v.(bool) - req.ImageGen = new(useRunnerForImageGen) - } + useRunnerForImageGen, _ := d.Get("image_gen").(bool) + req.ImageGen = new(useRunnerForImageGen) runner, _, err := client.Actions.CreateHostedRunner(ctx, orgName, req) if err != nil { @@ -323,14 +262,16 @@ func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.Reso return nil } -func resourceGithubActionsHostedRunnerRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - err := checkOrganization(meta) +func resourceGithubActionsHostedRunnerRead(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + err := checkOrganization(m) if err != nil { return diag.FromErr(err) } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + meta, _ := m.(*Owner) + client := meta.v3client + orgName := meta.name + runnerIDStr := d.Id() runnerID, err := strconv.ParseInt(runnerIDStr, 10, 64) if err != nil { @@ -396,14 +337,15 @@ func resourceGithubActionsHostedRunnerRead(ctx context.Context, d *schema.Resour return nil } -func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - err := checkOrganization(meta) +func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + err := checkOrganization(m) if err != nil { return diag.FromErr(err) } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + meta, _ := m.(*Owner) + client := meta.v3client + orgName := meta.name runnerID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { @@ -475,14 +417,15 @@ func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.Reso return nil } -func resourceGithubActionsHostedRunnerDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - err := checkOrganization(meta) +func resourceGithubActionsHostedRunnerDelete(ctx context.Context, d *schema.ResourceData, m any) diag.Diagnostics { + err := checkOrganization(m) if err != nil { return diag.FromErr(err) } - client := meta.(*Owner).v3client - orgName := meta.(*Owner).name + meta, _ := m.(*Owner) + client := meta.v3client + orgName := meta.name runnerID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { @@ -532,3 +475,69 @@ func waitForRunnerDeletion(ctx context.Context, client *github.Client, orgName s _, err := conf.WaitForStateContext(ctx) return err } + +func expandImage(imageList []any) (*github.HostedRunnerImage, error) { + if len(imageList) == 0 { + return &github.HostedRunnerImage{}, nil + } + runnerImage := &github.HostedRunnerImage{} + + imageMap, ok := imageList[0].(map[string]any) + if !ok { + return nil, fmt.Errorf("expected map[string]any, got %T", imageList[0]) + } + + if id, ok := imageMap["id"].(string); ok { + runnerImage.ID = id + } + if source, ok := imageMap["source"].(string); ok { + runnerImage.Source = source + } + + return runnerImage, nil +} + +func flattenImage(image *github.HostedRunnerImageDetail) []any { + if image == nil { + return []any{} + } + + result := make(map[string]any) + + result["id"] = image.GetID() + result["source"] = image.GetSource() + result["size_gb"] = image.GetSizeGB() + + return []any{result} +} + +func flattenMachineSizeDetails(details *github.HostedRunnerMachineSpec) []any { + if details == nil { + return []any{} + } + + result := make(map[string]any) + result["id"] = details.GetID() + result["cpu_cores"] = details.GetCPUCores() + result["memory_gb"] = details.GetMemoryGB() + result["storage_gb"] = details.GetStorageGB() + + return []any{result} +} + +func flattenPublicIPs(ips []*github.HostedRunnerPublicIP) []any { + if ips == nil { + return []any{} + } + + result := make([]any, 0, len(ips)) + for _, ip := range ips { + ipResult := make(map[string]any) + ipResult["enabled"] = ip.GetEnabled() + ipResult["prefix"] = ip.GetPrefix() + ipResult["length"] = ip.GetLength() + result = append(result, ipResult) + } + + return result +} From 5536de94a608f0f0d64cbaee87a1a36e128ada36 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Wed, 8 Jul 2026 22:21:03 +0300 Subject: [PATCH 18/22] Address Copilot review comments Signed-off-by: Timo Sand --- github/acc_test.go | 18 ++++++++---------- .../resource_github_actions_hosted_runner.go | 16 +++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/github/acc_test.go b/github/acc_test.go index dad9fd83b4..28e9037fe1 100644 --- a/github/acc_test.go +++ b/github/acc_test.go @@ -385,18 +385,16 @@ func sweepRunnerGroupRunners(ctx context.Context, rg *github.RunnerGroup) error for _, r := range runners.GetRunners() { _, resp, err := client.Actions.DeleteHostedRunner(ctx, owner, r.GetID()) if err != nil { - if err != nil { - if resp != nil && resp.StatusCode == http.StatusNotFound { - continue - } - if _, ok := errors.AsType[*github.AcceptedError](err); ok { - if err := waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute); err != nil { - return fmt.Errorf("failed to wait for runner deletion %s: %w", r.GetName(), err) - } - continue + if resp != nil && resp.StatusCode == http.StatusNotFound { + continue + } + if _, ok := errors.AsType[*github.AcceptedError](err); ok { + if err := waitForRunnerDeletion(ctx, client, owner, r.GetID(), 5*time.Minute); err != nil { + return fmt.Errorf("failed to wait for runner deletion %s: %w", r.GetName(), err) } - return fmt.Errorf("failed to delete hosted runner %s: %w", r.GetName(), err) + continue } + return fmt.Errorf("failed to delete hosted runner %s: %w", r.GetName(), err) } if resp != nil && resp.StatusCode == http.StatusAccepted { diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index 13d13c1b8e..a62e675636 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -228,9 +228,7 @@ func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.Reso runner, _, err := client.Actions.CreateHostedRunner(ctx, orgName, req) if err != nil { - if _, ok := errors.AsType[*github.AcceptedError](err); !ok { - return diag.FromErr(err) - } + return diag.FromErr(err) } d.SetId(strconv.Itoa(int(runner.GetID()))) @@ -241,7 +239,7 @@ func resourceGithubActionsHostedRunnerCreate(ctx context.Context, d *schema.Reso if err := d.Set("platform", runner.GetPlatform()); err != nil { return diag.FromErr(err) } - if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { + if err := d.Set("last_active_on", runner.GetLastActiveOn().Format(time.RFC3339)); err != nil { return diag.FromErr(err) } @@ -297,7 +295,7 @@ func resourceGithubActionsHostedRunnerRead(ctx context.Context, d *schema.Resour if err := d.Set("platform", runner.GetPlatform()); err != nil { return diag.FromErr(err) } - if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { + if err := d.Set("last_active_on", runner.GetLastActiveOn().Format(time.RFC3339)); err != nil { return diag.FromErr(err) } if err := d.Set("public_ip_enabled", runner.GetPublicIPEnabled()); err != nil { @@ -395,7 +393,7 @@ func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.Reso if err := d.Set("platform", runner.GetPlatform()); err != nil { return diag.FromErr(err) } - if err := d.Set("last_active_on", runner.GetLastActiveOn().GoString()); err != nil { + if err := d.Set("last_active_on", runner.GetLastActiveOn().Format(time.RFC3339)); err != nil { return diag.FromErr(err) } @@ -432,19 +430,19 @@ func resourceGithubActionsHostedRunnerDelete(ctx context.Context, d *schema.Reso return diag.FromErr(err) } - runner, resp, err := client.Actions.DeleteHostedRunner(ctx, orgName, runnerID) + _, resp, err := client.Actions.DeleteHostedRunner(ctx, orgName, runnerID) if err != nil { if resp != nil && resp.StatusCode == http.StatusNotFound { return nil } if _, ok := errors.AsType[*github.AcceptedError](err); ok { - return diag.FromErr(waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete))) + return diag.FromErr(waitForRunnerDeletion(ctx, client, orgName, runnerID, d.Timeout(schema.TimeoutDelete))) } return diag.FromErr(err) } if resp != nil && resp.StatusCode == http.StatusAccepted { - return diag.FromErr(waitForRunnerDeletion(ctx, client, orgName, runner.GetID(), d.Timeout(schema.TimeoutDelete))) + return diag.FromErr(waitForRunnerDeletion(ctx, client, orgName, runnerID, d.Timeout(schema.TimeoutDelete))) } return nil From e808920e6909503041f9b765cc9fbdbde96b0a77 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 10 Jul 2026 17:59:00 +0300 Subject: [PATCH 19/22] Address review comments Signed-off-by: Timo Sand --- .../resource_github_actions_hosted_runner.go | 111 ++++++++++-------- ...ource_github_actions_hosted_runner_test.go | 74 +++++++----- 2 files changed, 111 insertions(+), 74 deletions(-) diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index a62e675636..51be69aeab 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-github/v88/github" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "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" @@ -31,6 +32,8 @@ func resourceGithubActionsHostedRunner() *schema.Resource { Delete: schema.DefaultTimeout(10 * time.Minute), }, + CustomizeDiff: customdiff.All(resourceGithubActionsHostedRunnerValidation), + Schema: map[string]*schema.Schema{ "name": { Type: schema.TypeString, @@ -341,6 +344,11 @@ func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.Reso return diag.FromErr(err) } + // Only update the runner if any of the configured fields have changed + if !d.HasChanges("name", "size", "runner_group_id", "maximum_runners", "public_ip_enabled", "image_version") { + return nil + } + meta, _ := m.(*Owner) client := meta.v3client orgName := meta.name @@ -350,66 +358,55 @@ func resourceGithubActionsHostedRunnerUpdate(ctx context.Context, d *schema.Reso return diag.FromErr(err) } - req := github.UpdateHostedRunnerRequest{} + name, _ := d.Get("name").(string) + size, _ := d.Get("size").(string) + runnerGroupID, _ := d.Get("runner_group_id").(int) + maximumRunners, _ := d.Get("maximum_runners").(int) + publicIPEnabled, _ := d.Get("public_ip_enabled").(bool) - if d.HasChange("name") { - name, _ := d.Get("name").(string) - req.Name = new(name) - } - if d.HasChange("size") { - size, _ := d.Get("size").(string) - req.Size = new(size) - } - if d.HasChange("runner_group_id") { - runnerGroupID, _ := d.Get("runner_group_id").(int) - req.RunnerGroupID = new(int64(runnerGroupID)) - } - if d.HasChange("maximum_runners") { - maximumRunners, _ := d.Get("maximum_runners").(int) - req.MaximumRunners = new(int64(maximumRunners)) - } - if d.HasChange("public_ip_enabled") { - publicIPEnabled, _ := d.Get("public_ip_enabled").(bool) - req.EnableStaticIP = new(publicIPEnabled) + req := github.UpdateHostedRunnerRequest{ + Name: new(name), + Size: new(size), + RunnerGroupID: new(int64(runnerGroupID)), + MaximumRunners: new(int64(maximumRunners)), + EnableStaticIP: new(publicIPEnabled), } + + // image_version is only settable for runners with a custom image, so we only include it in the request if it has changed if d.HasChange("image_version") { imageVersion, _ := d.Get("image_version").(string) req.ImageVersion = new(imageVersion) } - // Only update the runner if any of the configured fields have changed - if d.HasChanges("name", "size", "runner_group_id", "maximum_runners", "public_ip_enabled", "image_version") { - - runner, _, err := client.Actions.UpdateHostedRunner(ctx, orgName, runnerID, req) - if err != nil { - if _, ok := errors.AsType[*github.AcceptedError](err); !ok { - return diag.FromErr(err) - } - } - - if err := d.Set("status", runner.GetStatus()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("platform", runner.GetPlatform()); err != nil { - return diag.FromErr(err) - } - if err := d.Set("last_active_on", runner.GetLastActiveOn().Format(time.RFC3339)); err != nil { + runner, _, err := client.Actions.UpdateHostedRunner(ctx, orgName, runnerID, req) + if err != nil { + if _, ok := errors.AsType[*github.AcceptedError](err); !ok { return diag.FromErr(err) } + } - if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { - if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { - return diag.FromErr(err) - } - } + if err := d.Set("status", runner.GetStatus()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("platform", runner.GetPlatform()); err != nil { + return diag.FromErr(err) + } + if err := d.Set("last_active_on", runner.GetLastActiveOn().Format(time.RFC3339)); err != nil { + return diag.FromErr(err) + } - if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { + if machineSizeDetails := runner.GetMachineSizeDetails(); machineSizeDetails != nil { + if err := d.Set("machine_size_details", flattenMachineSizeDetails(machineSizeDetails)); err != nil { return diag.FromErr(err) } + } - if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { - return diag.FromErr(err) - } + if err := d.Set("maximum_runners", runner.GetMaximumRunners()); err != nil { + return diag.FromErr(err) + } + + if err := d.Set("public_ips", flattenPublicIPs(runner.GetPublicIPs())); err != nil { + return diag.FromErr(err) } return nil @@ -539,3 +536,25 @@ func flattenPublicIPs(ips []*github.HostedRunnerPublicIP) []any { return result } + +func resourceGithubActionsHostedRunnerValidation(ctx context.Context, d *schema.ResourceDiff, meta any) error { + // validates that image_version is only set for images with source "custom" + if d.HasChange("image_version") { + imageList, ok := d.GetOk("image") + imageSeq, okSeq := imageList.([]any) + if !ok || !okSeq || len(imageSeq) == 0 { + return fmt.Errorf("`image_version` can only be set when `image` is configured") + } + + imageMap, ok := imageSeq[0].(map[string]any) + if !ok { + return fmt.Errorf("unexpected type for image: %T", imageSeq[0]) + } + + source, ok := imageMap["source"].(string) + if !ok || source != "custom" { + return fmt.Errorf("`image_version` can only be set when `image[0].source` is 'custom'") + } + } + return nil +} diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index 4bb4f12470..ba7c6568cc 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -2,8 +2,10 @@ package github import ( "fmt" + "regexp" "testing" + "github.com/hashicorp/terraform-plugin-testing/compare" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" @@ -14,13 +16,11 @@ import ( func TestAccGithubActionsHostedRunner(t *testing.T) { t.Parallel() - skipUnlessHasPaidOrgs(t) - - runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) t.Run("creates_hosted_runners_without_error", func(t *testing.T) { t.Parallel() + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-%s", testResourcePrefix, randomID) @@ -45,14 +45,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { { Config: config, ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("runner_group_id"), knownvalue.NotNull()), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.NotNull()), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("status"), knownvalue.NotNull()), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("platform"), knownvalue.NotNull()), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("image").AtSliceIndex(0).AtMapKey("id"), knownvalue.NotNull()), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("image").AtSliceIndex(0).AtMapKey("source"), knownvalue.NotNull()), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("image").AtSliceIndex(0).AtMapKey("size_gb"), knownvalue.NotNull()), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("id"), knownvalue.NotNull()), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.NotNull()), @@ -67,6 +62,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("creates_hosted_runner_with_optional_parameters", func(t *testing.T) { t.Parallel() + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-optional-%s", testResourcePrefix, randomID) @@ -92,12 +88,6 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("2-core")), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(2)), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("public_ip_enabled"), knownvalue.Bool(true)), - }, }, }, }) @@ -106,6 +96,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("updates_hosted_runner_configuration", func(t *testing.T) { t.Parallel() + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-update-%s", testResourcePrefix, randomID) @@ -139,6 +130,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, hostedRunnerName, runnerGroup.GetID()) + compareMaxRunnersUpdated := statecheck.CompareValue(compare.ValuesDiffer()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, @@ -146,9 +138,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { { Config: configBefore, ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(2)), + compareMaxRunnersUpdated.AddStateValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners")), }, }, { @@ -160,8 +150,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }, ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(fmt.Sprintf("%s-updated", hostedRunnerName))), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners"), knownvalue.Int64Exact(3)), + compareMaxRunnersUpdated.AddStateValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners")), }, }, }, @@ -171,6 +160,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("updates_size_field", func(t *testing.T) { t.Parallel() + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-size-%s", testResourcePrefix, randomID) @@ -202,6 +192,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { } `, hostedRunnerName, runnerGroup.GetID()) + compareSizeUpdated := statecheck.CompareValue(compare.ValuesDiffer()) resource.Test(t, resource.TestCase{ PreCheck: func() { skipUnlessHasPaidOrgs(t) }, ProviderFactories: providerFactories, @@ -209,7 +200,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { { Config: configBefore, ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("4-core")), + compareSizeUpdated.AddStateValue("github_actions_hosted_runner.test", tfjsonpath.New("size")), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.Int64Exact(4)), }, }, @@ -221,7 +212,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }, }, ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("size"), knownvalue.StringExact("8-core")), + compareSizeUpdated.AddStateValue("github_actions_hosted_runner.test", tfjsonpath.New("size")), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.Int64Exact(8)), }, }, @@ -232,6 +223,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("imports_hosted_runner", func(t *testing.T) { t.Parallel() + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-import-%s", testResourcePrefix, randomID) @@ -255,10 +247,6 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("name"), knownvalue.StringExact(hostedRunnerName)), - }, }, { ResourceName: "github_actions_hosted_runner.test", @@ -273,6 +261,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { t.Run("deletes_hosted_runner", func(t *testing.T) { t.Parallel() + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-delete-%s", testResourcePrefix, randomID) @@ -296,9 +285,6 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { Steps: []resource.TestStep{ { Config: config, - ConfigStateChecks: []statecheck.StateCheck{ - statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("id"), knownvalue.NotNull()), - }, }, { Config: config, @@ -307,4 +293,36 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { }, }) }) + + t.Run("validates_image_version_only_allowed_custom_image", func(t *testing.T) { + t.Parallel() + + runnerGroup := mustCreateTestOrganizationActionsRunnerGroup(t) + randomID := acctest.RandString(5) + hostedRunnerName := fmt.Sprintf("%simage-version-%s", testResourcePrefix, randomID) + + config := fmt.Sprintf(` +resource "github_actions_hosted_runner" "test" { + name = "%s" + + image { + id = "2306" + source = "github" + } + image_version = "1.0.0" + size = "4-core" + runner_group_id = "%d" +}`, hostedRunnerName, runnerGroup.GetID()) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { skipUnlessHasPaidOrgs(t) }, + ProviderFactories: providerFactories, + Steps: []resource.TestStep{ + { + Config: config, + ExpectError: regexp.MustCompile("`image_version` can only be set when `image\\[0\\].source` is 'custom'"), + }, + }, + }) + }) } From 9daec983d1bbac14a312a72ca5abb30d9b3f6008 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 10 Jul 2026 18:00:24 +0300 Subject: [PATCH 20/22] Update RESOURCES file Signed-off-by: Timo Sand --- RESOURCES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RESOURCES.md b/RESOURCES.md index dd00ac0a50..819cc792bf 100644 --- a/RESOURCES.md +++ b/RESOURCES.md @@ -122,7 +122,7 @@ The overall status of each resource or data source is captured in this document | --- | --- | --- | --- | --- | --- | --- | --- | | `github_actions_environment_secret` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_environment_variable` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | -| `github_actions_hosted_runner` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | +| `github_actions_hosted_runner` | ⚠️ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ❌ | | `github_actions_organization_oidc_subject_claim_customization_template` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_organization_permissions` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_organization_secret` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | From 87fa3464cd168a804ed16edb5290cd37d802dc82 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 10 Jul 2026 18:32:06 +0300 Subject: [PATCH 21/22] Update to use generated docs Signed-off-by: Timo Sand --- docs/resources/actions_hosted_runner.md | 172 ++++++++---------- .../actions_hosted_runner/example_2.tf | 18 -- .../actions_hosted_runner/example_3.tf | 15 -- .../resource_advanced.tf} | 6 +- .../resource_github_actions_hosted_runner.go | 44 ++--- .../resources/actions_hosted_runner.md.tmpl | 116 +++++------- 6 files changed, 149 insertions(+), 222 deletions(-) delete mode 100644 examples/resources/actions_hosted_runner/example_2.tf delete mode 100644 examples/resources/actions_hosted_runner/example_3.tf rename examples/resources/{actions_hosted_runner/example_1.tf => github_actions_hosted_runner/resource_advanced.tf} (64%) diff --git a/docs/resources/actions_hosted_runner.md b/docs/resources/actions_hosted_runner.md index 5f0c4d1ba2..6cc2692a56 100644 --- a/docs/resources/actions_hosted_runner.md +++ b/docs/resources/actions_hosted_runner.md @@ -1,48 +1,51 @@ --- page_title: "github_actions_hosted_runner (Resource) - GitHub" +subcategory: "" description: |- - Creates and manages GitHub-hosted runners within a GitHub organization + This resource allows you to create and manage GitHub-hosted runners within your GitHub organization. You must have admin access to an organization to use this resource. --- # github_actions_hosted_runner (Resource) This resource allows you to create and manage GitHub-hosted runners within your GitHub organization. You must have admin access to an organization to use this resource. -GitHub-hosted runners are fully managed virtual machines that run your GitHub Actions workflows. Unlike self-hosted runners, GitHub handles the infrastructure, maintenance, and scaling. +## Notes -## Example Usage +- This resource is **organization-only** and cannot be used with individual accounts. +- Image IDs for GitHub-owned images are numeric strings (e.g., "2306" for Ubuntu Latest 24.04), not names like "ubuntu-latest". +- Deletion of hosted runners is asynchronous. The provider will poll for up to 10 minutes (configurable via timeouts) to confirm deletion. +- Runner creation and updates may take several minutes as GitHub provisions the infrastructure. -### Basic Usage +## Getting Available Images and Sizes -```terraform -resource "github_actions_runner_group" "example" { - name = "example-runner-group" - visibility = "all" -} +To get a list of available images: -resource "github_actions_hosted_runner" "example" { - name = "example-hosted-runner" +```bash +curl -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/orgs/YOUR_ORG/actions/hosted-runners/images/github-owned +``` - image { - id = "2306" - source = "github" - } +To get available machine sizes: - size = "4-core" - runner_group_id = github_actions_runner_group.example.id -} +```bash +curl -H "Authorization: Bearer YOUR_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/orgs/YOUR_ORG/actions/hosted-runners/machine-sizes ``` -### Advanced Usage with Optional Parameters +## Example Usage + +### Advanced Usage ```terraform -resource "github_actions_runner_group" "advanced" { - name = "advanced-runner-group" - visibility = "selected" +resource "github_actions_runner_group" "example" { + name = "example-runner-group" + visibility = "all" } -resource "github_actions_hosted_runner" "advanced" { - name = "advanced-hosted-runner" +resource "github_actions_hosted_runner" "example" { + name = "example-hosted-runner" image { id = "2306" @@ -50,104 +53,79 @@ resource "github_actions_hosted_runner" "advanced" { } size = "8-core" - runner_group_id = github_actions_runner_group.advanced.id + runner_group_id = github_actions_runner_group.example.id maximum_runners = 10 public_ip_enabled = true } ``` -## Argument Reference + +## Schema -The following arguments are supported: +### Required -- `name` - (Required) Name of the hosted runner. Must be between 1 and 64 characters and may only contain alphanumeric characters, '.', '-', and '_'. -- `image` - (Required) Image configuration for the hosted runner. Cannot be changed after creation. Block supports: - - `id` - (Required) The image ID. For GitHub-owned images, use numeric IDs like "2306" for Ubuntu Latest 24.04. To get available images, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/images/github-owned`. - - `source` - (Optional) The image source. Valid values are "github", "partner", or "custom". Defaults to "github". -- `size` - (Required) Machine size for the hosted runner (e.g., "4-core", "8-core"). Can be updated to scale the runner. To list available sizes, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/machine-sizes`. -- `runner_group_id` - (Required) The ID of the runner group to assign this runner to. -- `maximum_runners` - (Optional) Maximum number of runners to scale up to. Runners will not auto-scale above this number. Use this setting to limit costs. -- `public_ip_enabled` - (Optional) Whether to enable static public IP for the runner. Note there are account limits. To list limits, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/limits`. Defaults to false. -- `image_version` - (Optional) The version of the runner image to deploy. This is only relevant for runners using custom images. +- `image` (Block List, Min: 1, Max: 1) Image configuration for the hosted runner. Cannot be changed after creation. (see [below for nested schema](#nestedblock--image)) +- `name` (String) Name of the hosted runner. Must be between 1 and 64 characters and may only contain upper and lowercase letters a-z, numbers 0-9, '.', '-', and '_'. +- `runner_group_id` (Number) The ID of the runner group to assign this runner to. +- `size` (String) Machine size for the hosted runner (e.g., `4-core`, `8-core`). Can be updated to scale the runner. To list available sizes, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/machine-sizes`. -## Timeouts +### Optional -The `timeouts` block allows you to specify timeouts for certain actions: +- `image_gen` (Boolean) Whether this runner should be used to generate custom images. Cannot be changed after creation. +- `image_version` (String) The version of the runner image to deploy. This is only relevant for runners using custom images. +- `maximum_runners` (Number) Maximum number of runners to scale up to. Runners will not auto-scale above this number. Use this setting to limit costs. +- `public_ip_enabled` (Boolean) Whether to enable static public IP for the runner. Note there are account limits. To list limits, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/limits`. Defaults to false. +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) -- `delete` - (Defaults to 10 minutes) Used for waiting for the hosted runner deletion to complete. +### Read-Only -Example: +- `id` (String) The hosted runner ID. +- `last_active_on` (String) Timestamp when the runner was last active. +- `machine_size_details` (List of Object) Detailed machine size specifications. (see [below for nested schema](#nestedatt--machine_size_details)) +- `platform` (String) Platform of the runner. +- `public_ips` (List of Object) List of public IP ranges assigned to this runner. (see [below for nested schema](#nestedatt--public_ips)) +- `status` (String) Current status of the runner. -```terraform -resource "github_actions_hosted_runner" "example" { - name = "example-hosted-runner" + +### Nested Schema for `image` - image { - id = "2306" - source = "github" - } +Required: - size = "4-core" - runner_group_id = github_actions_runner_group.example.id +- `id` (String) The image ID. - timeouts { - delete = "15m" - } -} -``` +Optional: -## Attributes Reference +- `source` (String) The image source (github, partner, or custom). -In addition to the arguments above, the following attributes are exported: +Read-Only: -- `id` - The ID of the hosted runner. -- `status` - Current status of the runner (e.g., "Ready", "Provisioning"). -- `platform` - Platform of the runner (e.g., "linux-x64", "win-x64"). -- `image` - In addition to the arguments above, the image block exports: - - `size_gb` - The size of the image in gigabytes. -- `machine_size_details` - Detailed specifications of the machine size: - - `id` - Machine size identifier. - - `cpu_cores` - Number of CPU cores. - - `memory_gb` - Amount of memory in gigabytes. - - `storage_gb` - Amount of storage in gigabytes. -- `public_ips` - List of public IP ranges assigned to this runner (only if `public_ip_enabled` is true): - - `enabled` - Whether this IP range is enabled. - - `prefix` - IP address prefix. - - `length` - Subnet length. -- `last_active_on` - Timestamp (RFC3339) when the runner was last active. +- `size_gb` (Number) The size of the image in GB. -## Import -Hosted runners can be imported using the runner ID: + +### Nested Schema for `timeouts` -```shell -terraform import github_actions_hosted_runner.example 123456 -``` +Optional: -## Notes +- `delete` (String) -- This resource is **organization-only** and cannot be used with individual accounts. -- The `image` field cannot be changed after the runner is created. Changing it will force recreation of the runner. -- The `size` field can be updated to scale the runner up or down as needed. -- Image IDs for GitHub-owned images are numeric strings (e.g., "2306" for Ubuntu Latest 24.04), not names like "ubuntu-latest". -- Deletion of hosted runners is asynchronous. The provider will poll for up to 10 minutes (configurable via timeouts) to confirm deletion. -- Runner creation and updates may take several minutes as GitHub provisions the infrastructure. -- Static public IPs are subject to account limits. Check your organization's limits before enabling. -## Getting Available Images and Sizes + +### Nested Schema for `machine_size_details` -To get a list of available images: +Read-Only: -```bash -curl -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/orgs/YOUR_ORG/actions/hosted-runners/images/github-owned -``` +- `cpu_cores` (Number) +- `id` (String) +- `memory_gb` (Number) +- `storage_gb` (Number) -To get available machine sizes: -```bash -curl -H "Authorization: Bearer YOUR_TOKEN" \ - -H "Accept: application/vnd.github+json" \ - https://api.github.com/orgs/YOUR_ORG/actions/hosted-runners/machine-sizes -``` + +### Nested Schema for `public_ips` + +Read-Only: + +- `enabled` (Boolean) +- `length` (Number) +- `prefix` (String) diff --git a/examples/resources/actions_hosted_runner/example_2.tf b/examples/resources/actions_hosted_runner/example_2.tf deleted file mode 100644 index ee7e3f69f7..0000000000 --- a/examples/resources/actions_hosted_runner/example_2.tf +++ /dev/null @@ -1,18 +0,0 @@ -resource "github_actions_runner_group" "advanced" { - name = "advanced-runner-group" - visibility = "selected" -} - -resource "github_actions_hosted_runner" "advanced" { - name = "advanced-hosted-runner" - - image { - id = "2306" - source = "github" - } - - size = "8-core" - runner_group_id = github_actions_runner_group.advanced.id - maximum_runners = 10 - public_ip_enabled = true -} diff --git a/examples/resources/actions_hosted_runner/example_3.tf b/examples/resources/actions_hosted_runner/example_3.tf deleted file mode 100644 index 195b7aeb8c..0000000000 --- a/examples/resources/actions_hosted_runner/example_3.tf +++ /dev/null @@ -1,15 +0,0 @@ -resource "github_actions_hosted_runner" "example" { - name = "example-hosted-runner" - - image { - id = "2306" - source = "github" - } - - size = "4-core" - runner_group_id = github_actions_runner_group.example.id - - timeouts { - delete = "15m" - } -} diff --git a/examples/resources/actions_hosted_runner/example_1.tf b/examples/resources/github_actions_hosted_runner/resource_advanced.tf similarity index 64% rename from examples/resources/actions_hosted_runner/example_1.tf rename to examples/resources/github_actions_hosted_runner/resource_advanced.tf index ef5ae400c3..f43c543ee1 100644 --- a/examples/resources/actions_hosted_runner/example_1.tf +++ b/examples/resources/github_actions_hosted_runner/resource_advanced.tf @@ -11,6 +11,8 @@ resource "github_actions_hosted_runner" "example" { source = "github" } - size = "4-core" - runner_group_id = github_actions_runner_group.example.id + size = "8-core" + runner_group_id = github_actions_runner_group.example.id + maximum_runners = 10 + public_ip_enabled = true } diff --git a/github/resource_github_actions_hosted_runner.go b/github/resource_github_actions_hosted_runner.go index 51be69aeab..3657063d8f 100644 --- a/github/resource_github_actions_hosted_runner.go +++ b/github/resource_github_actions_hosted_runner.go @@ -32,6 +32,8 @@ func resourceGithubActionsHostedRunner() *schema.Resource { Delete: schema.DefaultTimeout(10 * time.Minute), }, + Description: "This resource allows you to create and manage GitHub-hosted runners within your GitHub organization. You must have admin access to an organization to use this resource.", + CustomizeDiff: customdiff.All(resourceGithubActionsHostedRunnerValidation), Schema: map[string]*schema.Schema{ @@ -48,10 +50,11 @@ func resourceGithubActionsHostedRunner() *schema.Resource { Description: "Name of the hosted runner. Must be between 1 and 64 characters and may only contain upper and lowercase letters a-z, numbers 0-9, '.', '-', and '_'.", }, "image": { - Type: schema.TypeList, - Required: true, - ForceNew: true, - MaxItems: 1, + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Description: "Image configuration for the hosted runner. Cannot be changed after creation.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { @@ -73,35 +76,34 @@ func resourceGithubActionsHostedRunner() *schema.Resource { }, }, }, - Description: "Image configuration for the hosted runner. Cannot be changed after creation.", }, "size": { Type: schema.TypeString, Required: true, - Description: "Machine size (e.g., '4-core', '8-core'). Can be updated to scale the runner.", + Description: "Machine size for the hosted runner (e.g., `4-core`, `8-core`). Can be updated to scale the runner. To list available sizes, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/machine-sizes`.", }, "runner_group_id": { Type: schema.TypeInt, Required: true, - Description: "The runner group ID.", + Description: "The ID of the runner group to assign this runner to.", }, "maximum_runners": { Type: schema.TypeInt, Optional: true, Computed: true, ValidateDiagFunc: validation.ToDiagFunc(validation.IntAtLeast(1)), - Description: "Maximum number of runners to scale up to.", + Description: "Maximum number of runners to scale up to. Runners will not auto-scale above this number. Use this setting to limit costs.", }, "public_ip_enabled": { Type: schema.TypeBool, Optional: true, Default: false, - Description: "Whether to enable static public IP.", + Description: "Whether to enable static public IP for the runner. Note there are account limits. To list limits, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/limits`. Defaults to false.", }, "image_version": { Type: schema.TypeString, Optional: true, - Description: "The version of the runner image to deploy. This is relevant only for runners using custom images.", + Description: "The version of the runner image to deploy. This is only relevant for runners using custom images.", }, "image_gen": { Type: schema.TypeBool, @@ -120,14 +122,10 @@ func resourceGithubActionsHostedRunner() *schema.Resource { Computed: true, Description: "Current status of the runner.", }, - "platform": { - Type: schema.TypeString, - Computed: true, - Description: "Platform of the runner.", - }, "machine_size_details": { - Type: schema.TypeList, - Computed: true, + Type: schema.TypeList, + Computed: true, + Description: "Detailed machine size specifications.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "id": { @@ -152,11 +150,16 @@ func resourceGithubActionsHostedRunner() *schema.Resource { }, }, }, - Description: "Detailed machine size specifications.", + }, + "platform": { + Type: schema.TypeString, + Computed: true, + Description: "Platform of the runner.", }, "public_ips": { - Type: schema.TypeList, - Computed: true, + Type: schema.TypeList, + Computed: true, + Description: "List of public IP ranges assigned to this runner.", Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "enabled": { @@ -176,7 +179,6 @@ func resourceGithubActionsHostedRunner() *schema.Resource { }, }, }, - Description: "List of public IP ranges assigned to this runner.", }, "last_active_on": { Type: schema.TypeString, diff --git a/templates/resources/actions_hosted_runner.md.tmpl b/templates/resources/actions_hosted_runner.md.tmpl index 166b5c73e7..23de6892ba 100644 --- a/templates/resources/actions_hosted_runner.md.tmpl +++ b/templates/resources/actions_hosted_runner.md.tmpl @@ -1,86 +1,20 @@ --- page_title: "{{.Name}} ({{.Type}}) - {{.RenderedProviderName}}" +subcategory: "" description: |- - Creates and manages GitHub-hosted runners within a GitHub organization +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} --- # {{.Name}} ({{.Type}}) -This resource allows you to create and manage GitHub-hosted runners within your GitHub organization. You must have admin access to an organization to use this resource. - -GitHub-hosted runners are fully managed virtual machines that run your GitHub Actions workflows. Unlike self-hosted runners, GitHub handles the infrastructure, maintenance, and scaling. - -## Example Usage - -### Basic Usage - -{{ tffile "examples/resources/actions_hosted_runner/example_1.tf" }} - -### Advanced Usage with Optional Parameters - -{{ tffile "examples/resources/actions_hosted_runner/example_2.tf" }} - -## Argument Reference - -The following arguments are supported: - -- `name` - (Required) Name of the hosted runner. Must be between 1 and 64 characters and may only contain alphanumeric characters, '.', '-', and '_'. -- `image` - (Required) Image configuration for the hosted runner. Cannot be changed after creation. Block supports: - - `id` - (Required) The image ID. For GitHub-owned images, use numeric IDs like "2306" for Ubuntu Latest 24.04. To get available images, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/images/github-owned`. - - `source` - (Optional) The image source. Valid values are "github", "partner", or "custom". Defaults to "github". -- `size` - (Required) Machine size for the hosted runner (e.g., "4-core", "8-core"). Can be updated to scale the runner. To list available sizes, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/machine-sizes`. -- `runner_group_id` - (Required) The ID of the runner group to assign this runner to. -- `maximum_runners` - (Optional) Maximum number of runners to scale up to. Runners will not auto-scale above this number. Use this setting to limit costs. -- `public_ip_enabled` - (Optional) Whether to enable static public IP for the runner. Note there are account limits. To list limits, use the GitHub API: `GET /orgs/{org}/actions/hosted-runners/limits`. Defaults to false. -- `image_version` - (Optional) The version of the runner image to deploy. This is only relevant for runners using custom images. - -## Timeouts - -The `timeouts` block allows you to specify timeouts for certain actions: - -- `delete` - (Defaults to 10 minutes) Used for waiting for the hosted runner deletion to complete. - -Example: - -{{ tffile "examples/resources/actions_hosted_runner/example_3.tf" }} - -## Attributes Reference - -In addition to the arguments above, the following attributes are exported: - -- `id` - The ID of the hosted runner. -- `status` - Current status of the runner (e.g., "Ready", "Provisioning"). -- `platform` - Platform of the runner (e.g., "linux-x64", "win-x64"). -- `image` - In addition to the arguments above, the image block exports: - - `size_gb` - The size of the image in gigabytes. -- `machine_size_details` - Detailed specifications of the machine size: - - `id` - Machine size identifier. - - `cpu_cores` - Number of CPU cores. - - `memory_gb` - Amount of memory in gigabytes. - - `storage_gb` - Amount of storage in gigabytes. -- `public_ips` - List of public IP ranges assigned to this runner (only if `public_ip_enabled` is true): - - `enabled` - Whether this IP range is enabled. - - `prefix` - IP address prefix. - - `length` - Subnet length. -- `last_active_on` - Timestamp (RFC3339) when the runner was last active. - -## Import - -Hosted runners can be imported using the runner ID: - -```shell -terraform import github_actions_hosted_runner.example 123456 -``` +{{ .Description | trimspace }} ## Notes - This resource is **organization-only** and cannot be used with individual accounts. -- The `image` field cannot be changed after the runner is created. Changing it will force recreation of the runner. -- The `size` field can be updated to scale the runner up or down as needed. - Image IDs for GitHub-owned images are numeric strings (e.g., "2306" for Ubuntu Latest 24.04), not names like "ubuntu-latest". - Deletion of hosted runners is asynchronous. The provider will poll for up to 10 minutes (configurable via timeouts) to confirm deletion. - Runner creation and updates may take several minutes as GitHub provisions the infrastructure. -- Static public IPs are subject to account limits. Check your organization's limits before enabling. ## Getting Available Images and Sizes @@ -99,3 +33,47 @@ curl -H "Authorization: Bearer YOUR_TOKEN" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/orgs/YOUR_ORG/actions/hosted-runners/machine-sizes ``` + +{{ if .HasExamples -}} +## Example Usage + +{{- range .ExampleFiles }} + +{{- $parts := split . "resource_" -}} +{{- $lastItem := index $parts 1 }} +{{- $fileNameParts := split $lastItem "." }} +{{- $fileName := index $fileNameParts 0 }} + +### {{ $fileName | title }} Usage + +{{ tffile . }} +{{- end }} +{{- end }} + +{{ .SchemaMarkdown | trimspace }} +{{- if or .HasImport .HasImportIDConfig .HasImportIdentityConfig }} + +## Import + +Import is supported using the following syntax: +{{- end }} +{{- if .HasImportIdentityConfig }} + +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: + +{{tffile .ImportIdentityConfigFile }} + +{{ .IdentitySchemaMarkdown | trimspace }} +{{- end }} +{{- if .HasImportIDConfig }} + +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: + +{{tffile .ImportIDConfigFile }} +{{- end }} +{{- if .HasImport }} + +The [`terraform import` command](https://developer.hashicorp.com/terraform/cli/commands/import) can be used, for example: + +{{codefile "shell" .ImportFile }} +{{- end }} From 08c26773a7f10270f4978ca6dc85ec783b252a5d Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Fri, 10 Jul 2026 18:50:28 +0300 Subject: [PATCH 22/22] Cleanup test templates Signed-off-by: Timo Sand --- RESOURCES.md | 2 +- ...ource_github_actions_hosted_runner_test.go | 49 ++++--------------- 2 files changed, 11 insertions(+), 40 deletions(-) diff --git a/RESOURCES.md b/RESOURCES.md index 819cc792bf..d9256bdee1 100644 --- a/RESOURCES.md +++ b/RESOURCES.md @@ -122,7 +122,7 @@ The overall status of each resource or data source is captured in this document | --- | --- | --- | --- | --- | --- | --- | --- | | `github_actions_environment_secret` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_environment_variable` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | -| `github_actions_hosted_runner` | ⚠️ | ✅ | ✅ | ✅ | ⚠️ | ✅ | ❌ | +| `github_actions_hosted_runner` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | `github_actions_organization_oidc_subject_claim_customization_template` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_organization_permissions` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | | `github_actions_organization_secret` | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | ❓ | diff --git a/github/resource_github_actions_hosted_runner_test.go b/github/resource_github_actions_hosted_runner_test.go index ba7c6568cc..2a8f2c8c09 100644 --- a/github/resource_github_actions_hosted_runner_test.go +++ b/github/resource_github_actions_hosted_runner_test.go @@ -100,7 +100,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-update-%s", testResourcePrefix, randomID) - configBefore := fmt.Sprintf(` + configTmpl := ` resource "github_actions_hosted_runner" "test" { name = "%s" @@ -111,24 +111,9 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { size = "4-core" runner_group_id = "%d" - maximum_runners = 2 + maximum_runners = %d } - `, hostedRunnerName, runnerGroup.GetID()) - - configAfter := fmt.Sprintf(` - resource "github_actions_hosted_runner" "test" { - name = "%s-updated" - - image { - id = "2306" - source = "github" - } - - size = "4-core" - runner_group_id = "%d" - maximum_runners = 3 - } - `, hostedRunnerName, runnerGroup.GetID()) + ` compareMaxRunnersUpdated := statecheck.CompareValue(compare.ValuesDiffer()) resource.Test(t, resource.TestCase{ @@ -136,13 +121,13 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: configBefore, + Config: fmt.Sprintf(configTmpl, hostedRunnerName, runnerGroup.GetID(), 2), ConfigStateChecks: []statecheck.StateCheck{ compareMaxRunnersUpdated.AddStateValue("github_actions_hosted_runner.test", tfjsonpath.New("maximum_runners")), }, }, { - Config: configAfter, + Config: fmt.Sprintf(configTmpl, fmt.Sprintf("%s-updated", hostedRunnerName), runnerGroup.GetID(), 3), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("github_actions_hosted_runner.test", plancheck.ResourceActionUpdate), @@ -164,7 +149,7 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { randomID := acctest.RandString(5) hostedRunnerName := fmt.Sprintf("%srunner-size-%s", testResourcePrefix, randomID) - configBefore := fmt.Sprintf(` + configTmpl := ` resource "github_actions_hosted_runner" "test" { name = "%s" @@ -173,24 +158,10 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { source = "github" } - size = "4-core" + size = "%s" runner_group_id = "%d" } - `, hostedRunnerName, runnerGroup.GetID()) - - configAfter := fmt.Sprintf(` - resource "github_actions_hosted_runner" "test" { - name = "%s" - - image { - id = "2306" - source = "github" - } - - size = "8-core" - runner_group_id = "%d" - } - `, hostedRunnerName, runnerGroup.GetID()) + ` compareSizeUpdated := statecheck.CompareValue(compare.ValuesDiffer()) resource.Test(t, resource.TestCase{ @@ -198,14 +169,14 @@ func TestAccGithubActionsHostedRunner(t *testing.T) { ProviderFactories: providerFactories, Steps: []resource.TestStep{ { - Config: configBefore, + Config: fmt.Sprintf(configTmpl, hostedRunnerName, "4-core", runnerGroup.GetID()), ConfigStateChecks: []statecheck.StateCheck{ compareSizeUpdated.AddStateValue("github_actions_hosted_runner.test", tfjsonpath.New("size")), statecheck.ExpectKnownValue("github_actions_hosted_runner.test", tfjsonpath.New("machine_size_details").AtSliceIndex(0).AtMapKey("cpu_cores"), knownvalue.Int64Exact(4)), }, }, { - Config: configAfter, + Config: fmt.Sprintf(configTmpl, hostedRunnerName, "8-core", runnerGroup.GetID()), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction("github_actions_hosted_runner.test", plancheck.ResourceActionUpdate),