Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 19 additions & 2 deletions cmd/wfctl/infra_bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ var bootstrapSecrets = func(ctx context.Context, provider secrets.Provider, cfg
// the provider once and subsequent lookups are O(1). Resolved lazily on
// the first write-only Get.
var listSet map[string]struct{}
var githubListFoldSet map[string]struct{}
var listErr error
var listDone bool
lookupViaList := func(key string) (bool, error) {
Expand All @@ -571,17 +572,32 @@ var bootstrapSecrets = func(ctx context.Context, provider secrets.Provider, cfg
listErr = err
if err == nil {
listSet = make(map[string]struct{}, len(names))
if provider.Name() == "github" {
githubListFoldSet = make(map[string]struct{}, len(names))
}
for _, n := range names {
listSet[n] = struct{}{}
if githubListFoldSet != nil {
githubListFoldSet[strings.ToUpper(n)] = struct{}{}
}
}
}
listDone = true
}
if listErr != nil && !errors.Is(listErr, secrets.ErrUnsupported) {
return false, fmt.Errorf("list secrets to check %q: %w", key, listErr)
}
_, ok := listSet[key]
return ok, nil
if _, ok := listSet[key]; ok {
return true, nil
}
// GitHub Actions secret names are case-insensitive and are commonly
// reported uppercased by the API. Generated subkeys such as
// SPACES_access_key must still match SPACES_ACCESS_KEY.
if githubListFoldSet != nil {
_, ok := githubListFoldSet[strings.ToUpper(key)]
return ok, nil
}
Comment thread
intel352 marked this conversation as resolved.
return false, nil
}
secretExists := func(key string) (bool, error) {
_, err := provider.Get(ctx, key)
Expand Down Expand Up @@ -667,6 +683,7 @@ var bootstrapSecrets = func(ctx context.Context, provider secrets.Provider, cfg
// write-only providers (GitHub Actions) where Get is unsupported.
listDone = false
listSet = nil
githubListFoldSet = nil
} else {
// Normal path: check that EVERY expected stored key is already present
// before skipping. provider_credential writes multiple sub-keys; if a
Expand Down
37 changes: 36 additions & 1 deletion cmd/wfctl/infra_bootstrap_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,15 @@ type writeOnlyProvider struct {
getCalls int
listCalls int
listOK bool
name string
}

func (p *writeOnlyProvider) Name() string { return "write-only-fake" }
func (p *writeOnlyProvider) Name() string {
if p.name != "" {
return p.name
}
return "write-only-fake"
}

func (p *writeOnlyProvider) Get(_ context.Context, _ string) (string, error) {
p.getCalls++
Expand Down Expand Up @@ -81,6 +87,35 @@ func TestBootstrapSecrets_WriteOnlyProviderSkipsExisting(t *testing.T) {
}
}

// TestBootstrapSecrets_GitHubProviderCredentialMatchesUppercaseList verifies
// GitHub's write-only secret list can satisfy mixed-case generated key probes.
// GitHub Actions secret names are case-insensitive, and the API reports common
// subkey names as uppercase (SPACES_ACCESS_KEY / SPACES_SECRET_KEY). Without
// this, auto-bootstrap attempts to recreate an existing upstream provider
// credential and DigitalOcean refuses the duplicate name.
func TestBootstrapSecrets_GitHubProviderCredentialMatchesUppercaseList(t *testing.T) {
withStubGenerator(t, func(_ context.Context, _ string, _ map[string]any) (string, error) {
t.Fatal("generator must not be called when GitHub-listed sub-keys already exist")
return "", nil
})
p := &writeOnlyProvider{
name: "github",
existing: []string{"SPACES_ACCESS_KEY", "SPACES_SECRET_KEY"},
listOK: true,
}
cfg := &SecretsConfig{
Generate: []SecretGen{
{Key: "SPACES", Type: "provider_credential", Source: "digitalocean.spaces"},
},
}
if _, _, err := bootstrapSecrets(context.Background(), p, cfg, nil); err != nil {
t.Fatalf("bootstrapSecrets: %v", err)
}
if len(p.stored) != 0 {
t.Fatalf("stored = %v, want empty (GitHub-listed secrets already exist)", p.stored)
}
}

// TestBootstrapSecrets_WriteOnlyProviderGeneratesWhenMissing verifies the
// fallback still generates when List shows the name is absent.
func TestBootstrapSecrets_WriteOnlyProviderGeneratesWhenMissing(t *testing.T) {
Expand Down
Loading