From 965281081d10bf4fa1a8f391b370a1078b13e3ac Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 25 Dec 2025 22:29:01 +0200 Subject: [PATCH 01/16] Convert `resourceGithubActionsOrganizationSecret` to use `StateUpgraders` for schema migrations Signed-off-by: Timo Sand --- ...rate_github_actions_organization_secret.go | 85 ++++++++++++++----- ...github_actions_organization_secret_test.go | 84 ++++++++---------- ...urce_github_actions_organization_secret.go | 8 +- 3 files changed, 106 insertions(+), 71 deletions(-) diff --git a/github/migrate_github_actions_organization_secret.go b/github/migrate_github_actions_organization_secret.go index f973ed2842..37d2b72db5 100644 --- a/github/migrate_github_actions_organization_secret.go +++ b/github/migrate_github_actions_organization_secret.go @@ -1,36 +1,79 @@ package github import ( - "fmt" + "context" "log" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) -func resourceGithubActionsOrganizationSecretMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Actions Organization Secret State v0; migrating to v1") - return migrateGithubActionsOrganizationSecretStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) +func resourceGithubActionsOrganizationSecretResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the secret.", + ValidateDiagFunc: validateSecretNameFunc, + }, + "encrypted_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"plaintext_value"}, + Description: "Encrypted value of the secret using the GitHub public key in Base64 format.", + ValidateDiagFunc: toDiagFunc(validation.StringIsBase64, "encrypted_value"), + }, + "plaintext_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"encrypted_value"}, + Description: "Plaintext value of the secret to be encrypted.", + }, + "visibility": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateValueFunc([]string{"all", "private", "selected"}), + Description: "Configures the access that repositories have to the organization secret. Must be one of 'all', 'private', or 'selected'. 'selected_repository_ids' is required if set to 'selected'.", + }, + "selected_repository_ids": { + Type: schema.TypeSet, + Elem: &schema.Schema{ + Type: schema.TypeInt, + }, + Set: schema.HashInt, + Optional: true, + ForceNew: true, + Description: "An array of repository ids that can access the organization secret.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' creation.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' update.", + }, + }, } } -func migrateGithubActionsOrganizationSecretStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes before migration: %#v", is.Attributes) - +func resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes before migration: %#v", rawState) // Add the destroy_on_drift field with default value true if it doesn't exist - if _, ok := is.Attributes["destroy_on_drift"]; !ok { - is.Attributes["destroy_on_drift"] = "true" + if _, ok := rawState["destroy_on_drift"]; !ok { + rawState["destroy_on_drift"] = true } - log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes after State Migration: %#v", is.Attributes) + log.Printf("[DEBUG] GitHub Actions Organization Secret Attributes after migration: %#v", rawState) - return is, nil + return rawState, nil } diff --git a/github/migrate_github_actions_organization_secret_test.go b/github/migrate_github_actions_organization_secret_test.go index ef6f032963..9091bf63ea 100644 --- a/github/migrate_github_actions_organization_secret_test.go +++ b/github/migrate_github_actions_organization_secret_test.go @@ -3,13 +3,10 @@ package github import ( "reflect" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestMigrateGithubActionsOrganizationSecretStateV0toV1(t *testing.T) { - // Secret without destroy_on_drift should get default value - oldAttributes := map[string]string{ +func testResourceGithubActionsOrganizationSecretInstanceStateDataV0() map[string]any { + return map[string]any{ "id": "test-secret", "secret_name": "test-secret", "visibility": "private", @@ -17,53 +14,42 @@ func TestMigrateGithubActionsOrganizationSecretStateV0toV1(t *testing.T) { "updated_at": "2023-01-01T00:00:00Z", "plaintext_value": "secret-value", } +} - newState, err := migrateGithubActionsOrganizationSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - "destroy_on_drift": "true", - } - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } +func testResourceGithubActionsOrganizationSecretInstanceStateDataV0_WithDrift() map[string]any { + v0 := testResourceGithubActionsOrganizationSecretInstanceStateDataV0() + v0["destroy_on_drift"] = false + return v0 +} - // Secret with existing destroy_on_drift should be preserved - oldAttributesWithDrift := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "destroy_on_drift": "false", - } +func testResourceGithubActionsOrganizationSecretInstanceStateDataV1() map[string]any { + v0 := testResourceGithubActionsOrganizationSecretInstanceStateDataV0() + v0["destroy_on_drift"] = true + return v0 +} - newState2, err := migrateGithubActionsOrganizationSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributesWithDrift, +func TestGithub_MigrateActionsOrganizationSecretStateV0toV1(t *testing.T) { + t.Run("without destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsOrganizationSecretInstanceStateDataV1() + actual, err := resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsOrganizationSecretInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } }) - if err != nil { - t.Fatal(err) - } - expectedAttributesWithDrift := map[string]string{ - "id": "test-secret", - "secret_name": "test-secret", - "visibility": "private", - "destroy_on_drift": "false", - } - if !reflect.DeepEqual(newState2.Attributes, expectedAttributesWithDrift) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributesWithDrift, newState2.Attributes) - } + t.Run("with destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsOrganizationSecretInstanceStateDataV0_WithDrift() + actual, err := resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsOrganizationSecretInstanceStateDataV0_WithDrift(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) } diff --git a/github/resource_github_actions_organization_secret.go b/github/resource_github_actions_organization_secret.go index b6ef18e943..0f0cc4a99b 100644 --- a/github/resource_github_actions_organization_secret.go +++ b/github/resource_github_actions_organization_secret.go @@ -33,7 +33,13 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource { // Schema migration added in v6.7.1 to handle the addition of destroy_on_drift field // Resources created before v6.7.0 need the field populated with default value SchemaVersion: 1, - MigrateState: resourceGithubActionsOrganizationSecretMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsOrganizationSecretResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsOrganizationSecretInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "secret_name": { From 19e5dafd2879c8108457bd103cef32a5bcc29fe7 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 25 Dec 2025 22:46:26 +0200 Subject: [PATCH 02/16] Convert `resourceGithubActionsSecret` to use `StateUpgraders` for schema migrations Signed-off-by: Timo Sand --- github/migrate_github_actions_secret.go | 72 ++++++++++++----- github/migrate_github_actions_secret_test.go | 84 ++++++++------------ github/resource_github_actions_secret.go | 8 +- 3 files changed, 93 insertions(+), 71 deletions(-) diff --git a/github/migrate_github_actions_secret.go b/github/migrate_github_actions_secret.go index 9bce957eea..5be1ce20bb 100644 --- a/github/migrate_github_actions_secret.go +++ b/github/migrate_github_actions_secret.go @@ -1,36 +1,66 @@ package github import ( - "fmt" + "context" "log" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func resourceGithubActionsSecretMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Actions Secret State v0; migrating to v1") - return migrateGithubActionsSecretStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) +func resourceGithubActionsSecretResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the repository.", + }, + "secret_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the secret.", + ValidateDiagFunc: validateSecretNameFunc, + }, + "encrypted_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"plaintext_value"}, + Description: "Encrypted value of the secret using the GitHub public key in Base64 format.", + }, + "plaintext_value": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + Sensitive: true, + ConflictsWith: []string{"encrypted_value"}, + Description: "Plaintext value of the secret to be encrypted.", + }, + "created_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' creation.", + }, + "updated_at": { + Type: schema.TypeString, + Computed: true, + Description: "Date of 'actions_secret' update.", + }, + }, } } -func migrateGithubActionsSecretStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Actions Secret Attributes before migration: %#v", is.Attributes) - +func resourceGithubActionsSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Secret State before migration: %#v", rawState) // Add the destroy_on_drift field with default value true if it doesn't exist - if _, ok := is.Attributes["destroy_on_drift"]; !ok { - is.Attributes["destroy_on_drift"] = "true" + if _, ok := rawState["destroy_on_drift"]; !ok { + rawState["destroy_on_drift"] = true } - log.Printf("[DEBUG] GitHub Actions Secret Attributes after State Migration: %#v", is.Attributes) + log.Printf("[DEBUG] GitHub Actions Secret State after migration: %#v", rawState) - return is, nil + return rawState, nil } diff --git a/github/migrate_github_actions_secret_test.go b/github/migrate_github_actions_secret_test.go index 9f7374de05..7729d0191a 100644 --- a/github/migrate_github_actions_secret_test.go +++ b/github/migrate_github_actions_secret_test.go @@ -3,13 +3,10 @@ package github import ( "reflect" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestMigrateGithubActionsSecretStateV0toV1(t *testing.T) { - // Secret without destroy_on_drift should get default value - oldAttributes := map[string]string{ +func testResourceGithubActionsSecretInstanceStateDataV0() map[string]any { + return map[string]any{ "id": "test-secret", "repository": "test-repo", "secret_name": "test-secret", @@ -17,53 +14,42 @@ func TestMigrateGithubActionsSecretStateV0toV1(t *testing.T) { "updated_at": "2023-01-01T00:00:00Z", "plaintext_value": "secret-value", } +} - newState, err := migrateGithubActionsSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - "destroy_on_drift": "true", - } - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } +func testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = false + return v0 +} - // Secret with existing destroy_on_drift should be preserved - oldAttributesWithDrift := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "destroy_on_drift": "false", - } +func testResourceGithubActionsSecretInstanceStateDataV1() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = true + return v0 +} - newState2, err := migrateGithubActionsSecretStateV0toV1(&terraform.InstanceState{ - ID: "test-secret", - Attributes: oldAttributesWithDrift, +func TestGithub_MigrateActionsSecretStateV0toV1(t *testing.T) { + t.Run("without destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV1() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } }) - if err != nil { - t.Fatal(err) - } - expectedAttributesWithDrift := map[string]string{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "destroy_on_drift": "false", - } - if !reflect.DeepEqual(newState2.Attributes, expectedAttributesWithDrift) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributesWithDrift, newState2.Attributes) - } + t.Run("with destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0_WithDrift(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) } diff --git a/github/resource_github_actions_secret.go b/github/resource_github_actions_secret.go index 6788348274..137116c808 100644 --- a/github/resource_github_actions_secret.go +++ b/github/resource_github_actions_secret.go @@ -26,7 +26,13 @@ func resourceGithubActionsSecret() *schema.Resource { // Schema migration added to handle the addition of destroy_on_drift field // Resources created before this field was added need it populated with default value SchemaVersion: 1, - MigrateState: resourceGithubActionsSecretMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubActionsSecretResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubActionsSecretInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "repository": { From 1727e17b539fc9366a4f1766902bd15c7436bc7d Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 25 Dec 2025 23:36:42 +0200 Subject: [PATCH 03/16] Convert `resourceGithubRepositoryWebhook` and `resourceGithubOrganizationWebhook` schema migrations to use StateUpgraders Signed-off-by: Timo Sand --- github/migrate_github_organization_webhook.go | 70 ++++++++++++++++++ ...igrate_github_organization_webhook_test.go | 20 ++++++ github/migrate_github_repository_webhook.go | 71 ++++++++++++------- .../migrate_github_repository_webhook_test.go | 45 ++++++------ .../resource_github_organization_webhook.go | 8 ++- github/resource_github_repository_webhook.go | 8 ++- 6 files changed, 174 insertions(+), 48 deletions(-) create mode 100644 github/migrate_github_organization_webhook.go create mode 100644 github/migrate_github_organization_webhook_test.go diff --git a/github/migrate_github_organization_webhook.go b/github/migrate_github_organization_webhook.go new file mode 100644 index 0000000000..b7d86e55a7 --- /dev/null +++ b/github/migrate_github_organization_webhook.go @@ -0,0 +1,70 @@ +package github + +import ( + "context" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGithubOrganizationWebhookResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "events": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "configuration": { + Type: schema.TypeMap, + Optional: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, + } +} + +func resourceGithubOrganizationWebhookInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Organization Webhook State before migration: %#v", rawState) + + prefix := "configuration." + delete(rawState, prefix+"%") + + // Read & delete old keys + oldKeys := make(map[string]any) + for k, v := range rawState { + if strings.HasPrefix(k, prefix) { + oldKeys[k] = v + + // Delete old keys + delete(rawState, k) + } + } + + // Write new keys + for k, v := range oldKeys { + newKey := "configuration.0." + strings.TrimPrefix(k, prefix) + rawState[newKey] = v + } + + rawState[prefix+"#"] = "1" + + log.Printf("[DEBUG] GitHub Organization Webhook State after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/migrate_github_organization_webhook_test.go b/github/migrate_github_organization_webhook_test.go new file mode 100644 index 0000000000..481d16a0ee --- /dev/null +++ b/github/migrate_github_organization_webhook_test.go @@ -0,0 +1,20 @@ +package github + +import ( + "reflect" + "testing" +) + +func TestGithub_MigrateOrganizationWebhookStateV0toV1(t *testing.T) { + t.Run("migrates state without errors", func(t *testing.T) { + expected := testResourceGithubWebhookInstanceStateDataV1() + actual, err := resourceGithubOrganizationWebhookInstanceStateUpgradeV0(t.Context(), testResourceGithubWebhookInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/migrate_github_repository_webhook.go b/github/migrate_github_repository_webhook.go index 5379928dec..53ec661701 100644 --- a/github/migrate_github_repository_webhook.go +++ b/github/migrate_github_repository_webhook.go @@ -1,54 +1,75 @@ package github import ( - "fmt" + "context" "log" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func resourceGithubWebhookMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Webhook State v0; migrating to v1") - return migrateGithubWebhookStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) +func resourceGithubRepositoryWebhookResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "repository": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "events": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + "configuration": { + Type: schema.TypeMap, + Optional: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + }, } } -func migrateGithubWebhookStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil - } - - log.Printf("[DEBUG] GitHub Webhook Attributes before migration: %#v", is.Attributes) +func resourceGithubRepositoryWebhookInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Repository Webhook State before migration: %#v", rawState) prefix := "configuration." - - delete(is.Attributes, prefix+"%") + delete(rawState, prefix+"%") // Read & delete old keys - oldKeys := make(map[string]string) - for k, v := range is.Attributes { + oldKeys := make(map[string]any) + for k, v := range rawState { if strings.HasPrefix(k, prefix) { oldKeys[k] = v // Delete old keys - delete(is.Attributes, k) + delete(rawState, k) } } // Write new keys for k, v := range oldKeys { newKey := "configuration.0." + strings.TrimPrefix(k, prefix) - is.Attributes[newKey] = v + rawState[newKey] = v } - is.Attributes[prefix+"#"] = "1" - log.Printf("[DEBUG] GitHub Webhook Attributes after State Migration: %#v", is.Attributes) + rawState[prefix+"#"] = "1" + + log.Printf("[DEBUG] GitHub Repository Webhook State after migration: %#v", rawState) - return is, nil + return rawState, nil } diff --git a/github/migrate_github_repository_webhook_test.go b/github/migrate_github_repository_webhook_test.go index 159345a010..876405c253 100644 --- a/github/migrate_github_repository_webhook_test.go +++ b/github/migrate_github_repository_webhook_test.go @@ -3,36 +3,39 @@ package github import ( "reflect" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestMigrateGithubWebhookStateV0toV1(t *testing.T) { - oldAttributes := map[string]string{ +func testResourceGithubWebhookInstanceStateDataV0() map[string]any { + return map[string]any{ "configuration.%": "4", "configuration.content_type": "form", "configuration.insecure_ssl": "0", "configuration.secret": "blablah", "configuration.url": "https://google.co.uk/", } +} - newState, err := migrateGithubWebhookStateV0toV1(&terraform.InstanceState{ - ID: "nonempty", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } - - expectedAttributes := map[string]string{ +func testResourceGithubWebhookInstanceStateDataV1() map[string]any { + v0 := testResourceGithubWebhookInstanceStateDataV0() + return map[string]any{ "configuration.#": "1", - "configuration.0.content_type": "form", - "configuration.0.insecure_ssl": "0", - "configuration.0.secret": "blablah", - "configuration.0.url": "https://google.co.uk/", - } - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) + "configuration.0.content_type": v0["configuration.content_type"], + "configuration.0.insecure_ssl": v0["configuration.insecure_ssl"], + "configuration.0.secret": v0["configuration.secret"], + "configuration.0.url": v0["configuration.url"], } } + +func TestGithub_MigrateRepositoryWebhookStateV0toV1(t *testing.T) { + t.Run("migrates state without errors", func(t *testing.T) { + expected := testResourceGithubWebhookInstanceStateDataV1() + actual, err := resourceGithubRepositoryWebhookInstanceStateUpgradeV0(t.Context(), testResourceGithubWebhookInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/resource_github_organization_webhook.go b/github/resource_github_organization_webhook.go index 42127b59d8..9b20168dec 100644 --- a/github/resource_github_organization_webhook.go +++ b/github/resource_github_organization_webhook.go @@ -22,7 +22,13 @@ func resourceGithubOrganizationWebhook() *schema.Resource { }, SchemaVersion: 1, - MigrateState: resourceGithubWebhookMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubOrganizationWebhookResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubOrganizationWebhookInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "events": { diff --git a/github/resource_github_repository_webhook.go b/github/resource_github_repository_webhook.go index fbd7dc274d..e7db374412 100644 --- a/github/resource_github_repository_webhook.go +++ b/github/resource_github_repository_webhook.go @@ -34,7 +34,13 @@ func resourceGithubRepositoryWebhook() *schema.Resource { }, SchemaVersion: 1, - MigrateState: resourceGithubWebhookMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubRepositoryWebhookResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubRepositoryWebhookInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "repository": { From 17002c5ba3575a2d9b4aad753c4e67335d177c5b Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 25 Dec 2025 23:51:08 +0200 Subject: [PATCH 04/16] Convert `resourceGithubRepository` schema migration to use StateUpgraders Signed-off-by: Timo Sand --- github/migrate_github_repository.go | 218 ++++++++++++++++++++--- github/migrate_github_repository_test.go | 33 ++-- github/resource_github_repository.go | 8 +- 3 files changed, 221 insertions(+), 38 deletions(-) diff --git a/github/migrate_github_repository.go b/github/migrate_github_repository.go index 098b7ecd43..a5a570461d 100644 --- a/github/migrate_github_repository.go +++ b/github/migrate_github_repository.go @@ -1,40 +1,216 @@ package github import ( - "fmt" + "context" "log" "strings" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func resourceGithubRepositoryMigrateState(v int, is *terraform.InstanceState, meta any) (*terraform.InstanceState, error) { - switch v { - case 0: - log.Printf("[INFO] Found GitHub Repository State v0; migrating to v1") - return migrateGithubRepositoryStateV0toV1(is) - default: - return is, fmt.Errorf("unexpected schema version: %d", v) - } -} +func resourceGithubRepositoryResourceV0() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "full_name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"name"}, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ConflictsWith: []string{"full_name"}, + }, + "only_protected_branches": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, -func migrateGithubRepositoryStateV0toV1(is *terraform.InstanceState) (*terraform.InstanceState, error) { - if is.Empty() { - log.Printf("[DEBUG] Empty InstanceState; nothing to migrate.") - return is, nil + "description": { + Type: schema.TypeString, + Default: nil, + Optional: true, + }, + "homepage_url": { + Type: schema.TypeString, + Default: "", + Optional: true, + }, + "private": { + Type: schema.TypeBool, + Computed: true, + }, + "visibility": { + Type: schema.TypeString, + Computed: true, + }, + "has_issues": { + Type: schema.TypeBool, + Computed: true, + }, + "has_projects": { + Type: schema.TypeBool, + Computed: true, + }, + "has_downloads": { + Type: schema.TypeBool, + Computed: true, + }, + "has_wiki": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_merge_commit": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_squash_merge": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_rebase_merge": { + Type: schema.TypeBool, + Computed: true, + }, + "allow_auto_merge": { + Type: schema.TypeBool, + Computed: true, + }, + "squash_merge_commit_title": { + Type: schema.TypeString, + Computed: true, + }, + "squash_merge_commit_message": { + Type: schema.TypeString, + Computed: true, + }, + "merge_commit_title": { + Type: schema.TypeString, + Computed: true, + }, + "merge_commit_message": { + Type: schema.TypeString, + Computed: true, + }, + "default_branch": { + Type: schema.TypeString, + Computed: true, + }, + "archived": { + Type: schema.TypeBool, + Computed: true, + }, + "branches": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "protected": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "pages": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "source": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "branch": { + Type: schema.TypeString, + Computed: true, + }, + "path": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "cname": { + Type: schema.TypeString, + Computed: true, + }, + "custom_404": { + Type: schema.TypeBool, + Computed: true, + }, + "html_url": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "url": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "topics": { + Type: schema.TypeList, + Elem: &schema.Schema{Type: schema.TypeString}, + Computed: true, + }, + "html_url": { + Type: schema.TypeString, + Computed: true, + }, + "ssh_clone_url": { + Type: schema.TypeString, + Computed: true, + }, + "svn_url": { + Type: schema.TypeString, + Computed: true, + }, + "git_clone_url": { + Type: schema.TypeString, + Computed: true, + }, + "http_clone_url": { + Type: schema.TypeString, + Computed: true, + }, + "node_id": { + Type: schema.TypeString, + Computed: true, + }, + "repo_id": { + Type: schema.TypeInt, + Computed: true, + }, + }, } +} - log.Printf("[DEBUG] GitHub Repository Attributes before migration: %#v", is.Attributes) +func resourceGithubRepositoryInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Repository State before migration: %#v", rawState) prefix := "branches." - for k := range is.Attributes { + for k := range rawState { if strings.HasPrefix(k, prefix) { - delete(is.Attributes, k) + delete(rawState, k) } } - log.Printf("[DEBUG] GitHub Repository Attributes after State Migration: %#v", is.Attributes) - - return is, nil + log.Printf("[DEBUG] GitHub Repository State after migration: %#v", rawState) + return rawState, nil } diff --git a/github/migrate_github_repository_test.go b/github/migrate_github_repository_test.go index 5c416889db..2d7effb10a 100644 --- a/github/migrate_github_repository_test.go +++ b/github/migrate_github_repository_test.go @@ -3,28 +3,29 @@ package github import ( "reflect" "testing" - - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func TestMigrateGithubRepositoryStateV0toV1(t *testing.T) { - oldAttributes := map[string]string{ +func testResourceGithubRepositoryInstanceStateDataV0() map[string]any { + return map[string]any{ "branches.#": "1", "branches.0.name": "foobar", "branches.0.protected": "false", } +} - newState, err := migrateGithubRepositoryStateV0toV1(&terraform.InstanceState{ - ID: "nonempty", - Attributes: oldAttributes, - }) - if err != nil { - t.Fatal(err) - } +func testResourceGithubRepositoryInstanceStateDataV1() map[string]any { + return map[string]any{} +} - expectedAttributes := map[string]string{} - if !reflect.DeepEqual(newState.Attributes, expectedAttributes) { - t.Fatalf("Expected attributes:\n%#v\n\nGiven:\n%#v\n", - expectedAttributes, newState.Attributes) - } +func TestGithub_MigrateRepositoryStateV0toV1(t *testing.T) { + t.Run("migrates state without errors", func(t *testing.T) { + expected := testResourceGithubRepositoryInstanceStateDataV1() + actual, err := resourceGithubRepositoryInstanceStateUpgradeV0(t.Context(), testResourceGithubRepositoryInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) } diff --git a/github/resource_github_repository.go b/github/resource_github_repository.go index 4d69afbb15..9540f83eff 100644 --- a/github/resource_github_repository.go +++ b/github/resource_github_repository.go @@ -32,7 +32,13 @@ func resourceGithubRepository() *schema.Resource { }, SchemaVersion: 1, - MigrateState: resourceGithubRepositoryMigrateState, + StateUpgraders: []schema.StateUpgrader{ + { + Type: resourceGithubRepositoryResourceV0().CoreConfigSchema().ImpliedType(), + Upgrade: resourceGithubRepositoryInstanceStateUpgradeV0, + Version: 0, + }, + }, Schema: map[string]*schema.Schema{ "name": { From f67519f2a7d0761d1a9c7979f1be751d2ee02501 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Thu, 8 Jan 2026 22:43:41 +0200 Subject: [PATCH 05/16] Restructure github_actions_secret state migration files Signed-off-by: Timo Sand --- github/migrate_github_actions_secret_test.go | 55 ------------------- github/resource_github_actions_secret.go | 12 ++++ ...source_github_actions_secret_schema_v0.go} | 15 ----- github/resource_github_actions_secret_test.go | 50 +++++++++++++++++ 4 files changed, 62 insertions(+), 70 deletions(-) delete mode 100644 github/migrate_github_actions_secret_test.go rename github/{migrate_github_actions_secret.go => resource_github_actions_secret_schema_v0.go} (73%) diff --git a/github/migrate_github_actions_secret_test.go b/github/migrate_github_actions_secret_test.go deleted file mode 100644 index 7729d0191a..0000000000 --- a/github/migrate_github_actions_secret_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package github - -import ( - "reflect" - "testing" -) - -func testResourceGithubActionsSecretInstanceStateDataV0() map[string]any { - return map[string]any{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - } -} - -func testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() map[string]any { - v0 := testResourceGithubActionsSecretInstanceStateDataV0() - v0["destroy_on_drift"] = false - return v0 -} - -func testResourceGithubActionsSecretInstanceStateDataV1() map[string]any { - v0 := testResourceGithubActionsSecretInstanceStateDataV0() - v0["destroy_on_drift"] = true - return v0 -} - -func TestGithub_MigrateActionsSecretStateV0toV1(t *testing.T) { - t.Run("without destroy_on_drift", func(t *testing.T) { - expected := testResourceGithubActionsSecretInstanceStateDataV1() - actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0(), nil) - if err != nil { - t.Fatalf("error migrating state: %s", err) - } - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) - } - }) - - t.Run("with destroy_on_drift", func(t *testing.T) { - expected := testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() - actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0_WithDrift(), nil) - if err != nil { - t.Fatalf("error migrating state: %s", err) - } - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) - } - }) -} diff --git a/github/resource_github_actions_secret.go b/github/resource_github_actions_secret.go index 137116c808..789b6e7054 100644 --- a/github/resource_github_actions_secret.go +++ b/github/resource_github_actions_secret.go @@ -292,3 +292,15 @@ func encryptPlaintext(plaintext, publicKeyB64 string) ([]byte, error) { return cipherText, nil } + +func resourceGithubActionsSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Secret State before migration: %#v", rawState) + // Add the destroy_on_drift field with default value true if it doesn't exist + if _, ok := rawState["destroy_on_drift"]; !ok { + rawState["destroy_on_drift"] = true + } + + log.Printf("[DEBUG] GitHub Actions Secret State after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/migrate_github_actions_secret.go b/github/resource_github_actions_secret_schema_v0.go similarity index 73% rename from github/migrate_github_actions_secret.go rename to github/resource_github_actions_secret_schema_v0.go index 5be1ce20bb..8035226177 100644 --- a/github/migrate_github_actions_secret.go +++ b/github/resource_github_actions_secret_schema_v0.go @@ -1,9 +1,6 @@ package github import ( - "context" - "log" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -52,15 +49,3 @@ func resourceGithubActionsSecretResourceV0() *schema.Resource { }, } } - -func resourceGithubActionsSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { - log.Printf("[DEBUG] GitHub Actions Secret State before migration: %#v", rawState) - // Add the destroy_on_drift field with default value true if it doesn't exist - if _, ok := rawState["destroy_on_drift"]; !ok { - rawState["destroy_on_drift"] = true - } - - log.Printf("[DEBUG] GitHub Actions Secret State after migration: %#v", rawState) - - return rawState, nil -} diff --git a/github/resource_github_actions_secret_test.go b/github/resource_github_actions_secret_test.go index 3ea8ef697f..928be8a78e 100644 --- a/github/resource_github_actions_secret_test.go +++ b/github/resource_github_actions_secret_test.go @@ -3,6 +3,7 @@ package github import ( "encoding/base64" "fmt" + "reflect" "strings" "testing" @@ -557,3 +558,52 @@ func TestGithubActionsSecretIssue964Solution(t *testing.T) { t.Logf("SUCCESS: Issue #964 solved - secret with destroy_on_drift=false does not get recreated on external changes") }) } + +func testResourceGithubActionsSecretInstanceStateDataV0() map[string]any { + return map[string]any{ + "id": "test-secret", + "repository": "test-repo", + "secret_name": "test-secret", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + "plaintext_value": "secret-value", + } +} + +func testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = false + return v0 +} + +func testResourceGithubActionsSecretInstanceStateDataV1() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = true + return v0 +} + +func TestGithub_MigrateActionsSecretStateV0toV1(t *testing.T) { + t.Run("without destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV1() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) + + t.Run("with destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0_WithDrift(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} From b0ad87ed005d5b046a7e06bca8b09f5a9caa2775 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:07:04 +0200 Subject: [PATCH 06/16] Rename to new naming scheme `_migration.go` Signed-off-by: Timo Sand --- ...t_schema_v0.go => resource_github_actions_secret_migration.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename github/{resource_github_actions_secret_schema_v0.go => resource_github_actions_secret_migration.go} (100%) diff --git a/github/resource_github_actions_secret_schema_v0.go b/github/resource_github_actions_secret_migration.go similarity index 100% rename from github/resource_github_actions_secret_schema_v0.go rename to github/resource_github_actions_secret_migration.go From 32395ce54c2bbb8ad7d354a32f9ddb916fc66531 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:10:50 +0200 Subject: [PATCH 07/16] Mmove migration code to _migration.go file and exxtract tests to _migartion_test.go file Signed-off-by: Timo Sand --- github/resource_github_actions_secret.go | 12 ---- ...esource_github_actions_secret_migration.go | 15 +++++ ...ce_github_actions_secret_migration_test.go | 55 +++++++++++++++++++ github/resource_github_actions_secret_test.go | 50 ----------------- 4 files changed, 70 insertions(+), 62 deletions(-) create mode 100644 github/resource_github_actions_secret_migration_test.go diff --git a/github/resource_github_actions_secret.go b/github/resource_github_actions_secret.go index 789b6e7054..137116c808 100644 --- a/github/resource_github_actions_secret.go +++ b/github/resource_github_actions_secret.go @@ -292,15 +292,3 @@ func encryptPlaintext(plaintext, publicKeyB64 string) ([]byte, error) { return cipherText, nil } - -func resourceGithubActionsSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { - log.Printf("[DEBUG] GitHub Actions Secret State before migration: %#v", rawState) - // Add the destroy_on_drift field with default value true if it doesn't exist - if _, ok := rawState["destroy_on_drift"]; !ok { - rawState["destroy_on_drift"] = true - } - - log.Printf("[DEBUG] GitHub Actions Secret State after migration: %#v", rawState) - - return rawState, nil -} diff --git a/github/resource_github_actions_secret_migration.go b/github/resource_github_actions_secret_migration.go index 8035226177..5be1ce20bb 100644 --- a/github/resource_github_actions_secret_migration.go +++ b/github/resource_github_actions_secret_migration.go @@ -1,6 +1,9 @@ package github import ( + "context" + "log" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -49,3 +52,15 @@ func resourceGithubActionsSecretResourceV0() *schema.Resource { }, } } + +func resourceGithubActionsSecretInstanceStateUpgradeV0(ctx context.Context, rawState map[string]any, meta any) (map[string]any, error) { + log.Printf("[DEBUG] GitHub Actions Secret State before migration: %#v", rawState) + // Add the destroy_on_drift field with default value true if it doesn't exist + if _, ok := rawState["destroy_on_drift"]; !ok { + rawState["destroy_on_drift"] = true + } + + log.Printf("[DEBUG] GitHub Actions Secret State after migration: %#v", rawState) + + return rawState, nil +} diff --git a/github/resource_github_actions_secret_migration_test.go b/github/resource_github_actions_secret_migration_test.go new file mode 100644 index 0000000000..7729d0191a --- /dev/null +++ b/github/resource_github_actions_secret_migration_test.go @@ -0,0 +1,55 @@ +package github + +import ( + "reflect" + "testing" +) + +func testResourceGithubActionsSecretInstanceStateDataV0() map[string]any { + return map[string]any{ + "id": "test-secret", + "repository": "test-repo", + "secret_name": "test-secret", + "created_at": "2023-01-01T00:00:00Z", + "updated_at": "2023-01-01T00:00:00Z", + "plaintext_value": "secret-value", + } +} + +func testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = false + return v0 +} + +func testResourceGithubActionsSecretInstanceStateDataV1() map[string]any { + v0 := testResourceGithubActionsSecretInstanceStateDataV0() + v0["destroy_on_drift"] = true + return v0 +} + +func TestGithub_MigrateActionsSecretStateV0toV1(t *testing.T) { + t.Run("without destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV1() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) + + t.Run("with destroy_on_drift", func(t *testing.T) { + expected := testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() + actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0_WithDrift(), nil) + if err != nil { + t.Fatalf("error migrating state: %s", err) + } + + if !reflect.DeepEqual(expected, actual) { + t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) + } + }) +} diff --git a/github/resource_github_actions_secret_test.go b/github/resource_github_actions_secret_test.go index 928be8a78e..3ea8ef697f 100644 --- a/github/resource_github_actions_secret_test.go +++ b/github/resource_github_actions_secret_test.go @@ -3,7 +3,6 @@ package github import ( "encoding/base64" "fmt" - "reflect" "strings" "testing" @@ -558,52 +557,3 @@ func TestGithubActionsSecretIssue964Solution(t *testing.T) { t.Logf("SUCCESS: Issue #964 solved - secret with destroy_on_drift=false does not get recreated on external changes") }) } - -func testResourceGithubActionsSecretInstanceStateDataV0() map[string]any { - return map[string]any{ - "id": "test-secret", - "repository": "test-repo", - "secret_name": "test-secret", - "created_at": "2023-01-01T00:00:00Z", - "updated_at": "2023-01-01T00:00:00Z", - "plaintext_value": "secret-value", - } -} - -func testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() map[string]any { - v0 := testResourceGithubActionsSecretInstanceStateDataV0() - v0["destroy_on_drift"] = false - return v0 -} - -func testResourceGithubActionsSecretInstanceStateDataV1() map[string]any { - v0 := testResourceGithubActionsSecretInstanceStateDataV0() - v0["destroy_on_drift"] = true - return v0 -} - -func TestGithub_MigrateActionsSecretStateV0toV1(t *testing.T) { - t.Run("without destroy_on_drift", func(t *testing.T) { - expected := testResourceGithubActionsSecretInstanceStateDataV1() - actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0(), nil) - if err != nil { - t.Fatalf("error migrating state: %s", err) - } - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) - } - }) - - t.Run("with destroy_on_drift", func(t *testing.T) { - expected := testResourceGithubActionsSecretInstanceStateDataV0_WithDrift() - actual, err := resourceGithubActionsSecretInstanceStateUpgradeV0(t.Context(), testResourceGithubActionsSecretInstanceStateDataV0_WithDrift(), nil) - if err != nil { - t.Fatalf("error migrating state: %s", err) - } - - if !reflect.DeepEqual(expected, actual) { - t.Fatalf("\n\nexpected:\n\n%#v\n\ngot:\n\n%#v\n\n", expected, actual) - } - }) -} From 653fcde2aecdd9bf7dc474945bc75c34ec777985 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:16:55 +0200 Subject: [PATCH 08/16] refactor(github): rename actions organization secret migration files Rename migrate_github_actions_organization_secret*.go to resource_github_actions_organization_secret_migration*.go for consistent naming with resource files. --- ...o => resource_github_actions_organization_secret_migration.go} | 0 ...resource_github_actions_organization_secret_migration_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename github/{migrate_github_actions_organization_secret.go => resource_github_actions_organization_secret_migration.go} (100%) rename github/{migrate_github_actions_organization_secret_test.go => resource_github_actions_organization_secret_migration_test.go} (100%) diff --git a/github/migrate_github_actions_organization_secret.go b/github/resource_github_actions_organization_secret_migration.go similarity index 100% rename from github/migrate_github_actions_organization_secret.go rename to github/resource_github_actions_organization_secret_migration.go diff --git a/github/migrate_github_actions_organization_secret_test.go b/github/resource_github_actions_organization_secret_migration_test.go similarity index 100% rename from github/migrate_github_actions_organization_secret_test.go rename to github/resource_github_actions_organization_secret_migration_test.go From cd67a89a94355fa89e7734d1b13a8dd5eaf2df55 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:18:30 +0200 Subject: [PATCH 09/16] refactor(github): rename branch protection migration file Rename migrate_github_branch_protection.go to resource_github_branch_protection_migration.go for consistent naming with resource files. --- ...otection.go => resource_github_branch_protection_migration.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename github/{migrate_github_branch_protection.go => resource_github_branch_protection_migration.go} (100%) diff --git a/github/migrate_github_branch_protection.go b/github/resource_github_branch_protection_migration.go similarity index 100% rename from github/migrate_github_branch_protection.go rename to github/resource_github_branch_protection_migration.go From a314f3cb35571f9a8a53e5e485769770dc28e82f Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:19:22 +0200 Subject: [PATCH 10/16] refactor(github): rename organization webhook migration files Rename migrate_github_organization_webhook*.go to resource_github_organization_webhook_migration*.go for consistent naming with resource files. --- ...bhook.go => resource_github_organization_webhook_migration.go} | 0 ....go => resource_github_organization_webhook_migration_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename github/{migrate_github_organization_webhook.go => resource_github_organization_webhook_migration.go} (100%) rename github/{migrate_github_organization_webhook_test.go => resource_github_organization_webhook_migration_test.go} (100%) diff --git a/github/migrate_github_organization_webhook.go b/github/resource_github_organization_webhook_migration.go similarity index 100% rename from github/migrate_github_organization_webhook.go rename to github/resource_github_organization_webhook_migration.go diff --git a/github/migrate_github_organization_webhook_test.go b/github/resource_github_organization_webhook_migration_test.go similarity index 100% rename from github/migrate_github_organization_webhook_test.go rename to github/resource_github_organization_webhook_migration_test.go From 6e7061ac1828a722e3d01fced4564bccce71c472 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:21:26 +0200 Subject: [PATCH 11/16] refactor(github): rename repository migration files Rename migrate_github_repository*.go to resource_github_repository_migration*.go for consistent naming with resource files. --- ...thub_repository.go => resource_github_repository_migration.go} | 0 ...itory_test.go => resource_github_repository_migration_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename github/{migrate_github_repository.go => resource_github_repository_migration.go} (100%) rename github/{migrate_github_repository_test.go => resource_github_repository_migration_test.go} (100%) diff --git a/github/migrate_github_repository.go b/github/resource_github_repository_migration.go similarity index 100% rename from github/migrate_github_repository.go rename to github/resource_github_repository_migration.go diff --git a/github/migrate_github_repository_test.go b/github/resource_github_repository_migration_test.go similarity index 100% rename from github/migrate_github_repository_test.go rename to github/resource_github_repository_migration_test.go From cb29e9a5d79fe4ba0ec33a666cb8c269594eefa6 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:23:18 +0200 Subject: [PATCH 12/16] refactor(github): rename repository webhook migration files Rename migrate_github_repository_webhook*.go to resource_github_repository_webhook_migration*.go for consistent naming with resource files. --- ...webhook.go => resource_github_repository_webhook_migration.go} | 0 ...st.go => resource_github_repository_webhook_migration_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename github/{migrate_github_repository_webhook.go => resource_github_repository_webhook_migration.go} (100%) rename github/{migrate_github_repository_webhook_test.go => resource_github_repository_webhook_migration_test.go} (100%) diff --git a/github/migrate_github_repository_webhook.go b/github/resource_github_repository_webhook_migration.go similarity index 100% rename from github/migrate_github_repository_webhook.go rename to github/resource_github_repository_webhook_migration.go diff --git a/github/migrate_github_repository_webhook_test.go b/github/resource_github_repository_webhook_migration_test.go similarity index 100% rename from github/migrate_github_repository_webhook_test.go rename to github/resource_github_repository_webhook_migration_test.go From f81dd66339401be7bd2c0cecd93845a2c4be9645 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:43:27 +0200 Subject: [PATCH 13/16] refactor(github): migrate actions secret to context-aware CRUD Migrate resource_github_actions_secret.go from legacy CRUD functions to Context-aware functions (CreateContext, ReadContext, DeleteContext). - Add diag import - Update function signatures to accept context.Context - Return diag.Diagnostics instead of error - Use StateContext for importer Ref: #2996 --- github/resource_github_actions_secret.go | 50 ++++++++++++------------ 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/github/resource_github_actions_secret.go b/github/resource_github_actions_secret.go index 137116c808..7f82291a22 100644 --- a/github/resource_github_actions_secret.go +++ b/github/resource_github_actions_secret.go @@ -10,17 +10,18 @@ import ( "strings" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "golang.org/x/crypto/nacl/box" ) func resourceGithubActionsSecret() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsSecretCreateOrUpdate, - Read: resourceGithubActionsSecretRead, - Delete: resourceGithubActionsSecretDelete, + CreateContext: resourceGithubActionsSecretCreateOrUpdate, + ReadContext: resourceGithubActionsSecretRead, + DeleteContext: resourceGithubActionsSecretDelete, Importer: &schema.ResourceImporter{ - State: resourceGithubActionsSecretImport, + StateContext: resourceGithubActionsSecretImport, }, // Schema migration added to handle the addition of destroy_on_drift field @@ -85,10 +86,9 @@ func resourceGithubActionsSecret() *schema.Resource { } } -func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsSecretCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() repo := d.Get("repository").(string) secretName := d.Get("secret_name").(string) @@ -97,7 +97,7 @@ func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) keyId, publicKey, err := getPublicKeyDetails(owner, repo, meta) if err != nil { - return err + return diag.FromErr(err) } if encryptedText, ok := d.GetOk("encrypted_value"); ok { @@ -105,7 +105,7 @@ func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) } else { encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey) if err != nil { - return err + return diag.FromErr(err) } encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) } @@ -119,21 +119,20 @@ func resourceGithubActionsSecretCreateOrUpdate(d *schema.ResourceData, meta any) _, err = client.Actions.CreateOrUpdateRepoSecret(ctx, owner, repo, eSecret) if err != nil { - return err + return diag.FromErr(err) } d.SetId(buildTwoPartID(repo, secretName)) - return resourceGithubActionsSecretRead(d, meta) + return resourceGithubActionsSecretRead(ctx, d, meta) } -func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error { +func resourceGithubActionsSecretRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() repoName, secretName, err := parseTwoPartID(d.Id(), "repository", "secret_name") if err != nil { - return err + return diag.FromErr(err) } secret, _, err := client.Actions.GetRepoSecret(ctx, owner, repoName, secretName) @@ -147,11 +146,11 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error { return nil } } - return err + return diag.FromErr(err) } if err = d.Set("created_at", secret.CreatedAt.String()); err != nil { - return err + return diag.FromErr(err) } // This is a drift detection mechanism based on timestamps. @@ -179,48 +178,47 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error { // Alternative approach: set sensitive values to empty to trigger update plan // This tells Terraform that the current state is unknown and needs reconciliation if err = d.Set("encrypted_value", ""); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", ""); err != nil { - return err + return diag.FromErr(err) } log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update") } } else { // No drift detected, preserve the configured values in state if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil { - return err + return diag.FromErr(err) } } // Always update the timestamp to prevent repeated drift detection if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubActionsSecretDelete(d *schema.ResourceData, meta any) error { +func resourceGithubActionsSecretDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) repoName, secretName, err := parseTwoPartID(d.Id(), "repository", "secret_name") if err != nil { - return err + return diag.FromErr(err) } _, err = client.Actions.DeleteRepoSecret(ctx, orgName, repoName, secretName) - return err + return diag.FromErr(err) } -func resourceGithubActionsSecretImport(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { +func resourceGithubActionsSecretImport(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() parts := strings.Split(d.Id(), "/") if len(parts) != 2 { From 055b0ff7052940ba7ed742621128a97c20aa3ba0 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:47:06 +0200 Subject: [PATCH 14/16] refactor(actions-org-secret): migrate to context-aware CRUD functions Migrate resource_github_actions_organization_secret.go from legacy CRUD to context-aware CRUD functions (CreateContext, ReadContext, DeleteContext) per Terraform Plugin SDK v2 best practices. Ref: https://github.com/integrations/terraform-provider-github/issues/2996 --- ...urce_github_actions_organization_secret.go | 52 +++++++++---------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/github/resource_github_actions_organization_secret.go b/github/resource_github_actions_organization_secret.go index 0f0cc4a99b..becfdbbc9d 100644 --- a/github/resource_github_actions_organization_secret.go +++ b/github/resource_github_actions_organization_secret.go @@ -4,22 +4,22 @@ import ( "context" "encoding/base64" "errors" - "fmt" "log" "net/http" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func resourceGithubActionsOrganizationSecret() *schema.Resource { return &schema.Resource{ - Create: resourceGithubActionsOrganizationSecretCreateOrUpdate, - Read: resourceGithubActionsOrganizationSecretRead, - Delete: resourceGithubActionsOrganizationSecretDelete, + CreateContext: resourceGithubActionsOrganizationSecretCreateOrUpdate, + ReadContext: resourceGithubActionsOrganizationSecretRead, + DeleteContext: resourceGithubActionsOrganizationSecretDelete, Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { if err := d.Set("secret_name", d.Id()); err != nil { return nil, err } @@ -104,10 +104,9 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource { } } -func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubActionsOrganizationSecretCreateOrUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() secretName := d.Get("secret_name").(string) plaintextValue := d.Get("plaintext_value").(string) @@ -117,7 +116,7 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat selectedRepositories, hasSelectedRepositories := d.GetOk("selected_repository_ids") if visibility != "selected" && hasSelectedRepositories { - return fmt.Errorf("cannot use selected_repository_ids without visibility being set to selected") + return diag.Errorf("cannot use selected_repository_ids without visibility being set to selected") } selectedRepositoryIDs := []int64{} @@ -132,7 +131,7 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat keyId, publicKey, err := getOrganizationPublicKeyDetails(owner, meta) if err != nil { - return err + return diag.FromErr(err) } if encryptedText, ok := d.GetOk("encrypted_value"); ok { @@ -140,7 +139,7 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat } else { encryptedBytes, err := encryptPlaintext(plaintextValue, publicKey) if err != nil { - return err + return diag.FromErr(err) } encryptedValue = base64.StdEncoding.EncodeToString(encryptedBytes) } @@ -156,17 +155,16 @@ func resourceGithubActionsOrganizationSecretCreateOrUpdate(d *schema.ResourceDat _, err = client.Actions.CreateOrUpdateOrgSecret(ctx, owner, eSecret) if err != nil { - return err + return diag.FromErr(err) } d.SetId(secretName) - return resourceGithubActionsOrganizationSecretRead(d, meta) + return resourceGithubActionsOrganizationSecretRead(ctx, d, meta) } -func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta any) error { +func resourceGithubActionsOrganizationSecretRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name - ctx := context.Background() secret, _, err := client.Actions.GetOrgSecret(ctx, owner, d.Id()) if err != nil { @@ -179,14 +177,14 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an return nil } } - return err + return diag.FromErr(err) } if err = d.Set("created_at", secret.CreatedAt.String()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("visibility", secret.Visibility); err != nil { - return err + return diag.FromErr(err) } selectedRepositoryIDs := []int64{} @@ -198,7 +196,7 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an for { results, resp, err := client.Actions.ListSelectedReposForOrgSecret(ctx, owner, d.Id(), opt) if err != nil { - return err + return diag.FromErr(err) } for _, repo := range results.Repositories { @@ -213,7 +211,7 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an } if err = d.Set("selected_repository_ids", selectedRepositoryIDs); err != nil { - return err + return diag.FromErr(err) } // This is a drift detection mechanism based on timestamps. @@ -241,39 +239,39 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an // Alternative approach: set sensitive values to empty to trigger update plan // This tells Terraform that the current state is unknown and needs reconciliation if err = d.Set("encrypted_value", ""); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", ""); err != nil { - return err + return diag.FromErr(err) } log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update") } } else { // No drift detected, preserve the configured values in state if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil { - return err + return diag.FromErr(err) } } // Always update the timestamp to prevent repeated drift detection if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubActionsOrganizationSecretDelete(d *schema.ResourceData, meta any) error { +func resourceGithubActionsOrganizationSecretDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client orgName := meta.(*Owner).name - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) log.Printf("[INFO] Deleting secret: %s", d.Id()) _, err := client.Actions.DeleteOrgSecret(ctx, orgName, d.Id()) - return err + return diag.FromErr(err) } func getOrganizationPublicKeyDetails(owner string, meta any) (keyId, pkValue string, err error) { From 21780c885649f177766f7e9c4ddc609de1fdc6b6 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:50:22 +0200 Subject: [PATCH 15/16] refactor(org-webhook): migrate to context-aware CRUD functions Migrate resource_github_organization_webhook.go from legacy CRUD to context-aware CRUD functions (CreateContext, ReadContext, UpdateContext, DeleteContext) per Terraform Plugin SDK v2 best practices. Ref: https://github.com/integrations/terraform-provider-github/issues/2996 --- .../resource_github_organization_webhook.go | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/github/resource_github_organization_webhook.go b/github/resource_github_organization_webhook.go index 9b20168dec..5c6dfa65e7 100644 --- a/github/resource_github_organization_webhook.go +++ b/github/resource_github_organization_webhook.go @@ -8,15 +8,16 @@ import ( "strconv" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubOrganizationWebhook() *schema.Resource { return &schema.Resource{ - Create: resourceGithubOrganizationWebhookCreate, - Read: resourceGithubOrganizationWebhookRead, - Update: resourceGithubOrganizationWebhookUpdate, - Delete: resourceGithubOrganizationWebhookDelete, + CreateContext: resourceGithubOrganizationWebhookCreate, + ReadContext: resourceGithubOrganizationWebhookRead, + UpdateContext: resourceGithubOrganizationWebhookUpdate, + DeleteContext: resourceGithubOrganizationWebhookDelete, Importer: &schema.ResourceImporter{ StateContext: schema.ImportStatePassthroughContext, }, @@ -79,21 +80,20 @@ func resourceGithubOrganizationWebhookObject(d *schema.ResourceData) *github.Hoo return hook } -func resourceGithubOrganizationWebhookCreate(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookCreate(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 webhookObj := resourceGithubOrganizationWebhookObject(d) - ctx := context.Background() hook, _, err := client.Organizations.CreateHook(ctx, orgName, webhookObj) if err != nil { - return err + return diag.FromErr(err) } d.SetId(strconv.FormatInt(hook.GetID(), 10)) @@ -105,16 +105,16 @@ func resourceGithubOrganizationWebhookCreate(d *schema.ResourceData, meta any) e } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } - return resourceGithubOrganizationWebhookRead(d, meta) + return resourceGithubOrganizationWebhookRead(ctx, d, meta) } -func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookRead(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 @@ -122,9 +122,9 @@ func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) err orgName := meta.(*Owner).name hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -143,20 +143,20 @@ func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) err return nil } } - return err + return diag.FromErr(err) } if err = d.Set("etag", resp.Header.Get("ETag")); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("url", hook.GetURL()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("active", hook.GetActive()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("events", hook.Events); err != nil { - return err + return diag.FromErr(err) } // GitHub returns the secret as a string of 8 astrisks "********" @@ -172,16 +172,16 @@ func resourceGithubOrganizationWebhookRead(d *schema.ResourceData, meta any) err } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubOrganizationWebhookUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookUpdate(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 @@ -190,23 +190,23 @@ func resourceGithubOrganizationWebhookUpdate(d *schema.ResourceData, meta any) e webhookObj := resourceGithubOrganizationWebhookObject(d) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, _, err = client.Organizations.EditHook(ctx, orgName, hookID, webhookObj) if err != nil { - return err + return diag.FromErr(err) } - return resourceGithubOrganizationWebhookRead(d, meta) + return resourceGithubOrganizationWebhookRead(ctx, d, meta) } -func resourceGithubOrganizationWebhookDelete(d *schema.ResourceData, meta any) error { +func resourceGithubOrganizationWebhookDelete(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 @@ -214,12 +214,12 @@ func resourceGithubOrganizationWebhookDelete(d *schema.ResourceData, meta any) e orgName := meta.(*Owner).name hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, err = client.Organizations.DeleteHook(ctx, orgName, hookID) - return err + return diag.FromErr(err) } func webhookConfigFromInterface(config map[string]any) *github.HookConfig { From 8b6a21f86412338c074863c2f3342af60f57d757 Mon Sep 17 00:00:00 2001 From: Timo Sand Date: Sat, 10 Jan 2026 22:53:59 +0200 Subject: [PATCH 16/16] refactor(repo-webhook): migrate to context-aware CRUD functions Migrate resource_github_repository_webhook.go from legacy CRUD to context-aware CRUD functions (CreateContext, ReadContext, UpdateContext, DeleteContext) per Terraform Plugin SDK v2 best practices. Ref: https://github.com/integrations/terraform-provider-github/issues/2996 --- github/resource_github_repository_webhook.go | 54 ++++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/github/resource_github_repository_webhook.go b/github/resource_github_repository_webhook.go index e7db374412..db105e35ae 100644 --- a/github/resource_github_repository_webhook.go +++ b/github/resource_github_repository_webhook.go @@ -10,17 +10,18 @@ import ( "strings" "github.com/google/go-github/v81/github" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) func resourceGithubRepositoryWebhook() *schema.Resource { return &schema.Resource{ - Create: resourceGithubRepositoryWebhookCreate, - Read: resourceGithubRepositoryWebhookRead, - Update: resourceGithubRepositoryWebhookUpdate, - Delete: resourceGithubRepositoryWebhookDelete, + CreateContext: resourceGithubRepositoryWebhookCreate, + ReadContext: resourceGithubRepositoryWebhookRead, + UpdateContext: resourceGithubRepositoryWebhookUpdate, + DeleteContext: resourceGithubRepositoryWebhookDelete, Importer: &schema.ResourceImporter{ - State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { + StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { parts := strings.Split(d.Id(), "/") if len(parts) != 2 { return nil, fmt.Errorf("invalid ID specified: supplied ID must be written as /") @@ -104,17 +105,16 @@ func resourceGithubRepositoryWebhookObject(d *schema.ResourceData) *github.Hook return hook } -func resourceGithubRepositoryWebhookCreate(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) hk := resourceGithubRepositoryWebhookObject(d) - ctx := context.Background() hook, _, err := client.Repositories.CreateHook(ctx, owner, repoName, hk) if err != nil { - return err + return diag.FromErr(err) } d.SetId(strconv.FormatInt(hook.GetID(), 10)) @@ -126,22 +126,22 @@ func resourceGithubRepositoryWebhookCreate(d *schema.ResourceData, meta any) err } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } - return resourceGithubRepositoryWebhookRead(d, meta) + return resourceGithubRepositoryWebhookRead(ctx, d, meta) } -func resourceGithubRepositoryWebhookRead(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) if !d.IsNewResource() { ctx = context.WithValue(ctx, ctxEtag, d.Get("etag").(string)) } @@ -160,16 +160,16 @@ func resourceGithubRepositoryWebhookRead(d *schema.ResourceData, meta any) error return nil } } - return err + return diag.FromErr(err) } if err = d.Set("url", hook.GetURL()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("active", hook.GetActive()); err != nil { - return err + return diag.FromErr(err) } if err = d.Set("events", hook.Events); err != nil { - return err + return diag.FromErr(err) } // GitHub returns the secret as a string of 8 astrisks "********" @@ -185,13 +185,13 @@ func resourceGithubRepositoryWebhookRead(d *schema.ResourceData, meta any) error } if err = d.Set("configuration", interfaceFromWebhookConfig(hook.Config)); err != nil { - return err + return diag.FromErr(err) } return nil } -func resourceGithubRepositoryWebhookUpdate(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name @@ -199,29 +199,29 @@ func resourceGithubRepositoryWebhookUpdate(d *schema.ResourceData, meta any) err hk := resourceGithubRepositoryWebhookObject(d) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, _, err = client.Repositories.EditHook(ctx, owner, repoName, hookID, hk) if err != nil { - return err + return diag.FromErr(err) } - return resourceGithubRepositoryWebhookRead(d, meta) + return resourceGithubRepositoryWebhookRead(ctx, d, meta) } -func resourceGithubRepositoryWebhookDelete(d *schema.ResourceData, meta any) error { +func resourceGithubRepositoryWebhookDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { client := meta.(*Owner).v3client owner := meta.(*Owner).name repoName := d.Get("repository").(string) hookID, err := strconv.ParseInt(d.Id(), 10, 64) if err != nil { - return unconvertibleIdErr(d.Id(), err) + return diag.FromErr(unconvertibleIdErr(d.Id(), err)) } - ctx := context.WithValue(context.Background(), ctxId, d.Id()) + ctx = context.WithValue(ctx, ctxId, d.Id()) _, err = client.Repositories.DeleteHook(ctx, owner, repoName, hookID) - return handleArchivedRepoDelete(err, "repository webhook", d.Id(), owner, repoName) + return diag.FromErr(handleArchivedRepoDelete(err, "repository webhook", d.Id(), owner, repoName)) }