Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
542d54b
fix(plugin/external): handle empty ConfigMessage for input-only STRIC…
intel352 May 12, 2026
d2b7529
fix: address Copilot review — comment scope + test asserts both nil +…
intel352 May 12, 2026
fa6a9fc
docs(#617): design for godo removal from workflow core
intel352 May 13, 2026
13a0e8c
docs(#617): revise design per adversarial review cycle 1
intel352 May 13, 2026
021285e
docs(#617): revise design per adversarial review cycle 2
intel352 May 13, 2026
801fae0
docs(#617): incorporate adversarial cycle 3 minor amendments (PASS)
intel352 May 13, 2026
899e8f0
docs(#617): implementation plan (5 tasks, 1 PR)
intel352 May 13, 2026
e6b6872
docs(#617): revise plan per adversarial review cycle 1 (plan phase)
intel352 May 13, 2026
064c557
docs(#617): revise plan per adversarial review cycle 2 (plan phase)
intel352 May 13, 2026
249179e
docs(#617): revise plan per adversarial review cycle 3 (plan phase)
intel352 May 13, 2026
15d1694
docs(#617): revise plan per adversarial review cycle 4 (plan phase)
intel352 May 13, 2026
a019f13
docs(#617): revise plan per adversarial review cycle 5 (plan phase)
intel352 May 13, 2026
5653c46
docs(#617): revise plan per adversarial review cycle 6 (plan phase)
intel352 May 13, 2026
5f0f60a
docs(#617): revise plan per adversarial review cycle 7 (plan phase)
intel352 May 13, 2026
ed4215e
docs(#617): convert task headings to H3 for scope-manifest check
intel352 May 13, 2026
f8ef762
chore: lock scope for issue #617 godo removal (alignment passed)
intel352 May 13, 2026
589ef78
feat(#617): delete legacy DO modules (godo importers)
intel352 May 13, 2026
b09726b
feat(#617): strip DO registration sites + remap wfctl detection hooks
intel352 May 13, 2026
ee6fca2
feat(#617): actionable migration errors for legacy DO types
intel352 May 13, 2026
78ea625
feat(#617): drop godo from go.mod + add CI grep gate
intel352 May 13, 2026
9cc47bd
feat(#617): wfctl modernize rule + migration guide + CHANGELOG
intel352 May 13, 2026
fe62140
test(#617): bump modernize rule-count expectation to include legacy-d…
intel352 May 13, 2026
407c6a9
fix(#617): make legacy DO step modernize findings non-fixable; fix mi…
intel352 May 13, 2026
d52429b
docs(#617): update migration guide step table to reflect non-fixable …
intel352 May 13, 2026
3f90451
fix(lint): add return after t.Fatal to resolve SA5011 nil-dereference…
intel352 May 13, 2026
d868c05
fix: address Copilot review round 2 — doc YAML shape + test coverage gap
intel352 May 13, 2026
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
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,20 @@ jobs:

echo ""
echo "Results: $total configs, $passed passed, $warned warnings"

godo-banned:
name: Verify godo is not imported (issue #617)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Grep gate — *.go files must not import godo
run: |
! grep -rn --include="*.go" \
--exclude-dir=_worktrees \
--exclude-dir=.worktrees \
--exclude-dir=.claude \
--exclude="godo_absent_test.go" \
"digitalocean/godo" .
- name: Grep gate — go.mod files must not list godo
run: |
! grep -qH "digitalocean/godo" go.mod example/go.mod
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## 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`](https://github.com/GoCodeAlone/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](docs/migrations/v0.52.0-godo-removal.md) 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
Expand Down
21 changes: 11 additions & 10 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,11 +322,11 @@ flowchart TD
| `step.argo_logs` | Retrieves logs from an Argo Workflow | platform |
| `step.argo_delete` | Deletes an Argo Workflow | platform |
| `step.argo_list` | Lists Argo Workflows for a namespace | platform |
| `step.do_deploy` | Deploys to DigitalOcean App Platform | platform |
| `step.do_status` | Retrieves DigitalOcean App Platform deployment status | platform |
| `step.do_logs` | Fetches DigitalOcean App Platform runtime logs | platform |
| `step.do_scale` | Scales a DigitalOcean App Platform component | platform |
| `step.do_destroy` | Destroys a DigitalOcean App Platform deployment | platform |

**DigitalOcean IaC steps** were removed from workflow core in v0.52.0 and moved to the
[workflow-plugin-digitalocean](https://github.com/GoCodeAlone/workflow-plugin-digitalocean)
external plugin. After loading the plugin, use the generic `step.iac_*` pipeline steps.
See [v0.52.0 migration guide](docs/migrations/v0.52.0-godo-removal.md).

### Expression Syntax

Expand Down Expand Up @@ -510,11 +510,12 @@ Strict mode applies to **both** direct dot-access (`{{ .steps.auth.field }}`) an
| `platform.autoscaling` | Auto-scaling policy and target management | platform |
| `platform.region` | Multi-region deployment configuration | platform |
| `platform.region_router` | Routes traffic across regions by weight, latency, or failover | platform |
| `platform.doks` | DigitalOcean Kubernetes Service (DOKS) deployment | platform |
| `platform.do_app` | DigitalOcean App Platform deployment (deploy, scale, logs, destroy) | platform |
| `platform.do_networking` | DigitalOcean VPC and firewall management | platform |
| `platform.do_dns` | DigitalOcean domain and DNS record management | platform |
| `platform.do_database` | DigitalOcean Managed Database (PostgreSQL, MySQL, Redis) | platform |

**DigitalOcean IaC modules** were removed from workflow core in v0.52.0 and moved to the
[workflow-plugin-digitalocean](https://github.com/GoCodeAlone/workflow-plugin-digitalocean)
external plugin. After loading the plugin, use the generic `infra.*` module
types with `provider: digitalocean` and the generic `step.iac_*` pipeline
steps. See [v0.52.0 migration guide](docs/migrations/v0.52.0-godo-removal.md).
| `iac.provider` | Cloud provider configuration (aws, gcp, azure, digitalocean) for IaC operations | platform |
| `iac.state` | IaC state persistence (memory, filesystem, spaces, gcs, azure_blob, postgres) | platform |
| `infra.vpc` | Virtual Private Cloud and subnet management | platform |
Expand Down
1 change: 0 additions & 1 deletion cmd/wfctl/ci_run_dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ func resolveDeployInfoFromConfig(wfCfg *config.WorkflowConfig, envName, provider
// references the provider.
deployTargetTypes := []string{
"infra.container_service",
"platform.do_app",
"platform.app_platform",
"infra.k8s_cluster",
}
Expand Down
29 changes: 29 additions & 0 deletions cmd/wfctl/ci_validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import (
"time"

"github.com/GoCodeAlone/workflow/config"
"github.com/GoCodeAlone/workflow/internal/legacydo"
"github.com/GoCodeAlone/workflow/schema"
"github.com/GoCodeAlone/workflow/validation"
"gopkg.in/yaml.v3"
)

// ciFileResult holds the outcome of validating a single config file.
Expand Down Expand Up @@ -131,10 +133,37 @@ func ciValidateFile(cfgPath string, strict, immutableConfig bool, immutableSecti
opts = append(opts, schema.WithAllowEmptyModules())
}
opts = append(opts, schema.WithSkipWorkflowTypeCheck(), schema.WithSkipTriggerTypeCheck())
// Pass legacy DO module types through schema so the migration error fires
// below instead of a generic "unknown module type" (issue #617).
for t := range legacydo.ModuleTypes {
opts = append(opts, schema.WithExtraModuleTypes(t))
}
if err := schema.ValidateConfig(cfg, opts...); err != nil {
errs = append(errs, fmt.Errorf("schema: %w", err))
}

// Post-validate sweep: accumulate legacy DO module/step errors (issue #617).
for _, m := range cfg.Modules {
if legacydo.IsModuleType(m.Type) {
errs = append(errs, legacydo.FormatModuleError(m.Type, m.Name, false))
}
}
for _, rawPipeline := range cfg.Pipelines {
yamlBytes, err := yaml.Marshal(rawPipeline)
if err != nil {
continue
}
var pipeCfg config.PipelineConfig
if err := yaml.Unmarshal(yamlBytes, &pipeCfg); err != nil {
continue
}
for _, s := range pipeCfg.Steps {
if legacydo.IsStepType(s.Type) {
errs = append(errs, legacydo.FormatStepError(s.Type, false))
}
}
}

// CI config check.
if immutableConfig {
if cfg.CI == nil {
Expand Down
20 changes: 10 additions & 10 deletions cmd/wfctl/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ func runDeployCloud(args []string) error {
fmt.Fprintf(fs.Output(), `Usage: wfctl deploy cloud [options]

Deploy infrastructure defined in a workflow config to a cloud environment.
Discovers cloud.account and platform.* modules, validates credentials,
Discovers cloud.account, platform.*, and infra.* modules, validates credentials,
shows a deployment plan, and applies changes.

Options:
Expand Down Expand Up @@ -829,15 +829,15 @@ Options:
return fmt.Errorf("parse config %s: %w", cfg, yamlErr)
}

// Discover cloud accounts and platform modules
// Discover cloud accounts and deploy-target modules (platform.* or infra.*)
var cloudAccounts []moduleEntry
var platformModules []moduleEntry
var deployTargetModules []moduleEntry
for _, m := range parsed.Modules {
if m.Type == "cloud.account" {
cloudAccounts = append(cloudAccounts, m)
}
if strings.HasPrefix(m.Type, "platform.") {
platformModules = append(platformModules, m)
if strings.HasPrefix(m.Type, "platform.") || strings.HasPrefix(m.Type, "infra.") {
deployTargetModules = append(deployTargetModules, m)
}
}

Expand Down Expand Up @@ -896,13 +896,13 @@ Options:
fmt.Println()
}

// Report platform modules (deployment plan)
if len(platformModules) == 0 {
return fmt.Errorf("no platform.* modules found in config — nothing to deploy")
// Report deploy-target modules (deployment plan)
if len(deployTargetModules) == 0 {
return fmt.Errorf("no platform.* or infra.* modules found in config — nothing to deploy")
}

fmt.Printf("Infrastructure Modules (%d):\n", len(platformModules))
for _, pm := range platformModules {
fmt.Printf("Infrastructure Modules (%d):\n", len(deployTargetModules))
for _, pm := range deployTargetModules {
account, _ := pm.Config["account"].(string)
detail := pm.Type
if account != "" {
Expand Down
5 changes: 2 additions & 3 deletions cmd/wfctl/deploy_providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ var resolveIaCProvider = discoverAndLoadIaCProvider
// double parse — and either may be empty without affecting the
// other.
type iacPluginManifest struct {
Name string `json:"name"`
Version string `json:"version"`
Name string `json:"name"`
Version string `json:"version"`
Capabilities struct {
IaCProvider struct {
Name string `json:"name"`
Expand Down Expand Up @@ -418,7 +418,6 @@ func newPluginDeployProvider(providerName string, wfCfg *config.WorkflowConfig,
// behaviour is predictable rather than silently wrong.
deployTargetTypes := []string{
"infra.container_service",
"platform.do_app",
"platform.app_platform",
"infra.k8s_cluster",
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/wfctl/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func TestRunDeployCloudValidTarget(t *testing.T) {
if err == nil {
t.Fatal("expected error when no platform modules found")
}
if !strings.Contains(err.Error(), "no platform.* modules found") {
if !strings.Contains(err.Error(), "no platform.* or infra.* modules found") {
t.Errorf("expected no platform modules error, got: %v", err)
}
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/wfctl/infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@ func secretGenKeys(cfg *config.WorkflowConfig) []string {
}

// parseInfraResourceSpecs reads an infra config (resolving imports:) and
// returns ResourceSpecs for all infra.* and platform.* modules.
// returns ResourceSpecs for all infra.* and platform.* (e.g., platform.kubernetes, platform.ecs) modules.
func parseInfraResourceSpecs(cfgFile string) ([]interfaces.ResourceSpec, error) {
cfg, err := config.LoadFromFile(cfgFile)
if err != nil {
Expand Down Expand Up @@ -574,7 +574,7 @@ func planResourcesForEnv(path, envName string) ([]*config.ResolvedModule, error)
}

func isContainerType(t string) bool {
return t == "infra.container_service" || t == "platform.do_app"
return t == "infra.container_service"
}

// loadCurrentState loads ResourceStates from the configured iac.state backend.
Expand Down
4 changes: 2 additions & 2 deletions cmd/wfctl/infra_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ func hasInfraModules(cfgFile string) bool {
return false
}

// hasPlatformModules reports whether cfgFile contains any modules with the legacy
// platform.* type prefix.
// hasPlatformModules reports whether cfgFile contains any modules with the
// platform.* type prefix (e.g., platform.kubernetes, platform.ecs).
func hasPlatformModules(cfgFile string) bool {
cfg, err := config.LoadFromFile(cfgFile)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion cmd/wfctl/infra_apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1987,7 +1987,7 @@ modules:
if err := os.WriteFile(legacyOnly, []byte(`
modules:
- name: app
type: platform.do_app
type: example.legacy_unknown
config: {}
`), 0o600); err != nil {
t.Fatal(err)
Expand Down
87 changes: 87 additions & 0 deletions cmd/wfctl/legacy_do_types_removed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package main

import (
"os"
"path/filepath"
"strings"
"testing"
)

// TestLegacyDOTypesAbsent_FromTypeRegistry locks the post-cutover state of
// cmd/wfctl/type_registry.go for issue #617. If any legacy type leaks back in,
// this test fires and the CI gate fires.
func TestLegacyDOTypesAbsent_FromTypeRegistry(t *testing.T) {
modules := KnownModuleTypes()
steps := KnownStepTypes()
legacyModules := []string{
"platform.do_app", "platform.do_database", "platform.do_dns",
"platform.do_networking", "platform.doks",
}
legacySteps := []string{
"step.do_deploy", "step.do_status", "step.do_logs",
"step.do_scale", "step.do_destroy",
}
for _, tname := range legacyModules {
if _, ok := modules[tname]; ok {
t.Errorf("module type registry still contains legacy DO type %q (issue #617)", tname)
}
}
for _, tname := range legacySteps {
if _, ok := steps[tname]; ok {
t.Errorf("step type registry still contains legacy DO type %q (issue #617)", tname)
}
}
}

// TestValidateFile_LegacyDOModule_ReturnsActionableError verifies that
// wfctl validate emits the actionable migration error when the config
// references a removed legacy DO module type (issue #617). Covers AC3
// on the validate path (the engine path is covered by
// TestLegacyDOModuleError_PluginNotLoaded in the workflow package).
func TestValidateFile_LegacyDOModule_ReturnsActionableError(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "legacy.yaml")
yamlContent := []byte("modules:\n - name: api\n type: platform.do_app\n config: {}\n")
if err := os.WriteFile(cfgPath, yamlContent, 0o600); err != nil {
t.Fatal(err)
}
err := validateFile(cfgPath, false, false, false)
if err == nil {
t.Fatal("expected error for legacy DO module type")
}
msg := err.Error()
for _, want := range []string{
"removed from workflow core",
"workflow-plugin-digitalocean",
"infra.container_service",
} {
if !strings.Contains(msg, want) {
t.Errorf("error missing %q; got: %s", want, msg)
}
}
}

// TestCIValidateFile_LegacyDOStep_ReturnsActionableError covers ciValidateFile's
// accumulating return for legacy DO step types.
func TestCIValidateFile_LegacyDOStep_ReturnsActionableError(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "legacy.yaml")
yamlContent := []byte("pipelines:\n deploy:\n steps:\n - type: step.do_deploy\n")
if err := os.WriteFile(cfgPath, yamlContent, 0o600); err != nil {
t.Fatal(err)
}
errs := ciValidateFile(cfgPath, false, false, "")
if len(errs) == 0 {
t.Fatal("expected error for legacy DO step type")
}
found := false
for _, e := range errs {
if strings.Contains(e.Error(), "step.iac_apply") && strings.Contains(e.Error(), "removed from workflow core") {
found = true
break
}
}
if !found {
t.Errorf("expected actionable migration error in errs; got: %v", errs)
}
}
1 change: 1 addition & 0 deletions cmd/wfctl/modernize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ func TestModernizeAllRulesRegistered(t *testing.T) {
"empty-routes",
"camelcase-config",
"request-parse-config",
"legacy-do-types",
}
if len(rules) != len(expectedIDs) {
t.Errorf("expected %d rules, got %d", len(expectedIDs), len(rules))
Expand Down
Loading
Loading