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