Skip to content

Commit 3436b5c

Browse files
authored
Merge branch 'main' into feature/allow-change-default-branch
2 parents 3eea72b + e72a4fb commit 3436b5c

17 files changed

Lines changed: 414 additions & 136 deletions

.github/workflows/release.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,63 @@ permissions:
1414
contents: write
1515

1616
jobs:
17+
pre-release-tests:
18+
name: Run tests before release
19+
runs-on: ubuntu-latest
20+
env:
21+
test_stacks_directory: test_tf_stacks # root directory for test stacks
22+
pre_release_tests: provider_only # directory name for pre-release tests
23+
permissions:
24+
contents: read
25+
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
29+
30+
- name: Setup Go
31+
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
32+
with:
33+
go-version-file: go.mod
34+
cache: true
35+
36+
- name: Build provider
37+
run: go build -o terraform-provider-github
38+
39+
- name: Setup dev overrides
40+
run: |
41+
ROOT_DIR=$(pwd)
42+
cat > ~/.terraformrc << EOF
43+
provider_installation {
44+
dev_overrides {
45+
"integrations/github" = "${ROOT_DIR}"
46+
}
47+
direct {}
48+
}
49+
EOF
50+
51+
- name: Verify dev overrides setup
52+
run: cat ~/.terraformrc
53+
54+
- name: Setup Terraform
55+
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
56+
with:
57+
terraform_version: 1.x
58+
59+
- name: Check Terraform version
60+
run: terraform version
61+
62+
- name: Terraform init
63+
continue-on-error: true # continue even if init fails
64+
run: terraform -chdir=./${{ env.test_stacks_directory }}/${{ env.pre_release_tests }} init
65+
66+
- name: Terraform validate
67+
run: terraform -chdir=./${{ env.test_stacks_directory }}/${{ env.pre_release_tests }} validate
68+
69+
- name: Clean up
70+
run: rm -f ~/.terraformrc terraform-provider-github
71+
1772
goreleaser:
73+
needs: [ pre-release-tests ] # runs only if pre-release tests pass
1874
runs-on: ubuntu-latest
1975
steps:
2076
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0

github/resource_github_actions_organization_secret.go

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
1717
return &schema.Resource{
1818
Create: resourceGithubActionsOrganizationSecretCreateOrUpdate,
1919
Read: resourceGithubActionsOrganizationSecretRead,
20-
Update: resourceGithubActionsOrganizationSecretCreateOrUpdate,
2120
Delete: resourceGithubActionsOrganizationSecretDelete,
2221
Importer: &schema.ResourceImporter{
2322
State: func(d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
@@ -61,8 +60,8 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
6160
"visibility": {
6261
Type: schema.TypeString,
6362
Required: true,
64-
ValidateDiagFunc: validateValueFunc([]string{"all", "private", "selected"}),
6563
ForceNew: true,
64+
ValidateDiagFunc: validateValueFunc([]string{"all", "private", "selected"}),
6665
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'.",
6766
},
6867
"selected_repository_ids": {
@@ -72,6 +71,7 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
7271
},
7372
Set: schema.HashInt,
7473
Optional: true,
74+
ForceNew: true,
7575
Description: "An array of repository ids that can access the organization secret.",
7676
},
7777
"created_at": {
@@ -85,9 +85,11 @@ func resourceGithubActionsOrganizationSecret() *schema.Resource {
8585
Description: "Date of 'actions_secret' update.",
8686
},
8787
"destroy_on_drift": {
88-
Type: schema.TypeBool,
89-
Default: true,
90-
Optional: true,
88+
Type: schema.TypeBool,
89+
Default: true,
90+
Optional: true,
91+
ForceNew: true,
92+
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.",
9193
},
9294
},
9395
}
@@ -171,12 +173,6 @@ func resourceGithubActionsOrganizationSecretRead(d *schema.ResourceData, meta an
171173
return err
172174
}
173175

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

235252
// 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]any{
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]any{
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]any{
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 & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ func resourceGithubActionsSecret() *schema.Resource {
1818
return &schema.Resource{
1919
Create: resourceGithubActionsSecretCreateOrUpdate,
2020
Read: resourceGithubActionsSecretRead,
21-
Update: resourceGithubActionsSecretCreateOrUpdate,
2221
Delete: resourceGithubActionsSecretDelete,
2322
Importer: &schema.ResourceImporter{
2423
State: resourceGithubActionsSecretImport,
@@ -73,6 +72,7 @@ func resourceGithubActionsSecret() *schema.Resource {
7372
Type: schema.TypeBool,
7473
Default: true,
7574
Optional: true,
75+
ForceNew: true,
7676
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.",
7777
},
7878
},
@@ -144,12 +144,6 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error {
144144
return err
145145
}
146146

147-
if err = d.Set("encrypted_value", d.Get("encrypted_value")); err != nil {
148-
return err
149-
}
150-
if err = d.Set("plaintext_value", d.Get("plaintext_value")); err != nil {
151-
return err
152-
}
153147
if err = d.Set("created_at", secret.CreatedAt.String()); err != nil {
154148
return err
155149
}
@@ -165,17 +159,36 @@ func resourceGithubActionsSecretRead(d *schema.ResourceData, meta any) error {
165159
// timestamp we've persisted in the state. In that case, we can no longer
166160
// trust that the value (which we don't see) is equal to what we've declared
167161
// previously.
168-
//
169-
// The only solution to enforce consistency between is to mark the resource
170-
// as deleted (unset the ID) in order to fix potential drift by recreating
171-
// the resource.
172162
destroyOnDrift := d.Get("destroy_on_drift").(bool)
173-
if updatedAt, ok := d.GetOk("updated_at"); ok && destroyOnDrift && updatedAt != secret.UpdatedAt.String() {
163+
storedUpdatedAt, hasStoredUpdatedAt := d.GetOk("updated_at")
164+
165+
if hasStoredUpdatedAt && storedUpdatedAt != secret.UpdatedAt.String() {
174166
log.Printf("[INFO] The secret %s has been externally updated in GitHub", d.Id())
175-
d.SetId("")
176-
}
177167

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

0 commit comments

Comments
 (0)