Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ flowchart TD
| `step.db_create_partition` | Creates a time-based table partition | pipelinesteps |
| `step.db_sync_partitions` | Ensures future partitions exist for a partitioned table | pipelinesteps |
| `step.json_response` | Writes HTTP JSON response with custom status code and headers. Supports `status_from` to dynamically resolve the HTTP status code from the pipeline context at runtime | pipelinesteps |
| `step.response` | Alias for `step.json_response` for concise pipeline-authored HTTP JSON responses | pipelinesteps |
| `step.raw_response` | Writes a raw HTTP response with arbitrary content type | pipelinesteps |
| `step.pipeline_output` | Marks structured data as the pipeline's return value for extraction by `engine.ExecutePipeline()` | pipelinesteps |
| `step.json_parse` | Parses a JSON string (or `[]byte`) in the pipeline context into a structured object | pipelinesteps |
Expand Down
95 changes: 95 additions & 0 deletions cmd/wfctl/modernize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,100 @@ pipelines:
}
}

func TestModernizeDBConfigAliases(t *testing.T) {
input := `
pipelines:
test:
steps:
- name: fetch_user
type: step.db_query
config:
module: my-db
query: "SELECT * FROM users WHERE id = ?"
args:
- u1
mode: one
- name: save_user
type: step.db_exec
config:
module: my-db
query: "UPDATE users SET name = ? WHERE id = ?"
args:
- Ada
- u1
mode: many
`
rule := findRule("db-config-aliases")
if rule == nil {
t.Fatal("db-config-aliases rule not found")
}

doc := parseTestYAML(t, input)
findings := rule.Check(doc, []byte(input))
if len(findings) < 4 {
t.Fatalf("expected alias findings, got %d: %v", len(findings), findings)
}
changes := rule.Fix(doc)
if len(changes) < 4 {
t.Fatalf("expected alias changes, got %d: %v", len(changes), changes)
}

out, _ := yaml.Marshal(doc)
result := string(out)
for _, absent := range []string{"module:", "args:", "mode: one", "mode: many"} {
if strings.Contains(result, absent) {
t.Fatalf("expected %q to be modernized, got:\n%s", absent, result)
}
}
for _, present := range []string{"database: my-db", "params:", "mode: single", "mode: list"} {
if !strings.Contains(result, present) {
t.Fatalf("expected %q in modernized output, got:\n%s", present, result)
}
}
}

func TestModernizeDBConfigAliasesCanonicalWins(t *testing.T) {
input := `
pipelines:
test:
steps:
- name: fetch_user
type: step.db_query
config:
database: canonical-db
module: alias-db
query: "SELECT * FROM users WHERE id = ?"
params:
- canonical
args:
- alias
mode: one
`
rule := findRule("db-config-aliases")
if rule == nil {
t.Fatal("db-config-aliases rule not found")
}
doc := parseTestYAML(t, input)
changes := rule.Fix(doc)
if len(changes) == 0 {
t.Fatal("expected alias cleanup changes")
}

out, _ := yaml.Marshal(doc)
result := string(out)
for _, absent := range []string{"module:", "args:"} {
if strings.Contains(result, absent) {
t.Fatalf("expected %q to be removed, got:\n%s", absent, result)
}
}
if strings.Contains(result, "alias-db") || strings.Contains(result, "alias") {
t.Fatalf("expected canonical fields to win, got:\n%s", result)
}
if !strings.Contains(result, "database: canonical-db") || !strings.Contains(result, "- canonical") {
t.Fatalf("expected canonical fields to remain, got:\n%s", result)
}
}

func TestDbQueryIndexCheck(t *testing.T) {
input := `
pipelines:
Expand Down Expand Up @@ -551,6 +645,7 @@ func TestModernizeAllRulesRegistered(t *testing.T) {
"absolute-dbpath",
"empty-routes",
"camelcase-config",
"db-config-aliases",
"request-parse-config",
"legacy-do-types",
"legacy-aws-types",
Expand Down
19 changes: 12 additions & 7 deletions cmd/wfctl/type_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func KnownModuleTypes() map[string]ModuleTypeInfo {
Type: "http.server",
Plugin: "http",
Stateful: false,
ConfigKeys: []string{"address", "readTimeout", "writeTimeout", "idleTimeout"},
ConfigKeys: []string{"address", "port", "readTimeout", "writeTimeout", "idleTimeout"},
},
"http.client": {
Type: "http.client",
Expand Down Expand Up @@ -712,7 +712,7 @@ func KnownStepTypes() map[string]StepTypeInfo {
"step.conditional": {
Type: "step.conditional",
Plugin: "pipelinesteps",
ConfigKeys: []string{"condition", "then", "else"},
ConfigKeys: []string{"field", "routes", "default", "if", "then", "else"},
},
"step.set": {
Type: "step.set",
Expand Down Expand Up @@ -772,22 +772,22 @@ func KnownStepTypes() map[string]StepTypeInfo {
"step.request_parse": {
Type: "step.request_parse",
Plugin: "pipelinesteps",
ConfigKeys: []string{"body", "query", "headers"},
ConfigKeys: []string{"path_params", "query_params", "parse_body", "parse_headers", "format"},
},
Comment thread
intel352 marked this conversation as resolved.
"step.db_query": {
Type: "step.db_query",
Plugin: "pipelinesteps",
ConfigKeys: []string{"database", "query", "params", "tenantKey"},
ConfigKeys: []string{"database", "module", "query", "params", "args", "mode", "tenantKey"},
},
"step.db_exec": {
Type: "step.db_exec",
Plugin: "pipelinesteps",
ConfigKeys: []string{"database", "query", "params", "tenantKey"},
ConfigKeys: []string{"database", "module", "query", "params", "args", "mode", "tenantKey"},
},
"step.db_query_cached": {
Type: "step.db_query_cached",
Plugin: "pipelinesteps",
ConfigKeys: []string{"database", "query", "params", "cache_key", "cache_ttl", "scan_fields"},
ConfigKeys: []string{"database", "module", "query", "params", "args", "mode", "cache_key", "cache_ttl", "scan_fields"},
},
"step.db_create_partition": {
Type: "step.db_create_partition",
Expand All @@ -802,7 +802,12 @@ func KnownStepTypes() map[string]StepTypeInfo {
"step.json_response": {
Type: "step.json_response",
Plugin: "pipelinesteps",
ConfigKeys: []string{"status", "body", "headers"},
ConfigKeys: []string{"status", "status_from", "body", "body_from", "headers"},
},
"step.response": {
Type: "step.response",
Plugin: "pipelinesteps",
ConfigKeys: []string{"status", "status_from", "body", "body_from", "headers"},
},
Comment thread
intel352 marked this conversation as resolved.
"step.static_file": {
Type: "step.static_file",
Expand Down
65 changes: 65 additions & 0 deletions cmd/wfctl/type_registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,22 @@ func TestKnownModuleTypesStateful(t *testing.T) {
}
}

func TestKnownModuleTypesHTTPServerListsPortAlias(t *testing.T) {
info, ok := KnownModuleTypes()["http.server"]
if !ok {
t.Fatal("http.server not found")
}
keys := make(map[string]bool, len(info.ConfigKeys))
for _, key := range info.ConfigKeys {
keys[key] = true
}
for _, key := range []string{"address", "port"} {
if !keys[key] {
t.Fatalf("http.server ConfigKeys missing %q: %v", key, info.ConfigKeys)
}
}
}

func TestKnownStepTypesPopulated(t *testing.T) {
types := KnownStepTypes()
if len(types) == 0 {
Expand All @@ -94,6 +110,7 @@ func TestKnownStepTypesPopulated(t *testing.T) {
"step.db_query",
"step.publish",
"step.http_call",
"step.response",
"step.cache_get",
"step.auth_validate",
"step.authz_check",
Expand Down Expand Up @@ -187,6 +204,54 @@ func TestStepTypeCount(t *testing.T) {
}
}

func TestKnownStepTypesRequestParseConfigKeys(t *testing.T) {
info, ok := KnownStepTypes()["step.request_parse"]
if !ok {
t.Fatal("step.request_parse not found")
}
want := []string{"path_params", "query_params", "parse_body", "parse_headers", "format"}
if !sameStringSet(info.ConfigKeys, want) {
t.Fatalf("step.request_parse ConfigKeys = %v, want %v", info.ConfigKeys, want)
}
}

func TestKnownStepTypesResponseAliasesMatchConfigKeys(t *testing.T) {
types := KnownStepTypes()
jsonInfo, ok := types["step.json_response"]
if !ok {
t.Fatal("step.json_response not found")
}
aliasInfo, ok := types["step.response"]
if !ok {
t.Fatal("step.response not found")
}
if !sameStringSet(jsonInfo.ConfigKeys, aliasInfo.ConfigKeys) {
t.Fatalf("step.json_response ConfigKeys = %v, step.response ConfigKeys = %v", jsonInfo.ConfigKeys, aliasInfo.ConfigKeys)
}
}

func sameStringSet(got, want []string) bool {
if len(got) != len(want) {
return false
}
seen := make(map[string]int, len(got))
for _, v := range got {
seen[v]++
}
for _, v := range want {
if seen[v] == 0 {
return false
}
seen[v]--
}
for _, count := range seen {
if count != 0 {
return false
}
}
return true
}

// TestKnownStepTypesCoverAllPlugins ensures KnownStepTypes() is in sync with all step
// types registered by the built-in plugins. Any step type registered by a DefaultPlugin
// that is not listed in KnownStepTypes() will cause this test to fail, preventing silent
Expand Down
34 changes: 34 additions & 0 deletions decisions/0048-wfctl-owned-go-api-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# 0048. Generate Go API Docs With wfctl

**Status:** Accepted
**Date:** 2026-06-07
**Decision-makers:** workflow maintainers
**Related:** docs/plans/2026-06-07-workflow-docs-ecosystem-design.md

## Context

The public website already syncs Workflow and plugin Markdown, but it does not
publish Go API references for Workflow packages or plugin packages. The API
reference needs released-version awareness, registry-driven plugin discovery,
and Go package parsing. The website is a renderer and release surface; Workflow
already owns `wfctl`, plugin contracts, registry metadata, and Go package
semantics.

## Decision

We will put Go API extraction in `wfctl docs generate` and let
`gocodealone-website` call that command during docs sync. The generator uses Go
toolchain and stdlib package documentation APIs, emits Markdown plus version
metadata, and reads the registry snapshot to discover public plugin repos.

Rejected alternatives: website-only Node generation would scatter Go package
rules into the renderer repo; linking only to pkg.go.dev would not provide a
coherent Workflow ecosystem reference or version navigation.

## Consequences

- The docs pipeline dogfoods `wfctl` and stays Go-native for Go semantics.
- Website sync gains a Go/wfctl dependency but remains mostly renderer logic.
- Versioning and plugin fallbacks can be tested at the CLI layer.
- If generation breaks, website can keep last committed docs while `wfctl`
reports per-repo warnings.
6 changes: 6 additions & 0 deletions docs/WFCTL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2812,12 +2812,18 @@ wfctl modernize [options] <config.yaml|directory>
|----|----------|---------|-------------|
| `hyphen-steps` | error | yes | Rename hyphenated step names to underscores |
| `conditional-field` | error | yes | Convert template syntax in conditional field to dot-path |
| `db-config-aliases` | warning | yes | Rewrite DB step aliases (`module`, `args`, `one`, `many`) to canonical config |
| `db-query-mode` | warning | yes | Add mode:single when downstream uses .row/.found |
| `db-query-index` | error | yes | Convert .steps.X.row.Y to index syntax |
| `database-to-sqlite` | warning | yes | Convert database.workflow to storage.sqlite |
| `absolute-dbpath` | warning | no | Warn on absolute dbPath values |
| `empty-routes` | error | no | Detect empty routes in step.conditional |
| `camelcase-config` | warning | no | Detect snake_case config keys |
| `request-parse-config` | warning | no | Detect request parsing config that should be reviewed |

Modernize rewrites accepted compatibility aliases back to canonical config where
a deterministic fix exists. Runtime compatibility remains additive; canonical
docs and generated schemas prefer the stable spelling.

**Plugin-provided rules** are loaded when `--plugin-dir` is specified. Each installed plugin can declare its own migration rules in its `plugin.json` manifest under the `modernizeRules` key. Rules from all plugins in the directory are merged with the built-in rules.

Expand Down
Loading
Loading