diff --git a/cmd/state/main.go b/cmd/state/main.go index 1108865693..56850b62da 100644 --- a/cmd/state/main.go +++ b/cmd/state/main.go @@ -251,7 +251,9 @@ func run(args []string, isInteractive bool, cfg *config.Instance, out output.Out if childCmd != nil { cmdName = childCmd.JoinedSubCommandNames() + " " } - err = errs.AddTips(err, locale.Tl("err_tip_run_help", "Run → [ACTIONABLE]`state {{.V0}}--help`[/RESET] for general help", cmdName)) + if !out.Type().IsStructured() { + err = errs.AddTips(err, locale.Tl("err_tip_run_help", "Run → [ACTIONABLE]`state {{.V0}}--help`[/RESET] for general help", cmdName)) + } errors.ReportError(err, cmds.Command(), an) } diff --git a/internal/output/json.go b/internal/output/json.go index 5411d4a530..38f28c9b8d 100644 --- a/internal/output/json.go +++ b/internal/output/json.go @@ -65,7 +65,8 @@ func (f *JSON) Fprint(writer io.Writer, value interface{}) { } type StructuredError struct { - Error string `json:"error"` + Error string `json:"error"` + Tips []string `json:"tips,omitempty"` } // Error will marshal and print the given value to the error writer @@ -120,11 +121,11 @@ func toStructuredError(v interface{}) StructuredError { case StructuredError: return vv case error: - return StructuredError{locale.JoinedErrorMessage(vv)} + return StructuredError{Error: locale.JoinedErrorMessage(vv)} case string: - return StructuredError{vv} + return StructuredError{Error: vv} } message := fmt.Sprintf("Not a recognized error format: %v", v) multilog.Error(message) - return StructuredError{message} + return StructuredError{Error: message} } diff --git a/internal/output/mediator.go b/internal/output/mediator.go index 6b3cc2a680..c71acb111e 100644 --- a/internal/output/mediator.go +++ b/internal/output/mediator.go @@ -56,7 +56,7 @@ func mediatorValue(v interface{}, format Format) interface{} { if vt, ok := v.(StructuredMarshaller); ok { return vt.MarshalStructured(format) } - return StructuredError{locale.Tr("err_no_structured_output", string(format))} + return StructuredError{Error: locale.Tr("err_no_structured_output", string(format))} } if vt, ok := v.(Marshaller); ok { return vt.MarshalOutput(format) diff --git a/internal/output/mediator_test.go b/internal/output/mediator_test.go index 1b4adbe5f7..2648eb0117 100644 --- a/internal/output/mediator_test.go +++ b/internal/output/mediator_test.go @@ -67,7 +67,7 @@ func Test_mediatorValue(t *testing.T) { "unstructured", JSONFormatName, }, - StructuredError{locale.Tr("err_no_structured_output", string(JSONFormatName))}, + StructuredError{Error: locale.Tr("err_no_structured_output", string(JSONFormatName))}, }, } for _, tt := range tests { diff --git a/internal/runbits/errors/errors.go b/internal/runbits/errors/errors.go index 3a3946e8cb..108eab2424 100644 --- a/internal/runbits/errors/errors.go +++ b/internal/runbits/errors/errors.go @@ -67,17 +67,7 @@ func (o *OutputError) MarshalOutput(f output.Format) interface{} { } // Concatenate error tips - errorTips := []string{} - err := o.error - for _, err := range errs.Unpack(err) { - if v, ok := err.(ErrorTips); ok { - for _, tip := range v.ErrorTips() { - if !funk.Contains(errorTips, tip) { - errorTips = append(errorTips, tip) - } - } - } - } + errorTips := getErrorTips(o.error) errorTips = append(errorTips, locale.Tl("err_help_forum", "Ask For Help → [ACTIONABLE]{{.V0}}[/RESET]", constants.ForumsURL)) // Print tips @@ -92,6 +82,23 @@ func (o *OutputError) MarshalOutput(f output.Format) interface{} { return strings.Join(outLines, "\n") } +func getErrorTips(err error) []string { + errorTips := []string{} + for _, err := range errs.Unpack(err) { + v, ok := err.(ErrorTips) + if !ok { + continue + } + for _, tip := range v.ErrorTips() { + if funk.Contains(errorTips, tip) { + continue + } + errorTips = append(errorTips, tip) + } + } + return errorTips +} + func (o *OutputError) MarshalStructured(f output.Format) interface{} { var userFacingError errs.UserFacingError var message string @@ -100,7 +107,7 @@ func (o *OutputError) MarshalStructured(f output.Format) interface{} { } else { message = locale.JoinedErrorMessage(o.error) } - return output.StructuredError{message} + return output.StructuredError{message, getErrorTips(o.error)} } func trimError(msg string) string { diff --git a/internal/updater/fetcher.go b/internal/updater/fetcher.go index c7fca8e2ce..b748a584c2 100644 --- a/internal/updater/fetcher.go +++ b/internal/updater/fetcher.go @@ -3,37 +3,45 @@ package updater import ( "crypto/sha256" "fmt" + "io/ioutil" "github.com/ActiveState/cli/internal/analytics" anaConst "github.com/ActiveState/cli/internal/analytics/constants" "github.com/ActiveState/cli/internal/analytics/dimensions" "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/fileutils" - "github.com/ActiveState/cli/internal/httpreq" "github.com/ActiveState/cli/internal/logging" + "github.com/ActiveState/cli/internal/retryhttp" "github.com/ActiveState/cli/internal/rtutils/ptr" ) const CfgUpdateTag = "update_tag" type Fetcher struct { - httpreq *httpreq.Client - an analytics.Dispatcher + retryhttp *retryhttp.Client + an analytics.Dispatcher } func NewFetcher(an analytics.Dispatcher) *Fetcher { - return &Fetcher{httpreq.New(), an} + return &Fetcher{retryhttp.DefaultClient, an} } func (f *Fetcher) Fetch(update *UpdateInstaller, targetDir string) error { logging.Debug("Fetching update: %s", update.url) - b, _, err := f.httpreq.Get(update.url) + resp, err := f.retryhttp.Get(update.url) if err != nil { msg := fmt.Sprintf("Fetch %s failed", update.url) f.analyticsEvent(update.AvailableUpdate.Version, msg) return errs.Wrap(err, msg) } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + msg := "Could not read response body" + f.analyticsEvent(update.AvailableUpdate.Version, msg) + return errs.Wrap(err, msg) + } + if err := verifySha(b, update.AvailableUpdate.Sha256); err != nil { msg := "Could not verify sha256" f.analyticsEvent(update.AvailableUpdate.Version, msg) diff --git a/test/integration/activate_int_test.go b/test/integration/activate_int_test.go index 1a21a927ec..4f6c8a43c0 100644 --- a/test/integration/activate_int_test.go +++ b/test/integration/activate_int_test.go @@ -423,6 +423,7 @@ version: %s `, constants.BranchName, constants.Version)) ts.PrepareActiveStateYAML(content) + ts.PrepareCommitIdFile("59404293-e5a9-4fd0-8843-77cd4761b5b5") // Pull to ensure we have an up to date config file cp := ts.Spawn("pull") @@ -453,7 +454,7 @@ func (suite *ActivateIntegrationTestSuite) TestActivate_NamespaceWins() { suite.Require().NoError(err) // Create the project file at the root of the temp dir - ts.PrepareProject("ActiveState-CLI/Python3", "") + ts.PrepareProject("ActiveState-CLI/Python3", "59404293-e5a9-4fd0-8843-77cd4761b5b5") // Pull to ensure we have an up to date config file cp := ts.Spawn("pull") diff --git a/test/integration/checkout_int_test.go b/test/integration/checkout_int_test.go index 405b5ec4cb..f8f823b578 100644 --- a/test/integration/checkout_int_test.go +++ b/test/integration/checkout_int_test.go @@ -246,6 +246,12 @@ func (suite *CheckoutIntegrationTestSuite) TestJSON() { cp := ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/small-python", "-o", "json")) cp.ExpectExitCode(0) AssertValidJSON(suite.T(), cp) + + cp = ts.SpawnWithOpts(e2e.OptArgs("checkout", "ActiveState-CLI/Bogus-Project-That-Doesnt-Exist", "-o", "json")) + cp.Expect("does not exist") // error + cp.Expect(`"tips":["If this is a private project`) // tip + cp.ExpectNotExitCode(0) + AssertValidJSON(suite.T(), cp) } func (suite *CheckoutIntegrationTestSuite) TestCheckoutCaseInsensitive() {