diff --git a/cmd/state/internal/cmdtree/publish.go b/cmd/state/internal/cmdtree/publish.go index 24c52193a1..43ebe5312a 100644 --- a/cmd/state/internal/cmdtree/publish.go +++ b/cmd/state/internal/cmdtree/publish.go @@ -85,7 +85,7 @@ func newPublish(prime *primer.Values) *captain.Command { Name: locale.Tl("filepath", "filepath"), Description: locale.Tl("author_upload_filepath_description", "A tar.gz or zip archive containing the source files of the ingredient."), Value: ¶ms.Filepath, - Required: false, + Required: true, }, }, func(_ *captain.Command, _ []string) error { diff --git a/internal/gqlclient/gqlclient.go b/internal/gqlclient/gqlclient.go index f0b1cedfa9..d79a946aff 100644 --- a/internal/gqlclient/gqlclient.go +++ b/internal/gqlclient/gqlclient.go @@ -153,7 +153,7 @@ func (c *Client) RunWithContext(ctx context.Context, request Request, response i name := strutils.Summarize(request.Query(), 25) defer profile.Measure(fmt.Sprintf("gqlclient:RunWithContext:(%s)", name), time.Now()) - if fileRequest, ok := request.(RequestWithFiles); ok && len(fileRequest.Files()) > 0 { + if fileRequest, ok := request.(RequestWithFiles); ok { return c.runWithFiles(ctx, fileRequest, response) } @@ -247,30 +247,31 @@ func (c *Client) runWithFiles(ctx context.Context, gqlReq RequestWithFiles, resp } // Map - mapField, err := mw.CreateFormField("map") - if err != nil { - reqErrChan <- errs.Wrap(err, "Could not create form field map") - return - } - for n, f := range gqlReq.Files() { - if _, err := mapField.Write([]byte(fmt.Sprintf(`{"%d": ["%s"]}`, n, f.Field))); err != nil { - reqErrChan <- errs.Wrap(err, "Could not write map field") - return - } - } - - // File upload - for n, file := range gqlReq.Files() { - part, err := mw.CreateFormFile(fmt.Sprintf("%d", n), file.Name) + if len(gqlReq.Files()) > 0 { + mapField, err := mw.CreateFormField("map") if err != nil { - reqErrChan <- errs.Wrap(err, "Could not create form file") + reqErrChan <- errs.Wrap(err, "Could not create form field map") return } - - _, err = io.Copy(part, file.R) - if err != nil { - reqErrChan <- errs.Wrap(err, "Could not read file") - return + for n, f := range gqlReq.Files() { + if _, err := mapField.Write([]byte(fmt.Sprintf(`{"%d": ["%s"]}`, n, f.Field))); err != nil { + reqErrChan <- errs.Wrap(err, "Could not write map field") + return + } + } + // File upload + for n, file := range gqlReq.Files() { + part, err := mw.CreateFormFile(fmt.Sprintf("%d", n), file.Name) + if err != nil { + reqErrChan <- errs.Wrap(err, "Could not create form file") + return + } + + _, err = io.Copy(part, file.R) + if err != nil { + reqErrChan <- errs.Wrap(err, "Could not read file") + return + } } } }() @@ -304,20 +305,46 @@ func (c *Client) runWithFiles(ctx context.Context, gqlReq RequestWithFiles, resp } req = req.WithContext(ctx) c.Log(fmt.Sprintf(">> Raw Request: %s\n", req.URL.String())) - res, resErr := http.DefaultClient.Do(req) - if reqErr := <-reqErrChan; reqErr != nil { - return reqErr + + var res *http.Response + resErrChan := make(chan error) + go func() { + var err error + res, err = http.DefaultClient.Do(req) + resErrChan <- err + }() + + // Due to the streaming uploads the request error can happen both before and after the http request itself, hence + // the creative select case you see before you. + wait := true + for wait { + select { + case err := <-reqErrChan: + if err != nil { + c.Log(fmt.Sprintf("Request Error: %s", err)) + return err + } + case err := <-resErrChan: + wait = false + if err != nil { + c.Log(fmt.Sprintf("Response Error: %s", err)) + return err + } + } } - if resErr != nil { - return resErr + + if res == nil { + return errs.New("Received empty response") } + defer res.Body.Close() var buf bytes.Buffer if _, err := io.Copy(&buf, res.Body); err != nil { + c.Log(fmt.Sprintf("Read Error: %s", err)) return errors.Wrap(err, "reading body") } resp := buf.Bytes() - c.Log(fmt.Sprintf("<< %s\n", string(resp))) + c.Log(fmt.Sprintf("<< Response code: %d, body: %s\n", res.StatusCode, string(resp))) // Work around API's that don't follow the graphql standard // https://activestatef.atlassian.net/browse/PB-4291 diff --git a/internal/locale/locales/en-us.yaml b/internal/locale/locales/en-us.yaml index 3f94ab5820..4fa9621c4d 100644 --- a/internal/locale/locales/en-us.yaml +++ b/internal/locale/locales/en-us.yaml @@ -2086,7 +2086,7 @@ uploadingredient_editor_opening: Opening editor to edit ingredient meta information. Alternatively you may manually edit the following file: [ACTIONABLE]{{.V0}}[/RESET]. uploadingredient_success: other: | - Successfully uploaded as: + Successfully published as: Ingredient ID: [[ACTIONABLE]{{.V0}}[/RESET] Ingredient Version ID: [ACTIONABLE]{{.V1}}[/RESET] Revision: [ACTIONABLE]{{.V2}}[/RESET] diff --git a/internal/runners/packages/search.go b/internal/runners/packages/search.go index c5b2d42b79..03c394ffc7 100644 --- a/internal/runners/packages/search.go +++ b/internal/runners/packages/search.go @@ -40,16 +40,19 @@ func NewSearch(prime primeable) *Search { func (s *Search) Run(params SearchRunParams, nstype model.NamespaceType) error { logging.Debug("ExecuteSearch") - language, err := targetedLanguage(params.Language, s.proj) - if err != nil { - return locale.WrapError(err, fmt.Sprintf("%s_err_cannot_obtain_language", nstype)) - } + var ns model.Namespace + if params.Ingredient.Namespace == "" { + language, err := targetedLanguage(params.Language, s.proj) + if err != nil { + return locale.WrapError(err, fmt.Sprintf("%s_err_cannot_obtain_language", nstype)) + } - ns := model.NewNamespacePkgOrBundle(language, nstype) - if params.Ingredient.Namespace != "" { + ns = model.NewNamespacePkgOrBundle(language, nstype) + } else { ns = model.NewRawNamespace(params.Ingredient.Namespace) } + var err error var packages []*model.IngredientAndVersion if params.ExactTerm { packages, err = model.SearchIngredientsStrict(ns.String(), params.Ingredient.Name, true, true, params.Timestamp.Time) @@ -67,7 +70,7 @@ func (s *Search) Run(params SearchRunParams, nstype model.NamespaceType) error { ) } - s.out.Print(output.Prepare(formatSearchResults(packages), packages)) + s.out.Print(output.Prepare(formatSearchResults(packages, params.Ingredient.Namespace != ""), packages)) return nil } @@ -142,7 +145,7 @@ type searchPackageRow struct { type searchOutput []searchPackageRow -func formatSearchResults(packages []*model.IngredientAndVersion) *searchOutput { +func formatSearchResults(packages []*model.IngredientAndVersion, showNamespace bool) *searchOutput { rows := make(searchOutput, len(packages)) filterNilStr := func(s *string) string { @@ -153,8 +156,12 @@ func formatSearchResults(packages []*model.IngredientAndVersion) *searchOutput { } for i, pack := range packages { + name := filterNilStr(pack.Ingredient.Name) + if showNamespace { + name = fmt.Sprintf("%s/%s", *pack.Ingredient.PrimaryNamespace, name) + } row := searchPackageRow{ - Pkg: filterNilStr(pack.Ingredient.Name), + Pkg: name, Version: pack.Version, versions: len(pack.Versions), Modules: makeModules(pack.Ingredient.NormalizedName, pack), diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index 9390134d53..a552a4415a 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -81,7 +81,7 @@ func (r *Runner) Run(params *Params) error { !strings.HasSuffix(strings.ToLower(params.Filepath), ".tar.gz") { return locale.NewInputError("err_uploadingredient_file_not_supported", "Expected file extension to be either .zip or .tar.gz: '{{.V0}}'", params.Filepath) } - } else if !params.Editor { + } else if !params.Edit { return locale.NewInputError("err_uploadingredient_file_required", "You have to supply the source archive unless editing.") } @@ -188,7 +188,7 @@ func (r *Runner) Run(params *Params) error { cont, err := r.prompt.Confirm( "", - locale.Tl("uploadingredient_confirm", `Upload following ingredient? + locale.Tl("uploadingredient_confirm", `Publish following ingredient? {{.V0}} `, string(b)), @@ -198,11 +198,11 @@ func (r *Runner) Run(params *Params) error { return errs.Wrap(err, "Confirmation failed") } if !cont { - r.out.Print(locale.Tl("uploadingredient_cancel", "Upload cancelled")) + r.out.Print(locale.Tl("uploadingredient_cancel", "Publish cancelled")) return nil } - r.out.Notice(locale.Tl("uploadingredient_uploading", "Uploading ingredient...")) + r.out.Notice(locale.Tl("uploadingredient_uploading", "Publishing ingredient...")) pr, err := request.Publish(reqVars, params.Filepath) if err != nil { @@ -214,8 +214,8 @@ func (r *Runner) Run(params *Params) error { return locale.WrapError(err, "err_uploadingredient_publish", "Could not publish ingredient") } - if result.Error != "" { - return locale.NewError("err_uploadingredient_publish_api", "API responded with error: {{.V0}}", result.Message) + if result.Publish.Error != "" { + return locale.NewError("err_uploadingredient_publish_api", "API responded with error: {{.V0}}", result.Publish.Error) } r.out.Print(output.Prepare( diff --git a/pkg/platform/api/graphql/model/publish.go b/pkg/platform/api/graphql/model/publish.go index 532ad8869b..9599ba2455 100644 --- a/pkg/platform/api/graphql/model/publish.go +++ b/pkg/platform/api/graphql/model/publish.go @@ -1,8 +1,8 @@ package model type PublishResult struct { - ErrorResponse Publish struct { + ErrorResponse IngredientID string `json:"ingredientID"` IngredientVersionID string `json:"ingredientVersionID"` Revision int `json:"revision"` diff --git a/pkg/platform/api/graphql/request/publish.go b/pkg/platform/api/graphql/request/publish.go index 3b5416b08f..ce88286f51 100644 --- a/pkg/platform/api/graphql/request/publish.go +++ b/pkg/platform/api/graphql/request/publish.go @@ -14,20 +14,24 @@ import ( ) func Publish(vars PublishVariables, filepath string) (*PublishInput, error) { - f, err := os.Open(filepath) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, locale.WrapInputError(err, "err_upload_file_not_found", "Could not find file at {{.V0}}", filepath) + var f *os.File + if filepath != "" { + var err error + f, err = os.Open(filepath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return nil, locale.WrapInputError(err, "err_upload_file_not_found", "Could not find file at {{.V0}}", filepath) + } + return nil, errs.Wrap(err, "Could not open file %s", filepath) } - return nil, errs.Wrap(err, "Could not open file %s", filepath) - } - checksum, err := fileutils.Sha256Hash(filepath) - if err != nil { - return nil, locale.WrapError(err, "err_upload_file_checksum", "Could not calculate checksum for file") - } + checksum, err := fileutils.Sha256Hash(filepath) + if err != nil { + return nil, locale.WrapError(err, "err_upload_file_checksum", "Could not calculate checksum for file") + } - vars.FileChecksum = checksum + vars.FileChecksum = checksum + } return &PublishInput{ Variables: vars, @@ -148,6 +152,9 @@ func (p *PublishInput) Close() error { } func (p *PublishInput) Files() []gqlclient.File { + if p.file == nil { + return []gqlclient.File{} + } return []gqlclient.File{ { Field: "variables.input.file", // this needs to map to the graphql input, eg. variables.input.file @@ -166,6 +173,10 @@ func (p *PublishInput) Query() string { ingredientVersionID revision } + ... on Error{ + __typename + error: message + } } } ` diff --git a/test/integration/publish_int_test.go b/test/integration/publish_int_test.go index 62cb9ea294..4d65f2e3b1 100644 --- a/test/integration/publish_int_test.go +++ b/test/integration/publish_int_test.go @@ -1,6 +1,7 @@ package integration import ( + "fmt" "os" "regexp" "testing" @@ -11,7 +12,9 @@ import ( "github.com/ActiveState/cli/internal/strutils" "github.com/ActiveState/cli/internal/testhelpers/e2e" "github.com/ActiveState/cli/internal/testhelpers/tagsuite" + "github.com/ActiveState/cli/pkg/platform/api/graphql/request" "github.com/stretchr/testify/suite" + "gopkg.in/yaml.v3" ) var editorFileRx = regexp.MustCompile(`file:\s*?(.*?)\.\s`) @@ -24,7 +27,7 @@ func (suite *PublishIntegrationTestSuite) TestPublish() { suite.OnlyRunForTags(tagsuite.Publish) // For development convenience, should not be committed without commenting out.. - // os.Setenv(constants.APIHostEnvVarName, "staging.activestate.build") + // os.Setenv(constants.APIHostEnvVarName, "pr11496.activestate.build") if v := os.Getenv(constants.APIHostEnvVarName); v == "" || v == constants.DefaultAPIHost { suite.T().Skipf("Skipping test as %s is not set, this test can only be run against non-production envs.", constants.APIHostEnvVarName) @@ -39,9 +42,15 @@ func (suite *PublishIntegrationTestSuite) TestPublish() { } type expect struct { - confirmPrompt []string - immediateOutput string - exitCode int + confirmPrompt []string + immediateOutput string + exitBeforePrompt bool + exitCode int + } + + type invocation struct { + input input + expect expect } tempFile := fileutils.TempFilePathUnsafe("", "*.zip") @@ -61,57 +70,66 @@ func (suite *PublishIntegrationTestSuite) TestPublish() { user := ts.CreateNewUser() tests := []struct { - name string - input input - expect expect + name string + invocations []invocation }{ { "New ingredient with file arg and flags", - input{ - []string{tempFile, - "--name", "im-a-name-test1", - "--namespace", "org/{{.Username}}", - "--version", "2.3.4", - "--description", "im-a-description", - "--author", "author-name ", + []invocation{ + { + input{ + []string{ + tempFile, + "--name", "im-a-name-test1", + "--namespace", "org/{{.Username}}", + "--version", "2.3.4", + "--description", "im-a-description", + "--author", "author-name ", + }, + nil, + nil, + true, + }, + expect{ + []string{ + `Publish following ingredient?`, + `name: im-a-name-test1`, + `namespace: org/{{.Username}}`, + `version: 2.3.4`, + `description: im-a-description`, + `name: author-name`, + `email: author-email@domain.tld`, + }, + "", + false, + 0, + }, }, - nil, - nil, - true, - }, - expect{ - []string{ - `Upload following ingredient?`, - `name: im-a-name-test1`, - `namespace: org/{{.Username}}`, - `version: 2.3.4`, - `description: im-a-description`, - `name: author-name`, - `email: author-email@domain.tld`, - }, - "", - 0, }, }, { "New ingredient with invalid filename", - input{ + []invocation{{input{ []string{tempFileInvalid}, nil, nil, true, }, - expect{ - []string{}, - "Expected file extension to be either", - 1, + expect{ + []string{}, + "Expected file extension to be either", + false, + 1, + }, + }, }, }, { "New ingredient with meta file", - input{ - []string{"--meta", "{{.MetaFile}}", tempFile}, - ptr.To(` + []invocation{{ + input{ + []string{"--meta", "{{.MetaFile}}", tempFile}, + ptr.To(` name: im-a-name-test2 namespace: org/{{.Username}} version: 2.3.4 @@ -120,28 +138,31 @@ authors: - name: author-name email: author-email@domain.tld `), - nil, - true, - }, - expect{ - []string{ - `Upload following ingredient?`, - `name: im-a-name-test2`, - `namespace: org/{{.Username}}`, - `version: 2.3.4`, - `description: im-a-description`, - `name: author-name`, - `email: author-email@domain.tld`, + nil, + true, }, - "", - 0, - }, + expect{ + []string{ + `Publish following ingredient?`, + `name: im-a-name-test2`, + `namespace: org/{{.Username}}`, + `version: 2.3.4`, + `description: im-a-description`, + `name: author-name`, + `email: author-email@domain.tld`, + }, + "", + false, + 0, + }, + }}, }, { "New ingredient with meta file and flags", - input{ - []string{"--meta", "{{.MetaFile}}", tempFile, "--name", "im-a-name-from-flag", "--author", "author-name-from-flag "}, - ptr.To(` + []invocation{{ + input{ + []string{"--meta", "{{.MetaFile}}", tempFile, "--name", "im-a-name-from-flag", "--author", "author-name-from-flag "}, + ptr.To(` name: im-a-name namespace: org/{{.Username}} version: 2.3.4 @@ -150,29 +171,32 @@ authors: - name: author-name email: author-email@domain.tld `), - nil, - true, - }, - expect{ - []string{ - `Upload following ingredient?`, - `name: im-a-name-from-flag`, - `namespace: org/{{.Username}}`, - `version: 2.3.4`, - `description: im-a-description`, - `name: author-name-from-flag`, - `email: author-email-from-flag@domain.tld`, + nil, + true, }, - "", - 0, - }, + expect{ + []string{ + `Publish following ingredient?`, + `name: im-a-name-from-flag`, + `namespace: org/{{.Username}}`, + `version: 2.3.4`, + `description: im-a-description`, + `name: author-name-from-flag`, + `email: author-email-from-flag@domain.tld`, + }, + "", + false, + 0, + }, + }}, }, { "New ingredient with editor flag", - input{ - []string{tempFile, "--editor"}, - nil, - ptr.To(` + []invocation{{ + input{ + []string{tempFile, "--editor"}, + nil, + ptr.To(` name: im-a-name-test3 namespace: org/{{.Username}} version: 2.3.4 @@ -181,101 +205,222 @@ authors: - name: author-name email: author-email@domain.tld `), - true, - }, - expect{ - []string{ - `Upload following ingredient?`, - `name: im-a-name-test3`, - `namespace: org/{{.Username}}`, - `version: 2.3.4`, - `description: im-a-description`, - `name: author-name`, - `email: author-email@domain.tld`, + true, }, - "", - 0, - }, + expect{ + []string{ + `Publish following ingredient?`, + `name: im-a-name-test3`, + `namespace: org/{{.Username}}`, + `version: 2.3.4`, + `description: im-a-description`, + `name: author-name`, + `email: author-email@domain.tld`, + }, + "", + false, + 0, + }, + }}, }, { - "Cancel upload", - input{ - []string{tempFile, "--name", "bogus", "--namespace", "org/{{.Username}}"}, - nil, - nil, - false, - }, - expect{ - []string{`name: bogus`}, - "", - 0, + "Cancel Publish", + []invocation{{ + input{ + []string{tempFile, "--name", "bogus", "--namespace", "org/{{.Username}}"}, + nil, + nil, + false, + }, + expect{ + []string{`name: bogus`}, + "", + false, + 0, + }, + }}, + }, + { + "Edit ingredient without file arg and with flags", + []invocation{ + { // Create ingredient + input{ + []string{tempFile, + "--name", "editable", + "--namespace", "org/{{.Username}}", + "--version", "1.0.0", + }, + nil, + nil, + true, + }, + expect{ + []string{ + `Publish following ingredient?`, + `name: editable`, + }, + "", + false, + 0, + }, + }, + { // Edit ingredient + input{ + []string{ + tempFile, + "--edit", + "--name", "editable", + "--namespace", "org/{{.Username}}", + "--version", "1.0.1", + "--author", "author-name-edited ", + }, + nil, + nil, + true, + }, + expect{ + []string{ + `Publish following ingredient?`, + `name: editable`, + `namespace: org/{{.Username}}`, + `version: 1.0.1`, + `name: author-name-edited`, + `email: author-email-edited@domain.tld`, + }, + "", + false, + 0, + }, + }, + { // Must supply version + input{ + []string{ + "--edit", + "--name", "editable", + "--description", "foo", + }, + nil, + nil, + false, + }, + expect{ + []string{ + `You did not provide a unique version number`, + }, + "", + true, + 1, + }, + }, + { // description editing not supported + input{ + []string{ + "--edit", + "--name", "editable", + "--description", "foo", + }, + nil, + nil, + false, + }, + expect{ + []string{ + `You did not provide a unique version number`, + }, + "", + true, + 1, + }, + }, }, }, - // --edit tests are currently not addressed, tracked here: https://activestatef.atlassian.net/browse/DX-1944 } - for _, tt := range tests { + for n, tt := range tests { suite.Run(tt.name, func() { - ts.T = suite.T() // This differs per subtest - templateVars := map[string]interface{}{ "Username": user.Username, "Email": user.Email, } - if tt.input.metafile != nil { - inputMetaParsed, err := strutils.ParseTemplate(*tt.input.metafile, templateVars, nil) - suite.Require().NoError(err) - metafile, err := fileutils.WriteTempFile("metafile.yaml", []byte(inputMetaParsed)) - suite.Require().NoError(err) - templateVars["MetaFile"] = metafile - } + for _, inv := range tt.invocations { + suite.Run(fmt.Sprintf("%s invocation %d", tt.name, n), func() { + ts.T = suite.T() // This differs per subtest + if inv.input.metafile != nil { + inputMetaParsed, err := strutils.ParseTemplate(*inv.input.metafile, templateVars, nil) + suite.Require().NoError(err) + metafile, err := fileutils.WriteTempFile("metafile.yaml", []byte(inputMetaParsed)) + suite.Require().NoError(err) + templateVars["MetaFile"] = metafile + } - args := make([]string, len(tt.input.args)) - copy(args, tt.input.args) + args := make([]string, len(inv.input.args)) + copy(args, inv.input.args) - for k, v := range args { - vp, err := strutils.ParseTemplate(v, templateVars, nil) - suite.Require().NoError(err) - args[k] = vp - } + for k, v := range args { + vp, err := strutils.ParseTemplate(v, templateVars, nil) + suite.Require().NoError(err) + args[k] = vp + } - cp := ts.SpawnWithOpts( - e2e.WithArgs(append([]string{"publish"}, args...)...), - ) + cp := ts.SpawnWithOpts( + e2e.WithArgs(append([]string{"publish"}, args...)...), + ) - if tt.expect.immediateOutput != "" { - cp.Expect(tt.expect.immediateOutput) - } + if inv.expect.immediateOutput != "" { + cp.Expect(inv.expect.immediateOutput) + } - // Send custom input via --editor - if tt.input.editorValue != nil { - cp.Expect("Press enter when done editing") - snapshot := cp.Snapshot() - match := editorFileRx.FindSubmatch([]byte(snapshot)) - if len(match) != 2 { - suite.Fail("Could not match rx in snapshot: %s", editorFileRx.String()) - } - fpath := match[1] - inputEditorValue, err := strutils.ParseTemplate(*tt.input.editorValue, templateVars, nil) - suite.Require().NoError(err) - suite.Require().NoError(fileutils.WriteFile(string(fpath), []byte(inputEditorValue))) - cp.SendLine("") - } + // Send custom input via --editor + if inv.input.editorValue != nil { + cp.Expect("Press enter when done editing") + snapshot := cp.Snapshot() + match := editorFileRx.FindSubmatch([]byte(snapshot)) + if len(match) != 2 { + suite.Fail("Could not match rx in snapshot: %s", editorFileRx.String()) + } + fpath := match[1] + inputEditorValue, err := strutils.ParseTemplate(*inv.input.editorValue, templateVars, nil) + suite.Require().NoError(err) + suite.Require().NoError(fileutils.WriteFile(string(fpath), []byte(inputEditorValue))) + cp.SendLine("") + } - for _, value := range tt.expect.confirmPrompt { - v, err := strutils.ParseTemplate(value, templateVars, nil) - suite.Require().NoError(err) - cp.Expect(v) - } + if inv.expect.exitBeforePrompt { + cp.ExpectExitCode(inv.expect.exitCode) + return + } - if tt.input.confirmUpload { - cp.SendLine("Y") - } else { - cp.SendLine("n") - cp.Expect("Upload cancelled") - } + for _, value := range inv.expect.confirmPrompt { + v, err := strutils.ParseTemplate(value, templateVars, nil) + suite.Require().NoError(err) + cp.Expect(v) + } - cp.ExpectExitCode(tt.expect.exitCode) + cp.Expect("Y/n") + + snapshot := cp.MatchState().TermState.String() + rx := regexp.MustCompile(`(?s)Publish following ingredient\?(.*)\(Y/n`) + match := rx.FindSubmatch([]byte(snapshot)) + suite.Require().NotNil(match, fmt.Sprintf("Could not match '%s' against: %s", rx.String(), snapshot)) + + meta := request.PublishVariables{} + suite.Require().NoError(yaml.Unmarshal(match[1], &meta)) + + if inv.input.confirmUpload { + cp.SendLine("Y") + } else { + cp.SendLine("n") + cp.Expect("Publish cancelled") + } + + cp.Expect("Successfully published") + cp.ExpectExitCode(inv.expect.exitCode) + + cp = ts.Spawn("search", meta.Namespace+"/"+meta.Name, "--ts=now") + cp.Expect(meta.Version) + cp.ExpectExitCode(0) + }) + } }) } }