Skip to content

Commit 9df038a

Browse files
authored
fix: add consistency check after creating actions variables (#13)
Ensures newly created repository variables are consistently available by introducing a state check with retries after creation. Improves reliability when creating multiple variables in quick succession, preventing issues caused by eventual consistency in the API.
1 parent 00f24af commit 9df038a

2 files changed

Lines changed: 113 additions & 2 deletions

File tree

github/resource_github_actions_variable.go

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package github
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"log"
68
"net/http"
9+
"time"
710

811
"github.com/google/go-github/v74/github"
912
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -58,8 +61,9 @@ func resourceGithubActionsVariableCreate(d *schema.ResourceData, meta any) error
5861
ctx := context.Background()
5962

6063
repo := d.Get("repository").(string)
64+
variableName := d.Get("variable_name").(string)
6165
variable := &github.ActionsVariable{
62-
Name: d.Get("variable_name").(string),
66+
Name: variableName,
6367
Value: d.Get("value").(string),
6468
}
6569

@@ -68,7 +72,12 @@ func resourceGithubActionsVariableCreate(d *schema.ResourceData, meta any) error
6872
return err
6973
}
7074

71-
d.SetId(buildTwoPartID(repo, d.Get("variable_name").(string)))
75+
d.SetId(buildTwoPartID(repo, variableName))
76+
77+
if err := waitForVariableConsistency(ctx, client, owner, repo, variableName); err != nil {
78+
return err
79+
}
80+
7281
return resourceGithubActionsVariableRead(d, meta)
7382
}
7483

@@ -148,3 +157,50 @@ func resourceGithubActionsVariableDelete(d *schema.ResourceData, meta any) error
148157

149158
return err
150159
}
160+
161+
func waitForVariableConsistency(
162+
ctx context.Context,
163+
client *github.Client,
164+
owner, repo, variableName string,
165+
) error {
166+
// Derived context with overall timeout for the wait.
167+
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Minute)
168+
defer cancel()
169+
170+
// Initial delay to match previous behaviour.
171+
select {
172+
case <-ctxWithTimeout.Done():
173+
return fmt.Errorf("context done before waiting for variable %s: %w", variableName, ctxWithTimeout.Err())
174+
case <-time.After(time.Second):
175+
}
176+
177+
backoff := time.Second
178+
maxBackoff := 10 * time.Second
179+
180+
for {
181+
_, _, err := client.Actions.GetRepoVariable(ctxWithTimeout, owner, repo, variableName)
182+
if err == nil {
183+
return nil
184+
}
185+
186+
var ghErr *github.ErrorResponse
187+
if errors.As(err, &ghErr) && ghErr.Response.StatusCode == http.StatusNotFound {
188+
// Variable not visible yet, wait with exponential backoff.
189+
select {
190+
case <-ctxWithTimeout.Done():
191+
return fmt.Errorf("timeout waiting for variable %s to be consistent: %w", variableName, ctxWithTimeout.Err())
192+
case <-time.After(backoff):
193+
}
194+
195+
// Exponential backoff with cap.
196+
backoff *= 2
197+
if backoff > maxBackoff {
198+
backoff = maxBackoff
199+
}
200+
continue
201+
}
202+
203+
// Non-404 error: likely not transient.
204+
return fmt.Errorf("error waiting for variable %s to be consistent: %w", variableName, err)
205+
}
206+
}

github/resource_github_actions_variable_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,61 @@ func TestAccGithubActionsVariable(t *testing.T) {
8989
})
9090
})
9191

92+
t.Run("creates multiple repository variables without error", func(t *testing.T) {
93+
config := fmt.Sprintf(`
94+
resource "github_repository" "test" {
95+
name = "tf-acc-test-%s"
96+
}
97+
98+
resource "github_actions_variable" "variable1" {
99+
repository = github_repository.test.name
100+
variable_name = "test_variable_1"
101+
value = "value1"
102+
}
103+
104+
resource "github_actions_variable" "variable2" {
105+
repository = github_repository.test.name
106+
variable_name = "test_variable_2"
107+
value = "value2"
108+
}
109+
110+
resource "github_actions_variable" "variable3" {
111+
repository = github_repository.test.name
112+
variable_name = "test_variable_3"
113+
value = "value3"
114+
}
115+
`, randomID)
116+
117+
testCase := func(t *testing.T, mode string) {
118+
resource.Test(t, resource.TestCase{
119+
PreCheck: func() { skipUnlessMode(t, mode) },
120+
Providers: testAccProviders,
121+
Steps: []resource.TestStep{
122+
{
123+
Config: config,
124+
Check: resource.ComposeTestCheckFunc(
125+
resource.TestCheckResourceAttr("github_actions_variable.variable1", "value", "value1"),
126+
resource.TestCheckResourceAttr("github_actions_variable.variable2", "value", "value2"),
127+
resource.TestCheckResourceAttr("github_actions_variable.variable3", "value", "value3"),
128+
),
129+
},
130+
},
131+
})
132+
}
133+
134+
t.Run("with an anonymous account", func(t *testing.T) {
135+
t.Skip("anonymous account not supported for this operation")
136+
})
137+
138+
t.Run("with an individual account", func(t *testing.T) {
139+
testCase(t, individual)
140+
})
141+
142+
t.Run("with an organization account", func(t *testing.T) {
143+
testCase(t, organization)
144+
})
145+
})
146+
92147
t.Run("deletes repository variables without error", func(t *testing.T) {
93148
config := fmt.Sprintf(`
94149
resource "github_repository" "test" {

0 commit comments

Comments
 (0)