Skip to content

Commit fc4c3e7

Browse files
intel352claude
andauthored
feat: v0.18.7 — plan/apply equivalence (env-override name, configHash determinism, ResolveSizing, Close logging) (#472)
* fix(config): ResolveForEnv lifts Config["name"] into ResolvedModule.Name When an env override sets a "name" key in Config, promote it to ResolvedModule.Name and delete it from Config. This ensures that ResourceSpec.Name carries the env-resolved name (e.g. "bmw-staging-vpc" instead of "bmw-vpc") so plan and apply agree on resource identity. Empty string is ignored to prevent accidental erasure of the module name. Closes follow-up #32. Tests: LiftsConfigNameIntoIdentity, PreservesNameWhenNoOverride, EmptyNameFieldIgnored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(platform): configHash sorts keys explicitly for determinism Use explicit sorted kv-pair encoding before SHA-256 so configHash produces the same value regardless of Go's randomised map-iteration order. Closes issue where successive applies without config changes produced spurious "update" plan actions. Add differ_hash_test.go (internal package) with stability test (100 iterations), empty-map sentinel, and inequality sanity check. Update hashConfig helper in differ_test.go to match the new encoding. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(wfctl): invoke provider.ResolveSizing before plan for sized specs For each ResourceSpec with a non-empty Size field, call provider.ResolveSizing(type, size, hints) inside applyWithProviderAndStore before ComputePlan. The returned ProviderSizing.InstanceType and extra Specs are merged into spec.Config so that plan and apply agree on the concrete instance type (e.g. Size:"m" → instance_type:"s-1vcpu-2gb"). If the provider returns nil (no resolution needed), the spec is unchanged. Also aligns configHashMap in infra.go with platform.configHash: use sorted kv-pair encoding so ResourceState.ConfigHash values written during apply are comparable on the next run's ComputePlan. Tests: TestApplyInfraModules_CallsResolveSizing_ForEachSpec verifies ResolveSizing is called exactly once per sized spec and that spec.Config is enriched before Apply. Updated TestApplyWithProvider_NoChanges and TestApplyWithProvider_DeletesRemovedResource to use configHashMap(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(wfctl): log provider Close() errors as stderr warnings Replace `defer closer.Close() //nolint:errcheck` with an explicit defer that writes `warning: provider %q shutdown: %v` to stderr in four files: - cmd/wfctl/infra_apply.go - cmd/wfctl/infra_destroy.go - cmd/wfctl/infra_status_drift.go (two closures: status + drift) - cmd/wfctl/infra_bootstrap.go Plugin subprocess leaks now surface instead of being silently discarded. Test: TestApplyWithProvider_LogsCloseError injects an error-producing io.Closer via resolveIaCProvider override, redirects os.Stderr via os.Pipe, and asserts that the warning message appears in captured output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(wfctl): plan-vs-apply equivalence test harness Add infra_plan_apply_equivalence_test.go with recordingProvider and TestPlanApplyEquivalence_EnvOverrideNames — the regression gate for Bug #32 and the class of env-override name divergences: 1. Build a BMW-shaped infra.yaml with env overrides that rename every resource (bmw-vpc → bmw-staging-vpc, etc.). 2. Call planResourcesForEnv("staging") — capture intended names. 3. Call applyInfraModules with a recording fake provider that captures actual spec.Name values passed to Apply. 4. Assert the two name sets are identical. Also add TestPlanResourcesForEnv_UsesEnvOverrideNames to infra_env_wire_test.go — unit-level assertion that planResourcesForEnv returns env-override names, not raw module names. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: CHANGELOG v0.18.7 — plan/apply equivalence Document the five fixes: ResolveForEnv name lift, configHash determinism, ResolveSizing invocation, Close() error logging, and the plan-vs-apply equivalence test harness. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): update configHashIntegration to sorted kv encoding configHashIntegration in module/infra_module_integration_test.go was using the old json.Marshal(map) hash format, producing a different hash than platform.configHash (which uses sorted kv-pairs). This caused TestInfraModule_DriftDetectionFlow to emit a spurious "update" action and fail. Updated to match the platform canonical encoding exactly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(lint): eliminate G602 and guard ResolveSizing on abstract sizes only Two related fixes to the ResolveSizing loop in applyWithProviderAndStore: 1. G602 (gosec slice index out-of-range false positive): replace indexing via specs[i] with a local pointer `spec := &specs[i]` so gosec can confirm the slice access is safe. 2. isAbstractSize guard (Copilot #2): add isAbstractSize helper that returns true only for xs/s/m/l/xl. ResolveSizing is now skipped for provider-specific slugs (e.g. "db-s-1vcpu-1gb") which are already concrete and must not be re-resolved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(config): scope ResolveForEnv name-lift to infra.* modules only The env-override name lift in ResolveForEnv previously applied to all module types. This was too broad — non-infra modules can legitimately carry a 'name' key in their Config for display purposes. Added strings.HasPrefix(resolved.Type, "infra.") guard so only infra.* modules have their Config["name"] lifted into ResolvedModule.Name. All other module types are unaffected. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(test): add t.Cleanup for stderr restore in LogsCloseError test TestApplyWithProvider_LogsCloseError redirects os.Stderr but did not register a cleanup handler. If the test failed early (e.g. at os.Pipe), stderr would remain redirected for subsequent tests. Added t.Cleanup that restores oldStderr and closes both pipe ends, guaranteeing the redirect is always undone regardless of test outcome. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: export platform.ConfigHash and delegate from configHashMap configHashMap in cmd/wfctl/infra.go duplicated the sorted kv-pair encoding logic from platform.configHash. Any future change to the hashing algorithm would require updating two places. Added exported platform.ConfigHash wrapper that delegates to the package-internal configHash function. configHashMap now delegates to platform.ConfigHash, eliminating the duplication and ensuring the two are always byte-for-byte identical. Removed now-unused crypto/sha256 and sort imports from infra.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent cae4247 commit fc4c3e7

15 files changed

Lines changed: 646 additions & 28 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.18.7] - 2026-04-23
9+
10+
### Fixed
11+
12+
- **`ResolveForEnv` lifts `Config["name"]` into `ResolvedModule.Name`** — when an environment override sets `config.name`, the value is promoted to `ResolvedModule.Name` and removed from `Config`. This closes the plan-vs-apply divergence where `wfctl infra plan --env staging` displayed `bmw-staging-vpc` but `wfctl infra apply --env staging` created a resource named `bmw-vpc` (the raw module name). Downstream `ResourceSpec.Name` now carries the env-resolved identity in both paths. (closes follow-up #32)
13+
- **`platform.configHash` — deterministic key ordering**`configHash` now sorts map keys explicitly before JSON-marshalling, matching the DO plugin's existing pattern. Previously, Go's randomised map-iteration order could produce different hashes for identical configs on successive runs, generating spurious "update" plan actions on second apply with no config change.
14+
- **`applyWithProviderAndStore` calls `provider.ResolveSizing`** — for each spec with a non-empty `Size` field, `ResolveSizing(type, size, hints)` is now called before `platform.ComputePlan`. The returned `ProviderSizing.InstanceType` and extra `Specs` are merged into `spec.Config` so that plan and apply agree on the concrete instance type.
15+
- **Provider `closer.Close()` errors logged as warnings**`defer closer.Close() //nolint:errcheck` replaced with an explicit defer that writes `warning: provider %q shutdown: %v` to stderr in `infra_apply.go`, `infra_destroy.go`, `infra_status_drift.go`, and `infra_bootstrap.go`. Plugin subprocess leaks now surface instead of being silently discarded.
16+
- **`configHashMap` in `infra.go` uses sorted kv encoding** — aligns with `platform.configHash` so `ResourceState.ConfigHash` values written during apply are comparable to hashes computed by `ComputePlan` on the next run.
17+
18+
### Tests
19+
20+
- `config/module_resolve_env_test.go``TestResolveForEnv_LiftsConfigNameIntoIdentity`, `TestResolveForEnv_PreservesNameWhenNoOverride`, `TestResolveForEnv_EmptyNameFieldIgnored`
21+
- `platform/differ_hash_test.go``TestConfigHash_Stable_AcrossMapIterationOrder` (100 iterations), `TestConfigHash_EmptyMapReturnsEmpty`, `TestConfigHash_DifferentConfigsDifferentHashes`
22+
- `cmd/wfctl/infra_apply_test.go``TestApplyInfraModules_CallsResolveSizing_ForEachSpec`, `TestApplyWithProvider_LogsCloseError`
23+
- `cmd/wfctl/infra_plan_apply_equivalence_test.go``TestPlanApplyEquivalence_EnvOverrideNames` (end-to-end regression gate for plan-vs-apply name divergence)
24+
- `cmd/wfctl/infra_env_wire_test.go``TestPlanResourcesForEnv_UsesEnvOverrideNames`
25+
826
## [0.18.6] - 2026-04-23
927

1028
### Fixed

cmd/wfctl/infra.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"context"
5-
"crypto/sha256"
65
"encoding/json"
76
"flag"
87
"fmt"
@@ -353,13 +352,11 @@ func loadCurrentState(cfgFile string) []interfaces.ResourceState {
353352
return states
354353
}
355354

356-
// configHashMap computes a deterministic SHA-256 hex hash of a config map.
355+
// configHashMap delegates to platform.ConfigHash so that the CLI always
356+
// produces hashes byte-for-byte identical to those stored by ComputePlan.
357+
// The local duplication that previously existed here has been removed.
357358
func configHashMap(config map[string]any) string {
358-
if len(config) == 0 {
359-
return ""
360-
}
361-
data, _ := json.Marshal(config)
362-
return fmt.Sprintf("%x", sha256.Sum256(data))
359+
return platform.ConfigHash(config)
363360
}
364361

365362
// formatPlanTable renders an interfaces.IaCPlan as a human-readable table

cmd/wfctl/infra_apply.go

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strings"
78
"time"
89

@@ -157,7 +158,12 @@ func applyInfraModules(ctx context.Context, cfgFile, envName string) error { //n
157158
return fmt.Errorf("provider %q (%s): load provider: %w", moduleRef, g.provType, err)
158159
}
159160
if closer != nil {
160-
defer closer.Close() //nolint:errcheck
161+
provType := g.provType
162+
defer func() {
163+
if cerr := closer.Close(); cerr != nil {
164+
fmt.Fprintf(os.Stderr, "warning: provider %q shutdown: %v\n", provType, cerr)
165+
}
166+
}()
161167
}
162168
return applyWithProviderAndStore(ctx, provider, g.provType, g.specs, current, store)
163169
}
@@ -183,6 +189,32 @@ func applyWithProviderAndStore(ctx context.Context, provider interfaces.IaCProvi
183189
store = &noopStateStore{}
184190
}
185191

192+
// Resolve abstract sizing tiers into concrete provider-specific values
193+
// (e.g. Size: "m" → instance_type: "s-1vcpu-2gb") for each spec that
194+
// declares an abstract Size tier. Provider-specific slugs (e.g.
195+
// "db-s-1vcpu-1gb") are passed through as-is to avoid double-resolution.
196+
// The resolved values are merged into spec.Config so that plan output and
197+
// apply are always in sync.
198+
for i := range specs {
199+
spec := &specs[i]
200+
if spec.Size == "" || !isAbstractSize(spec.Size) {
201+
continue
202+
}
203+
sizing, err := provider.ResolveSizing(spec.Type, spec.Size, spec.Hints)
204+
if err != nil {
205+
return fmt.Errorf("%s/%s: resolve sizing: %w", spec.Type, spec.Name, err)
206+
}
207+
if sizing != nil {
208+
if spec.Config == nil {
209+
spec.Config = map[string]any{}
210+
}
211+
spec.Config["instance_type"] = sizing.InstanceType
212+
for k, v := range sizing.Specs {
213+
spec.Config[k] = v
214+
}
215+
}
216+
}
217+
186218
// Pass the full current state to ComputePlan so that resources which were
187219
// previously provisioned but are no longer in the desired spec set generate
188220
// delete actions rather than being silently ignored.
@@ -265,3 +297,15 @@ func applyWithProviderAndStore(ctx context.Context, provider interfaces.IaCProvi
265297
}
266298
return nil
267299
}
300+
301+
// isAbstractSize reports whether s is one of the canonical abstract size tiers
302+
// (xs/s/m/l/xl). Provider-specific slugs such as "db-s-1vcpu-1gb" return false
303+
// so that ResolveSizing is not called for already-concrete values.
304+
func isAbstractSize(s interfaces.Size) bool {
305+
switch s {
306+
case interfaces.SizeXS, interfaces.SizeS, interfaces.SizeM, interfaces.SizeL, interfaces.SizeXL:
307+
return true
308+
default:
309+
return false
310+
}
311+
}

cmd/wfctl/infra_apply_test.go

Lines changed: 169 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package main
22

33
import (
4+
"bytes"
45
"context"
5-
"crypto/sha256"
6-
"encoding/json"
76
"fmt"
87
"io"
98
"os"
@@ -184,13 +183,9 @@ func TestApplyWithProvider_NoChanges(t *testing.T) {
184183
Config: map[string]any{"engine": "postgres"},
185184
}
186185

187-
// Reproduce the hash that platform.ComputePlan computes via configHash:
188-
// sha256(json.Marshal(spec.Config)) in hex.
189-
cfgData, err := json.Marshal(spec.Config)
190-
if err != nil {
191-
t.Fatalf("marshal config: %v", err)
192-
}
193-
cfgHash := fmt.Sprintf("%x", sha256.Sum256(cfgData))
186+
// Reproduce the hash that platform.ComputePlan computes via configHash
187+
// (sorted kv-pair encoding):
188+
cfgHash := configHashMap(spec.Config)
194189

195190
current := []interfaces.ResourceState{{
196191
Name: spec.Name,
@@ -220,8 +215,7 @@ func TestApplyWithProvider_DeletesRemovedResource(t *testing.T) {
220215
{Name: "bmw-app", Type: "infra.container_service", Config: map[string]any{"image": "registry/app:latest"}},
221216
}
222217
// Current: bmw-app + old-db (removed from config, should be deleted).
223-
appData, _ := json.Marshal(specs[0].Config)
224-
appHash := fmt.Sprintf("%x", sha256.Sum256(appData))
218+
appHash := configHashMap(specs[0].Config)
225219
current := []interfaces.ResourceState{
226220
{Name: "bmw-app", Type: "infra.container_service", ConfigHash: appHash},
227221
{Name: "old-db", Type: "infra.database", ConfigHash: "oldhash"},
@@ -464,6 +458,98 @@ modules:
464458
}
465459
}
466460

461+
// ── TestApplyInfraModules_CallsResolveSizing_ForEachSpec ──────────────────────
462+
463+
// sizingCapture is an IaCProvider that records every ResolveSizing call and
464+
// returns a concrete ProviderSizing so we can assert spec.Config is enriched.
465+
type sizingCapture struct {
466+
applyCapture
467+
sizingCalls []struct {
468+
resType string
469+
size interfaces.Size
470+
}
471+
sizingResult *interfaces.ProviderSizing
472+
appliedSpecs []interfaces.ResourceSpec
473+
}
474+
475+
func (s *sizingCapture) ResolveSizing(resType string, size interfaces.Size, _ *interfaces.ResourceHints) (*interfaces.ProviderSizing, error) {
476+
s.mu.Lock()
477+
defer s.mu.Unlock()
478+
s.sizingCalls = append(s.sizingCalls, struct {
479+
resType string
480+
size interfaces.Size
481+
}{resType: resType, size: size})
482+
return s.sizingResult, nil
483+
}
484+
485+
func (s *sizingCapture) Apply(_ context.Context, plan *interfaces.IaCPlan) (*interfaces.ApplyResult, error) {
486+
s.mu.Lock()
487+
defer s.mu.Unlock()
488+
for _, a := range plan.Actions {
489+
s.appliedSpecs = append(s.appliedSpecs, a.Resource)
490+
}
491+
return &interfaces.ApplyResult{}, nil
492+
}
493+
494+
// TestApplyInfraModules_CallsResolveSizing_ForEachSpec verifies that
495+
// applyWithProviderAndStore invokes provider.ResolveSizing for each spec
496+
// that has a non-empty Size field, and that the resolved InstanceType and
497+
// extra Specs are merged into spec.Config before the plan is computed.
498+
func TestApplyInfraModules_CallsResolveSizing_ForEachSpec(t *testing.T) {
499+
specs := []interfaces.ResourceSpec{
500+
{Name: "db", Type: "infra.database", Size: interfaces.SizeM, Config: map[string]any{"engine": "postgres"}},
501+
{Name: "vpc", Type: "infra.vpc", Config: map[string]any{"region": "nyc3"}}, // no Size → ResolveSizing should NOT be called
502+
{Name: "app", Type: "infra.container_service", Size: interfaces.SizeS, Config: map[string]any{"image": "nginx"}},
503+
}
504+
505+
fake := &sizingCapture{
506+
sizingResult: &interfaces.ProviderSizing{
507+
InstanceType: "s-1vcpu-2gb",
508+
Specs: map[string]any{"memory_mb": 2048},
509+
},
510+
}
511+
512+
if err := applyWithProviderAndStore(t.Context(), fake, "fake-cloud", specs, nil, nil); err != nil {
513+
t.Fatalf("applyWithProviderAndStore: %v", err)
514+
}
515+
516+
// ResolveSizing should have been called twice (db + app), not for vpc.
517+
fake.mu.Lock()
518+
calls := fake.sizingCalls
519+
applied := fake.appliedSpecs
520+
fake.mu.Unlock()
521+
522+
if len(calls) != 2 {
523+
t.Errorf("ResolveSizing calls = %d, want 2 (only sized specs)", len(calls))
524+
}
525+
callTypes := map[string]interfaces.Size{}
526+
for _, c := range calls {
527+
callTypes[c.resType] = c.size
528+
}
529+
if callTypes["infra.database"] != interfaces.SizeM {
530+
t.Errorf("infra.database sizing call size = %q, want %q", callTypes["infra.database"], interfaces.SizeM)
531+
}
532+
if callTypes["infra.container_service"] != interfaces.SizeS {
533+
t.Errorf("infra.container_service sizing call size = %q, want %q", callTypes["infra.container_service"], interfaces.SizeS)
534+
}
535+
536+
// The applied specs should carry the resolved instance_type in their Config.
537+
if len(applied) == 0 {
538+
t.Fatal("no specs were applied — Apply was not called or plan had no actions")
539+
}
540+
for _, s := range applied {
541+
if s.Size == "" {
542+
continue // vpc — no sizing expected
543+
}
544+
if s.Config["instance_type"] != "s-1vcpu-2gb" {
545+
t.Errorf("spec %q: Config[instance_type] = %v, want s-1vcpu-2gb", s.Name, s.Config["instance_type"])
546+
}
547+
if s.Config["memory_mb"] != 2048 {
548+
t.Errorf("spec %q: Config[memory_mb] = %v, want 2048", s.Name, s.Config["memory_mb"])
549+
}
550+
}
551+
}
552+
467553
// TestHasInfraModules verifies detection of infra.* vs platform.* configs.
468554
func TestHasInfraModules(t *testing.T) {
469555
dir := t.TempDir()
@@ -495,3 +581,75 @@ modules:
495581
t.Error("hasInfraModules: want false for platform.* config, got true")
496582
}
497583
}
584+
585+
// ── TestApplyWithProvider_LogsCloseError ──────────────────────────────────────
586+
587+
// errCloser is an io.Closer that always returns an error.
588+
type errCloser struct{ msg string }
589+
590+
func (e *errCloser) Close() error { return fmt.Errorf("%s", e.msg) }
591+
592+
// TestApplyWithProvider_LogsCloseError verifies that when the provider closer
593+
// returns an error during applyInfraModules, a warning is written to stderr
594+
// (instead of silently discarding the error via nolint:errcheck).
595+
func TestApplyWithProvider_LogsCloseError(t *testing.T) {
596+
dir := t.TempDir()
597+
cfgPath := filepath.Join(dir, "infra.yaml")
598+
if err := os.WriteFile(cfgPath, []byte(`
599+
modules:
600+
- name: myprov
601+
type: iac.provider
602+
config:
603+
provider: fake-cloud
604+
- name: my-vpc
605+
type: infra.vpc
606+
config:
607+
provider: myprov
608+
region: nyc3
609+
`), 0o600); err != nil {
610+
t.Fatalf("write config: %v", err)
611+
}
612+
613+
// Override resolveIaCProvider to return a provider + error-producing closer.
614+
orig := resolveIaCProvider
615+
fake := &applyCapture{}
616+
closerErr := "shutdown-sentinel-error"
617+
resolveIaCProvider = func(_ context.Context, _ string, _ map[string]any) (interfaces.IaCProvider, io.Closer, error) {
618+
return fake, &errCloser{msg: closerErr}, nil
619+
}
620+
t.Cleanup(func() { resolveIaCProvider = orig })
621+
622+
// Redirect stderr to capture warning output.
623+
oldStderr := os.Stderr
624+
r, w, pipeErr := os.Pipe()
625+
if pipeErr != nil {
626+
t.Fatalf("os.Pipe: %v", pipeErr)
627+
}
628+
os.Stderr = w
629+
t.Cleanup(func() {
630+
os.Stderr = oldStderr
631+
_ = w.Close()
632+
_ = r.Close()
633+
})
634+
635+
err := applyInfraModules(context.Background(), cfgPath, "")
636+
637+
w.Close()
638+
os.Stderr = oldStderr
639+
640+
var buf bytes.Buffer
641+
if _, readErr := buf.ReadFrom(r); readErr != nil {
642+
t.Fatalf("read stderr: %v", readErr)
643+
}
644+
stderrOutput := buf.String()
645+
646+
if err != nil {
647+
t.Fatalf("applyInfraModules returned unexpected error: %v", err)
648+
}
649+
if !strings.Contains(stderrOutput, closerErr) {
650+
t.Errorf("stderr = %q, want it to contain %q", stderrOutput, closerErr)
651+
}
652+
if !strings.Contains(stderrOutput, "warning") {
653+
t.Errorf("stderr = %q, want it to contain 'warning'", stderrOutput)
654+
}
655+
}

cmd/wfctl/infra_bootstrap.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,12 @@ func bootstrapStateBackend(ctx context.Context, cfgFile string) error {
149149
return fmt.Errorf("load provider %q for state backend bootstrap: %w", provType, err)
150150
}
151151
if closer != nil {
152-
defer closer.Close() //nolint:errcheck
152+
pType := provType
153+
defer func() {
154+
if cerr := closer.Close(); cerr != nil {
155+
fmt.Fprintf(os.Stderr, "warning: provider %q shutdown: %v\n", pType, cerr)
156+
}
157+
}()
153158
}
154159

155160
result, err := provider.BootstrapStateBackend(ctx, cfg)

cmd/wfctl/infra_destroy.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"strings"
78

89
"github.com/GoCodeAlone/workflow/config"
@@ -109,7 +110,12 @@ func destroyInfraModules(ctx context.Context, cfgFile, envName string) error { /
109110
return fmt.Errorf("load provider %q: %w", moduleRef, err)
110111
}
111112
if closer != nil {
112-
defer closer.Close() //nolint:errcheck
113+
provType := g.provType
114+
defer func() {
115+
if cerr := closer.Close(); cerr != nil {
116+
fmt.Fprintf(os.Stderr, "warning: provider %q shutdown: %v\n", provType, cerr)
117+
}
118+
}()
113119
}
114120

115121
fmt.Printf("Destroying %d resource(s) via provider %q (%s)...\n", len(g.refs), moduleRef, g.provType)

0 commit comments

Comments
 (0)