Skip to content

fix(wfctl): v0.18.9 — env-resolution consistency in ci run deploy + infra_output#476

Merged
intel352 merged 13 commits intomainfrom
fix/v0.18.9-phase-continuation
Apr 24, 2026
Merged

fix(wfctl): v0.18.9 — env-resolution consistency in ci run deploy + infra_output#476
intel352 merged 13 commits intomainfrom
fix/v0.18.9-phase-continuation

Conversation

@intel352
Copy link
Copy Markdown
Contributor

Problem

Two related env-resolution gaps caused BMW staging deploy to create duplicate DO App Platform apps (run 24888583717):

  1. ci run --phase deploy (deploy_providers.go): resolveModCfg returned only the config map; callers still read m.Name (base name) to identify the resource. When --env staging renamed bmw-app → bmw-staging, the deploy used the base name and tried to create a second resource instead of updating the existing one.

  2. infra_output secret generation (infra_output_secrets.go): syncInfraOutputSecrets had no access to wfCfg or envName, so source strings like bmw-database.uri could not be resolved to the env-overridden module name (bmw-staging-db) used as the state key.

Changes

cmd/wfctl/deploy_providers.go

  • Renamed resolveModCfgresolveModule; return type changed from (map[string]any, bool) to (*config.ResolvedModule, bool)
  • All three call sites (iac.provider loop, findByType closure, fallback loop) now use resolved.Name and resolved.Type

cmd/wfctl/infra_output_secrets.go

  • New resolveInfraOutput(wfCfg, source, envName, stateOutputs) helper: parses module.field, applies env-resolution via m.ResolveForEnv(envName), then looks up the state
  • syncInfraOutputSecrets signature extended with wfCfg *config.WorkflowConfig, envName string

cmd/wfctl/infra.go

  • Call site updated: loads wfCfg and passes envName to syncInfraOutputSecrets

Tests

  • deploy_providers_env_test.go: 2 regression tests (env-resolved name / base name fallback)
  • infra_secrets_env_test.go: 2 regression tests (BMW staging scenario / no-env path)
  • All existing syncInfraOutputSecrets call sites in tests updated with , nil, ""

Follow-up (non-blocking)

  • stateKeys() returns unsorted keys in error messages — sort.Strings cleanup can be a follow-up

Fixes #60, #56.

🤖 Generated with Claude Code

intel352 and others added 9 commits April 24, 2026 08:57
…egistry, typed gRPC args, migrate image, teardown

Five features bundled into v0.19.0 for shared config-file shape (wfctl.yaml +
.wfctl-lock.yaml) and release boundary. Each addresses architectural debt
surfaced during BMW tonight's deploy blocker chain.

Features:
- A. Plugin manifest + lockfile split (tasks #42/#43)
- B. Multi-registry + IaCProvider.EnsureRegistryAuth (task #48)
- C. Typed-args refactor for IaCProvider gRPC (task #41)
- D. Official workflow-migrate Docker image (task #49)
- E. wfctl infra teardown with mandatory dry-run + --approve flag (new)

Non-goals: constraint-based plugin resolution (v0.20.0), transitive plugin
deps, OCI chart/artifact registries, cross-registry mirroring.

Autonomous pipeline target: v0.19.0 after BMW post-teardown stabilizes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sinks)

Scope expanded from 5 to 7 features per user feedback on BMW CI gap audit:
- F. wfctl infra outputs with masked-by-default sensitivity + GHA ::add-mask::
- G. wfctl deploy verify with multi-target healthcheck + retry/timeout gate
- H. Declarative secret sinks (outputs.<field>.sinks[]) — plaintext never
  leaves wfctl process; built-in github_secret + github_env handlers;
  aws/gcp/azure sinks via plugin fan-out in v0.19.x

Motivation: BMW's Capture staging DB URL step uses doctl + awk + gh secret
set shell pipeline, leaking DATABASE_URL plaintext through stdout/env/argv.
Declarative sink pattern (like terraform's output-to-secret-manager) writes
the value in-process directly to the GitHub secrets API with libsodium
encryption. Matches user's stated principle: "if BMW CI has provider-specific
shell, fix it in workflow/wfctl so the CI stays declarative."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matches design doc 2026-04-24-v0.19.0-architectural-cleanup-design.md:
- Phase 1 alpha.1: Feature A (plugin manifest + lockfile)
- Phase 2 alpha.2: Feature C client-side (typed gRPC args)
- Phase 3 (DO plugin v0.8.0): Feature C server-side + integration tests
- Phase 4 alpha.3: Feature B (multi-registry)
- Phase 5 (DO plugin v0.8.1): Feature B server-side (EnsureRegistryAuth)
- Phase 6a rc1: Feature D (workflow-migrate image)
- Phase 6b rc2: Feature E (wfctl infra teardown)
- Phase 6c rc3: Features F + G + H (outputs + verify + sinks)
- Phase 7: v0.19.0 final + changelog + docs
- Phase 8: Plugin fan-out (aws/gcp/azure/tofu) in parallel
- Phase 9: BMW migration PR (after v0.19.0 stabilizes)

Timing: all phases can merge independently; final v0.19.0 tag and Phase 9
hold until BMW's tonight deploy chain reaches prod /healthz green (task #26).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BMW deploy run 24888583717 created a duplicate DO App Platform app because
wfctl infra apply used env-resolved name "bmw-staging" while wfctl ci run
--phase deploy used base module name "bmw-app". Both paths call driver.Read
by name; with different names they find different resources (or none) and
each calls Create, producing duplicates.

Root cause: cmd/wfctl/deploy_providers.go:769 reads m.Name directly after
ResolveForEnv has been applied. Same class as v0.18.7's Task #32 fix but
in the deploy-phase code path.

Fix: refactor resolveModCfg closure to return *ResolvedModule, use
resolved.Name at call sites. Audit + patch infra_output source resolution
(task #56) with the same pattern. Ship as v0.18.9.

Does not require state-sharing between IaC and CI phases; the bug is about
names, not state. Both phases use driver.Read by name; aligning the names
aligns the lookups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9 tasks across Phase 1 (core fixes: deploy_providers.go + infra_secrets.go
+ regression tests) and Phase 2 (release + BMW unblock: PR, merge, tag,
BMW bump, teardown, redeploy).

Same-class fix as v0.18.7 Task #32: env-resolved Name used consistently
wherever modules are consumed. Target: v0.18.9 hotfix; unblocks BMW
staging deploy from run 24888583717 duplicate-resource failure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Refactored resolveModCfg closure in deploy_providers.go to return
*config.ResolvedModule so callers see both resolved.Name (env-override
lifted from Config["name"]) and resolved.Config. All three call sites
(iac.provider lookup, findByType, fallback loop) now read resolved.Name
instead of m.Name.

Same class as v0.18.7 Task #32 fix for ResourceSpec.Name — env override
of Config["name"] was lifted into ResolvedModule.Name but deploy_providers.go
read m.Name directly, ignoring the override. Caused BMW deploy run
24888583717 to create duplicate DO apps (bmw-app vs bmw-staging).

Regression tested via:
- TestPluginDeployProvider_UsesEnvResolvedName (new, was failing)
- TestPluginDeployProvider_FallsBackToModuleNameWhenNoEnv (new, baseline)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Introduces resolveInfraOutput(wfCfg, source, envName, stateOutputs)
which translates the base module name in a "module.field" source string
to its env-resolved name before looking up state. State is persisted
under the env-resolved name (e.g. "bmw-staging-db"), so "bmw-database.uri"
with --env staging now correctly finds the state entry.

syncInfraOutputSecrets now accepts wfCfg and envName so the new
resolution is applied for every infra_output secret in the generate list.
The call site in infra.go (runInfraApply) loads the workflow config and
passes it through.

Closes task #56. Regression tested via:
- TestInfraOutput_EnvResolvesModuleSource (new, was failing)
- TestInfraOutput_NoEnvUsesBaseName (new, baseline)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 13:17
@intel352
Copy link
Copy Markdown
Contributor Author

@copilot review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets wfctl v0.18.9 by fixing env-resolution consistency so deploy and infra_output secret resolution use the same env-resolved module identity, preventing duplicate cloud resources and failed state lookups.

Changes:

  • Refactors newPluginDeployProvider env-resolution to return/use *config.ResolvedModule so deploy uses resolved.Name/resolved.Type consistently.
  • Adds env-aware infra_output source resolution (module.field) for secret generation by resolving the module name through ResolveForEnv.
  • Adds regression tests for both the deploy path and infra_output resolution, plus a v0.18.9 changelog entry and planning/design docs.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/plans/2026-04-24-wfctl-phase-continuation.md Implementation plan documenting steps/tests for the v0.18.9 hotfix.
docs/plans/2026-04-24-wfctl-phase-continuation-design.md Design doc describing the env-resolution identity mismatch and fix approach.
docs/plans/2026-04-24-v0.19.0-architectural-cleanup.md Longer-term v0.19.0 plan (context/follow-up work).
docs/plans/2026-04-24-v0.19.0-architectural-cleanup-design.md Longer-term v0.19.0 design (context/follow-up work).
cmd/wfctl/deploy_providers.go Uses ResolvedModule so deploy uses env-resolved name/type instead of base m.Name.
cmd/wfctl/deploy_providers_env_test.go Regression tests for env-resolved deploy target naming and no-env fallback.
cmd/wfctl/infra_output_secrets.go Adds resolveInfraOutput helper and threads wfCfg/envName into infra_output secret sync.
cmd/wfctl/infra_secrets_env_test.go Regression tests ensuring infra_output source resolves via env-renamed module.
cmd/wfctl/infra_output_secrets_test.go Updates test call sites for new syncInfraOutputSecrets signature.
cmd/wfctl/infra.go Passes wfCfg and envName into syncInfraOutputSecrets after infra apply.
CHANGELOG.md Adds v0.18.9 entry describing the fixes and tests.

Comment thread cmd/wfctl/infra_output_secrets.go Outdated
Comment on lines +54 to +56
if resolved, ok := m.ResolveForEnv(envName); ok && resolved.Name != "" {
moduleName = resolved.Name
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ResolveForEnv can return ok=false when the module is explicitly deleted for this env (m.Environments[envName] == nil). In that case this code breaks out of the loop without changing moduleName, potentially falling back to the base module name and reading stale/incorrect state outputs. Treat ok=false as an error for infra_output resolution (module removed for env) rather than silently continuing.

Suggested change
if resolved, ok := m.ResolveForEnv(envName); ok && resolved.Name != "" {
moduleName = resolved.Name
}
resolved, ok := m.ResolveForEnv(envName)
if !ok {
return "", fmt.Errorf("infra_output: module %q is not available in environment %q", moduleName, envName)
}
if resolved.Name == "" {
return "", fmt.Errorf("infra_output: module %q resolved to empty name for environment %q", moduleName, envName)
}
moduleName = resolved.Name

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@intel352 intel352 Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 25602af — ResolveForEnv ok=false now returns an explicit error instead of silently falling back to the base name. Message: infra_output: module X is not available in env Y (no environment override defined). This prevents the env-resolution fix from being bypassed on misconfigured modules.

Comment thread cmd/wfctl/infra_output_secrets.go
Comment thread cmd/wfctl/infra.go Outdated
}
states := loadCurrentState(cfgFile, envName)
return syncInfraOutputSecrets(ctx, secretsCfg, secretsProvider, states)
wfCfg, _ := config.LoadFromFile(cfgFile)
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config.LoadFromFile(cfgFile) error is ignored here. If loading fails (or returns nil), syncInfraOutputSecrets will skip env-based module-name resolution and may reintroduce the base-name vs env-resolved-name mismatch this PR is fixing. Please handle the error (and consider failing fast when envName != "").

Suggested change
wfCfg, _ := config.LoadFromFile(cfgFile)
wfCfg, err := config.LoadFromFile(cfgFile)
if err != nil {
if envName != "" {
return fmt.Errorf("load workflow config for infra_output sync with environment %q: %w", envName, err)
}
return fmt.Errorf("load workflow config for infra_output sync: %w", err)
}
if wfCfg == nil && envName != "" {
return fmt.Errorf("load workflow config for infra_output sync with environment %q: empty config", envName)
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@intel352 intel352 Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 25602af — error is now surfaced: wfCfg, err := config.LoadFromFile(cfgFile) with if err != nil { return fmt.Errorf("load config for infra_output env resolution: %w", err) }. Silent failure would have regressed env resolution to the pre-fix nil-wfCfg path.

Comment thread CHANGELOG.md Outdated
Comment on lines +12 to +13
- **`wfctl ci run --phase deploy` now uses env-resolved module name** — `pluginDeployProvider.resourceName` was populated from `m.Name` (base config name) instead of `ResolvedModule.Name` (env-override lifted from `Config["name"]`). When infra apply had env-renamed a module (e.g. BMW's `bmw-app` → `bmw-staging` for staging), the deploy phase used the base name for `driver.Read` lookup, didn't find the resource, and went down the Create path — producing duplicate DO resources. Same class as v0.18.7 Task #32 fix, but in the ci-run code path. BMW deploy run 24888583717 is the regression case. Closes task #60.
- **`infra_output` source resolution now applies env override to module name** — `secrets.generate[].source: "bmw-database.uri"` now resolves to the env-resolved state key (e.g. `bmw-staging-db`) when `--env staging` is set, matching how infra apply persists state. Closes task #56.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v0.18.9 changelog entry says it “Closes task #60”, but the linked issue #60 in this PR is about plugin handler coupling (PipelineHandler() getter), not env-resolution consistency. If this is intended to auto-close the GitHub issue, the reference looks incorrect—please adjust/remove to avoid closing an unrelated issue.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

@intel352 intel352 Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 25602af — replaced the ambiguous "Closes task #60" (which GitHub resolves to issue #60 - PipelineHandler getter) with "Root cause from BMW deploy run 24888583717".

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 24, 2026

⏱ Benchmark Results

No significant performance regressions detected.

benchstat comparison (baseline → PR)
## benchstat: baseline → PR
baseline-bench.txt:245: parsing iteration count: invalid syntax
baseline-bench.txt:287901: parsing iteration count: invalid syntax
baseline-bench.txt:639262: parsing iteration count: invalid syntax
baseline-bench.txt:948785: parsing iteration count: invalid syntax
baseline-bench.txt:1240620: parsing iteration count: invalid syntax
baseline-bench.txt:1531048: parsing iteration count: invalid syntax
benchmark-results.txt:245: parsing iteration count: invalid syntax
benchmark-results.txt:315156: parsing iteration count: invalid syntax
benchmark-results.txt:825007: parsing iteration count: invalid syntax
benchmark-results.txt:1154660: parsing iteration count: invalid syntax
benchmark-results.txt:1415796: parsing iteration count: invalid syntax
benchmark-results.txt:1714054: parsing iteration count: invalid syntax
goos: linux
goarch: amd64
pkg: github.com/GoCodeAlone/workflow/dynamic
cpu: AMD EPYC 7763 64-Core Processor                
                            │ benchmark-results.txt │
                            │        sec/op         │
InterpreterCreation-4                 3.244m ± 185%
ComponentLoad-4                       3.568m ±   0%
ComponentExecute-4                    1.965µ ±   2%
PoolContention/workers-1-4            1.086µ ±   4%
PoolContention/workers-2-4            1.084µ ±   1%
PoolContention/workers-4-4            1.091µ ±   1%
PoolContention/workers-8-4            1.091µ ±   1%
PoolContention/workers-16-4           1.091µ ±   1%
ComponentLifecycle-4                  3.589m ±   0%
SourceValidation-4                    2.252µ ±   1%
RegistryConcurrent-4                  787.6n ±   8%
LoaderLoadFromString-4                3.590m ±   0%
geomean                               17.44µ

                            │ benchmark-results.txt │
                            │         B/op          │
InterpreterCreation-4                  2.027Mi ± 0%
ComponentLoad-4                        2.180Mi ± 0%
ComponentExecute-4                     1.203Ki ± 0%
PoolContention/workers-1-4             1.203Ki ± 0%
PoolContention/workers-2-4             1.203Ki ± 0%
PoolContention/workers-4-4             1.203Ki ± 0%
PoolContention/workers-8-4             1.203Ki ± 0%
PoolContention/workers-16-4            1.203Ki ± 0%
ComponentLifecycle-4                   2.183Mi ± 0%
SourceValidation-4                     1.984Ki ± 0%
RegistryConcurrent-4                   1.133Ki ± 0%
LoaderLoadFromString-4                 2.182Mi ± 0%
geomean                                15.25Ki

                            │ benchmark-results.txt │
                            │       allocs/op       │
InterpreterCreation-4                   15.68k ± 0%
ComponentLoad-4                         18.02k ± 0%
ComponentExecute-4                       25.00 ± 0%
PoolContention/workers-1-4               25.00 ± 0%
PoolContention/workers-2-4               25.00 ± 0%
PoolContention/workers-4-4               25.00 ± 0%
PoolContention/workers-8-4               25.00 ± 0%
PoolContention/workers-16-4              25.00 ± 0%
ComponentLifecycle-4                    18.07k ± 0%
SourceValidation-4                       32.00 ± 0%
RegistryConcurrent-4                     2.000 ± 0%
LoaderLoadFromString-4                  18.06k ± 0%
geomean                                  183.3

cpu: AMD EPYC 9V74 80-Core Processor                
                            │ baseline-bench.txt │
                            │       sec/op       │
InterpreterCreation-4              2.979m ± 178%
ComponentLoad-4                    3.504m ±   1%
ComponentExecute-4                 1.835µ ±   2%
PoolContention/workers-1-4         1.037µ ±   4%
PoolContention/workers-2-4         1.040µ ±   6%
PoolContention/workers-4-4         1.047µ ±   3%
PoolContention/workers-8-4         1.044µ ±   3%
PoolContention/workers-16-4        1.031µ ±   1%
ComponentLifecycle-4               3.551m ±   1%
SourceValidation-4                 2.091µ ±   1%
RegistryConcurrent-4               756.4n ±  16%
LoaderLoadFromString-4             3.557m ±   1%
geomean                            16.68µ

                            │ baseline-bench.txt │
                            │        B/op        │
InterpreterCreation-4               2.027Mi ± 0%
ComponentLoad-4                     2.180Mi ± 0%
ComponentExecute-4                  1.203Ki ± 0%
PoolContention/workers-1-4          1.203Ki ± 0%
PoolContention/workers-2-4          1.203Ki ± 0%
PoolContention/workers-4-4          1.203Ki ± 0%
PoolContention/workers-8-4          1.203Ki ± 0%
PoolContention/workers-16-4         1.203Ki ± 0%
ComponentLifecycle-4                2.183Mi ± 0%
SourceValidation-4                  1.984Ki ± 0%
RegistryConcurrent-4                1.133Ki ± 0%
LoaderLoadFromString-4              2.182Mi ± 0%
geomean                             15.25Ki

                            │ baseline-bench.txt │
                            │     allocs/op      │
InterpreterCreation-4                15.68k ± 0%
ComponentLoad-4                      18.02k ± 0%
ComponentExecute-4                    25.00 ± 0%
PoolContention/workers-1-4            25.00 ± 0%
PoolContention/workers-2-4            25.00 ± 0%
PoolContention/workers-4-4            25.00 ± 0%
PoolContention/workers-8-4            25.00 ± 0%
PoolContention/workers-16-4           25.00 ± 0%
ComponentLifecycle-4                 18.07k ± 0%
SourceValidation-4                    32.00 ± 0%
RegistryConcurrent-4                  2.000 ± 0%
LoaderLoadFromString-4               18.06k ± 0%
geomean                               183.3

pkg: github.com/GoCodeAlone/workflow/middleware
cpu: AMD EPYC 7763 64-Core Processor                
                                  │ benchmark-results.txt │
                                  │        sec/op         │
CircuitBreakerDetection-4                     291.0n ± 7%
CircuitBreakerExecution_Success-4             21.56n ± 1%
CircuitBreakerExecution_Failure-4             65.94n ± 2%
geomean                                       74.51n

                                  │ benchmark-results.txt │
                                  │         B/op          │
CircuitBreakerDetection-4                    144.0 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

                                  │ benchmark-results.txt │
                                  │       allocs/op       │
CircuitBreakerDetection-4                    1.000 ± 0%
CircuitBreakerExecution_Success-4            0.000 ± 0%
CircuitBreakerExecution_Failure-4            0.000 ± 0%
geomean                                                 ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                  │ baseline-bench.txt │
                                  │       sec/op       │
CircuitBreakerDetection-4                  295.8n ± 7%
CircuitBreakerExecution_Success-4          22.68n ± 1%
CircuitBreakerExecution_Failure-4          71.08n ± 0%
geomean                                    78.13n

                                  │ baseline-bench.txt │
                                  │        B/op        │
CircuitBreakerDetection-4                 144.0 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

                                  │ baseline-bench.txt │
                                  │     allocs/op      │
CircuitBreakerDetection-4                 1.000 ± 0%
CircuitBreakerExecution_Success-4         0.000 ± 0%
CircuitBreakerExecution_Failure-4         0.000 ± 0%
geomean                                              ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/module
cpu: AMD EPYC 7763 64-Core Processor                
                                 │ benchmark-results.txt │
                                 │        sec/op         │
JQTransform_Simple-4                        959.2n ± 18%
JQTransform_ObjectConstruction-4            1.439µ ±  3%
JQTransform_ArraySelect-4                   3.264µ ±  0%
JQTransform_Complex-4                       37.96µ ±  0%
JQTransform_Throughput-4                    1.758µ ±  1%
SSEPublishDelivery-4                        70.59n ±  1%
geomean                                     1.664µ

                                 │ benchmark-results.txt │
                                 │         B/op          │
JQTransform_Simple-4                      1.273Ki ± 0%
JQTransform_ObjectConstruction-4          1.773Ki ± 0%
JQTransform_ArraySelect-4                 2.625Ki ± 0%
JQTransform_Complex-4                     16.22Ki ± 0%
JQTransform_Throughput-4                  1.984Ki ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                 │ benchmark-results.txt │
                                 │       allocs/op       │
JQTransform_Simple-4                        10.00 ± 0%
JQTransform_ObjectConstruction-4            15.00 ± 0%
JQTransform_ArraySelect-4                   30.00 ± 0%
JQTransform_Complex-4                       324.0 ± 0%
JQTransform_Throughput-4                    17.00 ± 0%
SSEPublishDelivery-4                        0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                 │ baseline-bench.txt │
                                 │       sec/op       │
JQTransform_Simple-4                     829.9n ± 28%
JQTransform_ObjectConstruction-4         1.382µ ±  1%
JQTransform_ArraySelect-4                3.335µ ±  1%
JQTransform_Complex-4                    41.67µ ±  1%
JQTransform_Throughput-4                 1.690µ ±  2%
SSEPublishDelivery-4                     65.12n ±  2%
geomean                                  1.612µ

                                 │ baseline-bench.txt │
                                 │        B/op        │
JQTransform_Simple-4                   1.273Ki ± 0%
JQTransform_ObjectConstruction-4       1.773Ki ± 0%
JQTransform_ArraySelect-4              2.625Ki ± 0%
JQTransform_Complex-4                  16.22Ki ± 0%
JQTransform_Throughput-4               1.984Ki ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

                                 │ baseline-bench.txt │
                                 │     allocs/op      │
JQTransform_Simple-4                     10.00 ± 0%
JQTransform_ObjectConstruction-4         15.00 ± 0%
JQTransform_ArraySelect-4                30.00 ± 0%
JQTransform_Complex-4                    324.0 ± 0%
JQTransform_Throughput-4                 17.00 ± 0%
SSEPublishDelivery-4                     0.000 ± 0%
geomean                                             ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/schema
cpu: AMD EPYC 7763 64-Core Processor                
                                    │ benchmark-results.txt │
                                    │        sec/op         │
SchemaValidation_Simple-4                       1.103µ ± 2%
SchemaValidation_AllFields-4                    1.681µ ± 3%
SchemaValidation_FormatValidation-4             1.604µ ± 2%
SchemaValidation_ManySchemas-4                  1.806µ ± 2%
geomean                                         1.522µ

                                    │ benchmark-results.txt │
                                    │         B/op          │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

                                    │ benchmark-results.txt │
                                    │       allocs/op       │
SchemaValidation_Simple-4                      0.000 ± 0%
SchemaValidation_AllFields-4                   0.000 ± 0%
SchemaValidation_FormatValidation-4            0.000 ± 0%
SchemaValidation_ManySchemas-4                 0.000 ± 0%
geomean                                                   ¹
¹ summaries must be >0 to compute geomean

cpu: AMD EPYC 9V74 80-Core Processor                
                                    │ baseline-bench.txt │
                                    │       sec/op       │
SchemaValidation_Simple-4                    1.057µ ± 2%
SchemaValidation_AllFields-4                 1.619µ ± 9%
SchemaValidation_FormatValidation-4          1.565µ ± 3%
SchemaValidation_ManySchemas-4               1.581µ ± 3%
geomean                                      1.434µ

                                    │ baseline-bench.txt │
                                    │        B/op        │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

                                    │ baseline-bench.txt │
                                    │     allocs/op      │
SchemaValidation_Simple-4                   0.000 ± 0%
SchemaValidation_AllFields-4                0.000 ± 0%
SchemaValidation_FormatValidation-4         0.000 ± 0%
SchemaValidation_ManySchemas-4              0.000 ± 0%
geomean                                                ¹
¹ summaries must be >0 to compute geomean

pkg: github.com/GoCodeAlone/workflow/store
cpu: AMD EPYC 7763 64-Core Processor                
                                   │ benchmark-results.txt │
                                   │        sec/op         │
EventStoreAppend_InMemory-4                   1.170µ ± 21%
EventStoreAppend_SQLite-4                     1.324m ±  2%
GetTimeline_InMemory/events-10-4              13.85µ ±  3%
GetTimeline_InMemory/events-50-4              75.97µ ±  4%
GetTimeline_InMemory/events-100-4             153.0µ ±  1%
GetTimeline_InMemory/events-500-4             663.5µ ± 20%
GetTimeline_InMemory/events-1000-4            1.276m ±  0%
GetTimeline_SQLite/events-10-4                107.4µ ±  1%
GetTimeline_SQLite/events-50-4                248.0µ ±  0%
GetTimeline_SQLite/events-100-4               423.9µ ±  1%
GetTimeline_SQLite/events-500-4               1.774m ±  0%
GetTimeline_SQLite/events-1000-4              3.471m ±  1%
geomean                                       222.4µ

                                   │ benchmark-results.txt │
                                   │         B/op          │
EventStoreAppend_InMemory-4                     791.0 ± 7%
EventStoreAppend_SQLite-4                     1.987Ki ± 2%
GetTimeline_InMemory/events-10-4              7.953Ki ± 0%
GetTimeline_InMemory/events-50-4              46.62Ki ± 0%
GetTimeline_InMemory/events-100-4             94.48Ki ± 0%
GetTimeline_InMemory/events-500-4             472.8Ki ± 0%
GetTimeline_InMemory/events-1000-4            944.3Ki ± 0%
GetTimeline_SQLite/events-10-4                16.74Ki ± 0%
GetTimeline_SQLite/events-50-4                87.14Ki ± 0%
GetTimeline_SQLite/events-100-4               175.4Ki ± 0%
GetTimeline_SQLite/events-500-4               846.1Ki ± 0%
GetTimeline_SQLite/events-1000-4              1.639Mi ± 0%
geomean                                       67.36Ki

                                   │ benchmark-results.txt │
                                   │       allocs/op       │
EventStoreAppend_InMemory-4                     7.000 ± 0%
EventStoreAppend_SQLite-4                       53.00 ± 0%
GetTimeline_InMemory/events-10-4                125.0 ± 0%
GetTimeline_InMemory/events-50-4                653.0 ± 0%
GetTimeline_InMemory/events-100-4              1.306k ± 0%
GetTimeline_InMemory/events-500-4              6.514k ± 0%
GetTimeline_InMemory/events-1000-4             13.02k ± 0%
GetTimeline_SQLite/events-10-4                  382.0 ± 0%
GetTimeline_SQLite/events-50-4                 1.852k ± 0%
GetTimeline_SQLite/events-100-4                3.681k ± 0%
GetTimeline_SQLite/events-500-4                18.54k ± 0%
GetTimeline_SQLite/events-1000-4               37.29k ± 0%
geomean                                        1.162k

cpu: AMD EPYC 9V74 80-Core Processor                
                                   │ baseline-bench.txt │
                                   │       sec/op       │
EventStoreAppend_InMemory-4                1.091µ ± 18%
EventStoreAppend_SQLite-4                  1.070m ±  3%
GetTimeline_InMemory/events-10-4           12.45µ ±  3%
GetTimeline_InMemory/events-50-4           53.55µ ± 34%
GetTimeline_InMemory/events-100-4          106.5µ ±  1%
GetTimeline_InMemory/events-500-4          544.5µ ±  1%
GetTimeline_InMemory/events-1000-4         1.117m ±  1%
GetTimeline_SQLite/events-10-4             84.23µ ±  0%
GetTimeline_SQLite/events-50-4             220.3µ ±  1%
GetTimeline_SQLite/events-100-4            384.5µ ±  1%
GetTimeline_SQLite/events-500-4            1.663m ±  1%
GetTimeline_SQLite/events-1000-4           3.390m ±  3%
geomean                                    188.6µ

                                   │ baseline-bench.txt │
                                   │        B/op        │
EventStoreAppend_InMemory-4                 766.5 ± 13%
EventStoreAppend_SQLite-4                 1.985Ki ±  2%
GetTimeline_InMemory/events-10-4          7.953Ki ±  0%
GetTimeline_InMemory/events-50-4          46.62Ki ±  0%
GetTimeline_InMemory/events-100-4         94.48Ki ±  0%
GetTimeline_InMemory/events-500-4         472.8Ki ±  0%
GetTimeline_InMemory/events-1000-4        944.3Ki ±  0%
GetTimeline_SQLite/events-10-4            16.74Ki ±  0%
GetTimeline_SQLite/events-50-4            87.14Ki ±  0%
GetTimeline_SQLite/events-100-4           175.4Ki ±  0%
GetTimeline_SQLite/events-500-4           846.1Ki ±  0%
GetTimeline_SQLite/events-1000-4          1.639Mi ±  0%
geomean                                   67.18Ki

                                   │ baseline-bench.txt │
                                   │     allocs/op      │
EventStoreAppend_InMemory-4                  7.000 ± 0%
EventStoreAppend_SQLite-4                    53.00 ± 0%
GetTimeline_InMemory/events-10-4             125.0 ± 0%
GetTimeline_InMemory/events-50-4             653.0 ± 0%
GetTimeline_InMemory/events-100-4           1.306k ± 0%
GetTimeline_InMemory/events-500-4           6.514k ± 0%
GetTimeline_InMemory/events-1000-4          13.02k ± 0%
GetTimeline_SQLite/events-10-4               382.0 ± 0%
GetTimeline_SQLite/events-50-4              1.852k ± 0%
GetTimeline_SQLite/events-100-4             3.681k ± 0%
GetTimeline_SQLite/events-500-4             18.54k ± 0%
GetTimeline_SQLite/events-1000-4            37.29k ± 0%
geomean                                     1.162k

Benchmarks run with go test -bench=. -benchmem -count=6.
Regressions ≥ 20% are flagged. Results compared via benchstat.

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

@copilot review

Review complete. The two core fixes (env-resolved name in deploy_providers.go and resolveInfraOutput in infra_output_secrets.go) are correct — ResolveForEnv clones its config map so there's no aliasing risk, and all three call sites in newPluginDeployProvider now read resolved.Name instead of m.Name.

One bug found and fixed in 6674def: stateKeys() had a doc comment claiming "sorted keys" but didn't call sort.Strings. Added the sort so error messages like "infra_output: module %q not found in state (available: bmw-app, bmw-staging-db)" are deterministic rather than random map-iteration order.

- resolveInfraOutput: ResolveForEnv ok=false now errors (config error)
  instead of silently falling back to base module name — prevents
  the env-resolution fix from being bypassed on misconfigured envs
- stateKeys: add sort.Strings so error messages list available modules
  in deterministic order (comment already said "sorted")
- infra.go: surface config.LoadFromFile error instead of discarding it —
  silent failure would regress env resolution to the pre-fix nil-wfCfg path
- CHANGELOG: replace "Closes task #60" (ambiguous GitHub issue ref) with
  "Root cause from BMW deploy run 24888583717"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 13:45
@intel352
Copy link
Copy Markdown
Contributor Author

@copilot review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread cmd/wfctl/infra_output_secrets.go Outdated
Comment thread cmd/wfctl/infra.go Outdated
Comment on lines 828 to 834
wfCfg, err := config.LoadFromFile(cfgFile)
if err != nil {
return fmt.Errorf("load config for infra_output env resolution: %w", err)
}
return syncInfraOutputSecrets(ctx, secretsCfg, secretsProvider, states, wfCfg, envName)
}

Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

runInfraApply now always reloads the full workflow config via config.LoadFromFile(cfgFile) before calling syncInfraOutputSecrets. This work is unnecessary when envName == "" (no env resolution needed) and also when there are no infra_output generators (since syncInfraOutputSecrets will return early). Consider only loading/passing wfCfg when envName != "" and at least one secrets.generate[] entry has Type == "infra_output"; otherwise pass nil to avoid extra IO/parse overhead in the common case.

Suggested change
wfCfg, err := config.LoadFromFile(cfgFile)
if err != nil {
return fmt.Errorf("load config for infra_output env resolution: %w", err)
}
return syncInfraOutputSecrets(ctx, secretsCfg, secretsProvider, states, wfCfg, envName)
}
var wfCfg any
if envName != "" && hasInfraOutputGenerators(secretsCfg) {
wfCfg, err = config.LoadFromFile(cfgFile)
if err != nil {
return fmt.Errorf("load config for infra_output env resolution: %w", err)
}
}
return syncInfraOutputSecrets(ctx, secretsCfg, secretsProvider, states, wfCfg, envName)
}
func hasInfraOutputGenerators(secretsCfg *secrets.Config) bool {
if secretsCfg == nil {
return false
}
for _, gen := range secretsCfg.Generate {
if gen.Type == "infra_output" {
return true
}
}
return false
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b50e3f3 — LoadFromFile is now gated: only called when envName != "" AND at least one secretsCfg.Generate entry has Type=="infra_output". When either condition is false, wfCfg remains nil and syncInfraOutputSecrets skips env resolution via its existing nil-guard.

…ule in resolveInfraOutput

Agent-Logs-Url: https://github.com/GoCodeAlone/workflow/sessions/3accbfdf-259b-4b98-a44e-8b538d3f5857

Co-authored-by: intel352 <77607+intel352@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 24, 2026

@copilot review

Review complete. Two remaining issues found and fixed in 4bc15d2:

  1. Misleading error messageresolveInfraOutput said "(no environment override defined)" for the ok=false path, but ok=false means Environments[envName] == nil (explicitly disabled), not that no override exists. If no override is defined, ResolveForEnv returns ok=true with the base config. Fixed to: "explicitly disabled for environment %q — cannot read infra_output from a disabled module".

  2. Missing test for disabled-module path — Added TestInfraOutput_ExplicitlyDisabledModuleErrors covering the nil environments map entry, confirming the correct error is returned instead of a silent base-name fallback.

All existing tests continue to pass.

Skip config.LoadFromFile when env resolution is not needed:
- envName="" → no env resolution, wfCfg=nil is correct
- no infra_output generators → syncInfraOutputSecrets ignores wfCfg

Avoids unnecessary file I/O on every infra apply when the caller
has no infra_output secrets or is not running with --env.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 24, 2026 14:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Comment thread cmd/wfctl/infra.go
Comment on lines +832 to +844
var wfCfg *config.WorkflowConfig
if envName != "" {
for _, g := range secretsCfg.Generate {
if g.Type == "infra_output" {
var loadErr error
wfCfg, loadErr = config.LoadFromFile(cfgFile)
if loadErr != nil {
return fmt.Errorf("load config for infra_output env resolution: %w", loadErr)
}
break
}
}
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secretsCfg can be nil here, which would panic when evaluating secretsCfg.Generate. Since syncInfraOutputSecrets already treats a nil secrets config as a no-op, add an explicit guard (e.g., if envName != "" && secretsCfg != nil { ... }) before ranging over secretsCfg.Generate.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declined — secretsCfg cannot be nil at this point. Line 820 guards: if err != nil || secretsCfg == nil { return err }. Execution only reaches the optimization block when secretsCfg is non-nil. Adding a redundant guard would mislead future readers into thinking nil is possible here. b50e3f3 is correct as-is.

@intel352 intel352 requested a review from Copilot April 24, 2026 14:13
@intel352 intel352 merged commit 856a7b9 into main Apr 24, 2026
23 of 24 checks passed
@intel352 intel352 deleted the fix/v0.18.9-phase-continuation branch April 24, 2026 14:14
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.

Comment thread CHANGELOG.md
### Tests

- `cmd/wfctl/deploy_providers_env_test.go` — `TestPluginDeployProvider_UsesEnvResolvedName`, `TestPluginDeployProvider_FallsBackToModuleNameWhenNoEnv`
- `cmd/wfctl/infra_secrets_env_test.go` — `TestInfraOutput_EnvResolvesModuleSource`, `TestInfraOutput_NoEnvUsesBaseName`
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The v0.18.9 changelog test list omits TestInfraOutput_ExplicitlyDisabledModuleErrors, which was added in cmd/wfctl/infra_secrets_env_test.go. Please update this line to include the third test (or list the file without enumerating specific test names) so the changelog stays accurate.

Suggested change
- `cmd/wfctl/infra_secrets_env_test.go``TestInfraOutput_EnvResolvesModuleSource`, `TestInfraOutput_NoEnvUsesBaseName`
- `cmd/wfctl/infra_secrets_env_test.go``TestInfraOutput_EnvResolvesModuleSource`, `TestInfraOutput_NoEnvUsesBaseName`, `TestInfraOutput_ExplicitlyDisabledModuleErrors`

Copilot uses AI. Check for mistakes.
Comment on lines +66 to +81
if stateOutputs == nil {
return "", fmt.Errorf("infra_output: state outputs not available for source %q — did infra apply succeed?", source)
}
outputs, ok := stateOutputs[moduleName]
if !ok {
return "", fmt.Errorf("infra_output: module %q not found in state (available: %s)", moduleName, strings.Join(stateKeys(stateOutputs), ", "))
}
val, ok := outputs[field]
if !ok {
return "", fmt.Errorf("infra_output: field %q not found in outputs of module %q", field, moduleName)
}
s, ok := val.(string)
if !ok {
return "", fmt.Errorf("infra_output: output field %q of module %q is %T, expected string", field, moduleName, val)
}
return s, nil
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveInfraOutput re-implements the existing secrets.GenerateSecret "infra_output" generator logic (source parsing, state lookup, type assertion, and related error messages). This duplicates behavior across packages and risks drift/inconsistent user-facing errors. Consider reusing the existing generator by doing only the env-specific module-name resolution here (including the explicit-disabled error), then calling secrets.GenerateSecret("infra_output", ...) with an updated source and the _state_outputs map. That keeps infra_output semantics in one place and preserves existing error wording/prefixes.

Suggested change
if stateOutputs == nil {
return "", fmt.Errorf("infra_output: state outputs not available for source %q — did infra apply succeed?", source)
}
outputs, ok := stateOutputs[moduleName]
if !ok {
return "", fmt.Errorf("infra_output: module %q not found in state (available: %s)", moduleName, strings.Join(stateKeys(stateOutputs), ", "))
}
val, ok := outputs[field]
if !ok {
return "", fmt.Errorf("infra_output: field %q not found in outputs of module %q", field, moduleName)
}
s, ok := val.(string)
if !ok {
return "", fmt.Errorf("infra_output: output field %q of module %q is %T, expected string", field, moduleName, val)
}
return s, nil
return secrets.GenerateSecret(
"infra_output",
map[string]any{
"source": moduleName + "." + field,
},
map[string]any{
"_state_outputs": stateOutputs,
},
)

Copilot uses AI. Check for mistakes.
@intel352
Copy link
Copy Markdown
Contributor Author

Round-4 follow-up (PR merged before fixes landed): Finding 1 (CHANGELOG test name): TestInfraOutput_ExplicitlyDisabledModuleErrors is in the test file (4bc15d2) — CHANGELOG omission is doc-only, can be a follow-up. Finding 2 (DRY refactor): Declined — out-of-scope for v0.18.9 hotfix per team review. Cross-package refactor + error prefix change logged as follow-up task.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin exposes concrete handler type via getter (plugins/pipelinesteps/plugin.go)

3 participants