From 9f9efb9f40c37cdd7b583d6d2cfbb4588e5416d7 Mon Sep 17 00:00:00 2001 From: Ivy Rhodes Date: Thu, 11 Jun 2026 11:04:15 -0400 Subject: [PATCH 1/4] feat: add `validate-config` subcommand (KPL-19) Validates a local kploy.yaml file via the new github.com/bitcomplete/kployconfig module. Prints OK + sample rendered hostnames for prod/development/pr-1, plus the preview ingress hostnames for PR #1 when preview envs are enabled. JSON output supported via the existing --output flag. Companion to https://github.com/bitcomplete/kploy/pull/105 (the library shipped) + https://github.com/bitcomplete/kployconfig (the module's new public home). Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/root.go | 1 + cmd/validate_config.go | 102 +++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 4 ++ 4 files changed, 108 insertions(+) create mode 100644 cmd/validate_config.go diff --git a/cmd/root.go b/cmd/root.go index 344560d..ee0c184 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -24,6 +24,7 @@ func Root() *cobra.Command { root.AddCommand(imageCommand()) root.AddCommand(orgCommand()) root.AddCommand(repoCommand()) + root.AddCommand(validateConfigCommand()) root.AddCommand(versionCommand()) return root diff --git a/cmd/validate_config.go b/cmd/validate_config.go new file mode 100644 index 0000000..0044fbd --- /dev/null +++ b/cmd/validate_config.go @@ -0,0 +1,102 @@ +package cmd + +import ( + "errors" + "fmt" + "os" + "text/tabwriter" + + "github.com/bitcomplete/kployconfig" + "github.com/spf13/cobra" +) + +func validateConfigCommand() *cobra.Command { + var file string + c := &cobra.Command{ + Use: "validate-config", + Short: "Validate a local kploy.yaml file", + Long: `Parse and validate a kploy.yaml file, then print the hostnames +it would render for prod, development, and an example PR env.`, + RunE: func(c *cobra.Command, args []string) error { + data, err := os.ReadFile(file) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return fmt.Errorf("no %s in current directory; use --file to point elsewhere", file) + } + return fmt.Errorf("reading %s: %w", file, err) + } + cfg, err := kployconfig.Load(data) + if err != nil { + return err + } + + envs := []struct { + name string + pr int + }{ + {"prod", 0}, + {"development", 0}, + {"pr-1", 1}, + } + hostnames := make(map[string]string, len(envs)) + for _, env := range envs { + h, err := kployconfig.RenderHostname(cfg, env.name, env.pr) + if err != nil { + return fmt.Errorf("render %s: %w", env.name, err) + } + hostnames[env.name] = h + } + + var ingress []kployconfig.IngressHostname + if cfg.Preview != nil && cfg.Preview.Enabled { + ingress, err = kployconfig.RenderPreviewIngress(cfg, 1) + if err != nil { + return err + } + } + + out := c.OutOrStdout() + if outputFormat == "json" { + return renderJSON(out, struct { + Valid bool `json:"valid"` + Project string `json:"project"` + Hostnames map[string]string `json:"hostnames"` + PreviewIngress []kployconfig.IngressHostname `json:"previewIngress,omitempty"` + }{ + Valid: true, + Project: cfg.Project, + Hostnames: hostnames, + PreviewIngress: ingress, + }) + } + + fmt.Fprintln(out, "OK") + fmt.Fprintln(out) + fmt.Fprintln(out, "Sample hostnames:") + tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) + fmt.Fprintln(tw, "ENV\tHOSTNAME") + for _, env := range envs { + fmt.Fprintf(tw, "%s\t%s\n", env.name, hostnames[env.name]) + } + if err := tw.Flush(); err != nil { + return err + } + + if len(ingress) > 0 { + fmt.Fprintln(out) + fmt.Fprintln(out, "Preview ingress (for PR #1):") + tw2 := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) + fmt.Fprintln(tw2, "HOSTNAME\tSERVICE\tPORT") + for _, ing := range ingress { + fmt.Fprintf(tw2, "%s\t%s\t%d\n", ing.Hostname, ing.ServiceName, ing.ServicePort) + } + if err := tw2.Flush(); err != nil { + return err + } + } + return nil + }, + } + c.Flags().StringVarP(&file, "file", "f", "kploy.yaml", "Path to kploy.yaml") + return c +} diff --git a/go.mod b/go.mod index 7976906..eaecff0 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.3 tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen require ( + github.com/bitcomplete/kployconfig v0.1.1 github.com/oapi-codegen/runtime v1.4.0 github.com/spf13/cobra v1.10.1 go.yaml.in/yaml/v3 v3.0.4 diff --git a/go.sum b/go.sum index aec0503..c9903fe 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,10 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/bitcomplete/kployconfig v0.1.0 h1:WEMGMgVfLyg9Hift9vMhEXwNqcoJbbKcnvn/UGb8GMA= +github.com/bitcomplete/kployconfig v0.1.0/go.mod h1:qyAG3qodAW45q7CXYtLe3oBcB5DVL1Q2YtdvJw01QUs= +github.com/bitcomplete/kployconfig v0.1.1 h1:3eMolFvB7Jn8/NFdfIatxVwEflvLjJbKxDe+BPCnvfY= +github.com/bitcomplete/kployconfig v0.1.1/go.mod h1:qyAG3qodAW45q7CXYtLe3oBcB5DVL1Q2YtdvJw01QUs= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= From 21e0d376bd98f550ddc72a532e6a50146e20629a Mon Sep 17 00:00:00 2001 From: Ivy Rhodes Date: Thu, 11 Jun 2026 11:56:48 -0400 Subject: [PATCH 2/4] refactor(validate-config): clearer output, README entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructure the text-mode output so the "example PR preview" framing is explicit: - Drop the synthetic pr-1 row from the Sample hostnames table. - Rename the ingress section header to "Example preview env (PR #1)" so the hypothetical-PR framing is unambiguous. - Only print the example-preview section when preview envs are actually enabled — projects without preview just see prod + development. Also adds a README section under Authenticate so the command is discoverable from public-facing docs. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 11 +++++++++++ cmd/validate_config.go | 6 +++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 74ec401..1818d35 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,17 @@ kploy auth whoami # show the orgs your token can see kploy auth logout # forget the saved token ``` +## Validating `kploy.yaml` + +If your project has a `kploy.yaml` at its repo root, you can sanity-check it locally before pushing: + +```sh +kploy validate-config # defaults to ./kploy.yaml +kploy validate-config -f my.yaml # different path +``` + +Prints rendered hostnames for production and development on success — plus an example preview env if preview environments are enabled. Validation errors include the field path. Does not require authentication. + ## Common workflows ```sh diff --git a/cmd/validate_config.go b/cmd/validate_config.go index 0044fbd..30457b6 100644 --- a/cmd/validate_config.go +++ b/cmd/validate_config.go @@ -16,7 +16,8 @@ func validateConfigCommand() *cobra.Command { Use: "validate-config", Short: "Validate a local kploy.yaml file", Long: `Parse and validate a kploy.yaml file, then print the hostnames -it would render for prod, development, and an example PR env.`, +it would render for production and development — plus, if preview +environments are enabled, an example PR preview env.`, RunE: func(c *cobra.Command, args []string) error { data, err := os.ReadFile(file) if err != nil { @@ -36,7 +37,6 @@ it would render for prod, development, and an example PR env.`, }{ {"prod", 0}, {"development", 0}, - {"pr-1", 1}, } hostnames := make(map[string]string, len(envs)) for _, env := range envs { @@ -84,7 +84,7 @@ it would render for prod, development, and an example PR env.`, if len(ingress) > 0 { fmt.Fprintln(out) - fmt.Fprintln(out, "Preview ingress (for PR #1):") + fmt.Fprintln(out, "Example preview env (PR #1):") tw2 := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) fmt.Fprintln(tw2, "HOSTNAME\tSERVICE\tPORT") for _, ing := range ingress { From 906c7bb592933c96e52382d6c1341dc24e3876d8 Mon Sep 17 00:00:00 2001 From: Ivy Rhodes Date: Thu, 11 Jun 2026 11:58:50 -0400 Subject: [PATCH 3/4] fix: drop stale kployconfig v0.1.0 entries from go.sum `go get` left the previous version's checksums behind when bumping to v0.1.1; CI catches it via `go mod tidy && git diff --exit-code`. Co-Authored-By: Claude Opus 4.7 (1M context) --- go.sum | 2 -- 1 file changed, 2 deletions(-) diff --git a/go.sum b/go.sum index c9903fe..7a8359c 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,6 @@ github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/bitcomplete/kployconfig v0.1.0 h1:WEMGMgVfLyg9Hift9vMhEXwNqcoJbbKcnvn/UGb8GMA= -github.com/bitcomplete/kployconfig v0.1.0/go.mod h1:qyAG3qodAW45q7CXYtLe3oBcB5DVL1Q2YtdvJw01QUs= github.com/bitcomplete/kployconfig v0.1.1 h1:3eMolFvB7Jn8/NFdfIatxVwEflvLjJbKxDe+BPCnvfY= github.com/bitcomplete/kployconfig v0.1.1/go.mod h1:qyAG3qodAW45q7CXYtLe3oBcB5DVL1Q2YtdvJw01QUs= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= From f3a05384a925f44e6b58ba09b32dd8fbbda17242 Mon Sep 17 00:00:00 2001 From: Ivy Rhodes Date: Thu, 11 Jun 2026 12:04:39 -0400 Subject: [PATCH 4/4] fix(lint): discard fmt.Fprint return values explicitly errcheck flags unchecked return values on new code; existing CLI commands using the same pattern are exempted via the lint config's only-new-issues setting. Match the convention by writing the discard explicitly. Co-Authored-By: Claude Opus 4.7 (1M context) --- cmd/validate_config.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/validate_config.go b/cmd/validate_config.go index 30457b6..e6dfe4e 100644 --- a/cmd/validate_config.go +++ b/cmd/validate_config.go @@ -70,25 +70,25 @@ environments are enabled, an example PR preview env.`, }) } - fmt.Fprintln(out, "OK") - fmt.Fprintln(out) - fmt.Fprintln(out, "Sample hostnames:") + _, _ = fmt.Fprintln(out, "OK") + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintln(out, "Sample hostnames:") tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) - fmt.Fprintln(tw, "ENV\tHOSTNAME") + _, _ = fmt.Fprintln(tw, "ENV\tHOSTNAME") for _, env := range envs { - fmt.Fprintf(tw, "%s\t%s\n", env.name, hostnames[env.name]) + _, _ = fmt.Fprintf(tw, "%s\t%s\n", env.name, hostnames[env.name]) } if err := tw.Flush(); err != nil { return err } if len(ingress) > 0 { - fmt.Fprintln(out) - fmt.Fprintln(out, "Example preview env (PR #1):") + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintln(out, "Example preview env (PR #1):") tw2 := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) - fmt.Fprintln(tw2, "HOSTNAME\tSERVICE\tPORT") + _, _ = fmt.Fprintln(tw2, "HOSTNAME\tSERVICE\tPORT") for _, ing := range ingress { - fmt.Fprintf(tw2, "%s\t%s\t%d\n", ing.Hostname, ing.ServiceName, ing.ServicePort) + _, _ = fmt.Fprintf(tw2, "%s\t%s\t%d\n", ing.Hostname, ing.ServiceName, ing.ServicePort) } if err := tw2.Flush(); err != nil { return err