All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
- All legacy DigitalOcean IaC modules (
platform.do_app,platform.do_database,platform.do_dns,platform.do_networking,platform.doks) and the DO credential resolvercloud_account_do.go. - All legacy DigitalOcean pipeline steps (
step.do_deploy,step.do_status,step.do_logs,step.do_scale,step.do_destroy). - The
github.com/digitalocean/gododependency fromgo.mod(root andexample/).
DigitalOcean IaC moved to workflow-plugin-digitalocean v0.12.0+. After loading the plugin, replace legacy module types with infra.* types and provider: digitalocean. Run wfctl modernize --apply <config.yaml> to auto-rewrite supported types — then manually add provider: digitalocean to each rewritten module's config: block (the modernize rule does not inject the provider key; see the migration guide for the exact recipe). Two step types (step.do_logs, step.do_scale) have no 1:1 pipeline successor — workarounds documented in the migration guide.
Configs that still reference the legacy types now fail to load with an actionable error pointing to the plugin and the relevant infra.* successor.
module.PrevalidatedRollingDriverandmodule.PrevalidatedRollingDriverProvideras compatibility aliases for providers whose deployment strategy validates a candidate image before rolling the stable service. The legacyBlueGreenDrivernames remain available for existing providers andstep.deploy_blue_greenconfigs.
- Plugin-owned top-level wfctl commands now resolve project-local installs from
WFCTL_PLUGIN_DIRordata/pluginsbefore falling back to global plugins, so repo-pinned commands such aswfctl dns ...are not shadowed or missed by global-only lookup. wfctl validate --plugin-dirnow registers pluginresourceTypesfrom both flat manifests andcapabilities.iacProvider.resourceTypes, so plugin-backedinfra.*resource modules validate the same waywfctl infra plan/applyconsume them.
interfaces.IaCProvider.Applyremoved. Plugins must implement v2 dispatch (declareCapabilitiesResponse.compute_plan_version="v2"via the typed RPC) and drop theirApplyGo method.pb.IaCProviderRequired.ApplyRPC removed;ApplyRequest/ApplyResponse/ApplyResult/ActionResultproto messages deleted (ActionStatusenum survives — surfaced through engine-sideinterfaces.ActionOutcome).iac/wfctlhelpers/dispatch.gopackage deleted (ComputePlanVersionDeclarer,DispatchVersionFor,DispatchVersionV2); v2 is the only supported dispatch path.cmd/iac-codemoddeleted (the v1→v2 migration tool no longer has a target).plugin/sdk.IaCProvider.EffectiveComputePlanVersion()accessor deleted (post-cutover, "v1" is not a valid runtime value); the manifest field remains as a parse-time validation surface.- Load-time enforcement:
cmd/wfctl/deploy_providers.go'sdiscoverAndLoadIaCProvidernow calls the typedCapabilitiesRPC at plugin handshake (with a 10s bounded context that bypasses the adapter's lifetime cache) and rejects providers whosecompute_plan_version != "v2". - Makefile lint guard added:
grep -qE '^\s*rpc Apply\s*\(' plugin/external/proto/iac.protoruns as part of thelinttarget so a future PR cannot silently re-introduce the deleted RPC. - Minimum plugin versions: aws v2.0.0+, gcp v2.0.0+, azure v2.0.0+, digitalocean v2.0.0+.
*external.RemoteModule.Dependencies()now returns the yaml-leveldependsOn:keys instead of always returningnil. The v0.51.8 fix (PR #664) only reordered thecfg.Modulesslice — but modular'sapp.Init()then runs its ownDependencyAware-driven sort over the registered modules, andRemoteModule(the wrapper used for every external-plugin module) returnednilfromDependencies(), so modular saw every external-plugin module as a root and sorted alphabetically. BMW PR #280 image-launch surfaced this as the samebmw-eventbus/bmw-streamordering race that v0.51.8 was supposed to close. EngineBuildFromConfignow filtersmodCfg.DependsOnthroughfilterResolvableDeps(drops empty strings + names not present incfg.Modules— the same edge-set topoSortModules used for ordering) and callsSetDependencies(filtered)on each module that implementsinterface{ SetDependencies([]string) }, but only when the filtered slice is non-empty, immediately after the factory returns and beforeapp.RegisterModule. (Modules with no resolvable dependsOn — empty yaml + transform-injected modules whose dependsOn is all empty/ghost — are skipped, so a constructor-time default isn't clobbered withSetDependencies(nil).)RemoteModuleimplements that setter, defensively copies the slice, and modular's Init() walker then reads it via the existingDependencies()contract. 7 unit tests cover theRemoteModulecontract (default-nil, plumb, empty-slice, overwrite, defensive-copy aliasing, plus two type-assertion pins formodular.DependencyAwareand the engine'sSetDependenciesinterface) plus 4 engine-levelBuildFromConfigtests covering the production path (basic plumb + defensive copy via raw-slice recorder + back-compat skip + real-modular Init order). Built-in modules can opt in by implementing the same setter; existing behaviour is unchanged for modules that don't.
StdEngine.BuildFromConfignow topologically sortscfg.Modulesby yamldependsOn:before module registration. Previously the engine walkedcfg.Modulesin slice order and the modular framework'sapp.Init()then walked in registration order. For external-plugin modules (which do not implementDependencyAware), yaml-leveldependsOn:was validated by the schema but ignored at init time — a child module'sInit()could fire before its parent'sInit()had registered runtime state in a plugin-local registry (broker, factory table, etc.), causing startup races like the "broker not registered within 10s" failure that bit BMW PR #279. The newtopoSortModules(inengine_module_order.go) uses Kahn's algorithm with a stable tie-break on declared index, tolerates missing dependency targets (schema validation catches those separately), and returns an error listing every module that could not be ordered (cycle members and their downstream dependents — Kahn cannot distinguish them by inDegree alone) if dependencies form a loop. 12 unit tests cover the BMW shape, parallel chains, cycles + their downstream dependents, and edge cases. Operators who used the alphabetical-prefix workaround (renaming a dependency toaaa-…) can now revert to canonical names and rely ondependsOn:to drive the order.
-
wfctl doctor app <url>: explicitly-online, read-only probe of a deployed app's health endpoint. Runs N sequential + M lightly-concurrent requests and reports latency (p50/p99), failure-origin classification (platform-edge HTML vs. app-origin JSON, using content-type/body shape only), and health flip-flop rate. Flags:--health-path,--probes,--concurrency,--timeout,--format text|json,--strict. See ADR 0054. -
lint/lockiopackage: a reusablego/analysis-style checker for "lock held across store I/O," parameterized by lock acquire/release method names, restricted I/O method names, and an allowlist. ExposesFindViolations(for a host app's own shrink-only allowlist test) andNewAnalyzer(a realanalysis.Analyzerfor golangci-lint/go vet/multichecker integration). See ADR 0054. -
GitHub environment secret destinations for IaC output sync:
secretStorescan now useprovider: githubwithconfig.environment, including${WORKFLOW_ENV}, sowfctl infra apply --env stagingwrites to GitHub Actions environment secrets instead of repository secrets. -
secrets.generate[].storerouting:infra_outputgenerators can name a store fromsecretStores, allowing provisioning output such as database URLs to be piped directly into the intended secret manager location after apply. -
Engine-side sensitive-output routing (v0.27.0):
ResourceDriveroutputs flagged withSensitive: {key: true}on Create/Update are routed through the configuredsecrets.Providerand replaced in state withsecret_ref://<resource>_<key>placeholders. Plugins remain platform-agnostic: a plugin compiled into a wfctl-from-CI run, a wfctl-from-CLI run, or a workflow-cloud server transparently gets sensitive-output handling without each host writing its own routing. Read paths (adoption, refresh) are sanitize-only — noprovider.Setcalls from a Read path (prevents cache pollution). OnSaveResourcefailure afterprovider.Setsucceeded, the engine compensates withdriver.Delete+provider.Deleteto prevent orphan cloud resources + routed secrets. Seedocs/plans/2026-05-09-engine-sensitive-output-routing-design.md. -
iac/sensitivepackage:Route(ctx, provider, resourceName, *out) (sanitized, hydrated, error),Revoke(ctx, provider, resourceName, mergedKeys) error,IsPlaceholder(v any) bool,MaskSensitiveForDiff(driverKeys, desired, current) (map, map),Placeholder(resource, key) string,PlaceholderPrefix string("secret_ref://"),SecretKey(resource, key) string. Routing trigger is exclusively per-callout.Sensitive[k]==true;ResourceDriver.SensitiveKeys()remains a display-masking-only signal. Limitation: only string-typed sensitive output values are supported in v0.27.0. -
wfctl infra audit-state-secrets(with--prune): walks state.Outputs vs.secrets.Providerto detect orphans, missing routed values, legacy plaintext, and mistakensecret://...config-references in state. Distinct fromaudit-secretswhich audits thesecrets.generateconfig block. Exit codes: 0 = no findings, 1 = findings, 2 = audit error. For write-only providers (GitHub Actions Get returns ErrUnsupported), emits structured ADVISORY lines for each placeholder it cannot verify, but does not exit non-zero on those alone. -
interfaces.ResourceReplacer: optional driver interface for resources requiring orchestration beyond naive Delete-then-Create (e.g., Droplets with attached Block Storage Volumes). Drivers implementingReplace(ctx, oldRef, spec) → (*ResourceOutput, error)take ownership of the full Replace transition; non-implementing drivers continue to useDefaultReplace. Engine-side error-prefix backstop wraps non-conforming errors with"replace: driver: "for consistent operator attribution. See ADR 0014. -
wfctlhelpers.DefaultReplace: exported version of the priordoReplacebody. Drivers opting intoResourceReplacercan delegate specific specs back to engine-default Delete-then-Create without a sentinel-error round-trip. -
Plan-time JIT resolver:
wfctl infra planandwfctl infra applynow resolve${MODULE.field}andinfra_output-typed${SECRET}references against existing state outputs before computing the diff plan. Eliminates spuriousreplaceactions caused by drivers (e.g.DropletDriver.Diff) comparing template literals against real state values. Refs whose source module is not yet in state stay templated for apply-time JIT (existing W-5 path). See ADR 0013. -
jitsubst.TryResolveSpec: lenient sibling ofResolveSpecfor plan-time use. Substitutes resolvable refs and leaves unresolved refs verbatim (with diagnostics). StrictResolveSpecsemantics and error contract unchanged. -
wfctl infra bootstrap --force-rotate <name>flag for known-bad secret recovery. Repeatable flag; also accepts comma-separated values (e.g.--force-rotate FOO,BAR --force-rotate BAZ). Validates each name againstsecrets.generate[]before touching the store (fast-fail on typos), refusesprovider_credentialtypes (must rotate upstream), best-effort deletes the existing value, regenerates via the same code path as create-from-scratch, and emits an RFC3339 audit log line to stderr (wfctl: rotated secret <name> (replaced existing value at <timestamp>)). Supported rotatable types:random_hex,random_base64,random_alphanumeric. -
interfaces.DriftClassenum with constantsDriftClassUnknown(zero value, omitempty-safe),DriftClassInSync,DriftClassGhost,DriftClassConfig. AdditiveClass DriftClassfield onDriftResultwithjson:"class,omitempty"— backwards-compatible with all existing consumers. -
wfctl infra apply --refreshflag: detect drift first, prune ghost-in-state entries (cloud returns 404 but state has the resource) before running the normal plan+apply phase. Default is dry-run (prints "would prune" without mutating); pass--auto-approveto execute. -
wfctl infra apply --allow-protected-pruneflag: required two-key for pruning resources withprotected: truein their state Outputs. Without it, protected ghosts cause an immediate error with a clear message. -
wfctl infra driftCLI output now prints drift class column (GHOST / CONFIG / IN-SYNC) for actionable operator feedback. Providers returningDriftClassUnknownfall through to the legacy Drifted-bool behavior. -
docs/wfctl/drift-recovery.mdoperator procedure covering detect→dry-run→approve flow, protected-resource handling, audit-log format, and CI integration examples.
-
applyWithProviderAndStoreandapplyPrecomputedPlanWithStoresignatures: each gains trailingcfgFile stringandhydratedOut map[string]stringparameters. Existing tests can pass empty string and nil. The cfgFile is used to load the configuredsecrets.Providerfor sensitive-output routing; the hydratedOut map is filled by the helper so post-apply consumers can rehydratesecret_ref://placeholders without going throughprovider.Get(works on write-only providers like GitHub Actions). -
applyInfraModulesreturn signature: now returns(map[string]string, error)where the map is the hydrated routed-secret values. Callers update fromif err := applyInfraModules(...)toif _, err := applyInfraModules(...)(or capture the map for hand-off tosyncInfraOutputSecrets). -
syncInfraOutputSecretsandresolveInfraOutputsignatures: each gains a trailinghydrated map[string]stringparameter for in-process routed-secret hand-off. Callers without same-process apply context passnil; sensitive placeholders then surface a documented cold-start constraint error explaining the fallback path (secret://<resource>_<key>direct reference). -
adoptExistingResourcesandrunInfraRefreshOutputs: now route state writes throughpersistResourceWithSecretRoutingin read-mode (sanitize-only). Pre-existingsecret_ref://...placeholders are preserved across re-applies; newly-declared sensitive keys on Read paths are dropped (not routed) to prevent cache pollution. -
BREAKING (
wfctl infra plan): configs declaring at least oneiac.providermodule now require the plugin process to load successfully —planinvokes the same loader thatapplyuses soplatform.ComputePlancan dispatchResourceDriver.Difffor honest Replace-action classification (rev3 / W-3b T3.6b). On plugin-load failurewfctlexits non-zero and printserror: failed to load plugin "<name>": <reason>; wfctl infra plan now requires the plugin process to compute Diff (since v0.21.0)(theerror:prefix is added by wfctl's top-level printer; the underlying error returned from the command does not include it). There is no--no-providerescape hatch (rev3 YAGNI fix); operators who need pure offline validation should usewfctl validate. Configs without anyiac.providermodule fall back to the legacy ConfigHash compare path so minimal/legacy fixtures and out-of-band scripts continue to work. -
wfctl infra planempty-desired alignment with apply: when the desired state is empty (every resource removed from config) but providers are declared,plannow matchesapply's behavior under the per-provider grouping logic: the loop over groupOrder is empty and no delete actions are emitted. To preview whatapplywould tear down, usewfctl infra destroy --dry-run(which exercises the same provider load + lists tracked state). Pre-W-3b plan emitted Delete actions for every orphan in current state via the legacy single-call ComputePlan path; this is consistent with the broader rev3 alignment between plan and apply. -
driftInfraModulesusesDriftClassconstants for output classification; drift-found message updated to suggestwfctl infra apply --refresh. -
DriftResult.Expected,DriftResult.Actual, andDriftResult.Fieldsnow carryomitemptytags (additive — previously these fields serialised asnull/[]in JSON; they are now omitted entirely when empty, which is what most consumers expect).
- Greenfield envs: no action required. Plugins that opt into routing add
Sensitive: {key: true}to theirResourceOutputreturns on Create/Update. Operators add asecrets:block to their workflow config (recommendprovider: envwith a prefix for local runs). - Pre-existing state with plaintext secrets: run
wfctl infra audit-state-secretsto enumerate. Rotate viawfctl infra bootstrap --force-rotate <name>running v0.27.0 (the rotation regenerates with engine-side routing). - Apply runs against plugins that newly emit
Sensitiveoutputs without asecrets:block: the engine now hard-fails BEFORE the per-resource persistence loop with a named-resource diagnostic. Addsecrets:to your config to proceed.
Validation status: mechanical analysis only. v0.26.0 source (verified at
git tag v0.26.0) has no awareness of secret_ref:// prefix; the legacy
Outputs: r.Outputs path writes whatever the driver returned and reads
state values verbatim. So a v0.26.0 binary running against state written
by v0.27.0 will treat secret_ref://... strings as literal values
(documented in step 3 below). Live runtime smoke against a v0.26.0
binary is deferred to first-rollback-event; the test path is well-defined
and a stop-the-line gate.
To pin to v0.26.x:
- Pin
setup-wfctl@v0.26.xand rebuild. - State records written under v0.27.0 contain
secret_ref://...placeholders. v0.26.x consumers do not understand these and treat them as literal strings (e.g.,infra_outputgenerators copy the literal placeholder into a downstream secret). - Recovery: rotate the affected secrets via
wfctl infra bootstrap --force-rotate <name>running v0.27.0 first to regenerate plaintext state, OR manually edit the state record (filesystem JSON) to inline the value fromsecrets.Provider. - The new package
iac/sensitive, helperpersistResourceWithSecretRouting, andaudit-state-secretscommand are additive; reverting the call sites to v0.26.x literalOutputs: r.Outputsshape is a one-commit revert ofinfra_apply.go+infra_refresh_outputs.go.
remoteResourceDriver.Troubleshootmissingresource_typearg — the Troubleshoot method was the onlyremoteResourceDriverdispatcher that omitted"resource_type": d.resourceTypefrom itsInvokeServiceargs map, causing the plugin to return"missing resource_type arg"and silently swallow auto-troubleshoot output (regression observed during a downstream consumer's CI deployment). Fixed by adding the missing arg. Regression gate added:TestRemoteDriver_AllMethodsSendResourceTypeis a 9-case table test covering every public ResourceDriver method; any future omission causes an immediate CI failure.
interfaces.ProviderIDFormatenum (IDFormatUnknown,IDFormatUUID,IDFormatDomainName,IDFormatARN,IDFormatFreeform) withString()and aProviderIDValidatoroptional interface thatResourceDriverimplementations can adopt to declare their identifier shape.interfaces.ValidateProviderID(s string, format ProviderIDFormat) booldispatch function with three unexported validators:validateUUID(positional, no-alloc),validateDomainName(RFC 1035 relaxed),validateARN(6-segment colon split). Unknown and unrecognized formats always return true for forward compatibility.cmd/wfctl/infra_validation.go: two helpers wired intoapplyWithProviderAndStore:validateInputProviderIDs— soft-warn (log only) beforeprovider.Applywhen an update/delete action's current-state ProviderID does not match the driver's declared format; lets the driver's self-heal path recover without blocking the apply.validateOutputProviderID— hard-fail (return error) before state write when the driver returns a malformed ProviderID for a strict format (UUID/DomainName/ARN), preventing corrupt data from ever reaching the state store.
- Validation helpers are no-ops when the
ResourceDriverdoes not implementProviderIDValidator— fully backward compatible with existing plugins.
cmd/wfctl/deploy_providers.go:remoteResourceDriver.Troubleshootwas passinginterfaces.ResourceRefas a nested struct in theInvokeServiceargs map. The gRPC transport (structpb.NewStruct) cannot encode Go structs — the plugin received empty args and Troubleshoot was a silent no-op. Args are now flattened to scalar primitives:ref_name,ref_type,ref_provider_id,failure_msg.cmd/wfctl/infra_apply.go:applyWithProviderAndStorewas passing theIaCProvidertotroubleshootAfterFailure, whose type assertion againstinterfaces.Troubleshooteralways failed — the hook was a silent no-op. Now callsprovider.ResourceDriver(ref.Type)and passes the resultingResourceDrivertotroubleshootAfterFailure, enabling real plugin-backed diagnostics onwfctl infra applyfailure.cmd/wfctl/ci_output_summary.go:WriteStepSummaryuseddefer f.Close()which discards the close error (potential data loss on buffer flush). Now uses a named return and captures the close error, surfacing it only when there is no prior write error.
interfaces.Troubleshooteroptional interface onResourceDriver. Drivers implementing it return structured[]Diagnostic(phase, cause, timestamp, log-tail detail) that wfctl renders in CI output on deploy/apply failure. No changes required for drivers that don't implement it;codes.Unimplementedis swallowed silently.cmd/wfctl/ci_output.go: CI-provider detection (GitHub Actions, GitLab CI, Jenkins, CircleCI) with grouped output via provider-native markers (::group::,section_start, dashed separators).cmd/wfctl/ci_output_summary.go: Markdown step-summary writer for$GITHUB_STEP_SUMMARY(and equivalents) with resource, root cause, phase timings, and collapsible per-diagnostic log tails.
wfctl ci run --phase deployandwfctl infra applyautomatically invokeTroubleshooter.Troubleshooton any terminal failure (health-check timeout or driver error), render diagnostics, and write the step-summary. Original exit codes and error messages are preserved (observability is additive).
- Generic
IaCProvider.StreamLogsfor real-time log display during builds — deferred to v0.19.0 alongside plugin-manifest split (#42) and provider-agnostic CI summary (#63). - AWS, GCP, Azure, tofu Troubleshoot implementations — DO only in v0.7.8; others no-op.
wfctl ci run --phase deploynow uses env-resolved module name —pluginDeployProvider.resourceNamewas populated fromm.Name(base config name) instead ofResolvedModule.Name(env-override lifted fromConfig["name"]). When infra apply had env-renamed a module (e.g. BMW'sbmw-app→bmw-stagingfor staging), the deploy phase used the base name fordriver.Readlookup, 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. Root cause from BMW deploy run 24888583717.infra_outputsource 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 stagingis set, matching how infra apply persists state. Closes task #56.
cmd/wfctl/deploy_providers_env_test.go—TestPluginDeployProvider_UsesEnvResolvedName,TestPluginDeployProvider_FallsBackToModuleNameWhenNoEnvcmd/wfctl/infra_secrets_env_test.go—TestInfraOutput_EnvResolvesModuleSource,TestInfraOutput_NoEnvUsesBaseName
resolveStateStoreandloadCurrentStatenow acceptenvName— when--envis passed towfctl infra apply/destroy/status/drift, per-environment overrides on theiac.statemodule (e.g.region,bucketprefix) are now applied before the state backend is initialised. Previously the base config was always used, so remote backends (Spaces, S3) that declare credentials or endpoints only underenvironments.<env>:failed with "region or endpoint must be set", silently falling back to no-op persistence and causing every deploy to re-create already-provisioned resources (409 cascades).
cmd/wfctl/infra_state_store_test.go—TestResolveStateStore_NoEnv_FallsBackToBase,TestResolveStateStore_EnvOverride_UsesEnvConfig,TestApplyInfraModules_PersistsResourceState(end-to-end regression gate: verifies provider.Apply results are persisted to state store)
ResolveForEnvliftsConfig["name"]intoResolvedModule.Name— when an environment override setsconfig.name, the value is promoted toResolvedModule.Nameand removed fromConfig. This closes the plan-vs-apply divergence wherewfctl infra plan --env stagingdisplayedbmw-staging-vpcbutwfctl infra apply --env stagingcreated a resource namedbmw-vpc(the raw module name). DownstreamResourceSpec.Namenow carries the env-resolved identity in both paths. (closes follow-up #32)platform.configHash— deterministic key ordering —configHashnow 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.applyWithProviderAndStorecallsprovider.ResolveSizing— for each spec with a non-emptySizefield,ResolveSizing(type, size, hints)is now called beforeplatform.ComputePlan. The returnedProviderSizing.InstanceTypeand extraSpecsare merged intospec.Configso that plan and apply agree on the concrete instance type.- Provider
closer.Close()errors logged as warnings —defer closer.Close() //nolint:errcheckreplaced with an explicit defer that writeswarning: provider %q shutdown: %vto stderr ininfra_apply.go,infra_destroy.go,infra_status_drift.go, andinfra_bootstrap.go. Plugin subprocess leaks now surface instead of being silently discarded. configHashMapininfra.gouses sorted kv encoding — aligns withplatform.configHashsoResourceState.ConfigHashvalues written during apply are comparable to hashes computed byComputePlanon the next run.
config/module_resolve_env_test.go—TestResolveForEnv_LiftsConfigNameIntoIdentity,TestResolveForEnv_PreservesNameWhenNoOverride,TestResolveForEnv_EmptyNameFieldIgnoredplatform/differ_hash_test.go—TestConfigHash_Stable_AcrossMapIterationOrder(100 iterations),TestConfigHash_EmptyMapReturnsEmpty,TestConfigHash_DifferentConfigsDifferentHashescmd/wfctl/infra_apply_test.go—TestApplyInfraModules_CallsResolveSizing_ForEachSpec,TestApplyWithProvider_LogsCloseErrorcmd/wfctl/infra_plan_apply_equivalence_test.go—TestPlanApplyEquivalence_EnvOverrideNames(end-to-end regression gate for plan-vs-apply name divergence)cmd/wfctl/infra_env_wire_test.go—TestPlanResourcesForEnv_UsesEnvOverrideNames
wfctl infra apply— state persistence —applyWithProviderAndStorenow saves aResourceStaterecord for each successfully provisioned resource. Previously, state was never written after apply, so every subsequent run re-attempted creates on resources that already existed. Save failures log a loud warning but do not abort the apply (the cloud resource exists). Delete-action resources are removed from state after provider.Destroy succeeds.loadCurrentState— remote state backends —resolveStateStoresupports filesystem and DO Spaces backends; previously only the filesystem path was wired up.loadCurrentStatenow delegates toresolveStateStoreso Spaces-backed state is readable by plan and apply.wfctl infra destroy— direct-path for infra.* modules —runInfraDestroydispatches todestroyInfraModuleswhen the config containsinfra.*modules. Previously it required a non-existentpipelines.destroypipeline and always failed for modern configs.wfctl infra status/wfctl infra drift— direct-path for infra.* modules — same dispatch fix; now callsprovider.Status/provider.DetectDriftdirectly on tracked state records.wfctl infra bootstrap— provider-interface state bucket dispatch —bootstrapStateBackendnow dispatches through theIaCProviderplugin interface. The newIaCProvider.BootstrapStateBackend(ctx, cfg) (*BootstrapResult, error)method (added tointerfaces/iac_provider.go) lets each cloud provider implement its own bucket creation logic. Self-contained backends (filesystem,memory,postgres,"") remain no-ops in wfctl core; all other backends require aniac.providermodule in the config and delegate to the plugin. After a successful bootstrap, every entry inresult.EnvVarsis printed asexport KEY=VALUE(sorted, for deterministic CI capture) andresult.Bucketis written back to the on-disk config so downstream commands load state without re-setting env vars. Concrete provider implementations (e.g. DO Spaces) are delivered via the plugin — see follow-up task #31 (DO plugin v0.7.4).wfctl infra state— postgres backend wired —resolveStateStorenow instantiatesmodule.PostgresIaCStateStoreforbackend: postgresconfigs;s3/gcs/azurereturn a clear error with contribution guidance instead of silently falling back to noop.
- wfctl infra apply — infra.* module support —
runInfraApplynow detects configs using the newinfra.*module abstraction (v0.3.55+) and, instead of requiring apipelines.applysection, computes a diff plan locally viaplatform.ComputePlanand callsIaCProvider.Applydirectly. Previously, runningwfctl infra applyagainst a moderninfra.yamlfailed with "pipeline apply not found". The legacyplatform.*path is preserved; configs without anyinfra.*modules continue to use the pipeline runner unchanged. Configs that mixinfra.*andplatform.*module types now fail fast with a descriptive error.
- wfctl plugin install private repos — for
github.com/.../releases/download/...URLs, when a GitHub token is present,downloadURLnow uses the two-step GitHub REST API flow (releases/tags/:tagto resolve the asset ID, thenreleases/assets/:idwithAccept: application/octet-stream) instead of a plain GET. The direct download URL redirects to a signed S3 URL which does not propagate the Authorization header, causing 404s on private repos. Falls back to direct GET when no token is set (public repos unaffected).
- wfctl plugin install auth —
downloadURLnow sends anAuthorization: token <tok>header for anygithub.comURL. Token resolution order:RELEASES_TOKEN→GH_TOKEN→GITHUB_TOKEN. Falls back to unauthenticated for public repos or non-GitHub URLs. Enableswfctl plugin installto fetch release assets from private GitHub repositories when a token is present in the environment (e.g. BMW CI withRELEASES_TOKEN).
Post-hoc review (two independent agents audited commit a48a9b7) surfaced the
following items; all fixed in #467 (fix/v0.18.1-polish).
- gofmt alignment —
interfaces/iac_canonical_types.go,module/tenants.go,cmd/wfctl/scaffold_dockerfile_test.gohad minor formatting drift; corrected. - Missing nil-DB test —
TestNewSQLTenantRegistry_NilDBasserts the constructor returns a descriptive error ("cfg.DB is required") whenDBis nil. - Cache-disabled coverage —
TestNewSQLTenantRegistry_CacheDisabledexercises the fullEnsure → GetBySlug → Disable → GetBySlugsequence withCacheSize=-1, proving nil-cache code paths do not panic. - Transient-error propagation —
TestEnsure_PropagatesNonNotFoundErrorguards against a regression where a transient DB error could be silently swallowed and trigger a spuriousINSERT. - Stale domain-cache invalidation —
TestUpdate_InvalidatesStaleDomainCacheverifies that afterUpdatechanges a tenant's domains, the old domain key is evicted from the LRU cache so the nextGetByDomaingoes to the DB. - Mixed-case subdomain matching —
TestSubdomainSelector_MixedCaseconfirms that both the root domain and the incoming host are lowercased before comparison, returning a consistent lowercase tenant key. - Dockerfile validation subtests —
TestValidateBaseImage_FullyQualifiedRefsconverted tot.Runsubtests with five additional cases: digest-only ref, registry-with-port, tag+digest combined, empty string, anddocker.io/library/alpine. - Double-slug in Ensure error — the outer
fmt.ErrorfinEnsureno longer re-embeds the slug (it was already present in the wrappedGetBySlugerror). - Misleading scaffold comment — replaced the ambiguous comment at
scaffold_dockerfile.go:181with a precise description of the colon-after-last-slash heuristic used to distinguish tag separators from registry host:port colons. - Hand-rolled string helpers removed —
splitByNewline,splitLines, andjoinLinesinmodule/tenants_test.goreplaced withstrings.Split/strings.Join.
TenantSpec.Slugmust be all-lowercase.TenantSpec.Validate()now returnsErrValidationwhenSlug != strings.ToLower(Slug). Mixed-case slugs were previously accepted and stored verbatim, which caused cache-key collisions and inconsistent URL routing. If you have existing tenants with mixed-case slugs, lowercase theslugcolumn before upgrading.SubdomainSelectornow returns a lowercase subdomain key. The host header is already lowercased before suffix matching, so the returned key is always lowercase.HostSelectorhas had this behaviour since v0.18.0; this change makes both selectors consistent. If your code compared the key against a mixed-case string, update the comparison.
interfaces.Tenant,interfaces.TenantRegistry— core tenant struct and registry contract (Ensure,GetByID,GetBySlug,GetByDomain,List,Update,Disable).module.SQLTenantRegistry— PostgreSQL-backedTenantRegistrywith configurable table/column names (TenantSchemaConfig) and an LRU cache (configurable size + TTL).- Embedded SQL migrations —
module.TenantsMigrationsFS()returns anembed.FSwith goose-format up/down migrations (20260422000001_tenants.up.sql). - 7 tenant selectors (
interfaces.Selector) —host,subdomain,header,cookie,jwt_claim,session, andstatic; all are composable with the resolver. module.TenantContextResolver— HTTP middleware with three combination modes:first_match— first matching selector wins.all_must_match— all selectors must agree; disagreement emitstenant.mismatchevent viaTenantMismatchEmitter, returnsErrTenantMismatch, and theTenantMiddlewareresponds 403 +{"error":"tenant.mismatch"}.consensus— configurableMinVotesthreshold; defaults to simple majority.
module.WithTenant/module.TenantFromContext— inject and retrieve the resolved tenant fromcontext.Contextvia a typed unexported key.- 5 tenant pipeline steps —
tenant_ensure,tenant_list,tenant_get_by_domain,tenant_update,tenant_disable; each resolvesTenantRegistryfrom the app service registry at execute time via the narrowtenantAppProviderinterface. wfctl tenant— CLI subcommand withensure,list,get,update,disable; supports--format table|jsonand--dsn/WFCTL_TENANT_DSN.
- 32 canonical config key constants in
interfaces(KeyName,KeyRegion,KeyImage,KeyHTTPPort,KeyInternalPorts,KeyProtocol,KeyInstanceCount,KeySize,KeyEnvVars,KeyEnvVarsSecret,KeyVPCRef,KeyAutoscaling,KeyRoutes,KeyCORS,KeyDomains,KeyHealthCheck,KeyLivenessCheck,KeyIngress,KeyEgress,KeyAlerts,KeyLogDestinations,KeyTermination,KeyMaintenance,KeyJobs,KeyWorkers,KeyStaticSites,KeySidecars,KeyBuildCommand,KeyRunCommand,KeyDockerfilePath,KeySourceDir,KeyProviderSpecific). IsCanonicalKey(string) boolandCanonicalKeys() []stringhelpers.- JSON Schema for the canonical key set (machine-readable; used by wfctl validation).
- Canonical spec types —
Job,Worker,StaticSite,Sidecar,Portstructs with full JSON/YAML tags. interfaces.IaCProvider.SupportedCanonicalKeys() []string— new method on the IaC provider interface; implementations declare which canonical keys they support.
- 12 hook event types (
pre_build,pre_target_build,post_target_build,pre_container_build,post_container_build,pre_container_push,post_container_push,pre_artifacts_publish,post_artifacts_publish,pre_build_fail,post_build,install_verify) ininterfaces. wfctlhook dispatcher — priority-ordered subprocess dispatch with configurable timeout; plugins register handlers viaBuildHookDeclarationinplugin.json.plugin.jsonschema extensions —BuildHooks,CLICommands,MigrationDrivers,PortIntrospectinRegistryCapabilities;PluginRequirement.Verifyblock (Signature,SBOM,VulnPolicy) for supply-chain verification.install_verifyhook — emitted bywfctl plugin installafter tarball download, before extraction, whenreq.Verify != nil; non-zero dispatch aborts install.
plugin.ServePluginFull— single helper for full plugin lifecycle: CLI flag dispatch, build-hook event routing, and optional gRPC server start.- Dynamic CLI command registry — plugins declare top-level
wfctlsubcommands viaCLICommandDeclarationinplugin.json;wfctldiscovers and dispatches them at runtime.
interfaces.MigrationProvider— module contract (ProvidesMigrations() (fs.FS, error),MigrationsDependencies() []string) for modules to ship embedded SQL migrations.interfaces.MigrationDriver— driver interface for executing goose-format migrations against a target database.
wfctl scaffold dockerfile— generates canonical Dockerfiles with--mode(server|library) and--base-imageflags.- Port introspection aggregator —
wfctlaggregates declared ports from all loaded plugins usingPortPathsinplugin.json; plugins may also provide aPortIntrospecthook handler.
interfaces.IaCProvidergainsSupportedCanonicalKeys() []string(breaking for implementations outside this repo — addreturn interfaces.CanonicalKeys()as a temporary stub).
-
wfctl build audit— two-layer security audit combining CI config checks and per-target Dockerfile linting.Config-level checks (six):
ci.build.security.hardened=false→ WARN- Dockerfile containers without
sbomorprovenanceconfigured → WARN - Registries without a
retention:policy → WARN requires.pluginsorplugins.externaldeclared without a.wfctl.yamllockfile → WARN- Registry
auth.envpointing to an env var not set at audit time → WARN environments.local.build.security.hardened=false→ NOTE (expected for local dev)
Target-level checks:
- Calls
builder.SecurityLint(cfg)for each typed build target (go, nodejs, custom) and aggregates findings. - For each
method: dockerfilecontainer, lints the Dockerfile for:USER root→ CRITICAL- Missing
USERdirective → CRITICAL FROM <image>:latestwithout version pinning → WARNADD https?://URL (untrusted remote fetch) → WARN- Embedded secret patterns (
password=,token=,api_key=, etc.) → CRITICAL - Base image not in
ci.build.security.base_image_policy.allow_prefixes→ WARN (when policy is set)
Exit codes: CRITICAL findings always exit 1.
--strictalso exits 1 on any WARN. Plain runs exit 0 unless CRITICAL.
- When
ci.build.security.hardened=true,wfctl build imageappends--provenance=mode=maxand--sbom=trueto everydocker buildinvocation. - Emits a warning when
DOCKER_BUILDKITis not set to1, since BuildKit is required for provenance attestation to work.
plugins/registry-gitlab— full implementation replacing the stub:Login: usesgitlab-ci-token+$CI_JOB_TOKENin CI context; falls back tooauth2+auth.envtoken.Push:docker push <ref>(GitLab accepts anything under the logged-in registry path).Prune: calls GitLab API (GET /api/v4/projects/:id/registry/repositories+DELETE .../tags/:name) to delete tags beyondretention.keep_latest.
wfctl build— top-level orchestrator; chainsgo → ui → image → pushperci.buildconfig. Flags:--config,--dry-run,--only,--skip,--tag,--format json|yaml|table,--no-push,--env.wfctl build go— builds alltype: gotargets via the built-in Go builder plugin.--targetflag to select a single target.wfctl build ui— builds alltype: nodejstargets via the built-in Node.js builder plugin.wfctl build image— builds allci.build.containers[]entries. Supportsmethod: dockerfile(BuildKit with secrets/cache/platforms) andmethod: ko. External images (external: true) are resolved via thetag_fromchain instead of being built.wfctl build push— pushes each container's image refs to registries declared inpush_to[].wfctl build custom— runs alltype: customtargets via the custom builder plugin.
plugin/builder.Builderinterface —Name(),Validate(cfg),Build(ctx, cfg, out),SecurityLint(cfg)contract for all builder plugins.- Built-in
gobuilder — invokesgo buildwith ldflags, tags, CGO, cross-compilation. SecurityLint warns on secret-embedding ldflags, CGO without link_mode, and unknown builder images. - Built-in
nodejsbuilder — runsnpm ci && npm run <script>(or yarn/pnpm). SecurityLint warns on missingpackage-lock.json. - Built-in
custombuilder — runs arbitrary shell commands viash -c. Always emits a SecurityLint warn. - Builder registry (
plugin/builder/registry.go) —Register,Get,Listwith init-time registration.
wfctl registry login— logs into declared registries. DO provider usesdoctl registry login; GHCR provider usesdocker login ghcr.io --password-stdin.wfctl registry push— pushes image refs to declared registries.wfctl registry prune— runsdoctl registry garbage-collection+ tag pruning (DO provider); GH API version pruning (GHCR provider). Preserveslatest. Dry-run supported.- RegistryProvider interface (
plugin/registry/provider.go) —Name(),Login(),Push(),Prune()withContextcarryingio.Writer+dryRun. - DO provider (
plugins/registry-do) — full Login/Push/Prune implementation. - GHCR provider (
plugins/registry-github) — full Login/Push/Prune implementation via GH API. - Stub providers for GitLab, AWS, GCP, Azure — register and return
ErrNotImplemented; full implementations in future releases.
ci.build.targets[]— typed build targets withname,type,path,config,environmentsoverrides. Supersedesbinaries:(legacybinaries:auto-coerced with deprecation warning).ci.registries[]—CIRegistry+CIRegistryAuth+CIRegistryRetentiontypes. Retention supportskeep_latest,untagged_ttl,schedule.CIContainerTargetextensions —method,ko_*,platforms,build_args,secrets,cache,target,labels,extra_flags,external,source(withtag_fromchain),push_to.CIBuildSecurity—hardened,sbom,provenance,sign,non_root,base_image_policy. Defaults applied automatically atLoadFromFiletime:hardened: true,sbom: true,provenance: slsa-3,non_root: true.TagFromEntry—env+commandfields for tag resolution chains on external images.EnvironmentConfig.Build—*CIBuildConfigfield for per-environment build overrides consumed bywfctl dev up.PluginRequirementauth —source+auth.envfields for private plugin repos.
wfctl plugin install --from-config <file>— batch-installsrequires.plugins[]from a workflow config; skips already-installed plugins.wfctl plugin lock— regenerates the.wfctl.yamllockfile fromrequires.plugins[].- Private repo auth —
requires.plugins[].auth.envtriggersgit config --global url."...".insteadOf+GOPRIVATEinjection before fetching; undone after install.
- Hardened defaults applied at load time (
LoadFromFile→applyBuildDefaults()). CIConfig.ValidateWithWarnings()— emits"hardened defaults disabled — images may not meet supply-chain baseline"whensecurity.hardened: false.- SBOM generation (
build_sbom.go) — shells out tosyftbinary; attaches viaoras attachorcosign attach sbom. No-op whensecurity.sbom: false.
environments.local.buildoverrides — target config keys merged by name; env keys win over base.- Local hardening skip — when
env == "local", auto-applied hardened defaults are replaced with{hardened: false, sbom: false}for fast iteration. - Local Docker cache — container targets under
env == "local"getcache.from: [{type: local}]injected. wfctl dev upnow callsrunDevBuild(cfgPath, "local")before starting services.
external: trueon container targets — skipsdocker build; resolves image ref viasource.tag_fromchain (env var → shell command → fallback).ResolveTag(build_resolve_tag.go) — generic tag resolver used by external and built containers.
wfctl ci initnow emits three files for GitHub Actions:.github/workflows/ci.yml— build + test (unchanged).github/workflows/deploy.yml(new) — minimal ~45-line pipeline withworkflow_runtrigger,build-imagejob usingwfctl build --push --format json, chaineddeploy-*jobs with correct SHA pinning (${{ github.event.workflow_run.head_sha || github.sha }}), andconcurrencyblock..github/workflows/registry-retention.yml(new, conditional) — emitted when any registry hasretention.schedule; wrapswfctl registry prune.
- Tutorial:
docs/tutorials/build-deploy-pipeline.md— 16-section step-by-step guide from hello-world Go deployment to polyglot multi-registry pipeline with signing and attestation. - Manual (
docs/manual/build-deploy/): 9-page reference coveringci.buildschema,ci.registriesschema, deploy environments, builder plugins, CLI reference, auth providers, security hardening, local dev, and troubleshooting.
wfctl registrynow refers to container registry commands (login,push,prune). The plugin catalog command is renamed towfctl plugin-registry. A deprecation aliaswfctl registry(routing towfctl plugin-registry) is kept until v1.0.ci.build.securitydefaults are on — configs that omitci.build.securitygethardened: true,sbom: true,provenance: slsa-3,non_root: trueapplied automatically.
- Hardened defaults are on by default. Opt out with
ci.build.security.hardened: false— a supply-chain warning is emitted. See Security Hardening. - Rust, Python, JVM, cmake builder plugins ship as separate
workflow-plugin-builder-*packages. - Kubernetes, ECS, Nomad, Cloud Run deploy targets ship as separate provider plugins.
- GitLab/AWS/GCP/Azure registry providers are stub-registered in v0.14.0; full implementations tracked in the issue tracker.
- Build + Deploy Pipeline Tutorial
- Manual: ci.build schema
- Manual: CLI reference
- Manual: Security hardening
- GitLab registry auth (T31) — full GitLab Container Registry Login/Push/Prune implementation.
- BuildKit provenance attestation (T33) —
--provenance=mode=maxattestation via BuildKit. wfctl build --security-audit(T34) — Dockerfile and builder config linting; exits 1 on critical findings (e.g.USER root, embedded secrets, policy violations).
wfctl dev(cmd/wfctl/dev.go,dev_compose.go,dev_process.go,dev_k8s.go,dev_expose.go): local development cluster management. Subcommands:up,down,logs,status,restart. Three modes: docker-compose (default), process (--local, with hot-reload via fsnotify), and minikube (--k8s). Exposure integrations: Tailscale Funnel, Cloudflare Tunnel, ngrok (--expose). Auto-detectsenvironments.local.exposure.methodwhen--exposeis omitted.wfctl wizard(cmd/wfctl/wizard.go,wizard_models.go): interactive Bubbletea TUI wizard for project setup. Eleven screens: project info → services → infrastructure → infra resolution (per-env strategy) → environments → deployment → secret stores → secret routing → secret values → CI/CD → review → write. New screens vs prior version: per-environment infra resolution (container/provision/existing with connection details for "existing"), named secret store configuration with add/remove flow, per-secret store routing (← → to assign), and bulk hidden secret input with Ctrl+G auto-generation for keys/tokens. Generates a completeapp.yamlincludingsecretStores:and per-secretstore:routing.wfctl secrets setup(cmd/wfctl/secrets_setup.go): standalone interactive command to set all secrets for a given environment. Readssecrets.entriesfrom config, resolves store per secret (env override → per-secret store → defaultStore → legacy provider), prompts with hidden terminal input, and supports--auto-gen-keysflag to auto-generate random hex values for names ending in_KEY,_SECRET,_TOKEN, or_SIGNING.- Plugin manifest
moduleInfraRequirements(config/plugin_manifest.go):PluginManifestFilestruct withmoduleInfraRequirementsmap,ModuleInfraSpec, andInfraRequirement. Allows plugin authors to declare infrastructure dependencies (type, name, Docker image, ports, secrets, providers) per module type. - Multi-store secrets (
config/secrets_config.go,config/config.go):SecretStoreConfig,SecretStoresmap onWorkflowConfig,DefaultStoreonSecretsConfig, andStorefield onSecretEntryfor per-secret store routing. - Per-environment infra resolution (
config/infra_resolution.go):InfraEnvironmentResolutionwithstrategy(container/provision/existing),dockerImage,port,provider,config, andconnection(host/port/auth). AddedEnvironmentsmap toInfraResourceConfig. SecretsProvider.Check()(cmd/wfctl/secrets_providers.go):SecretStateenum (Set/NotSet/NoAccess/FetchError/Unconfigured) andCheck()method on the interface withenvProviderimplementation.SecretStatusnow includesStoreandStatefields.- Multi-store secret resolution (
cmd/wfctl/secrets_resolve.go):ResolveSecretStore(priority: env override → per-secret store → defaultStore → legacy provider → "env"),getProviderForStore(maps SecretStores config to providers),buildSecretStatuses(access-aware status forwfctl secrets list). detect_infra_needsplugin manifest integration (cmd/wfctl/plugin_infra.go,mcp/scaffold_tools.go):LoadPluginManifestsandDetectPluginInfraNeedsscan local plugin directories forplugin.jsonmanifests and surface module-type infra requirements. MCP tool gains optionalplugins_dirparameter.
-
docs/WFCTL.md: updatedwizardreference (11 screens, new navigation keys); addedsecrets setupcommand reference with flag table and examples; updatedsecrets listdescription to mention multi-store routing. -
docs/dsl-reference.md+cmd/wfctl/dsl-reference-embedded.md: expandedinfrastructurefields with per-env resolution strategies, connection config, and extended example; addedsecretStores:section with example and relationships; updatedsecrets:section withdefaultStore, per-secretstorefield, multi-store example, andsecrets setupCLI command. -
docs/plugin-manifest-guide.md: new guide for plugin authors on declaring infrastructure requirements inplugin.jsonviamoduleInfraRequirements. -
services:config section (config/services_config.go): new top-level YAML key for multi-service applications. Each service declares a binary path, scaling policy (replicas/min/max/metric/target), exposed ports, per-service modules/pipelines, and plugins. -
mesh:config section (config/services_config.go): inter-service communication config. Declares transport (nats/http/grpc), service discovery, NATS connection details, and explicit service-to-service routes with via/subject/endpoint. -
networking:config section (config/networking_config.go): network exposure and policy config. Declares ingress entries (service, port, TLS termination), inter-service network policies, and DNS records. -
security:config section (config/security_config.go): application security policies. Fields:tls(internal/external/provider/minVersion),network(defaultPolicy),identity(provider/perService),runtime(readOnlyFilesystem/noNewPrivileges/runAsNonRoot/drop+addCapabilities),scanning(containerScan/dependencyScan/sast). -
wfctl ports list(cmd/wfctl/ports.go): scans config modules,services[*].expose, andnetworking.ingressfor port bindings; prints a table with service, module, port, protocol, and exposure classification (public/internal). -
wfctl security audit(cmd/wfctl/security_cmd.go): reports TLS, network policy, ingress TLS, auth module, runtime hardening, and scanning issues with severity HIGH/WARN/INFO. Exits non-zero on any HIGH finding. -
wfctl security generate-network-policies(cmd/wfctl/security_cmd.go): generates KubernetesNetworkPolicyYAML fromnetworking.policies+mesh.routes; outputs one file per service to--outputdirectory (default:k8s/). -
Validation (
config/services_config_validate.go,config/networking_config_validate.go):ValidateServices(scaling constraints, port ranges),ValidateMeshRoutes(from/to reference known services, via transport valid),ValidateNetworking(ingress service/port exists and is exposed, TLS provider valid, policy from required),ValidateSecurity(TLS provider valid),CrossValidate(warns on exposed ports with no ingress route). All wired intowfctl validate. -
ci:config section (config/ci_config.go): new top-level YAML key declaring build (binaries/containers/assets), test (unit/integration/e2e with ephemeral deps), deploy (per-environment with strategy/healthCheck/approval), and infra phases. IncludesValidate()method enforcing required fields. -
environments:config section (config/environments_config.go): named deployment environments with provider, region, env vars, secrets provider, and exposure config (Tailscale Funnel, Cloudflare Tunnel, port-forward). -
secrets:config section (config/secrets_config.go): provider, rotation policy (withStrategyfield fordual-credential/graceful/immediate), and declared secret entries with per-secret rotation overrides. -
wfctl ci run(cmd/wfctl/ci_run.go): executes build, test, and deploy phases from theci:config section. Build phase cross-compiles Go binaries and builds containers. Test phase supports ephemeral Docker deps (postgres/redis/mysql) vianeeds:. Deploy phase is stubbed for Tier 2. -
wfctl ci init(cmd/wfctl/ci_init.go): generates bootstrap CI YAML for GitHub Actions (.github/workflows/ci.yml) or GitLab CI (.gitlab-ci.yml), with per-environment deploy jobs derived fromci.deploy.environments. -
wfctl secrets(cmd/wfctl/secrets.go,secrets_detect.go,secrets_providers.go): secret lifecycle management. Subcommands:detect(scan config for secret-like values),set(with--valueor--from-file),list,validate,init,rotate,sync.SecretsProviderinterface withenvbackend. -
wfctl validate: now validatesci:sections usingCIConfig.Validate()when present.
docs/dsl-reference.md+cmd/wfctl/dsl-reference-embedded.md: addedservices:,mesh:,networking:, andsecurity:sections with full field reference and examples; also addedci:,environments:, andsecrets:sections.docs/WFCTL.md: addedports list,security audit, andsecurity generate-network-policiescommand documentation; updated command tree and category table. Also addedci run,ci init, andsecretsdocumentation.
wfctl infra plan|apply|bootstrap|destroy|status|driftnow accept--env <name>.wfctl infra importdoes not currently accept--env; env-scoped imports will land alongside config-aware import in a follow-up.- Module configs support an
environments:block for per-environment resolution (provider/config/image). Set an env value tonullto skip the module in that env. - Top-level
environments:envVarsare merged into container resources during infra apply;regionandproviderdefault fromenvironments[env]when a module omits them. wfctl infranow honorsimports:(consistent with every other wfctl subcommand).
ci.deploy.environments[].requireApprovalcontinues to work viawfctl ci initemittingenvironment: <name>in generated GitHub Actions. No engine change needed — GitHub's native environment approval UI handles the gate.InfraResourceConfig(underinfrastructure.resources:) already had anEnvironmentsfield but was never wired towfctl infra(which parsesmodules:). Multi-env is now wired toModuleConfig;InfraResourceConfigconsumption is deferred to a follow-up andinfrastructure.resources:remains unused by wfctl infra commands in this release.
ModuleConfigpreviously lacked anEnvironmentsfield; it was defined on the unusedInfraResourceConfigtype. Multi-env is now wired to the schemawfctl infraactually parses.
- Editor schemas golden file updated to reflect v0.4.0 schema changes (release CI fix)
This release eliminates all type: "json" schema fields, replacing them with proper typed fields (map, array, or individual sub-fields). This improves the visual editor experience — config fields that previously rendered as raw JSON textareas now render as structured form widgets. See the migration guide for details.
- 88 config fields changed from
type: "json"to typed schemas. If your tooling parseswfctl editor-schemasoutput and relies on specific field types, those fields are nowmap,array, or individual typed fields instead ofjson. The engine runtime behavior is unchanged — this only affects editor/UI consumers. wfctl editor-schemasnow includesstepSchemasalongsidemoduleSchemasandcoercionRules. Consumers parsing this JSON output will see a new top-level key.- workflow-editor npm package: The static
MODULE_TYPESarray has been removed.MODULE_TYPE_MAPis still exported but now sourced fromengine-schemas.jsoninstead of a hand-maintained array. If you importedMODULE_TYPESdirectly, switch toMODULE_TYPE_MAPorgetEngineModuleTypes().
- DSL Reference (
docs/dsl-reference.md): Canonical specification for the workflow YAML DSL covering all 12 top-level sections (application, modules, workflows, pipelines, triggers, imports, config providers, platform/infrastructure/sidecars) wfctl dsl-referencecommand: Extracts the DSL reference as structured JSON for consumption by editors and IDE plugins- Step schemas in
wfctl editor-schemas: 182 step type schemas now exported alongside 279 module type schemas - Struct-tag reflection generator (
pkg/schema/reflect.go):GenerateConfigFields()produces[]ConfigFieldDeffrom Go structeditor:"..."tags, enabling config structs to be the source of truth for editor schemas - Editor struct tags on 5 key config structs (
DatabaseConfig,HTTPServer,RedisNoSQLConfig,HealthCheckerConfig,MetricsCollectorConfig) as the initial adoption of the reflection-based schema pattern - LSP hover documentation:
textDocument/hoverreturns DSL reference descriptions and schema details for YAML keys, module types, and step types — surfaces in both VS Code and JetBrains - LSP completion with DSL descriptions:
textDocument/completionsuggests top-level keys, module types, step types, and config keys with descriptions from the DSL reference and schema registry - CI contract test (
schema/schema_contract_test.go): Zero-tolerance enforcement fortype: "json"fields in both built-in registries and plugin schemas — any new json-typed field fails CI
- 88
FieldTypeJSONconfig fields converted to proper typed schemas across built-in modules (39), built-in steps (12), and plugins (37) - Schema contract test ratcheted from
maxAllowed: 51→ unconditional zero tolerance
- Switch yaegi dependency from
github.com/traefik/yaegiv0.16.1 togithub.com/GoCodeAlone/yaegiv0.17.0 (our maintained fork)- Eval/EvalPath now recover from panics instead of crashing the host process
- Fixed binary channel type alias nil pointer, binary-to-source interface conversion
- 11-45x faster
yaegi extractwith x/tools/go/packages - Generic function import support via
//yaegi:adddirective
- Dynamic field mapping with FieldMapping type supporting fallback chains and primary/resolve/set operations
- Schema-agnostic field resolution for REST API handler modules (42+ references refactored)
- Runtime field resolution from workflow context via FieldMapping.Resolve()
- Configurable field aliases in YAML (fieldMapping, transitionMap, summaryFields)
- Engine integration: fieldMapping/transitionMap/summaryFields wired from YAML config
- 18 unit tests for FieldMapping type
- Multi-chat UI view for responders with real-time conversation switching
- Updated screenshots and user guide documentation
- Go 1.26 upgrade and security fixes
- Bump modular to v1.13.0 (consolidated sub-modules)
- Extract
TemplateEnginetopipeline/package; decouple plugins frommodulepackage (SDK boundary)
- Implement
http.Hijackerandhttp.Flusherinterfaces ontrackedResponseWriter - Bump modular dependencies to v1.12.5 / v1.15.0 / v2.8.0
- Expr template engine —
${ }syntax for pipeline stepconfigvalues alongside the existing{{ }}Go template syntax; both may be mixed in the same string- Bracket notation for hyphenated step names:
${ steps["step-name"]["key"] } - Boolean/comparison guards:
${ status == "active" && count > 0 } - String concatenation:
${ "Hello " + body["name"] } - Template functions in expr context:
${ upper(name) }
- Bracket notation for hyphenated step names:
skip_ifexpr support — pipeline steps acceptskip_if: ${ expr }for inline conditional skippingwfctl expr-migratecommand — auto-converts Go template ({{ }}) expressions to expr syntax (${ }) with--dry-run,--output, and--inplacemodes; complex patterns receive# TODO: migratecomments- LSP hover/completion for
${ }— IDE plugins surface available namespaces and function signatures inside expr expressions - docs/dsl-reference.md expr syntax section — documents all expr namespaces, operators, and migration path
- docs/migrations/v0.5.0-expr-templates.md — step-by-step migration guide for upgrading configs from Go templates to expr syntax
- Eviction added to unbounded rate-limit, cache, and lock maps (memory leak)
- Panic recovery added to 9 goroutine sites
- EventProcessor goroutine leak and CronScheduler data race
- HTTP stability: ListenError channel, trackedResponseWriter race, SSE lock contention
- Release RLock before handler dispatch; panic recovery in handler goroutines
- AI Server Bootstrap
- cmd/server/main.go with HTTP mux and AI handler registration
- CLI flags for config, address, AI provider configuration
- Graceful shutdown with signal handling
- initAIService with conditional Anthropic/Copilot provider registration
- cmd/server/main_test.go with route verification tests
- Go Test Coverage Push to 80%+
- Root package (engine_test.go): 68.6% -> 80%+
- Module package: 77.1% -> 80%+
- Dynamic package: 75.4% -> 80%+
- AI packages: maintained at 85%+
- 11 New Playwright E2E Test Suites
- Shared helpers (helpers.ts) with complete module type map
- deep-module-coverage.spec.ts: all 30 module types verified
- deep-complex-workflows.spec.ts: multi-node workflow tests
- deep-property-editing.spec.ts: all field types tested
- deep-keyboard-shortcuts.spec.ts: shortcut verification
- deep-ai-panel.spec.ts: AI Copilot panel tests
- deep-component-browser.spec.ts: Component Browser tests
- deep-import-export.spec.ts: complex round-trip tests
- deep-edge-cases.spec.ts: edge case coverage
- deep-accessibility.spec.ts: a11y testing
- deep-toast-notifications.spec.ts: toast behavior tests
- deep-visual-regression.spec.ts: visual regression baselines
- EventBus Bridge
- EventBusBridge adapter bridging MessageBroker interface to EventBus
- WorkflowEventEmitter with lifecycle events (workflow.started, workflow.completed, workflow.failed, step.started, step.completed, step.failed)
- EventBus Trigger
- EventBusTrigger for native EventBus subscriptions
- Configurable topics, event filtering, and async mode
- Start/Stop with subscription lifecycle management
- Engine Integration
- Engine integration with workflow/step event emission
- canHandleTrigger support for "eventbus" trigger type
- TriggerWorkflow emits start/complete/fail events
- UI Updates
- messaging.broker.eventbus module type in NodePalette (30 total)
- Observability Foundation
- MetricsCollector wrapping Prometheus with 6 pre-registered metric vectors and
/metricsendpoint - HealthChecker with
/health,/ready,/liveendpoints and auto-discovery - RequestIDMiddleware with
X-Request-IDpropagation and UUID generation
- MetricsCollector wrapping Prometheus with 6 pre-registered metric vectors and
- Database Module
- WorkflowDatabase wrapping
database/sqlwith Query, Execute, InsertRow, UpdateRows, DeleteRows - SQL builder helpers (BuildInsertSQL, BuildUpdateSQL, BuildDeleteSQL)
- DatabaseIntegrationConnector adapter for integration workflows
- WorkflowDatabase wrapping
- Data Transformation
- DataTransformer with named pipelines of operations (extract, map, filter, convert)
- Dot-notation JSON path extraction with array index support
- Webhook Delivery
- WebhookSender with exponential backoff retry (configurable maxRetries, backoff, timeout)
- Dead letter queue for exhausted retries with manual RetryDeadLetter support
- AI Validation Loop
- Validator with compile-test-retry cycle (import validation, Yaegi compile, required function check)
- ValidateAndFix integrating AI regeneration on validation failure
- ContextEnrichedPrompt for module/service-aware generation
- Dynamic-to-Modular Bridge
- ModuleAdapter wrapping DynamicComponent as modular.Module
- Configurable provides/requires for dependency injection
- Engine integration via
dynamic.componentmodule type
- UI Updates
- 2 new categories: Database, Observability (10 total)
- 6 new MODULE_TYPES: database.workflow, metrics.collector, health.checker, http.middleware.requestid, data.transformer, webhook.sender (29 total)
- Updated component tests for new types and categories
- Core Engine
- Workflow engine with BuildFromConfig, TriggerWorkflow lifecycle
- HTTP, Messaging, State Machine, Event workflow handlers
- HTTP, Schedule, Event trigger system
- Module factory pattern for extensible module types
- Modular Framework
- Migration from GoCodeAlone/modular to CrisisTextLine/modular fork (v1.11.11)
- Integration of all modular modules: httpserver, httpclient, chimux, scheduler, eventbus, eventlogger, cache, database, auth, jsonschema, reverseproxy
- Dynamic Component System (Yaegi)
- Interpreter pool with sandboxed execution
- Component registry with lifecycle management
- File watcher for hot-reload
- Source validation (stdlib-only imports)
- AI-Powered Generation
- WorkflowGenerator interface with LLM + Copilot SDK backends
- Anthropic Claude direct API client with tool use
- GitHub Copilot SDK integration with session management
- Deploy service bridging AI generation to dynamic components
- ReactFlow UI
- Drag-and-drop node palette with 8 categorized module groups (23 types)
- Property panel for node configuration
- YAML import/export with round-trip fidelity
- Undo/redo with history management
- Validation (local + server) and Zustand state management
- Test Infrastructure
- Go unit tests: module 73%, ai/llm 85%, dynamic 74%
- Playwright E2E: app-load, node-operations, connections, import-export, toolbar
- Vitest component tests: 100 tests across 6 files
- CI/CD
- GitHub Actions: automated testing on Go 1.23/1.24, linting, multi-platform releases
- Code coverage reporting via Codecov
- Weekly dependency updates
- Upgraded to Modular v1.3.9 with IsVerboseConfig, SetVerboseConfig, SetLogger support
- Improved error handling for service registration and I/O operations
- Critical error checking issues identified by linters
- HTTP response writing error handling
- Service registration error handling in engine
- Enhanced dependency management with automated updates
- Improved error handling to prevent potential runtime issues
Previous version history was not maintained in changelog format.