feat: workflow v0.14.0 — wfctl build + deploy orchestration#413
Conversation
17-section design covering what wfctl needs to absorb so downstream deploy.yml files drop from 73-155 lines to ~25-50 lines: - New wfctl build command family (go, nodejs, custom builders; ko + dockerfile backends; CGO handling; polyglot targets via type: dispatch) - New wfctl registry container commands (login, push, prune) with per-provider plugins (DO, GH, GitLab, AWS, GCP, Azure) - Enhanced wfctl plugin install (batch, lockfile, private repos, git insteadOf, no hardcoded versions) - Hardened defaults (SBOM + provenance ON, non-root, distroless, --security-audit lint) - Configurability per Unix principle (pluggable deploy targets, health checks, hooks, backends, auth sources; standalone subcommand use; --format json; liberal skip/force flags; bring-your-own image via external: true) - Local dev symmetry (same infra.yaml drives wfctl dev up as CI) - Non-software builds via type: custom - Tutorial (16 progressive examples) + reference manual (9 pages) Two-phase rollout: workflow v0.14.0 ships primitives + docs; Phase 2 simplifies BMW, DND, core-dump deploy.yml via one PR each. GoReleaser trimmed to client-only for DND/core-dump; deleted for BMW. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
72 tasks across 11 phases for workflow v0.14.0: 1. Config schema (CIRegistry, CIContainerTarget extensions, CITarget with type: dispatch, CIBuildSecurity) 2. Builder plugin contract (Builder interface + registry) 3. wfctl build command family (go, ui, image, push, custom, orchestrator) 4. wfctl registry container commands (rename plugin catalog to plugin-registry; new login/push/prune; DO + GH providers; stubs for GitLab/AWS/GCP/Azure) 5. Enhanced wfctl plugin install (batch, lockfile, private, provider-agnostic auth) 6. Hardened defaults + --security-audit (SBOM + provenance on by default, Dockerfile linting, base image policy) 7. Local dev symmetry (environments.local.build merge; hardening skipped locally) 8. External image support (external: true, tag_from chain) 9. wfctl ci init emits ~45-line deploy.yml + retention.yml 10. Tutorial (16 sections) + manual (9 pages) 11. CHANGELOG + tag v0.14.0 Phase 12 (follow-up plans): one simplification PR per consumer (BMW, DND, core-dump) using the new primitives. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…he/labels/push_to Adds Method, KoPackage, KoBaseImage, KoBare, Platforms, BuildArgs, Secrets, Cache, Target, Labels, ExtraFlags, External, Source, PushTo fields plus sub-types CIContainerSecret, CIContainerCache, CIContainerCacheRef, CIExternalSource. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CITarget: name/type/path/config/environments fields
- CITargetOverride: per-env config map
- CIBuildConfig.UnmarshalYAML: coerces legacy binaries: entries to
{type: go} CITarget with deprecation warning; targets: used as-is
- CIBuildConfig.Targets replaces Binaries field
- Validate() and ci_run.go updated to use Targets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Container method validation: dockerfile requires dockerfile:, ko requires ko_package:, unknown methods rejected - Registry validation: duplicate names, unknown types (do/ghcr/ecr/gcr/ dockerhub/acr), push_to cross-references declared registries - Retention validation: keep_latest ≥ 1, untagged_ttl parses as duration - CITarget type validation: unknown types rejected (go/nodejs/rust/python/custom) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each builder plugin.go gains an init() that calls builder.Register(New()), and plugins/all imports them with blank imports so the CLI binary registers all three (go, nodejs, custom) without requiring explicit plugin install. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…+T19) T13 flag fix: adds --only, --skip, --tag, --format, --no-push, --env to build FlagSet so `wfctl build --help` shows all flags; routes image + push + custom subcommands in the switch; wires cfgPath through orchestrator. T19 orchestrator: runBuildOrchestrate chains go → nodejs → image → push in order, honoring --only/--skip/--no-push. End-to-end dry-run test confirms all four phases print for a fixture with go + nodejs + image targets. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n.go Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Release-scale PR for workflow v0.14.0 that introduces a config-driven build/deploy orchestration layer in wfctl, including a builder plugin contract, container registry provider framework, supply-chain hardening defaults, and extensive documentation.
Changes:
- Add
plugin/buildercontract + built-in builder plugins (go,nodejs,custom) and registration viaplugins/all. - Add
plugin/registryprovider interface + DO/GHCR providers (and stub providers), plus newwfctl registrycontainer-registry command plumbing. - Expand config schema for
ci.build.targets,ci.registries, hardened build security defaults, and per-environment build overrides; add tutorial/manual/changelog updates.
Reviewed changes
Copilot reviewed 97 out of 98 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/registry-gitlab/plugin_test.go | Tests for GitLab registry stub provider. |
| plugins/registry-gitlab/plugin.go | GitLab registry stub provider + registration. |
| plugins/registry-github/plugin_test.go | Tests for GHCR provider behaviors (dry-run/errors). |
| plugins/registry-github/plugin.go | GHCR provider implementation (login/push/prune). |
| plugins/registry-gcp/plugin.go | GCP registry stub provider + registration. |
| plugins/registry-do/prune_test.go | Tests for DO prune behavior (dry-run/no-op/errors). |
| plugins/registry-do/prune.go | DO tag-pruning helper for doctl JSON output. |
| plugins/registry-do/plugin_test.go | Tests for DO provider name/login behavior. |
| plugins/registry-do/plugin.go | DO registry provider implementation (login/push/prune). |
| plugins/registry-azure/plugin.go | Azure registry stub provider + registration. |
| plugins/registry-aws/plugin.go | AWS registry stub provider + registration. |
| plugins/builder-nodejs/plugin.go | Registers built-in nodejs builder plugin. |
| plugins/builder-nodejs/nodejs_builder_test.go | Tests for nodejs builder validate/build/lint. |
| plugins/builder-nodejs/nodejs_builder.go | Node.js builder implementation + SecurityLint. |
| plugins/builder-go/plugin.go | Registers built-in go builder plugin. |
| plugins/builder-go/go_builder_test.go | Tests for go builder validate/build/lint. |
| plugins/builder-go/go_builder.go | Go builder implementation + SecurityLint. |
| plugins/builder-custom/plugin.go | Registers built-in custom builder plugin. |
| plugins/builder-custom/custom_builder_test.go | Tests for custom builder validate/build/lint. |
| plugins/builder-custom/custom_builder.go | Custom builder implementation (shell command). |
| plugins/all/all_test.go | Ensures builder plugins registered when loading all. |
| plugins/all/all.go | Blank-import built-in builder plugins for registration. |
| plugin/registry/provider_test.go | Tests global registry provider registry functions. |
| plugin/registry/provider.go | Defines RegistryProvider interface + global registry. |
| plugin/builder/registry_test.go | Tests builder registry (register/get/list/reset). |
| plugin/builder/registry.go | Builder registry implementation (global map). |
| plugin/builder/builder_test.go | Tests builder interface contract types. |
| plugin/builder/builder.go | Defines Builder interface and related types. |
| docs/manual/build-deploy/README.md | Manual index page linking the 9 pages. |
| docs/manual/build-deploy/09-troubleshooting.md | Troubleshooting guide for build/registry/ci init. |
| docs/manual/build-deploy/08-local-dev.md | Local dev workflow and env override documentation. |
| docs/manual/build-deploy/07-security-hardening.md | Supply-chain hardening behavior and options. |
| docs/manual/build-deploy/06-auth-providers.md | Registry and private plugin auth documentation. |
| docs/manual/build-deploy/05-cli-reference.md | CLI reference for v0.14.0 wfctl commands. |
| docs/manual/build-deploy/04-builder-plugins.md | Builder plugin contract and built-in plugins docs. |
| docs/manual/build-deploy/03-ci-deploy-environments.md | Deploy environments schema + ci init semantics docs. |
| docs/manual/build-deploy/02-ci-registries-schema.md | ci.registries schema reference docs. |
| config/environments_config.go | Add EnvironmentConfig.Build for per-env build overrides. |
| config/config.go | Apply build security defaults after import merging. |
| config/ci_validate_build_test.go | Validation tests for containers/registries/targets/retention. |
| config/ci_target_test.go | Tests for targets syntax and legacy binaries coercion. |
| config/ci_target.go | Defines CITarget and binaries→targets YAML shim. |
| config/ci_registry_test.go | Tests for registry YAML unmarshal. |
| config/ci_registry.go | Adds CIRegistry/auth/retention schema types. |
| config/ci_hardened_defaults_test.go | Tests for build security defaults + warnings. |
| config/ci_container_target_test.go | Tests for container target schema unmarshal coverage. |
| config/ci_config_test.go | Updates CI config tests for targets vs binaries. |
| config/ci_build_security_test.go | Tests for build security schema and defaults. |
| config/ci_build_security.go | Build security config struct + ApplyDefaults behavior. |
| cmd/wfctl/wfctl.yaml | Update CLI workflows listing (registry/plugin-registry/build). |
| cmd/wfctl/registry_push_test.go | Tests for wfctl registry push edge cases. |
| cmd/wfctl/registry_push.go | Implements wfctl registry push command logic. |
| cmd/wfctl/registry_login_test.go | Tests for wfctl registry login behavior. |
| cmd/wfctl/registry_login.go | Implements wfctl registry login command logic. |
| cmd/wfctl/registry_container_test.go | Tests for container-registry dispatcher routing. |
| cmd/wfctl/registry_container.go | New wfctl registry dispatcher (login/push/prune/logout). |
| cmd/wfctl/registry_cmd_test.go | Tests for plugin-registry + deprecated alias behavior. |
| cmd/wfctl/registry_cmd.go | Introduces plugin-registry command + deprecated alias. |
| cmd/wfctl/plugin_registry.go | Implements plugin registry querying (GitHub APIs). |
| cmd/wfctl/plugin_lockfile_test.go | Adds tests for plugin install/lock additions. |
| cmd/wfctl/plugin_lock.go | Adds wfctl plugin lock implementation. |
| cmd/wfctl/plugin_install.go | Adds --from-config batch install option. |
| cmd/wfctl/plugin_from_config_test.go | Tests for installing plugins from workflow config. |
| cmd/wfctl/plugin_deps.go | Implements installFromWorkflowConfig batch installer. |
| cmd/wfctl/plugin_auth_test.go | Tests for private plugin auth + cleanup behavior. |
| cmd/wfctl/plugin_auth.go | Implements git URL rewrite + GOPRIVATE injection for auth. |
| cmd/wfctl/main.go | Adds build and plugin-registry commands to CLI map. |
| cmd/wfctl/dev_local_defaults_test.go | Tests local env default security/cache behavior. |
| cmd/wfctl/dev_build_test.go | Tests local build override merge + dev build invocation. |
| cmd/wfctl/dev_build.go | Adds env build override resolution + dev build hook. |
| cmd/wfctl/dev.go | Calls runDevBuild before starting local services. |
| cmd/wfctl/ci_run.go | Updates CI build phase to use Targets vs Binaries. |
| cmd/wfctl/ci_init_deploy_test.go | Tests for new deploy.yml/retention.yml emission logic. |
| cmd/wfctl/build_ui_test.go | Tests for wfctl build ui nodejs builder dispatch. |
| cmd/wfctl/build_ui.go | Implements wfctl build ui via nodejs builder plugin. |
| cmd/wfctl/build_test.go | Tests for top-level wfctl build dispatcher basics. |
| cmd/wfctl/build_sbom_test.go | Tests for SBOM generation/attachment dry-run paths. |
| cmd/wfctl/build_sbom.go | Implements SBOM generation (syft) + attach (oras/cosign). |
| cmd/wfctl/build_resolve_tag_test.go | Tests for tag resolution chain behavior. |
| cmd/wfctl/build_resolve_tag.go | Implements env/command-based tag resolution helper. |
| cmd/wfctl/build_push_test.go | Tests for build push behavior and validation errors. |
| cmd/wfctl/build_push.go | Implements wfctl build push via docker push. |
| cmd/wfctl/build_orchestrate_test.go | Tests for orchestrator flag plumbing/helpers. |
| cmd/wfctl/build_image_test.go | Tests for build image (dockerfile/ko/external) dry-run. |
| cmd/wfctl/build_image_external_test.go | Tests for external image tag_from resolution. |
| cmd/wfctl/build_go_test.go | Tests for wfctl build go builder dispatch. |
| cmd/wfctl/build_go.go | Implements wfctl build go via go builder plugin. |
| cmd/wfctl/build_custom_test.go | Tests for wfctl build custom builder dispatch. |
| cmd/wfctl/build_custom.go | Implements wfctl build custom via custom builder plugin. |
| cmd/wfctl/build.go | Implements top-level wfctl build dispatcher/orchestrator. |
| CHANGELOG.md | Adds v0.14.0 release notes and feature list. |
| envName string | ||
| ) | ||
| fs.StringVar(&cfgPath, "config", "workflow.yaml", "Path to workflow config file") | ||
| fs.StringVar(&cfgPath, "c", "workflow.yaml", "Path to workflow config file (short)") | ||
| fs.BoolVar(&dryRun, "dry-run", false, "Print planned actions without executing") | ||
| fs.StringVar(&only, "only", "", "Build only targets matching this name (comma-separated)") | ||
| fs.StringVar(&skip, "skip", "", "Skip targets matching this name (comma-separated)") | ||
| fs.StringVar(&tag, "tag", "", "Override image tag for all container targets") | ||
| fs.StringVar(&format, "format", "table", "Output format: table | json | yaml") | ||
| fs.BoolVar(&noPush, "no-push", false, "Build but do not push images to registries") | ||
| fs.StringVar(&envName, "env", "", "Environment name for per-env config overrides") | ||
| if err := fs.Parse(args); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if dryRun { | ||
| os.Setenv("WFCTL_BUILD_DRY_RUN", "1") //nolint:errcheck | ||
| defer os.Unsetenv("WFCTL_BUILD_DRY_RUN") //nolint:errcheck | ||
| } | ||
|
|
||
| cfg, err := config.LoadFromFile(cfgPath) | ||
| if err != nil { | ||
| return fmt.Errorf("wfctl build: load config: %w", err) | ||
| } | ||
| if cfg.CI == nil || cfg.CI.Build == nil { | ||
| fmt.Println("No build configuration, skipping build phase") | ||
| return nil | ||
| } | ||
|
|
||
| return runBuildOrchestrate(cfg, buildOpts{ | ||
| dryRun: dryRun, | ||
| only: splitCSV(only), | ||
| skip: splitCSV(skip), | ||
| tag: tag, | ||
| format: format, | ||
| noPush: noPush, | ||
| envName: envName, | ||
| cfgPath: cfgPath, | ||
| }) |
| // runRegistryPrune stub — full implementation in T25/T26. | ||
| func runRegistryPrune(args []string) error { | ||
| fmt.Println("wfctl registry prune: not yet implemented (T25/T26)") | ||
| return nil | ||
| } |
| "github.com/GoCodeAlone/workflow/config" | ||
| "github.com/GoCodeAlone/workflow/plugin/registry" | ||
| _ "github.com/GoCodeAlone/workflow/plugins/registry-do" | ||
| ) |
| // List tags sorted by updated_at, delete beyond keep_latest (preserve "latest"). | ||
| listArgs := []string{"registry", "repository", "list-tags", | ||
| "--format", "Tag,UpdatedAt", "--no-header", "--output", "json"} | ||
| listCmd := exec.CommandContext(ctx, "doctl", listArgs...) //nolint:gosec | ||
| listCmd.Env = append(os.Environ(), "DIGITALOCEAN_TOKEN="+token) | ||
| out, err := listCmd.Output() | ||
| if err != nil { | ||
| return fmt.Errorf("doctl list tags: %w", err) | ||
| } |
| cmd := exec.CommandContext(ctx, "doctl", deleteArgs...) //nolint:gosec | ||
| cmd.Env = append([]string{}, "DIGITALOCEAN_TOKEN="+token) | ||
| if out, err := cmd.CombinedOutput(); err != nil { | ||
| fmt.Fprintf(ctx.Out(), "warn: failed to delete tag %s: %v\n%s", t.Tag, err, out) |
| ref := *imageRef | ||
| if ref == "" { | ||
| ref = ctr.Name + ":latest" | ||
| } | ||
| targets := ctr.PushTo | ||
| if *registryName != "" { | ||
| targets = []string{*registryName} | ||
| } | ||
| for _, regName := range targets { | ||
| reg, ok := regs[regName] | ||
| if !ok { | ||
| return fmt.Errorf("registry push: container %q push_to references unknown registry %q", ctr.Name, regName) | ||
| } | ||
| jobs = append(jobs, pushJob{imageRef: ref, registryName: regName, registry: reg}) | ||
| } | ||
| } | ||
|
|
||
| if len(jobs) == 0 { | ||
| fmt.Println("registry push: no push targets found") | ||
| return nil | ||
| } | ||
|
|
||
| for _, job := range jobs { | ||
| if *dryRun { | ||
| fmt.Printf("[dry-run] push %s → %s (%s)\n", job.imageRef, job.registryName, job.registry.Path) | ||
| continue | ||
| } | ||
|
|
||
| provider, ok := registrypkg.Get(job.registry.Type) | ||
| if !ok { | ||
| // Fallback: docker push directly when no provider registered. | ||
| fmt.Printf("push %s → %s (docker push)\n", job.imageRef, job.registryName) | ||
| if err := dockerPushToRegistry(job.registry.Path + "/" + job.imageRef); err != nil { | ||
| return fmt.Errorf("push %s: %w", job.imageRef, err) | ||
| } | ||
| continue | ||
| } | ||
|
|
||
| ctx := registrypkg.NewContext(context.Background(), os.Stdout, false) | ||
| pcfg := registrypkg.ProviderConfig{Registry: job.registry} | ||
| if err := provider.Push(ctx, pcfg, job.imageRef); err != nil { | ||
| return fmt.Errorf("push %s via %s: %w", job.imageRef, job.registry.Type, err) | ||
| } |
| // Extract os/arch from Config map (set by backcompat shim or user config) | ||
| var osList, archList []string | ||
| if v, ok := bin.Config["os"]; ok { | ||
| if sl, ok := v.([]any); ok { | ||
| for _, s := range sl { osList = append(osList, fmt.Sprintf("%v", s)) } | ||
| } | ||
| } | ||
| if len(osList) == 0 { osList = []string{runtime.GOOS} } | ||
| if v, ok := bin.Config["arch"]; ok { | ||
| if sl, ok := v.([]any); ok { | ||
| for _, s := range sl { archList = append(archList, fmt.Sprintf("%v", s)) } | ||
| } | ||
| } | ||
| if len(archList) == 0 { archList = []string{runtime.GOARCH} } |
| if envMap, ok := bin.Config["env"].(map[string]any); ok { | ||
| for k, v := range envMap { | ||
| cmd.Env = append(cmd.Env, k+"="+os.ExpandEnv(fmt.Sprintf("%v", v))) | ||
| } |
| pm := packageManager(cfg.Fields) | ||
| if pm == "npm" { | ||
| // Also check npm_flags for install hints. | ||
| if flags, ok := cfg.Fields["npm_flags"].(string); ok { | ||
| if strings.Contains(flags, "--no-ci") { | ||
| findings = append(findings, builder.Finding{ | ||
| Severity: "warn", | ||
| Message: "npm_flags contains --no-ci; builds may not be reproducible", | ||
| }) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Warn if package-lock.json is absent (when path is a real directory). | ||
| cwd := cfg.Path | ||
| if cwd == "" { | ||
| cwd = "." | ||
| } | ||
| lockFile := filepath.Join(cwd, "package-lock.json") | ||
| if _, err := os.Stat(lockFile); os.IsNotExist(err) { | ||
| findings = append(findings, builder.Finding{ | ||
| Severity: "warn", | ||
| Message: "package-lock.json not found; commit it for reproducible installs", | ||
| File: lockFile, | ||
| }) | ||
| } |
| safeImages := map[string]bool{ | ||
| "golang:alpine": true, | ||
| "golang:bookworm": true, | ||
| "golang:bullseye": true, | ||
| } | ||
| if builderImage != "" { | ||
| imageBase := strings.SplitN(builderImage, ":", 2)[0] | ||
| if !safeImages[imageBase] && !strings.Contains(imageBase, "golang") { | ||
| findings = append(findings, builder.Finding{ | ||
| Severity: "warn", | ||
| Message: fmt.Sprintf("builder_image %q is not in known-safe list; prefer golang:alpine or golang:bookworm", builderImage), | ||
| }) | ||
| } |
⏱ Benchmark Results✅ No significant performance regressions detected. benchstat comparison (baseline → PR)
|
…Provider; import all providers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gs (doctl repo arg, env base, push ref, type-switch) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ild_image Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR ships workflow v0.14.0, adding wfctl build/registry orchestration, a builder/registry plugin contract, supply-chain hardening defaults, and accompanying schema + documentation so CI deploy pipelines can be generated/maintained with much less YAML.
Changes:
- Introduces
plugin/builder+ built-in builder plugins (go,nodejs,custom) and registers them viaplugins/all. - Adds
plugin/registry+ registry providers (DigitalOcean + GHCR implementations; AWS/Azure/GCP/GitLab stubs) and wires newwfctl registrycontainer subcommands. - Extends config schema (ci.build targets, registries, build security defaults, env build overrides) and adds extensive tests + new build/deploy manual pages.
Reviewed changes
Copilot reviewed 99 out of 109 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/registry-gitlab/plugin.go | Adds GitLab registry provider stub and registers it. |
| plugins/registry-gitlab/plugin_test.go | Verifies stub provider name and ErrNotImplemented behavior. |
| plugins/registry-github/plugin_test.go | Adds tests around GHCR provider login/prune behaviors. |
| plugins/registry-gcp/plugin.go | Adds GCP registry provider stub and registers it. |
| plugins/registry-do/plugin.go | Implements DigitalOcean registry provider (login/logout/push/prune). |
| plugins/registry-do/prune.go | Implements DO tag pruning logic via doctl JSON output. |
| plugins/registry-do/plugin_test.go | Tests DO provider name/login error cases and dry-run output. |
| plugins/registry-do/prune_test.go | Tests DO prune dry-run/no-op/missing-token cases. |
| plugins/registry-azure/plugin.go | Adds Azure registry provider stub and registers it. |
| plugins/registry-aws/plugin.go | Adds AWS registry provider stub and registers it. |
| plugins/builder-nodejs/plugin.go | Registers the built-in nodejs builder plugin. |
| plugins/builder-nodejs/nodejs_builder.go | Implements nodejs builder (install/build steps + SecurityLint). |
| plugins/builder-nodejs/nodejs_builder_test.go | Tests nodejs builder validation/dry-run and SecurityLint warnings. |
| plugins/builder-go/plugin.go | Registers the built-in go builder plugin. |
| plugins/builder-go/go_builder.go | Implements go builder (go build) + basic SecurityLint checks. |
| plugins/builder-go/go_builder_test.go | Tests go builder validation/dry-run and SecurityLint warnings. |
| plugins/builder-custom/plugin.go | Registers the built-in custom builder plugin. |
| plugins/builder-custom/custom_builder.go | Implements custom builder running sh -c with outputs/timeout. |
| plugins/builder-custom/custom_builder_test.go | Tests custom builder validation/dry-run and SecurityLint behavior. |
| plugins/all/all.go | Ensures built-in builder plugins are loaded/registered with all plugins. |
| plugins/all/all_test.go | Asserts builder plugins are registered after importing plugins/all. |
| plugin/registry/provider.go | Adds registry provider interface + global registry and Context helper. |
| plugin/registry/provider_test.go | Tests registry provider register/get/list behavior. |
| plugin/builder/builder.go | Defines builder plugin contract types (Builder/Config/Outputs/Finding). |
| plugin/builder/registry.go | Adds global builder registry with Register/Get/List/Reset. |
| plugin/builder/builder_test.go | Contract test for the Builder interface behavior. |
| plugin/builder/registry_test.go | Tests builder registry behavior (including overwrite semantics). |
| platform/providers/aws/drivers/sqs_test.go | Formatting/indentation fix in test mock struct fields. |
| platform/providers/aws/drivers/iam_test.go | Formatting/indentation fix in test mock struct fields. |
| platform/providers/aws/drivers/eks_cluster_test.go | Formatting/indentation fix in test mock struct fields. |
| platform/providers/aws/drivers/alb_test.go | Formatting/indentation fix in test mock struct fields. |
| module/pipeline_step_http_call_test.go | Formatting/alignment fix in step config map. |
| featureflag/launchdarkly/provider.go | Import ordering adjustment. |
| example/ecommerce-app/components/inventory_checker.go | Formatting/alignment tweak + adds reserved_until field. |
| example/chat-platform/components/escalation_handler.go | Formatting/alignment tweak in returned map. |
| example/chat-platform/components/data_retention.go | Formatting/alignment tweak in returned map. |
| example/chat-platform/components/ai_summarizer.go | Formatting/alignment tweak in keyword map. |
| docs/manual/build-deploy/README.md | Adds build+deploy manual index page. |
| docs/manual/build-deploy/02-ci-registries-schema.md | Documents ci.registries schema, retention, and validation rules. |
| docs/manual/build-deploy/03-ci-deploy-environments.md | Documents deploy environments schema and generated job ordering. |
| docs/manual/build-deploy/04-builder-plugins.md | Documents builder plugin contract and built-in builders. |
| docs/manual/build-deploy/05-cli-reference.md | Documents new/updated wfctl commands/flags and rename to plugin-registry. |
| docs/manual/build-deploy/06-auth-providers.md | Documents registry auth and private plugin repo auth workflow. |
| docs/manual/build-deploy/07-security-hardening.md | Documents hardened defaults, SBOM, provenance, and audit behavior. |
| docs/manual/build-deploy/08-local-dev.md | Documents local env overrides and dev build behavior. |
| docs/manual/build-deploy/09-troubleshooting.md | Adds troubleshooting guide for build/registry/config/CI issues. |
| config/environments_config.go | Adds per-environment Build overrides. |
| config/config.go | Extends requires.plugins[] with source/auth and applies build defaults after imports. |
| config/ci_target.go | Adds CITarget model + YAML shim migrating legacy binaries: to targets:. |
| config/ci_target_test.go | Tests new targets syntax, legacy binaries coercion, and env overrides. |
| config/ci_registry.go | Introduces CIRegistry/CIRegistryAuth/Retention types. |
| config/ci_registry_test.go | Tests registry YAML unmarshal with auth/retention fields. |
| config/ci_container_target_test.go | Tests container target unmarshal across dockerfile/ko/external fields. |
| config/ci_validate_build_test.go | Adds CI validation tests for container methods, registries, retention, target types. |
| config/ci_build_security.go | Adds supply-chain security config + ApplyDefaults implementation. |
| config/ci_build_security_test.go | Tests unmarshal and ApplyDefaults semantics. |
| config/ci_hardened_defaults_test.go | Tests LoadFromFile hardened defaults and ValidateWithWarnings behavior. |
| config/ci_config_test.go | Updates CI config tests to assert targets (coerced from binaries) behavior. |
| cmd/wfctl/wfctl.yaml | Updates command descriptions and adds build + plugin-registry commands. |
| cmd/wfctl/type_registry.go | Minor formatting cleanup. |
| cmd/wfctl/main.go | Wires new build and plugin-registry command handlers. |
| cmd/wfctl/registry_cmd.go | Introduces runPluginRegistry and a deprecated alias wrapper for legacy routing. |
| cmd/wfctl/registry_cmd_test.go | Tests deprecated alias and basic command wiring behaviors. |
| cmd/wfctl/registry_container.go | Adds new container-registry dispatcher (`wfctl registry login |
| cmd/wfctl/registry_container_test.go | Tests container-registry dispatcher routing/usage errors. |
| cmd/wfctl/registry_login.go | Implements wfctl registry login using registry providers. |
| cmd/wfctl/registry_login_test.go | Tests registry login behavior (dry-run, registry selection, errors). |
| cmd/wfctl/registry_push.go | Implements wfctl registry push orchestration via providers/fallback. |
| cmd/wfctl/registry_push_test.go | Tests registry push no-op/dry-run behavior. |
| cmd/wfctl/plugin_registry.go | Adds GitHub-backed plugin registry fetch/list/search helpers. |
| cmd/wfctl/plugin_install.go | Adds --from-config to install plugins from requires.plugins[]. |
| cmd/wfctl/plugin_from_config_test.go | Tests install-from-config behavior and flag wiring. |
| cmd/wfctl/plugin_deps.go | Adds installFromWorkflowConfig helper and integrates config parsing. |
| cmd/wfctl/plugin_auth.go | Adds private repo auth via git URL rewriting + GOPRIVATE management. |
| cmd/wfctl/plugin_auth_test.go | Tests private auth setup/cleanup and from-config auth scenarios. |
| cmd/wfctl/plugin_lock.go | Adds wfctl plugin lock lockfile regeneration command. |
| cmd/wfctl/plugin_lockfile_test.go | Adds tests for lockfile behaviors + from-config + lock generation. |
| cmd/wfctl/dev_build.go | Adds env build resolution + local defaults (hardening relaxed, cache injected). |
| cmd/wfctl/dev_build_test.go | Tests env override merge behavior and dev build dry-run. |
| cmd/wfctl/dev_local_defaults_test.go | Tests local hardening defaults and local cache injection. |
| cmd/wfctl/dev.go | Ensures wfctl dev up runs dev build before starting services. |
| cmd/wfctl/ci_run.go | Updates CI build phase to use targets and config-driven env extraction. |
| cmd/wfctl/ci_init_deploy_test.go | Tests new deploy.yml + retention workflow generation behaviors. |
| cmd/wfctl/build_test.go | Tests wfctl build dry-run and unknown subcommand error. |
| cmd/wfctl/build_orchestrate_test.go | Tests build orchestrator filtering helpers and flag presence expectations. |
| cmd/wfctl/build_go.go | Adds wfctl build go subcommand via builder registry. |
| cmd/wfctl/build_go_test.go | Tests wfctl build go dry-run and target selection errors. |
| cmd/wfctl/build_ui.go | Adds wfctl build ui subcommand via nodejs builder. |
| cmd/wfctl/build_ui_test.go | Tests wfctl build ui dry-run and target selection errors. |
| cmd/wfctl/build_custom.go | Adds wfctl build custom subcommand via custom builder. |
| cmd/wfctl/build_custom_test.go | Tests wfctl build custom dry-run and target selection errors. |
| cmd/wfctl/build_image_test.go | Tests build image dry-run paths for dockerfile/ko/external. |
| cmd/wfctl/build_image_external_test.go | Tests external image tag resolution and build skipping. |
| cmd/wfctl/build_push.go | Adds build-push step for containers based on push_to and tags. |
| cmd/wfctl/build_push_test.go | Tests build-push dry-run, unknown registry handling, and skip external. |
| cmd/wfctl/build_resolve_tag.go | Adds tag resolution chain (env var -> command -> fallback). |
| cmd/wfctl/build_resolve_tag_test.go | Tests tag resolution precedence and fallthrough behavior. |
| cmd/wfctl/build_sbom.go | Adds SBOM generation/attachment via syft + oras/cosign. |
| cmd/wfctl/build_sbom_test.go | Tests SBOM generation/attachment in dry-run and no-op cases. |
| // Write git insteadOf rule. | ||
| setArgs := []string{"config", "--global", | ||
| fmt.Sprintf("url.%s.insteadOf", rewriteURL), | ||
| targetURL, | ||
| } |
| if !ok { | ||
| // Fallback: docker push directly when no provider registered. | ||
| fmt.Printf("push %s → %s (docker push)\n", job.imageRef, job.registryName) | ||
| if err := dockerPushToRegistry(job.registry.Path + "/" + job.imageRef); err != nil { | ||
| return fmt.Errorf("push %s: %w", job.imageRef, err) |
| ref := *imageRef | ||
| if ref == "" { | ||
| ref = reg.Path + "/" + ctr.Name + ":latest" | ||
| } | ||
| jobs = append(jobs, pushJob{imageRef: ref, registryName: regName, registry: reg}) |
| if v, ok := bin.Config["os"]; ok { | ||
| switch val := v.(type) { | ||
| case []string: | ||
| osList = val | ||
| case []any: |
| lf, _ := loadPluginLockfile(*lockPath) | ||
| if lf == nil { | ||
| lf = &PluginLockfile{} |
| for _, req := range cfg.Requires.Plugins { | ||
| if _, exists := lf.Plugins[req.Name]; !exists { | ||
| lf.Plugins[req.Name] = PluginLockEntry{Version: req.Version} | ||
| } |
Summary
Ships workflow v0.14.0 with
wfctl build,wfctl registry(container), enhancedwfctl plugin install, hardened supply-chain defaults, builder plugin contract, and tutorial + 9-page manual — so downstream deploy.yml files drop from 73–155 lines to ~25–50 lines.go test -race -count=1 ./...)gofmtclean on all branch-modified filesWhat shipped
Phase 1 — Config schema foundations
ci.build.targets[]typed dispatch (go/nodejs/custom/…), replaces legacybinaries:ci.registries[]with auth + retentionCIContainerTargetextensions (BuildKit secrets/cache/platforms/ko/external)CIBuildSecuritywith hardened defaults applied atLoadFromFiletimePhase 2 — Builder plugin contract
plugin/builder.Builderinterfacego,nodejs,custombuilder pluginsPhase 3 —
wfctl buildcommand familywfctl buildorchestrator (go → ui → image → push)wfctl build go|ui|image|push|customsubcommands--dry-run,--only,--skip,--tag,--format,--no-push,--env,--security-auditPhase 4 —
wfctl registrycontainer commandswfctl registry login|push|prune(DO + GHCR full impl)RegistryProviderinterface +plugin/registrypackagewfctl registry→ container registries; plugin catalog renamed towfctl plugin-registryPhase 5 — Enhanced
wfctl plugin install--from-configbatch install fromrequires.plugins[]wfctl plugin lock)git config --global url.insteadOf+GOPRIVATEPhase 6 — Supply-chain hardening
syft+ OCI attachment (oras/cosign)ValidateWithWarnings()for opt-out detectionwfctl build --security-auditDockerfile linting (T34 deferred to v0.14.1)Phase 7 — Local dev symmetry
environments.local.buildper-target config overridesenv == "local"wfctl dev upcallsrunDevBuildbefore starting servicesPhase 8 — External image support
external: truecontainer targets resolved viatag_fromchainResolveTag(entries, fallback)— env var → shell command → fallbackPhase 9 —
wfctl ci initemitterdeploy.ymlwithworkflow_runtrigger,build-image+deploy-*jobs, concurrency, SHA pinningregistry-retention.ymlwhenretention.scheduleis declaredPhase 10 — Documentation
Phase 11 — Release prep
Deferred to v0.14.1
wfctl build --security-auditcommand (implementer-1 stalled)Links
Test plan
GOWORK=off go test -race -count=1 ./...— 129 packages, 0 failuresgofmt -lclean on all branch-modified filesWFCTL_BUILD_DRY_RUN=1 wfctl buildagainst tutorial fixture configs (dry-run verified)wfctl registry prune --dry-runagainst DO + GHCR (requires live credentials — team-lead to verify post-merge)🤖 Generated with Claude Code