Skip to content

Latest commit

 

History

History
942 lines (740 loc) · 72.2 KB

CHANGELOG.md

File metadata and controls

942 lines (740 loc) · 72.2 KB

Changelog

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.

v0.52.0 (2026-05-13) — BREAKING

Removed (issue #617)

  • All legacy DigitalOcean IaC modules (platform.do_app, platform.do_database, platform.do_dns, platform.do_networking, platform.doks) and the DO credential resolver cloud_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/godo dependency from go.mod (root and example/).

Migration

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.


[Unreleased]

Added

  • module.PrevalidatedRollingDriver and module.PrevalidatedRollingDriverProvider as compatibility aliases for providers whose deployment strategy validates a candidate image before rolling the stable service. The legacy BlueGreenDriver names remain available for existing providers and step.deploy_blue_green configs.

Fixed

  • Plugin-owned top-level wfctl commands now resolve project-local installs from WFCTL_PLUGIN_DIR or data/plugins before falling back to global plugins, so repo-pinned commands such as wfctl dns ... are not shadowed or missed by global-only lookup.
  • wfctl validate --plugin-dir now registers plugin resourceTypes from both flat manifests and capabilities.iacProvider.resourceTypes, so plugin-backed infra.* resource modules validate the same way wfctl infra plan/apply consume them.

Breaking changes (workflow#699 — IaCProvider.Apply hard-removal)

  • interfaces.IaCProvider.Apply removed. Plugins must implement v2 dispatch (declare CapabilitiesResponse.compute_plan_version="v2" via the typed RPC) and drop their Apply Go method.
  • pb.IaCProviderRequired.Apply RPC removed; ApplyRequest/ApplyResponse/ApplyResult/ActionResult proto messages deleted (ActionStatus enum survives — surfaced through engine-side interfaces.ActionOutcome).
  • iac/wfctlhelpers/dispatch.go package deleted (ComputePlanVersionDeclarer, DispatchVersionFor, DispatchVersionV2); v2 is the only supported dispatch path.
  • cmd/iac-codemod deleted (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's discoverAndLoadIaCProvider now calls the typed Capabilities RPC at plugin handshake (with a 10s bounded context that bypasses the adapter's lifetime cache) and rejects providers whose compute_plan_version != "v2".
  • Makefile lint guard added: grep -qE '^\s*rpc Apply\s*\(' plugin/external/proto/iac.proto runs as part of the lint target 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+.

Fixed (issue #663 — follow-up)

  • *external.RemoteModule.Dependencies() now returns the yaml-level dependsOn: keys instead of always returning nil. The v0.51.8 fix (PR #664) only reordered the cfg.Modules slice — but modular's app.Init() then runs its own DependencyAware-driven sort over the registered modules, and RemoteModule (the wrapper used for every external-plugin module) returned nil from Dependencies(), so modular saw every external-plugin module as a root and sorted alphabetically. BMW PR #280 image-launch surfaced this as the same bmw-eventbus/bmw-stream ordering race that v0.51.8 was supposed to close. Engine BuildFromConfig now filters modCfg.DependsOn through filterResolvableDeps (drops empty strings + names not present in cfg.Modules — the same edge-set topoSortModules used for ordering) and calls SetDependencies(filtered) on each module that implements interface{ SetDependencies([]string) }, but only when the filtered slice is non-empty, immediately after the factory returns and before app.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 with SetDependencies(nil).) RemoteModule implements that setter, defensively copies the slice, and modular's Init() walker then reads it via the existing Dependencies() contract. 7 unit tests cover the RemoteModule contract (default-nil, plumb, empty-slice, overwrite, defensive-copy aliasing, plus two type-assertion pins for modular.DependencyAware and the engine's SetDependencies interface) plus 4 engine-level BuildFromConfig tests 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.

Fixed (issue #663)

  • StdEngine.BuildFromConfig now topologically sorts cfg.Modules by yaml dependsOn: before module registration. Previously the engine walked cfg.Modules in slice order and the modular framework's app.Init() then walked in registration order. For external-plugin modules (which do not implement DependencyAware), yaml-level dependsOn: was validated by the schema but ignored at init time — a child module's Init() could fire before its parent's Init() 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 new topoSortModules (in engine_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 to aaa-…) can now revert to canonical names and rely on dependsOn: to drive the order.

Added

  • 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/lockio package: a reusable go/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. Exposes FindViolations (for a host app's own shrink-only allowlist test) and NewAnalyzer (a real analysis.Analyzer for golangci-lint/go vet/multichecker integration). See ADR 0054.

  • GitHub environment secret destinations for IaC output sync: secretStores can now use provider: github with config.environment, including ${WORKFLOW_ENV}, so wfctl infra apply --env staging writes to GitHub Actions environment secrets instead of repository secrets.

  • secrets.generate[].store routing: infra_output generators can name a store from secretStores, 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): ResourceDriver outputs flagged with Sensitive: {key: true} on Create/Update are routed through the configured secrets.Provider and replaced in state with secret_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 — no provider.Set calls from a Read path (prevents cache pollution). On SaveResource failure after provider.Set succeeded, the engine compensates with driver.Delete + provider.Delete to prevent orphan cloud resources + routed secrets. See docs/plans/2026-05-09-engine-sensitive-output-routing-design.md.

  • iac/sensitive package: 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-call out.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.Provider to detect orphans, missing routed values, legacy plaintext, and mistaken secret://... config-references in state. Distinct from audit-secrets which audits the secrets.generate config 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 implementing Replace(ctx, oldRef, spec) → (*ResourceOutput, error) take ownership of the full Replace transition; non-implementing drivers continue to use DefaultReplace. 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 prior doReplace body. Drivers opting into ResourceReplacer can delegate specific specs back to engine-default Delete-then-Create without a sentinel-error round-trip.

  • Plan-time JIT resolver: wfctl infra plan and wfctl infra apply now resolve ${MODULE.field} and infra_output-typed ${SECRET} references against existing state outputs before computing the diff plan. Eliminates spurious replace actions 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 of ResolveSpec for plan-time use. Substitutes resolvable refs and leaves unresolved refs verbatim (with diagnostics). Strict ResolveSpec semantics 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 against secrets.generate[] before touching the store (fast-fail on typos), refuses provider_credential types (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.DriftClass enum with constants DriftClassUnknown (zero value, omitempty-safe), DriftClassInSync, DriftClassGhost, DriftClassConfig. Additive Class DriftClass field on DriftResult with json:"class,omitempty" — backwards-compatible with all existing consumers.

  • wfctl infra apply --refresh flag: 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-approve to execute.

  • wfctl infra apply --allow-protected-prune flag: required two-key for pruning resources with protected: true in their state Outputs. Without it, protected ghosts cause an immediate error with a clear message.

  • wfctl infra drift CLI output now prints drift class column (GHOST / CONFIG / IN-SYNC) for actionable operator feedback. Providers returning DriftClassUnknown fall through to the legacy Drifted-bool behavior.

  • docs/wfctl/drift-recovery.md operator procedure covering detect→dry-run→approve flow, protected-resource handling, audit-log format, and CI integration examples.

Changed

  • applyWithProviderAndStore and applyPrecomputedPlanWithStore signatures: each gains trailing cfgFile string and hydratedOut map[string]string parameters. Existing tests can pass empty string and nil. The cfgFile is used to load the configured secrets.Provider for sensitive-output routing; the hydratedOut map is filled by the helper so post-apply consumers can rehydrate secret_ref:// placeholders without going through provider.Get (works on write-only providers like GitHub Actions).

  • applyInfraModules return signature: now returns (map[string]string, error) where the map is the hydrated routed-secret values. Callers update from if err := applyInfraModules(...) to if _, err := applyInfraModules(...) (or capture the map for hand-off to syncInfraOutputSecrets).

  • syncInfraOutputSecrets and resolveInfraOutput signatures: each gains a trailing hydrated map[string]string parameter for in-process routed-secret hand-off. Callers without same-process apply context pass nil; sensitive placeholders then surface a documented cold-start constraint error explaining the fallback path (secret://<resource>_<key> direct reference).

  • adoptExistingResources and runInfraRefreshOutputs: now route state writes through persistResourceWithSecretRouting in read-mode (sanitize-only). Pre-existing secret_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 one iac.provider module now require the plugin process to load successfully — plan invokes the same loader that apply uses so platform.ComputePlan can dispatch ResourceDriver.Diff for honest Replace-action classification (rev3 / W-3b T3.6b). On plugin-load failure wfctl exits non-zero and prints error: failed to load plugin "<name>": <reason>; wfctl infra plan now requires the plugin process to compute Diff (since v0.21.0) (the error: prefix is added by wfctl's top-level printer; the underlying error returned from the command does not include it). There is no --no-provider escape hatch (rev3 YAGNI fix); operators who need pure offline validation should use wfctl validate. Configs without any iac.provider module fall back to the legacy ConfigHash compare path so minimal/legacy fixtures and out-of-band scripts continue to work.

  • wfctl infra plan empty-desired alignment with apply: when the desired state is empty (every resource removed from config) but providers are declared, plan now matches apply's behavior under the per-provider grouping logic: the loop over groupOrder is empty and no delete actions are emitted. To preview what apply would tear down, use wfctl 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.

  • driftInfraModules uses DriftClass constants for output classification; drift-found message updated to suggest wfctl infra apply --refresh.

  • DriftResult.Expected, DriftResult.Actual, and DriftResult.Fields now carry omitempty tags (additive — previously these fields serialised as null / [] in JSON; they are now omitted entirely when empty, which is what most consumers expect).

Migration (engine-side sensitive-output routing, v0.27.0)

  • Greenfield envs: no action required. Plugins that opt into routing add Sensitive: {key: true} to their ResourceOutput returns on Create/Update. Operators add a secrets: block to their workflow config (recommend provider: env with a prefix for local runs).
  • Pre-existing state with plaintext secrets: run wfctl infra audit-state-secrets to enumerate. Rotate via wfctl infra bootstrap --force-rotate <name> running v0.27.0 (the rotation regenerates with engine-side routing).
  • Apply runs against plugins that newly emit Sensitive outputs without a secrets: block: the engine now hard-fails BEFORE the per-resource persistence loop with a named-resource diagnostic. Add secrets: to your config to proceed.

Rollback (engine-side sensitive-output routing, v0.27.0)

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:

  1. Pin setup-wfctl@v0.26.x and rebuild.
  2. 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_output generators copy the literal placeholder into a downstream secret).
  3. 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 from secrets.Provider.
  4. The new package iac/sensitive, helper persistResourceWithSecretRouting, and audit-state-secrets command are additive; reverting the call sites to v0.26.x literal Outputs: r.Outputs shape is a one-commit revert of infra_apply.go + infra_refresh_outputs.go.

[0.18.11.1] - 2026-04-25

Fixed

  • remoteResourceDriver.Troubleshoot missing resource_type arg — the Troubleshoot method was the only remoteResourceDriver dispatcher that omitted "resource_type": d.resourceType from its InvokeService args 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_AllMethodsSendResourceType is a 9-case table test covering every public ResourceDriver method; any future omission causes an immediate CI failure.

[0.18.11] - 2026-04-24

Added

  • interfaces.ProviderIDFormat enum (IDFormatUnknown, IDFormatUUID, IDFormatDomainName, IDFormatARN, IDFormatFreeform) with String() and a ProviderIDValidator optional interface that ResourceDriver implementations can adopt to declare their identifier shape.
  • interfaces.ValidateProviderID(s string, format ProviderIDFormat) bool dispatch 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 into applyWithProviderAndStore:
    • validateInputProviderIDs — soft-warn (log only) before provider.Apply when 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.

Changed

  • Validation helpers are no-ops when the ResourceDriver does not implement ProviderIDValidator — fully backward compatible with existing plugins.

[0.18.10.1] - 2026-04-24

Fixed

  • cmd/wfctl/deploy_providers.go: remoteResourceDriver.Troubleshoot was passing interfaces.ResourceRef as a nested struct in the InvokeService args 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: applyWithProviderAndStore was passing the IaCProvider to troubleshootAfterFailure, whose type assertion against interfaces.Troubleshooter always failed — the hook was a silent no-op. Now calls provider.ResourceDriver(ref.Type) and passes the resulting ResourceDriver to troubleshootAfterFailure, enabling real plugin-backed diagnostics on wfctl infra apply failure.
  • cmd/wfctl/ci_output_summary.go: WriteStepSummary used defer 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.

[0.18.10] - 2026-04-24

Added

  • interfaces.Troubleshooter optional interface on ResourceDriver. 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.Unimplemented is 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.

Changed

  • wfctl ci run --phase deploy and wfctl infra apply automatically invoke Troubleshooter.Troubleshoot on 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).

Not yet

  • Generic IaCProvider.StreamLogs for 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.

[0.18.9] - 2026-04-24

Fixed

  • 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. Root cause from BMW deploy run 24888583717.
  • 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.

Tests

  • cmd/wfctl/deploy_providers_env_test.go — TestPluginDeployProvider_UsesEnvResolvedName, TestPluginDeployProvider_FallsBackToModuleNameWhenNoEnv
  • cmd/wfctl/infra_secrets_env_test.go — TestInfraOutput_EnvResolvesModuleSource, TestInfraOutput_NoEnvUsesBaseName

[0.18.8] - 2026-04-24

Fixed

  • resolveStateStore and loadCurrentState now accept envName — when --env is passed to wfctl infra apply/destroy/status/drift, per-environment overrides on the iac.state module (e.g. region, bucket prefix) 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 under environments.<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).

Tests

  • 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)

[0.18.7] - 2026-04-23

Fixed

  • ResolveForEnv lifts Config["name"] into ResolvedModule.Name — when an environment override sets config.name, the value is promoted to ResolvedModule.Name and removed from Config. This closes the plan-vs-apply divergence where wfctl infra plan --env staging displayed bmw-staging-vpc but wfctl infra apply --env staging created a resource named bmw-vpc (the raw module name). Downstream ResourceSpec.Name now carries the env-resolved identity in both paths. (closes follow-up #32)
  • platform.configHash — deterministic key ordering — configHash now sorts map keys explicitly before JSON-marshalling, matching the DO plugin's existing pattern. Previously, Go's randomised map-iteration order could produce different hashes for identical configs on successive runs, generating spurious "update" plan actions on second apply with no config change.
  • applyWithProviderAndStore calls provider.ResolveSizing — for each spec with a non-empty Size field, ResolveSizing(type, size, hints) is now called before platform.ComputePlan. The returned ProviderSizing.InstanceType and extra Specs are merged into spec.Config so that plan and apply agree on the concrete instance type.
  • Provider closer.Close() errors logged as warnings — defer closer.Close() //nolint:errcheck replaced with an explicit defer that writes warning: provider %q shutdown: %v to stderr in infra_apply.go, infra_destroy.go, infra_status_drift.go, and infra_bootstrap.go. Plugin subprocess leaks now surface instead of being silently discarded.
  • configHashMap in infra.go uses sorted kv encoding — aligns with platform.configHash so ResourceState.ConfigHash values written during apply are comparable to hashes computed by ComputePlan on the next run.

Tests

  • config/module_resolve_env_test.go — TestResolveForEnv_LiftsConfigNameIntoIdentity, TestResolveForEnv_PreservesNameWhenNoOverride, TestResolveForEnv_EmptyNameFieldIgnored
  • platform/differ_hash_test.go — TestConfigHash_Stable_AcrossMapIterationOrder (100 iterations), TestConfigHash_EmptyMapReturnsEmpty, TestConfigHash_DifferentConfigsDifferentHashes
  • cmd/wfctl/infra_apply_test.go — TestApplyInfraModules_CallsResolveSizing_ForEachSpec, TestApplyWithProvider_LogsCloseError
  • cmd/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

[0.18.6] - 2026-04-23

Fixed

  • wfctl infra apply — state persistence — applyWithProviderAndStore now saves a ResourceState record 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 — resolveStateStore supports filesystem and DO Spaces backends; previously only the filesystem path was wired up. loadCurrentState now delegates to resolveStateStore so Spaces-backed state is readable by plan and apply.
  • wfctl infra destroy — direct-path for infra.* modules — runInfraDestroy dispatches to destroyInfraModules when the config contains infra.* modules. Previously it required a non-existent pipelines.destroy pipeline and always failed for modern configs.
  • wfctl infra status / wfctl infra drift — direct-path for infra.* modules — same dispatch fix; now calls provider.Status / provider.DetectDrift directly on tracked state records.
  • wfctl infra bootstrap — provider-interface state bucket dispatch — bootstrapStateBackend now dispatches through the IaCProvider plugin interface. The new IaCProvider.BootstrapStateBackend(ctx, cfg) (*BootstrapResult, error) method (added to interfaces/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 an iac.provider module in the config and delegate to the plugin. After a successful bootstrap, every entry in result.EnvVars is printed as export KEY=VALUE (sorted, for deterministic CI capture) and result.Bucket is 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 — resolveStateStore now instantiates module.PostgresIaCStateStore for backend: postgres configs; s3/gcs/azure return a clear error with contribution guidance instead of silently falling back to noop.

[0.18.5] - 2026-04-23

Fixed

  • wfctl infra apply — infra.* module support — runInfraApply now detects configs using the new infra.* module abstraction (v0.3.55+) and, instead of requiring a pipelines.apply section, computes a diff plan locally via platform.ComputePlan and calls IaCProvider.Apply directly. Previously, running wfctl infra apply against a modern infra.yaml failed with "pipeline apply not found". The legacy platform.* path is preserved; configs without any infra.* modules continue to use the pipeline runner unchanged. Configs that mix infra.* and platform.* module types now fail fast with a descriptive error.

[0.18.4] - 2026-04-23

Fixed

  • wfctl plugin install private repos — for github.com/.../releases/download/... URLs, when a GitHub token is present, downloadURL now uses the two-step GitHub REST API flow (releases/tags/:tag to resolve the asset ID, then releases/assets/:id with Accept: 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).

[0.18.3] - 2026-04-23

Fixed

  • wfctl plugin install auth — downloadURL now sends an Authorization: token <tok> header for any github.com URL. Token resolution order: RELEASES_TOKEN → GH_TOKEN → GITHUB_TOKEN. Falls back to unauthenticated for public repos or non-GitHub URLs. Enables wfctl plugin install to fetch release assets from private GitHub repositories when a token is present in the environment (e.g. BMW CI with RELEASES_TOKEN).

[0.18.1] - 2026-04-22

Fixed

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.go had minor formatting drift; corrected.
  • Missing nil-DB test — TestNewSQLTenantRegistry_NilDB asserts the constructor returns a descriptive error ("cfg.DB is required") when DB is nil.
  • Cache-disabled coverage — TestNewSQLTenantRegistry_CacheDisabled exercises the full Ensure → GetBySlug → Disable → GetBySlug sequence with CacheSize=-1, proving nil-cache code paths do not panic.
  • Transient-error propagation — TestEnsure_PropagatesNonNotFoundError guards against a regression where a transient DB error could be silently swallowed and trigger a spurious INSERT.
  • Stale domain-cache invalidation — TestUpdate_InvalidatesStaleDomainCache verifies that after Update changes a tenant's domains, the old domain key is evicted from the LRU cache so the next GetByDomain goes to the DB.
  • Mixed-case subdomain matching — TestSubdomainSelector_MixedCase confirms that both the root domain and the incoming host are lowercased before comparison, returning a consistent lowercase tenant key.
  • Dockerfile validation subtests — TestValidateBaseImage_FullyQualifiedRefs converted to t.Run subtests with five additional cases: digest-only ref, registry-with-port, tag+digest combined, empty string, and docker.io/library/alpine.
  • Double-slug in Ensure error — the outer fmt.Errorf in Ensure no longer re-embeds the slug (it was already present in the wrapped GetBySlug error).
  • Misleading scaffold comment — replaced the ambiguous comment at scaffold_dockerfile.go:181 with 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, and joinLines in module/tenants_test.go replaced with strings.Split / strings.Join.

Breaking changes

  • TenantSpec.Slug must be all-lowercase. TenantSpec.Validate() now returns ErrValidation when Slug != 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 the slug column before upgrading.
  • SubdomainSelector now returns a lowercase subdomain key. The host header is already lowercased before suffix matching, so the returned key is always lowercase. HostSelector has 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.

[0.18.0] - 2026-04-22

Added

Tenancy — multi-tenant first-class support

  • interfaces.Tenant, interfaces.TenantRegistry — core tenant struct and registry contract (Ensure, GetByID, GetBySlug, GetByDomain, List, Update, Disable).
  • module.SQLTenantRegistry — PostgreSQL-backed TenantRegistry with configurable table/column names (TenantSchemaConfig) and an LRU cache (configurable size + TTL).
  • Embedded SQL migrations — module.TenantsMigrationsFS() returns an embed.FS with goose-format up/down migrations (20260422000001_tenants.up.sql).
  • 7 tenant selectors (interfaces.Selector) — host, subdomain, header, cookie, jwt_claim, session, and static; 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 emits tenant.mismatch event via TenantMismatchEmitter, returns ErrTenantMismatch, and the TenantMiddleware responds 403 + {"error":"tenant.mismatch"}.
    • consensus — configurable MinVotes threshold; defaults to simple majority.
  • module.WithTenant / module.TenantFromContext — inject and retrieve the resolved tenant from context.Context via a typed unexported key.
  • 5 tenant pipeline steps — tenant_ensure, tenant_list, tenant_get_by_domain, tenant_update, tenant_disable; each resolves TenantRegistry from the app service registry at execute time via the narrow tenantAppProvider interface.
  • wfctl tenant — CLI subcommand with ensure, list, get, update, disable; supports --format table|json and --dsn / WFCTL_TENANT_DSN.

Canonical IaC key registry

  • 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) bool and CanonicalKeys() []string helpers.
  • JSON Schema for the canonical key set (machine-readable; used by wfctl validation).
  • Canonical spec types — Job, Worker, StaticSite, Sidecar, Port structs with full JSON/YAML tags.
  • interfaces.IaCProvider.SupportedCanonicalKeys() []string — new method on the IaC provider interface; implementations declare which canonical keys they support.

Build-pipeline hook system

  • 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) in interfaces.
  • wfctl hook dispatcher — priority-ordered subprocess dispatch with configurable timeout; plugins register handlers via BuildHookDeclaration in plugin.json.
  • plugin.json schema extensions — BuildHooks, CLICommands, MigrationDrivers, PortIntrospect in RegistryCapabilities; PluginRequirement.Verify block (Signature, SBOM, VulnPolicy) for supply-chain verification.
  • install_verify hook — emitted by wfctl plugin install after tarball download, before extraction, when req.Verify != nil; non-zero dispatch aborts install.

Plugin SDK & extensibility

  • 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 wfctl subcommands via CLICommandDeclaration in plugin.json; wfctl discovers and dispatches them at runtime.

Module contract extensions

  • 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 improvements

  • wfctl scaffold dockerfile — generates canonical Dockerfiles with --mode (server|library) and --base-image flags.
  • Port introspection aggregator — wfctl aggregates declared ports from all loaded plugins using PortPaths in plugin.json; plugins may also provide a PortIntrospect hook handler.

Changed

  • interfaces.IaCProvider gains SupportedCanonicalKeys() []string (breaking for implementations outside this repo — add return interfaces.CanonicalKeys() as a temporary stub).

[0.14.1] - 2026-04-19

Added

wfctl build audit — supply-chain security checks (T34)

  • wfctl build audit — two-layer security audit combining CI config checks and per-target Dockerfile linting.

    Config-level checks (six):

    1. ci.build.security.hardened=false → WARN
    2. Dockerfile containers without sbom or provenance configured → WARN
    3. Registries without a retention: policy → WARN
    4. requires.plugins or plugins.external declared without a .wfctl.yaml lockfile → WARN
    5. Registry auth.env pointing to an env var not set at audit time → WARN
    6. 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: dockerfile container, lints the Dockerfile for:
      • USER root → CRITICAL
      • Missing USER directive → CRITICAL
      • FROM <image>:latest without version pinning → WARN
      • ADD 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. --strict also exits 1 on any WARN. Plain runs exit 0 unless CRITICAL.

BuildKit provenance attestation (T33)

  • When ci.build.security.hardened=true, wfctl build image appends --provenance=mode=max and --sbom=true to every docker build invocation.
  • Emits a warning when DOCKER_BUILDKIT is not set to 1, since BuildKit is required for provenance attestation to work.

GitLab Container Registry provider (T31)

  • plugins/registry-gitlab — full implementation replacing the stub:
    • Login: uses gitlab-ci-token + $CI_JOB_TOKEN in CI context; falls back to oauth2 + auth.env token.
    • 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 beyond retention.keep_latest.

[0.14.0] - 2026-04-19

Added

wfctl build command family

  • wfctl build — top-level orchestrator; chains go → ui → image → push per ci.build config. Flags: --config, --dry-run, --only, --skip, --tag, --format json|yaml|table, --no-push, --env.
  • wfctl build go — builds all type: go targets via the built-in Go builder plugin. --target flag to select a single target.
  • wfctl build ui — builds all type: nodejs targets via the built-in Node.js builder plugin.
  • wfctl build image — builds all ci.build.containers[] entries. Supports method: dockerfile (BuildKit with secrets/cache/platforms) and method: ko. External images (external: true) are resolved via the tag_from chain instead of being built.
  • wfctl build push — pushes each container's image refs to registries declared in push_to[].
  • wfctl build custom — runs all type: custom targets via the custom builder plugin.

Builder plugin contract (plugin/builder, plugins/builder-*)

  • plugin/builder.Builder interface — Name(), Validate(cfg), Build(ctx, cfg, out), SecurityLint(cfg) contract for all builder plugins.
  • Built-in go builder — invokes go build with ldflags, tags, CGO, cross-compilation. SecurityLint warns on secret-embedding ldflags, CGO without link_mode, and unknown builder images.
  • Built-in nodejs builder — runs npm ci && npm run <script> (or yarn/pnpm). SecurityLint warns on missing package-lock.json.
  • Built-in custom builder — runs arbitrary shell commands via sh -c. Always emits a SecurityLint warn.
  • Builder registry (plugin/builder/registry.go) — Register, Get, List with init-time registration.

wfctl registry container commands

  • wfctl registry login — logs into declared registries. DO provider uses doctl registry login; GHCR provider uses docker login ghcr.io --password-stdin.
  • wfctl registry push — pushes image refs to declared registries.
  • wfctl registry prune — runs doctl registry garbage-collection + tag pruning (DO provider); GH API version pruning (GHCR provider). Preserves latest. Dry-run supported.
  • RegistryProvider interface (plugin/registry/provider.go) — Name(), Login(), Push(), Prune() with Context carrying io.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.

Config schema additions

  • ci.build.targets[] — typed build targets with name, type, path, config, environments overrides. Supersedes binaries: (legacy binaries: auto-coerced with deprecation warning).
  • ci.registries[] — CIRegistry + CIRegistryAuth + CIRegistryRetention types. Retention supports keep_latest, untagged_ttl, schedule.
  • CIContainerTarget extensions — method, ko_*, platforms, build_args, secrets, cache, target, labels, extra_flags, external, source (with tag_from chain), push_to.
  • CIBuildSecurity — hardened, sbom, provenance, sign, non_root, base_image_policy. Defaults applied automatically at LoadFromFile time: hardened: true, sbom: true, provenance: slsa-3, non_root: true.
  • TagFromEntry — env + command fields for tag resolution chains on external images.
  • EnvironmentConfig.Build — *CIBuildConfig field for per-environment build overrides consumed by wfctl dev up.
  • PluginRequirement auth — source + auth.env fields for private plugin repos.

Plugin install enhancements

  • wfctl plugin install --from-config <file> — batch-installs requires.plugins[] from a workflow config; skips already-installed plugins.
  • wfctl plugin lock — regenerates the .wfctl.yaml lockfile from requires.plugins[].
  • Private repo auth — requires.plugins[].auth.env triggers git config --global url."...".insteadOf + GOPRIVATE injection before fetching; undone after install.

Supply-chain hardening

  • Hardened defaults applied at load time (LoadFromFile → applyBuildDefaults()).
  • CIConfig.ValidateWithWarnings() — emits "hardened defaults disabled — images may not meet supply-chain baseline" when security.hardened: false.
  • SBOM generation (build_sbom.go) — shells out to syft binary; attaches via oras attach or cosign attach sbom. No-op when security.sbom: false.

Local dev symmetry

  • environments.local.build overrides — 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" get cache.from: [{type: local}] injected.
  • wfctl dev up now calls runDevBuild(cfgPath, "local") before starting services.

External image support

  • external: true on container targets — skips docker build; resolves image ref via source.tag_from chain (env var → shell command → fallback).
  • ResolveTag (build_resolve_tag.go) — generic tag resolver used by external and built containers.

CI init emitter

  • wfctl ci init now emits three files for GitHub Actions:
    • .github/workflows/ci.yml — build + test (unchanged)
    • .github/workflows/deploy.yml (new) — minimal ~45-line pipeline with workflow_run trigger, build-image job using wfctl build --push --format json, chained deploy-* jobs with correct SHA pinning (${{ github.event.workflow_run.head_sha || github.sha }}), and concurrency block.
    • .github/workflows/registry-retention.yml (new, conditional) — emitted when any registry has retention.schedule; wraps wfctl registry prune.

Documentation

  • 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 covering ci.build schema, ci.registries schema, deploy environments, builder plugins, CLI reference, auth providers, security hardening, local dev, and troubleshooting.

Changed

  • wfctl registry now refers to container registry commands (login, push, prune). The plugin catalog command is renamed to wfctl plugin-registry. A deprecation alias wfctl registry (routing to wfctl plugin-registry) is kept until v1.0.
  • ci.build.security defaults are on — configs that omit ci.build.security get hardened: true, sbom: true, provenance: slsa-3, non_root: true applied automatically.

Notes

  • 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.

Links

Deferred to v0.14.1

  • GitLab registry auth (T31) — full GitLab Container Registry Login/Push/Prune implementation.
  • BuildKit provenance attestation (T33) — --provenance=mode=max attestation 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).

[Unreleased]

Added

  • 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-detects environments.local.exposure.method when --expose is 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 complete app.yaml including secretStores: and per-secret store: routing.
  • wfctl secrets setup (cmd/wfctl/secrets_setup.go): standalone interactive command to set all secrets for a given environment. Reads secrets.entries from config, resolves store per secret (env override → per-secret store → defaultStore → legacy provider), prompts with hidden terminal input, and supports --auto-gen-keys flag to auto-generate random hex values for names ending in _KEY, _SECRET, _TOKEN, or _SIGNING.
  • Plugin manifest moduleInfraRequirements (config/plugin_manifest.go): PluginManifestFile struct with moduleInfraRequirements map, ModuleInfraSpec, and InfraRequirement. 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, SecretStores map on WorkflowConfig, DefaultStore on SecretsConfig, and Store field on SecretEntry for per-secret store routing.
  • Per-environment infra resolution (config/infra_resolution.go): InfraEnvironmentResolution with strategy (container/provision/existing), dockerImage, port, provider, config, and connection (host/port/auth). Added Environments map to InfraResourceConfig.
  • SecretsProvider.Check() (cmd/wfctl/secrets_providers.go): SecretState enum (Set/NotSet/NoAccess/FetchError/Unconfigured) and Check() method on the interface with envProvider implementation. SecretStatus now includes Store and State fields.
  • 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 for wfctl secrets list).
  • detect_infra_needs plugin manifest integration (cmd/wfctl/plugin_infra.go, mcp/scaffold_tools.go): LoadPluginManifests and DetectPluginInfraNeeds scan local plugin directories for plugin.json manifests and surface module-type infra requirements. MCP tool gains optional plugins_dir parameter.

Documentation

  • docs/WFCTL.md: updated wizard reference (11 screens, new navigation keys); added secrets setup command reference with flag table and examples; updated secrets list description to mention multi-store routing.

  • docs/dsl-reference.md + cmd/wfctl/dsl-reference-embedded.md: expanded infrastructure fields with per-env resolution strategies, connection config, and extended example; added secretStores: section with example and relationships; updated secrets: section with defaultStore, per-secret store field, multi-store example, and secrets setup CLI command.

  • docs/plugin-manifest-guide.md: new guide for plugin authors on declaring infrastructure requirements in plugin.json via moduleInfraRequirements.

  • 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, and networking.ingress for 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 Kubernetes NetworkPolicy YAML from networking.policies + mesh.routes; outputs one file per service to --output directory (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 into wfctl 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. Includes Validate() 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 (with Strategy field for dual-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 the ci: config section. Build phase cross-compiles Go binaries and builds containers. Test phase supports ephemeral Docker deps (postgres/redis/mysql) via needs:. 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 from ci.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 --value or --from-file), list, validate, init, rotate, sync. SecretsProvider interface with env backend.

  • wfctl validate: now validates ci: sections using CIConfig.Validate() when present.

Documentation

  • docs/dsl-reference.md + cmd/wfctl/dsl-reference-embedded.md: added services:, mesh:, networking:, and security: sections with full field reference and examples; also added ci:, environments:, and secrets: sections.
  • docs/WFCTL.md: added ports list, security audit, and security generate-network-policies command documentation; updated command tree and category table. Also added ci run, ci init, and secrets documentation.

[0.13.0] - 2026-04-17

Added

  • wfctl infra plan|apply|bootstrap|destroy|status|drift now accept --env <name>.
  • wfctl infra import does 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 to null to skip the module in that env.
  • Top-level environments: envVars are merged into container resources during infra apply; region and provider default from environments[env] when a module omits them.
  • wfctl infra now honors imports: (consistent with every other wfctl subcommand).

Notes

  • ci.deploy.environments[].requireApproval continues to work via wfctl ci init emitting environment: <name> in generated GitHub Actions. No engine change needed — GitHub's native environment approval UI handles the gate.
  • InfraResourceConfig (under infrastructure.resources:) already had an Environments field but was never wired to wfctl infra (which parses modules:). Multi-env is now wired to ModuleConfig; InfraResourceConfig consumption is deferred to a follow-up and infrastructure.resources: remains unused by wfctl infra commands in this release.

Fixed

  • ModuleConfig previously lacked an Environments field; it was defined on the unused InfraResourceConfig type. Multi-env is now wired to the schema wfctl infra actually parses.

[0.4.1] - 2026-03-27

Fixed

  • Editor schemas golden file updated to reflect v0.4.0 schema changes (release CI fix)

[0.4.0] - 2026-03-27

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.

Breaking Changes (Editor Schema)

  • 88 config fields changed from type: "json" to typed schemas. If your tooling parses wfctl editor-schemas output and relies on specific field types, those fields are now map, array, or individual typed fields instead of json. The engine runtime behavior is unchanged — this only affects editor/UI consumers.
  • wfctl editor-schemas now includes stepSchemas alongside moduleSchemas and coercionRules. Consumers parsing this JSON output will see a new top-level key.
  • workflow-editor npm package: The static MODULE_TYPES array has been removed. MODULE_TYPE_MAP is still exported but now sourced from engine-schemas.json instead of a hand-maintained array. If you imported MODULE_TYPES directly, switch to MODULE_TYPE_MAP or getEngineModuleTypes().

Added

  • 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-reference command: 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 []ConfigFieldDef from Go struct editor:"..." 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/hover returns 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/completion suggests 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 for type: "json" fields in both built-in registries and plugin schemas — any new json-typed field fails CI

Changed

  • 88 FieldTypeJSON config 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

Changed

  • Switch yaegi dependency from github.com/traefik/yaegi v0.16.1 to github.com/GoCodeAlone/yaegi v0.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 extract with x/tools/go/packages
    • Generic function import support via //yaegi:add directive

Added

  • 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

[0.5.3] - 2026-03-28

Changed

  • Bump modular to v1.13.0 (consolidated sub-modules)

[0.5.2] - 2026-03-28

Changed

  • Extract TemplateEngine to pipeline/ package; decouple plugins from module package (SDK boundary)

[0.5.1] - 2026-03-28

Fixed

  • Implement http.Hijacker and http.Flusher interfaces on trackedResponseWriter
  • Bump modular dependencies to v1.12.5 / v1.15.0 / v2.8.0

[0.5.0] - 2026-03-28

Added

  • Expr template engine — ${ } syntax for pipeline step config values 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) }
  • skip_if expr support — pipeline steps accept skip_if: ${ expr } for inline conditional skipping
  • wfctl expr-migrate command — auto-converts Go template ({{ }}) expressions to expr syntax (${ }) with --dry-run, --output, and --inplace modes; complex patterns receive # TODO: migrate comments
  • 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

Fixed

  • 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

[0.4.0] - 2026-02-11

Added

  • 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

[0.3.0] - 2026-02-10

Added

  • 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)

[0.2.0] - 2026-02-09

Added

  • Observability Foundation
    • MetricsCollector wrapping Prometheus with 6 pre-registered metric vectors and /metrics endpoint
    • HealthChecker with /health, /ready, /live endpoints and auto-discovery
    • RequestIDMiddleware with X-Request-ID propagation and UUID generation
  • Database Module
    • WorkflowDatabase wrapping database/sql with Query, Execute, InsertRow, UpdateRows, DeleteRows
    • SQL builder helpers (BuildInsertSQL, BuildUpdateSQL, BuildDeleteSQL)
    • DatabaseIntegrationConnector adapter for integration workflows
  • 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.component module 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

[0.1.0] - 2026-02-08

Added

  • 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

Changed

  • Upgraded to Modular v1.3.9 with IsVerboseConfig, SetVerboseConfig, SetLogger support
  • Improved error handling for service registration and I/O operations

Fixed

  • Critical error checking issues identified by linters
  • HTTP response writing error handling
  • Service registration error handling in engine

Security

  • Enhanced dependency management with automated updates
  • Improved error handling to prevent potential runtime issues

[Previous Versions]

Previous version history was not maintained in changelog format.