From eadd7e41d0ebb5e281cf9890a57bafeff4f48169 Mon Sep 17 00:00:00 2001 From: Leonard Sheng Sheng Lee <305414+sheeeng@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:58:49 +0200 Subject: [PATCH] refactor(runner_group): `selected_workflows` set Change `selected_workflows` from `TypeList` to `TypeSet` in both `github_actions_runner_group` and `github_enterprise_actions_runner_group` resources. A `TypeList` is order-sensitive, causing Terraform to detect spurious drift when the GitHub API returns workflows in a different order than specified in configuration. A `TypeSet` compares elements regardless of order, eliminating the persistent plan diff. Fix https://github.com/integrations/terraform-provider-github/issues/3478. --- docs/resources/actions_runner_group.md | 4 +- .../enterprise_actions_runner_group.md | 2 +- .../resource_github_actions_runner_group.go | 15 ++- ...e_github_actions_runner_group_migration.go | 98 +++++++++++++++++++ ...source_github_actions_runner_group_test.go | 11 +-- ..._github_enterprise_actions_runner_group.go | 15 ++- ...terprise_actions_runner_group_migration.go | 93 ++++++++++++++++++ 7 files changed, 221 insertions(+), 17 deletions(-) create mode 100644 github/resource_github_actions_runner_group_migration.go create mode 100644 github/resource_github_enterprise_actions_runner_group_migration.go diff --git a/docs/resources/actions_runner_group.md b/docs/resources/actions_runner_group.md index 5f04995a13..8a08d9de38 100644 --- a/docs/resources/actions_runner_group.md +++ b/docs/resources/actions_runner_group.md @@ -29,7 +29,7 @@ The following arguments are supported: - `name` - (Required) Name of the runner group - `restricted_to_workflows` - (Optional) If true, the runner group will be restricted to running only the workflows specified in the selected_workflows array. Defaults to false. - `selected_repository_ids` - (Optional) IDs of the repositories which should be added to the runner group -- `selected_workflows` - (Optional) List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to true. +- `selected_workflows` - (Optional) Set of workflows the runner group should be allowed to run. The order of items is not significant. This setting will be ignored unless restricted_to_workflows is set to true. - `visibility` - (Optional) Visibility of a runner group. Whether the runner group can include `all`, `selected`, or `private` repositories. A value of `private` is not currently supported due to limitations in the GitHub API. - `allows_public_repositories` - (Optional) Whether public repositories can be added to the runner group. Defaults to false. @@ -44,7 +44,7 @@ The following arguments are supported: - `selected_repositories_url` - GitHub API URL for the runner group's repositories - `visibility` - The visibility of the runner group - `restricted_to_workflows` - If true, the runner group will be restricted to running only the workflows specified in the selected_workflows array. Defaults to false. -- `selected_workflows` - List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to true. +- `selected_workflows` - Set of workflows the runner group should be allowed to run. The order of items is not significant. This setting will be ignored unless restricted_to_workflows is set to true. ## Import diff --git a/docs/resources/enterprise_actions_runner_group.md b/docs/resources/enterprise_actions_runner_group.md index 65b05fb95a..c423b51dd9 100644 --- a/docs/resources/enterprise_actions_runner_group.md +++ b/docs/resources/enterprise_actions_runner_group.md @@ -43,7 +43,7 @@ The following arguments are supported: - `selected_organization_ids` - (Optional) IDs of the organizations which should be added to the runner group - `allows_public_repositories` - (Optional) Whether public repositories can be added to the runner group. Defaults to false. - `restricted_to_workflows` - (Optional) If true, the runner group will be restricted to running only the workflows specified in the selected_workflows array. Defaults to false. -- `selected_workflows` - (Optional) List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to true. +- `selected_workflows` - (Optional) Set of workflows the runner group should be allowed to run. The order of items is not significant. This setting will be ignored unless restricted_to_workflows is set to true. ## Attributes Reference diff --git a/github/resource_github_actions_runner_group.go b/github/resource_github_actions_runner_group.go index b24066d745..7dcf91a496 100644 --- a/github/resource_github_actions_runner_group.go +++ b/github/resource_github_actions_runner_group.go @@ -23,6 +23,15 @@ func resourceGithubActionsRunnerGroup() *schema.Resource { StateContext: schema.ImportStatePassthroughContext, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsRunnerGroupV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsRunnerGroupStateUpgradeV0, + Version: 0, + }, + }, + Schema: map[string]*schema.Schema{ "id": { Type: schema.TypeString, @@ -87,7 +96,7 @@ func resourceGithubActionsRunnerGroup() *schema.Resource { Description: "If 'true', the runner group will be restricted to running only the workflows specified in the 'selected_workflows' array. Defaults to 'false'.", }, "selected_workflows": { - Type: schema.TypeList, + Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, Description: "List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to 'true'.", @@ -112,7 +121,7 @@ func resourceGithubActionsRunnerGroupCreate(d *schema.ResourceData, meta any) er selectedWorkflows := []string{} if workflows, ok := d.GetOk("selected_workflows"); ok { - for _, workflow := range workflows.([]any) { + for _, workflow := range workflows.(*schema.Set).List() { selectedWorkflows = append(selectedWorkflows, workflow.(string)) } } @@ -316,7 +325,7 @@ func resourceGithubActionsRunnerGroupUpdate(d *schema.ResourceData, meta any) er selectedWorkflows := []string{} allowsPublicRepositories := d.Get("allows_public_repositories").(bool) if workflows, ok := d.GetOk("selected_workflows"); ok { - for _, workflow := range workflows.([]any) { + for _, workflow := range workflows.(*schema.Set).List() { selectedWorkflows = append(selectedWorkflows, workflow.(string)) } } diff --git a/github/resource_github_actions_runner_group_migration.go b/github/resource_github_actions_runner_group_migration.go new file mode 100644 index 0000000000..9b24b76c09 --- /dev/null +++ b/github/resource_github_actions_runner_group_migration.go @@ -0,0 +1,98 @@ +package github + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubActionsRunnerGroupV0() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 0, + + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the runner group.", + }, + "allows_public_repositories": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether public repositories can be added to the runner group.", + }, + "default": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether this is the default runner group.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + Description: "An etag representing the runner group object", + }, + "inherited": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether the runner group is inherited from the enterprise level", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the runner group.", + }, + "runners_url": { + Type: schema.TypeString, + Computed: true, + Description: "The GitHub API URL for the runner group's runners.", + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Set: schema.HashInt, + Optional: true, + Description: "List of repository IDs that can access the runner group.", + }, + "selected_repositories_url": { + Type: schema.TypeString, + Computed: true, + Description: "GitHub API URL for the runner group's repositories.", + }, + "visibility": { + Type: schema.TypeString, + Required: true, + Description: "The visibility of the runner group.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "selected", "private"}, false)), + }, + "restricted_to_workflows": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If 'true', the runner group will be restricted to running only the workflows specified in the 'selected_workflows' array. Defaults to 'false'.", + }, + "selected_workflows": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to 'true'.", + }, + }, + } +} + +func resourceGithubActionsRunnerGroupStateUpgradeV0(_ context.Context, rawState map[string]any, _ any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Runner Group Attributes before migration: %#v", rawState) + + // No transformation needed. The SDK re-encodes selected_workflows from + // TypeList (index-based keys) to TypeSet (hash-based keys) automatically + // when it persists the state with the new schema version. + + log.Printf("[DEBUG] GitHub Actions Runner Group Attributes after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_actions_runner_group_test.go b/github/resource_github_actions_runner_group_test.go index a399e5f140..9653e0611a 100644 --- a/github/resource_github_actions_runner_group_test.go +++ b/github/resource_github_actions_runner_group_test.go @@ -68,16 +68,11 @@ func TestAccGithubActionsRunnerGroup(t *testing.T) { githubRepository := state.RootModule().Resources["github_repository.test"].Primary fullName := githubRepository.Attributes["full_name"] - runnerGroup := state.RootModule().Resources["github_actions_runner_group.test"].Primary - workflowActual := runnerGroup.Attributes["selected_workflows.0"] - workflowExpected := fmt.Sprintf("%s/.github/workflows/test.yml@refs/heads/main", fullName) - if workflowActual != workflowExpected { - return fmt.Errorf("actual selected workflows %s not the same as expected selected workflows %s", - workflowActual, workflowExpected) - } - return nil + return resource.TestCheckTypeSetElemAttr( + "github_actions_runner_group.test", "selected_workflows.*", workflowExpected, + )(state) }, resource.TestCheckResourceAttr( "github_actions_runner_group.test", "allows_public_repositories", diff --git a/github/resource_github_enterprise_actions_runner_group.go b/github/resource_github_enterprise_actions_runner_group.go index fa88980d53..1e359c8d9d 100644 --- a/github/resource_github_enterprise_actions_runner_group.go +++ b/github/resource_github_enterprise_actions_runner_group.go @@ -24,6 +24,15 @@ func resourceGithubActionsEnterpriseRunnerGroup() *schema.Resource { State: resourceGithubActionsEnterpriseRunnerGroupImport, }, + SchemaVersion: 1, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsEnterpriseRunnerGroupV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsEnterpriseRunnerGroupStateUpgradeV0, + Version: 0, + }, + }, + Schema: map[string]*schema.Schema{ "enterprise_slug": { Type: schema.TypeString, @@ -69,7 +78,7 @@ func resourceGithubActionsEnterpriseRunnerGroup() *schema.Resource { Description: "If 'true', the runner group will be restricted to running only the workflows specified in the 'selected_workflows' array. Defaults to 'false'.", }, "selected_workflows": { - Type: schema.TypeList, + Type: schema.TypeSet, Elem: &schema.Schema{Type: schema.TypeString}, Optional: true, Description: "List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to 'true'.", @@ -104,7 +113,7 @@ func resourceGithubActionsEnterpriseRunnerGroupCreate(d *schema.ResourceData, me selectedWorkflows := []string{} if workflows, ok := d.GetOk("selected_workflows"); ok { - for _, workflow := range workflows.([]any) { + for _, workflow := range workflows.(*schema.Set).List() { selectedWorkflows = append(selectedWorkflows, workflow.(string)) } } @@ -291,7 +300,7 @@ func resourceGithubActionsEnterpriseRunnerGroupUpdate(d *schema.ResourceData, me selectedWorkflows := []string{} allowsPublicRepositories := d.Get("allows_public_repositories").(bool) if workflows, ok := d.GetOk("selected_workflows"); ok { - for _, workflow := range workflows.([]any) { + for _, workflow := range workflows.(*schema.Set).List() { selectedWorkflows = append(selectedWorkflows, workflow.(string)) } } diff --git a/github/resource_github_enterprise_actions_runner_group_migration.go b/github/resource_github_enterprise_actions_runner_group_migration.go new file mode 100644 index 0000000000..1d890e9e73 --- /dev/null +++ b/github/resource_github_enterprise_actions_runner_group_migration.go @@ -0,0 +1,93 @@ +package github + +import ( + "context" + "log" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +func resourceGithubActionsEnterpriseRunnerGroupV0() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 0, + + Schema: map[string]*schema.Schema{ + "enterprise_slug": { + Type: schema.TypeString, + Required: true, + Description: "The slug of the enterprise.", + }, + "allows_public_repositories": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Whether public repositories can be added to the runner group.", + }, + "default": { + Type: schema.TypeBool, + Computed: true, + Description: "Whether this is the default runner group.", + }, + "etag": { + Type: schema.TypeString, + Computed: true, + Description: "An etag representing the runner group object", + }, + "name": { + Type: schema.TypeString, + Required: true, + Description: "Name of the runner group.", + }, + "runners_url": { + Type: schema.TypeString, + Computed: true, + Description: "The GitHub API URL for the runner group's runners.", + }, + "visibility": { + Type: schema.TypeString, + Required: true, + Description: "The visibility of the runner group.", + ValidateDiagFunc: validation.ToDiagFunc(validation.StringInSlice([]string{"all", "selected"}, false)), + }, + "restricted_to_workflows": { + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "If 'true', the runner group will be restricted to running only the workflows specified in the 'selected_workflows' array. Defaults to 'false'.", + }, + "selected_workflows": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Optional: true, + Description: "List of workflows the runner group should be allowed to run. This setting will be ignored unless restricted_to_workflows is set to 'true'.", + }, + "selected_organization_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Set: schema.HashInt, + Optional: true, + Description: "List of organization IDs that can access the runner group.", + }, + "selected_organizations_url": { + Type: schema.TypeString, + Computed: true, + Description: "GitHub API URL for the runner group's organizations.", + }, + }, + } +} + +func resourceGithubActionsEnterpriseRunnerGroupStateUpgradeV0(_ context.Context, rawState map[string]any, _ any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Enterprise Actions Runner Group Attributes before migration: %#v", rawState) + + // No transformation needed. The SDK re-encodes selected_workflows from + // TypeList (index-based keys) to TypeSet (hash-based keys) automatically + // when it persists the state with the new schema version. + + log.Printf("[DEBUG] GitHub Enterprise Actions Runner Group Attributes after migration: %#v", rawState) + + return rawState, nil +}