From 84e051d99505c7affe58348248683ad6571bc258 Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 9 Mar 2026 09:57:07 -0400 Subject: [PATCH 1/8] feature(service/validate): Add support for the 'service version validate' command. This command determines if a given version is valid for activation. --- pkg/api/interface.go | 1 + pkg/commands/commands.go | 2 + pkg/commands/service/version/validate.go | 108 +++++++++++++++++++++++ pkg/mock/api.go | 6 ++ 4 files changed, 117 insertions(+) create mode 100644 pkg/commands/service/version/validate.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index f7b41ff5e..454ba2496 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -39,6 +39,7 @@ type Interface interface { DeactivateVersion(context.Context, *fastly.DeactivateVersionInput) (*fastly.Version, error) LockVersion(context.Context, *fastly.LockVersionInput) (*fastly.Version, error) LatestVersion(context.Context, *fastly.LatestVersionInput) (*fastly.Version, error) + ValidateVersion(context.Context, *fastly.ValidateVersionInput) (bool, string, error) CreateDomain(context.Context, *fastly.CreateDomainInput) (*fastly.Domain, error) ListDomains(context.Context, *fastly.ListDomainsInput) ([]*fastly.Domain, error) diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index d757bb2f6..bfe3a3d1c 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -663,6 +663,7 @@ func Define( // nolint:revive // function-length serviceVersionStage := serviceversion.NewStageCommand(serviceVersionCmdRoot.CmdClause, data) serviceVersionUnstage := serviceversion.NewUnstageCommand(serviceVersionCmdRoot.CmdClause, data) serviceVersionUpdate := serviceversion.NewUpdateCommand(serviceVersionCmdRoot.CmdClause, data) + serviceVersionValidate := serviceversion.NewValidateCommand(serviceVersionCmdRoot.CmdClause, data) servicedomainCmdRoot := servicedomain.NewRootCommand(serviceCmdRoot.CmdClause, data) servicedomainCreate := servicedomain.NewCreateCommand(servicedomainCmdRoot.CmdClause, data) servicedomainDelete := servicedomain.NewDeleteCommand(servicedomainCmdRoot.CmdClause, data) @@ -1534,6 +1535,7 @@ func Define( // nolint:revive // function-length serviceVersionStage, serviceVersionUnstage, serviceVersionUpdate, + serviceVersionValidate, ssoCmdRoot, statsCmdRoot, statsHistorical, diff --git a/pkg/commands/service/version/validate.go b/pkg/commands/service/version/validate.go new file mode 100644 index 000000000..c113fca35 --- /dev/null +++ b/pkg/commands/service/version/validate.go @@ -0,0 +1,108 @@ +package version + +import ( + "context" + "fmt" + "io" + + "github.com/fastly/go-fastly/v13/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/errors" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" +) + +// ValidateCommand calls the Fastly API to validate a service version. +type ValidateCommand struct { + argparser.Base + argparser.JSONOutput + + input fastly.ValidateVersionInput + serviceVersion argparser.OptionalServiceVersion +} + +// NewValidateCommand returns a usable command registered under the parent. +func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCommand { + c := ValidateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.CmdClause = parent.Command("validate", "Validate a service version") + c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + return &c +} + +// Exec invokes the application logic for the command. +func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + serviceID, serviceVersion, err := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.Manifest, + Out: out, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + /*if c.Globals.Verbose() { + argparser.DisplayServiceID(serviceID, flag, source, out) + }*/ + + c.input.ServiceID = serviceID + c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + + valid, msg, err := c.Globals.APIClient.ValidateVersion(context.TODO(), &c.input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + }) + return err + } + + if ok, err := c.WriteJSON(out, map[string]any{ + "valid": valid, + "message": msg, + }); ok { + return err + } + + fmt.Fprintf(out, "\nService ID: %s\n", serviceID) + fmt.Fprintf(out, "Service Version: %d\n", c.input.ServiceVersion) + fmt.Fprintf(out, "Valid: %t\n", valid) + if msg != "" { + fmt.Fprintf(out, "Message: %s\n", msg) + } + fmt.Fprintln(out) + + return nil +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 82b7920b3..b0ba72c81 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -31,6 +31,7 @@ type API struct { DeactivateVersionFn func(context.Context, *fastly.DeactivateVersionInput) (*fastly.Version, error) LockVersionFn func(context.Context, *fastly.LockVersionInput) (*fastly.Version, error) LatestVersionFn func(context.Context, *fastly.LatestVersionInput) (*fastly.Version, error) + ValidateVersionFn func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) CreateDomainFn func(context.Context, *fastly.CreateDomainInput) (*fastly.Domain, error) ListDomainsFn func(context.Context, *fastly.ListDomainsInput) ([]*fastly.Domain, error) @@ -499,6 +500,11 @@ func (m API) LatestVersion(ctx context.Context, i *fastly.LatestVersionInput) (* return m.LatestVersionFn(ctx, i) } +// ValidateVersion implements Interface. +func (m API) ValidateVersion(ctx context.Context, i *fastly.ValidateVersionInput) (bool, string, error) { + return m.ValidateVersionFn(ctx, i) +} + // CreateDomain implements Interface. func (m API) CreateDomain(ctx context.Context, i *fastly.CreateDomainInput) (*fastly.Domain, error) { return m.CreateDomainFn(ctx, i) From d24ffca32f862ba8a36a47f951b2310b43f6d7f9 Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 16 Mar 2026 10:48:54 -0400 Subject: [PATCH 2/8] Update validate.go --- pkg/commands/service/version/validate.go | 36 +++++++++++------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/pkg/commands/service/version/validate.go b/pkg/commands/service/version/validate.go index c113fca35..f650e1a27 100644 --- a/pkg/commands/service/version/validate.go +++ b/pkg/commands/service/version/validate.go @@ -2,7 +2,6 @@ package version import ( "context" - "fmt" "io" "github.com/fastly/go-fastly/v13/fastly" @@ -11,6 +10,7 @@ import ( "github.com/fastly/cli/pkg/errors" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" ) // ValidateCommand calls the Fastly API to validate a service version. @@ -29,11 +29,14 @@ func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCo Globals: g, }, } + c.CmdClause = parent.Command("validate", "Validate a service version") + c.RegisterFlagBool(c.JSONFlag()) // --json c.RegisterFlag(argparser.StringFlagOpts{ Name: argparser.FlagServiceIDName, Description: argparser.FlagServiceIDDesc, Dst: &g.Manifest.Flag.ServiceID, Short: 's', + Required: true, }) c.RegisterFlag(argparser.StringFlagOpts{ Name: argparser.FlagVersionName, @@ -41,16 +44,6 @@ func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCo Dst: &c.serviceVersion.Value, Required: true, }) - - // Optional. - c.CmdClause = parent.Command("validate", "Validate a service version") - c.RegisterFlagBool(c.JSONFlag()) // --json - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, - Short: 's', - }) return &c } @@ -74,9 +67,6 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { }) return err } - /*if c.Globals.Verbose() { - argparser.DisplayServiceID(serviceID, flag, source, out) - }*/ c.input.ServiceID = serviceID c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number) @@ -96,13 +86,19 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { return err } - fmt.Fprintf(out, "\nService ID: %s\n", serviceID) - fmt.Fprintf(out, "Service Version: %d\n", c.input.ServiceVersion) - fmt.Fprintf(out, "Valid: %t\n", valid) - if msg != "" { - fmt.Fprintf(out, "Message: %s\n", msg) + if valid { + if msg != "" { + text.Success(out, "Service %s version %d is valid: %s", serviceID, c.input.ServiceVersion, msg) + } else { + text.Success(out, "Service %s version %d is valid", serviceID, c.input.ServiceVersion) + } + } else { + if msg != "" { + text.Error(out, "Service %s version %d is not valid: %s", serviceID, c.input.ServiceVersion, msg) + } else { + text.Error(out, "Service %s version %d is not valid", serviceID, c.input.ServiceVersion) + } } - fmt.Fprintln(out) return nil } From 702d696a850051f9df760bd09ada62f04c2f9750 Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 16 Mar 2026 15:44:55 -0400 Subject: [PATCH 3/8] Update serviceversion_test.go added 'TestVersionValidate' to existing test file. --- .../service/version/serviceversion_test.go | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/pkg/commands/service/version/serviceversion_test.go b/pkg/commands/service/version/serviceversion_test.go index a6268b8bc..02a71c09e 100644 --- a/pkg/commands/service/version/serviceversion_test.go +++ b/pkg/commands/service/version/serviceversion_test.go @@ -481,3 +481,113 @@ func lockVersionOK(_ context.Context, i *fastly.LockVersionInput) (*fastly.Versi func lockVersionError(_ context.Context, _ *fastly.LockVersionInput) (*fastly.Version, error) { return nil, testutil.Err } + +func TestVersionValidate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --service-id flag", + Args: "--version 1", + WantError: "error parsing arguments: required flag --service-id not provided", + }, + { + Name: "validate missing --version flag", + Args: "--service-id 123", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate successful - valid version without message", + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionValid(""), + }, + WantOutput: "Service 123 version 1 is valid", + }, + { + Name: "validate successful - valid version with message", + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionValid("All checks passed"), + }, + WantOutput: "Service 123 version 1 is valid: All checks passed", + }, + { + Name: "validate successful - invalid version without message", + Args: "--service-id 123 --version 2", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionInvalid(""), + }, + WantOutput: "Service 123 version 2 is not valid", + }, + { + Name: "validate successful - invalid version with message", + Args: "--service-id 123 --version 2", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionInvalid("Missing required backend"), + }, + WantOutput: "Service 123 version 2 is not valid: Missing required backend", + }, + { + Name: "validate with json output - valid version", + Args: "--service-id 123 --version 1 --json", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionValid("All checks passed"), + }, + WantOutput: validateVersionValidJSONOutput, + }, + { + Name: "validate with json output - invalid version", + Args: "--service-id 123 --version 2 --json", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionInvalid("Missing required backend"), + }, + WantOutput: validateVersionInvalidJSONOutput, + }, + { + Name: "validate error from API", + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionError, + }, + WantError: testutil.Err.Error(), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "validate"}, scenarios) +} + +func validateVersionValid(message string) func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) { + return func(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) { + return true, message, nil + } +} + +func validateVersionInvalid(message string) func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) { + return func(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) { + return false, message, nil + } +} + +func validateVersionError(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) { + return false, "", testutil.Err +} + +var validateVersionValidJSONOutput = strings.TrimSpace(` +{ + "message": "All checks passed", + "valid": true +} +`) + "\n" + +var validateVersionInvalidJSONOutput = strings.TrimSpace(` +{ + "message": "Missing required backend", + "valid": false +} +`) + "\n" From 2c0e11084467fa2ff9f3444dd727f366be4f4f75 Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 9 Mar 2026 09:57:07 -0400 Subject: [PATCH 4/8] feature(service/validate): Add support for the 'service version validate' command. This command determines if a given version is valid for activation. --- pkg/api/interface.go | 1 + pkg/commands/commands.go | 2 + pkg/commands/service/version/validate.go | 108 +++++++++++++++++++++++ pkg/mock/api.go | 6 ++ 4 files changed, 117 insertions(+) create mode 100644 pkg/commands/service/version/validate.go diff --git a/pkg/api/interface.go b/pkg/api/interface.go index d0d357462..93a20a4db 100644 --- a/pkg/api/interface.go +++ b/pkg/api/interface.go @@ -39,6 +39,7 @@ type Interface interface { DeactivateVersion(context.Context, *fastly.DeactivateVersionInput) (*fastly.Version, error) LockVersion(context.Context, *fastly.LockVersionInput) (*fastly.Version, error) LatestVersion(context.Context, *fastly.LatestVersionInput) (*fastly.Version, error) + ValidateVersion(context.Context, *fastly.ValidateVersionInput) (bool, string, error) CreateDomain(context.Context, *fastly.CreateDomainInput) (*fastly.Domain, error) ListDomains(context.Context, *fastly.ListDomainsInput) ([]*fastly.Domain, error) diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index db7612589..45b2f749b 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -712,6 +712,7 @@ func Define( // nolint:revive // function-length serviceVersionStage := serviceversion.NewStageCommand(serviceVersionCmdRoot.CmdClause, data) serviceVersionUnstage := serviceversion.NewUnstageCommand(serviceVersionCmdRoot.CmdClause, data) serviceVersionUpdate := serviceversion.NewUpdateCommand(serviceVersionCmdRoot.CmdClause, data) + serviceVersionValidate := serviceversion.NewValidateCommand(serviceVersionCmdRoot.CmdClause, data) servicedomainCmdRoot := servicedomain.NewRootCommand(serviceCmdRoot.CmdClause, data) servicedomainCreate := servicedomain.NewCreateCommand(servicedomainCmdRoot.CmdClause, data) servicedomainDelete := servicedomain.NewDeleteCommand(servicedomainCmdRoot.CmdClause, data) @@ -1601,6 +1602,7 @@ func Define( // nolint:revive // function-length serviceVersionStage, serviceVersionUnstage, serviceVersionUpdate, + serviceVersionValidate, }...) cmds = append(cmds, ssoCommands...) cmds = append(cmds, []argparser.Command{ diff --git a/pkg/commands/service/version/validate.go b/pkg/commands/service/version/validate.go new file mode 100644 index 000000000..c113fca35 --- /dev/null +++ b/pkg/commands/service/version/validate.go @@ -0,0 +1,108 @@ +package version + +import ( + "context" + "fmt" + "io" + + "github.com/fastly/go-fastly/v13/fastly" + + "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/errors" + fsterr "github.com/fastly/cli/pkg/errors" + "github.com/fastly/cli/pkg/global" +) + +// ValidateCommand calls the Fastly API to validate a service version. +type ValidateCommand struct { + argparser.Base + argparser.JSONOutput + + input fastly.ValidateVersionInput + serviceVersion argparser.OptionalServiceVersion +} + +// NewValidateCommand returns a usable command registered under the parent. +func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCommand { + c := ValidateCommand{ + Base: argparser.Base{ + Globals: g, + }, + } + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagVersionName, + Description: argparser.FlagVersionDesc, + Dst: &c.serviceVersion.Value, + Required: true, + }) + + // Optional. + c.CmdClause = parent.Command("validate", "Validate a service version") + c.RegisterFlagBool(c.JSONFlag()) // --json + c.RegisterFlag(argparser.StringFlagOpts{ + Name: argparser.FlagServiceIDName, + Description: argparser.FlagServiceIDDesc, + Dst: &g.Manifest.Flag.ServiceID, + Short: 's', + }) + return &c +} + +// Exec invokes the application logic for the command. +func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { + if c.Globals.Verbose() && c.JSONOutput.Enabled { + return fsterr.ErrInvalidVerboseJSONCombo + } + + serviceID, serviceVersion, err := argparser.ServiceDetails(argparser.ServiceDetailsOpts{ + APIClient: c.Globals.APIClient, + Manifest: *c.Globals.Manifest, + Out: out, + ServiceVersionFlag: c.serviceVersion, + VerboseMode: c.Globals.Flags.Verbose, + }) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + "Service Version": errors.ServiceVersion(serviceVersion), + }) + return err + } + /*if c.Globals.Verbose() { + argparser.DisplayServiceID(serviceID, flag, source, out) + }*/ + + c.input.ServiceID = serviceID + c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number) + + valid, msg, err := c.Globals.APIClient.ValidateVersion(context.TODO(), &c.input) + if err != nil { + c.Globals.ErrLog.AddWithContext(err, map[string]any{ + "Service ID": serviceID, + }) + return err + } + + if ok, err := c.WriteJSON(out, map[string]any{ + "valid": valid, + "message": msg, + }); ok { + return err + } + + fmt.Fprintf(out, "\nService ID: %s\n", serviceID) + fmt.Fprintf(out, "Service Version: %d\n", c.input.ServiceVersion) + fmt.Fprintf(out, "Valid: %t\n", valid) + if msg != "" { + fmt.Fprintf(out, "Message: %s\n", msg) + } + fmt.Fprintln(out) + + return nil +} diff --git a/pkg/mock/api.go b/pkg/mock/api.go index 51195b46e..a3f4f1b40 100644 --- a/pkg/mock/api.go +++ b/pkg/mock/api.go @@ -31,6 +31,7 @@ type API struct { DeactivateVersionFn func(context.Context, *fastly.DeactivateVersionInput) (*fastly.Version, error) LockVersionFn func(context.Context, *fastly.LockVersionInput) (*fastly.Version, error) LatestVersionFn func(context.Context, *fastly.LatestVersionInput) (*fastly.Version, error) + ValidateVersionFn func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) CreateDomainFn func(context.Context, *fastly.CreateDomainInput) (*fastly.Domain, error) ListDomainsFn func(context.Context, *fastly.ListDomainsInput) ([]*fastly.Domain, error) @@ -507,6 +508,11 @@ func (m API) LatestVersion(ctx context.Context, i *fastly.LatestVersionInput) (* return m.LatestVersionFn(ctx, i) } +// ValidateVersion implements Interface. +func (m API) ValidateVersion(ctx context.Context, i *fastly.ValidateVersionInput) (bool, string, error) { + return m.ValidateVersionFn(ctx, i) +} + // CreateDomain implements Interface. func (m API) CreateDomain(ctx context.Context, i *fastly.CreateDomainInput) (*fastly.Domain, error) { return m.CreateDomainFn(ctx, i) From e720a2c469dfbc2250930130441747e51b61acea Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 16 Mar 2026 10:48:54 -0400 Subject: [PATCH 5/8] Update validate.go --- pkg/commands/service/version/validate.go | 36 +++++++++++------------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/pkg/commands/service/version/validate.go b/pkg/commands/service/version/validate.go index c113fca35..f650e1a27 100644 --- a/pkg/commands/service/version/validate.go +++ b/pkg/commands/service/version/validate.go @@ -2,7 +2,6 @@ package version import ( "context" - "fmt" "io" "github.com/fastly/go-fastly/v13/fastly" @@ -11,6 +10,7 @@ import ( "github.com/fastly/cli/pkg/errors" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" + "github.com/fastly/cli/pkg/text" ) // ValidateCommand calls the Fastly API to validate a service version. @@ -29,11 +29,14 @@ func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCo Globals: g, }, } + c.CmdClause = parent.Command("validate", "Validate a service version") + c.RegisterFlagBool(c.JSONFlag()) // --json c.RegisterFlag(argparser.StringFlagOpts{ Name: argparser.FlagServiceIDName, Description: argparser.FlagServiceIDDesc, Dst: &g.Manifest.Flag.ServiceID, Short: 's', + Required: true, }) c.RegisterFlag(argparser.StringFlagOpts{ Name: argparser.FlagVersionName, @@ -41,16 +44,6 @@ func NewValidateCommand(parent argparser.Registerer, g *global.Data) *ValidateCo Dst: &c.serviceVersion.Value, Required: true, }) - - // Optional. - c.CmdClause = parent.Command("validate", "Validate a service version") - c.RegisterFlagBool(c.JSONFlag()) // --json - c.RegisterFlag(argparser.StringFlagOpts{ - Name: argparser.FlagServiceIDName, - Description: argparser.FlagServiceIDDesc, - Dst: &g.Manifest.Flag.ServiceID, - Short: 's', - }) return &c } @@ -74,9 +67,6 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { }) return err } - /*if c.Globals.Verbose() { - argparser.DisplayServiceID(serviceID, flag, source, out) - }*/ c.input.ServiceID = serviceID c.input.ServiceVersion = fastly.ToValue(serviceVersion.Number) @@ -96,13 +86,19 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { return err } - fmt.Fprintf(out, "\nService ID: %s\n", serviceID) - fmt.Fprintf(out, "Service Version: %d\n", c.input.ServiceVersion) - fmt.Fprintf(out, "Valid: %t\n", valid) - if msg != "" { - fmt.Fprintf(out, "Message: %s\n", msg) + if valid { + if msg != "" { + text.Success(out, "Service %s version %d is valid: %s", serviceID, c.input.ServiceVersion, msg) + } else { + text.Success(out, "Service %s version %d is valid", serviceID, c.input.ServiceVersion) + } + } else { + if msg != "" { + text.Error(out, "Service %s version %d is not valid: %s", serviceID, c.input.ServiceVersion, msg) + } else { + text.Error(out, "Service %s version %d is not valid", serviceID, c.input.ServiceVersion) + } } - fmt.Fprintln(out) return nil } From 235b539eb5df37b7aec6de1b636d7350c8cdf2b4 Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 16 Mar 2026 15:44:55 -0400 Subject: [PATCH 6/8] Update serviceversion_test.go added 'TestVersionValidate' to existing test file. --- .../service/version/serviceversion_test.go | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/pkg/commands/service/version/serviceversion_test.go b/pkg/commands/service/version/serviceversion_test.go index fc11f1d20..46669bd09 100644 --- a/pkg/commands/service/version/serviceversion_test.go +++ b/pkg/commands/service/version/serviceversion_test.go @@ -481,3 +481,113 @@ func lockVersionOK(_ context.Context, i *fastly.LockVersionInput) (*fastly.Versi func lockVersionError(_ context.Context, _ *fastly.LockVersionInput) (*fastly.Version, error) { return nil, testutil.Err } + +func TestVersionValidate(t *testing.T) { + scenarios := []testutil.CLIScenario{ + { + Name: "validate missing --service-id flag", + Args: "--version 1", + WantError: "error parsing arguments: required flag --service-id not provided", + }, + { + Name: "validate missing --version flag", + Args: "--service-id 123", + WantError: "error parsing arguments: required flag --version not provided", + }, + { + Name: "validate successful - valid version without message", + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionValid(""), + }, + WantOutput: "Service 123 version 1 is valid", + }, + { + Name: "validate successful - valid version with message", + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionValid("All checks passed"), + }, + WantOutput: "Service 123 version 1 is valid: All checks passed", + }, + { + Name: "validate successful - invalid version without message", + Args: "--service-id 123 --version 2", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionInvalid(""), + }, + WantOutput: "Service 123 version 2 is not valid", + }, + { + Name: "validate successful - invalid version with message", + Args: "--service-id 123 --version 2", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionInvalid("Missing required backend"), + }, + WantOutput: "Service 123 version 2 is not valid: Missing required backend", + }, + { + Name: "validate with json output - valid version", + Args: "--service-id 123 --version 1 --json", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionValid("All checks passed"), + }, + WantOutput: validateVersionValidJSONOutput, + }, + { + Name: "validate with json output - invalid version", + Args: "--service-id 123 --version 2 --json", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionInvalid("Missing required backend"), + }, + WantOutput: validateVersionInvalidJSONOutput, + }, + { + Name: "validate error from API", + Args: "--service-id 123 --version 1", + API: mock.API{ + ListVersionsFn: testutil.ListVersions, + ValidateVersionFn: validateVersionError, + }, + WantError: testutil.Err.Error(), + }, + } + + testutil.RunCLIScenarios(t, []string{root.CommandName, sub.CommandName, "validate"}, scenarios) +} + +func validateVersionValid(message string) func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) { + return func(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) { + return true, message, nil + } +} + +func validateVersionInvalid(message string) func(context.Context, *fastly.ValidateVersionInput) (bool, string, error) { + return func(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) { + return false, message, nil + } +} + +func validateVersionError(_ context.Context, _ *fastly.ValidateVersionInput) (bool, string, error) { + return false, "", testutil.Err +} + +var validateVersionValidJSONOutput = strings.TrimSpace(` +{ + "message": "All checks passed", + "valid": true +} +`) + "\n" + +var validateVersionInvalidJSONOutput = strings.TrimSpace(` +{ + "message": "Missing required backend", + "valid": false +} +`) + "\n" From d5c164299dfc5a4daa6de23656b0497e728cb64e Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Mon, 16 Mar 2026 15:55:24 -0400 Subject: [PATCH 7/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd5c321f7..dceda3d3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - feat(stats): add `stats origin-inspector` subcommand for origin-level metrics. [#1678](https://github.com/fastly/cli/pull/1678) - feat(apisecurity/discoveredoperations): add support for 'list' and 'update' support for 'API discovery'. [#1689](https://github.com/fastly/cli/pull/1689) - feat(apisecurity/operations): add CRUD support for 'API Inventory' operations. [#1689](https://github.com/fastly/cli/pull/1689) +- feat(service/version): add support for service validation. [#1695](https://github.com/fastly/cli/pull/1695) ### Dependencies: - build(deps): `golang.org/x/net` from 0.50.0 to 0.51.0 ([#1674](https://github.com/fastly/cli/pull/1674)) From 48f9476ed9672d86e67cd1dd890cc6cd1849db81 Mon Sep 17 00:00:00 2001 From: Richard Carillo Date: Tue, 17 Mar 2026 09:27:18 -0400 Subject: [PATCH 8/8] fix(lint): fixed double error reference --- pkg/commands/service/version/validate.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/commands/service/version/validate.go b/pkg/commands/service/version/validate.go index f650e1a27..765346e25 100644 --- a/pkg/commands/service/version/validate.go +++ b/pkg/commands/service/version/validate.go @@ -7,7 +7,6 @@ import ( "github.com/fastly/go-fastly/v13/fastly" "github.com/fastly/cli/pkg/argparser" - "github.com/fastly/cli/pkg/errors" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/text" @@ -63,7 +62,7 @@ func (c *ValidateCommand) Exec(_ io.Reader, out io.Writer) error { if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Service ID": serviceID, - "Service Version": errors.ServiceVersion(serviceVersion), + "Service Version": fsterr.ServiceVersion(serviceVersion), }) return err }