Skip to content

Commit 974120c

Browse files
committed
Merge v6.7.5 fixes into v6.8.1 patch release
2 parents c453326 + c853213 commit 974120c

5 files changed

Lines changed: 295 additions & 124 deletions

github/resource_github_actions_organization_secret.go

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
1616
return &schema.Resource{
1717
Create: resourceGithubActionsOrganizationSecretCreateOrUpdate,
1818
Read: resourceGithubActionsOrganizationSecretRead,
19-
Update: resourceGithubActionsOrganizationSecretCreateOrUpdate,
2019
Delete: resourceGithubActionsOrganizationSecretDelete,
2120
Importer: &schema.ResourceImporter{
2221
State: func(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
@@ -71,6 +70,7 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
7170
},
7271
Set: schema.HashInt,
7372
Optional: true,
73+
ForceNew: true,
7474
Description: "An array of repository ids that can access the organization secret.",
7575
},
7676
"created_at": {
@@ -84,9 +84,11 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
8484
Description: "Date of 'actions_secret' update.",
8585
},
8686
"destroy_on_drift": {
87-
Type: schema.TypeBool,
88-
Default: true,
89-
Optional: true,
87+
Type: schema.TypeBool,
88+
Default: true,
89+
Optional: true,
90+
ForceNew: true,
91+
Description: "Boolean indicating whether to recreate the secret if it's modified outside of Terraform. When `true` (default), Terraform will delete and recreate the secret if it detects external changes. When `false`, Terraform will acknowledge external changes but not recreate the secret.",
9092
},
9193
},
9294
}
@@ -169,12 +171,6 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta in
169171
return err
170172
}
171173

172-
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
173-
return err
174-
}
175-
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
176-
return err
177-
}
178174
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
179175
return err
180176
}
@@ -220,14 +216,35 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta in
220216
// timestamp we've persisted in the state. In that case, we can no longer
221217
// trust that the value (which we don't see) is equal to what we've declared
222218
// previously.
223-
//
224-
// The only solution to enforce consistency between is to mark the resource
225-
// as deleted (unset the ID) in order to fix potential drift by recreating
226-
// the resource.
227219
destroyOnDrift := d.Get("destroy_on_drift").(bool)
228-
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
220+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
221+
222+
if hasStoredUpdatedAt && storedUpdatedAt != secret.UpdatedAt.String() {
229223
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
230-
d.SetId("")
224+
225+
if destroyOnDrift {
226+
// Original behavior: mark for recreation
227+
d.SetId("")
228+
return nil
229+
} else {
230+
// Alternative approach: set sensitive values to empty to trigger update plan
231+
// This tells Terraform that the current state is unknown and needs reconciliation
232+
if err = d.Set("encrypted_value", ""); err != nil {
233+
return err
234+
}
235+
if err = d.Set("plaintext_value", ""); err != nil {
236+
return err
237+
}
238+
log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update")
239+
}
240+
} else {
241+
// No drift detected, preserve the configured values in state
242+
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
243+
return err
244+
}
245+
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
246+
return err
247+
}
231248
}
232249

233250
// Always update the timestamp to prevent repeated drift detection

github/resource_github_actions_organization_secret_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
1011
)
1112

1213
func TestAccGithubActionsOrganizationSecret(t *testing.T) {
@@ -184,4 +185,108 @@ func TestAccGithubActionsOrganizationSecret(t *testing.T) {
184185
testCase(t, organization)
185186
})
186187
})
188+
189+
// Unit tests for drift detection behavior
190+
t.Run("destroyOnDrift false clears sensitive values instead of recreating", func(t *testing.T) {
191+
originalTimestamp := "2023-01-01T00:00:00Z"
192+
newTimestamp := "2023-01-02T00:00:00Z"
193+
194+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]interface{}{
195+
"secret_name": "test-secret",
196+
"plaintext_value": "original-value",
197+
"encrypted_value": "original-encrypted",
198+
"visibility": "private",
199+
"destroy_on_drift": false,
200+
"updated_at": originalTimestamp,
201+
})
202+
d.SetId("test-secret")
203+
204+
// Simulate drift detection logic when destroy_on_drift is false
205+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
206+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
207+
208+
if hasStoredUpdatedAt && storedUpdatedAt != newTimestamp {
209+
if destroyOnDrift {
210+
// Would clear ID for recreation
211+
d.SetId("")
212+
} else {
213+
// Should clear sensitive values to trigger update
214+
d.Set("encrypted_value", "")
215+
d.Set("plaintext_value", "")
216+
}
217+
d.Set("updated_at", newTimestamp)
218+
}
219+
220+
// Should NOT have cleared the ID when destroy_on_drift=false
221+
if d.Id() == "" {
222+
t.Error("Expected ID to be preserved when destroy_on_drift=false, but it was cleared")
223+
}
224+
225+
// Should have cleared sensitive values to trigger update plan
226+
if plaintextValue := d.Get("plaintext_value").(string); plaintextValue != "" {
227+
t.Errorf("Expected plaintext_value to be cleared for update plan, got %s", plaintextValue)
228+
}
229+
230+
if encryptedValue := d.Get("encrypted_value").(string); encryptedValue != "" {
231+
t.Errorf("Expected encrypted_value to be cleared for update plan, got %s", encryptedValue)
232+
}
233+
234+
// Should have updated the timestamp
235+
if updatedAt := d.Get("updated_at").(string); updatedAt != newTimestamp {
236+
t.Errorf("Expected timestamp to be updated to %s, got %s", newTimestamp, updatedAt)
237+
}
238+
})
239+
240+
t.Run("destroyOnDrift true still recreates resource on drift", func(t *testing.T) {
241+
originalTimestamp := "2023-01-01T00:00:00Z"
242+
newTimestamp := "2023-01-02T00:00:00Z"
243+
244+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]interface{}{
245+
"secret_name": "test-secret",
246+
"plaintext_value": "original-value",
247+
"visibility": "private",
248+
"destroy_on_drift": true, // Explicitly set to true
249+
"updated_at": originalTimestamp,
250+
})
251+
d.SetId("test-secret")
252+
253+
// Simulate drift detection logic when destroy_on_drift is true
254+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
255+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
256+
257+
if hasStoredUpdatedAt && storedUpdatedAt != newTimestamp {
258+
if destroyOnDrift {
259+
// Should clear ID for recreation (original behavior)
260+
d.SetId("")
261+
return // Exit early like the real function would
262+
}
263+
}
264+
265+
// Should have cleared the ID for recreation when destroy_on_drift=true
266+
if d.Id() != "" {
267+
t.Error("Expected ID to be cleared for recreation when destroy_on_drift=true, but it was preserved")
268+
}
269+
})
270+
271+
t.Run("destroy_on_drift field defaults", func(t *testing.T) {
272+
// Test that destroy_on_drift defaults to true for backward compatibility
273+
schema := resourceGithubActionsOrganizationSecret().Schema["destroy_on_drift"]
274+
if schema.Default != true {
275+
t.Error("destroy_on_drift should default to true for backward compatibility")
276+
}
277+
})
278+
279+
t.Run("default destroy_on_drift is true", func(t *testing.T) {
280+
d := schema.TestResourceDataRaw(t, resourceGithubActionsOrganizationSecret().Schema, map[string]interface{}{
281+
"secret_name": "test-secret",
282+
"plaintext_value": "test-value",
283+
"visibility": "private",
284+
// destroy_on_drift not set, should default to true
285+
})
286+
287+
destroyOnDrift := d.Get("destroy_on_drift").(bool)
288+
if !destroyOnDrift {
289+
t.Error("Expected destroy_on_drift to default to true")
290+
}
291+
})
187292
}

github/resource_github_actions_secret.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ func resourceGithubActionsSecret() *schema.Resource {
7272
Type: schema.TypeBool,
7373
Default: true,
7474
Optional: true,
75+
ForceNew: true,
7576
Description: "Boolean indicating whether to recreate the secret if it's modified outside of Terraform. When `true` (default), Terraform will delete and recreate the secret if it detects external changes. When `false`, Terraform will acknowledge external changes but not recreate the secret.",
7677
},
7778
},
@@ -142,12 +143,6 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta interface{}) e
142143
return err
143144
}
144145

145-
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
146-
return err
147-
}
148-
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
149-
return err
150-
}
151146
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
152147
return err
153148
}
@@ -163,17 +158,36 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta interface{}) e
163158
// timestamp we've persisted in the state. In that case, we can no longer
164159
// trust that the value (which we don't see) is equal to what we've declared
165160
// previously.
166-
//
167-
// The only solution to enforce consistency between is to mark the resource
168-
// as deleted (unset the ID) in order to fix potential drift by recreating
169-
// the resource.
170161
destroyOnDrift := d.Get("destroy_on_drift").(bool)
171-
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
162+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
163+
164+
if hasStoredUpdatedAt && storedUpdatedAt != secret.UpdatedAt.String() {
172165
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
173-
d.SetId("")
174-
}
175166

176-
// Always update the timestamp to prevent repeated drift detection
167+
if destroyOnDrift {
168+
// Original behavior: mark for recreation
169+
d.SetId("")
170+
return nil
171+
} else {
172+
// Alternative approach: set sensitive values to empty to trigger update plan
173+
// This tells Terraform that the current state is unknown and needs reconciliation
174+
if err = d.Set("encrypted_value", ""); err != nil {
175+
return err
176+
}
177+
if err = d.Set("plaintext_value", ""); err != nil {
178+
return err
179+
}
180+
log.Printf("[INFO] Detected drift but destroy_on_drift=false, clearing sensitive values to trigger update")
181+
}
182+
} else {
183+
// No drift detected, preserve the configured values in state
184+
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
185+
return err
186+
}
187+
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
188+
return err
189+
}
190+
} // Always update the timestamp to prevent repeated drift detection
177191
if err = d.Set("updated_at", secret.UpdatedAt.String()); err != nil {
178192
return err
179193
}

0 commit comments

Comments
 (0)