From cd602387c69a559065ee76173c0788789bcc31f1 Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Tue, 27 Oct 2026 14:29:21 -0500 Subject: [PATCH 1/5] Fix state publish meta data issues found testing 0.48.2-RC1 - state export ingredient-meta writes plain yaml, unwrapped and without field help comments, so the file can be passed back with --meta. - Dependencies and conditions without a version range (a bare name in pyproject.toml, --depend without a version, or a condition inherited without requirements) get >=0. The platform rejected them with "Expecting an OBJECT with a field named 'versionRequirements'". - When --build can't determine the name or version, say which is missing, why pyproject.toml didn't provide it (no file, not in [project], or listed in [project].dynamic), and how to set it. - Answering 'n' at the publish confirmation offers to edit the meta data in the editor or save it to -.meta.yaml with the command to publish it, instead of only cancelling. With --build or --edit, a name, namespace or version change in the editor is saved to a file instead, as the package was already built and looked up with them. - Fields removed in the editor are removed from the request, and the publish API error is shown instead of . The platform requires the source with every publish, so --edit still needs --build or an archive; the readme now says so. Co-Authored-By: Claude Opus 5.5 --- changelog.md | 10 ++ internal/python/wheel/metadata.go | 65 ++++++-- internal/python/wheel/metadata_test.go | 27 +++- internal/runners/export/ingredientmeta.go | 9 +- internal/runners/publish/build.go | 32 +++- internal/runners/publish/build_test.go | 25 +++ internal/runners/publish/meta.go | 1 + internal/runners/publish/publish.go | 177 +++++++++++++++++++--- internal/runners/publish/publish_test.go | 43 ++++++ internal/runners/publish/readme.md | 20 ++- test/integration/publish_int_test.go | 2 + 11 files changed, 374 insertions(+), 37 deletions(-) diff --git a/changelog.md b/changelog.md index 0908eecfae..ce7dd80f61 100644 --- a/changelog.md +++ b/changelog.md @@ -22,6 +22,10 @@ and this project adheres to file, and artifacts are linked concurrently. - A `--meta` file's name, version, and description now feed the built wheel (flags take precedence over the meta file, which takes precedence over `pyproject.toml`). +- Answering `n` when `state publish` asks to confirm now offers to edit the ingredient meta data in your editor or save + it to a meta file to edit and publish later with `--meta`, instead of only cancelling. +- When `state publish --build` cannot determine the ingredient name or version, the error now says which is missing, + why `pyproject.toml` did not provide it (for example, a version listed in `[project].dynamic`), and how to set it. ### Fixed @@ -30,6 +34,12 @@ and this project adheres to up front. - `state publish --edit --meta` with an exported meta file now edits the version named in the file rather than the latest version. +- `state export ingredient-meta` now writes plain YAML without help comments or line wrapping, so its output can be + passed back with `--meta` as is. +- Dependencies and conditions without a version range (such as a bare `requests` in `pyproject.toml`) are now published + as accepting any version instead of failing with "Expecting an OBJECT with a field named 'versionRequirements'". +- Fields removed in the `state publish --editor` file are now removed from the publish instead of keeping their + previous values, and publish API errors are shown instead of ``. ### Security diff --git a/internal/python/wheel/metadata.go b/internal/python/wheel/metadata.go index 83c8de47da..a1a87bbe14 100644 --- a/internal/python/wheel/metadata.go +++ b/internal/python/wheel/metadata.go @@ -4,18 +4,40 @@ import ( "os" "path/filepath" "regexp" + "slices" "strings" "github.com/ActiveState/cli/internal/errs" "github.com/BurntSushi/toml" ) +// MissingMetadataError reports which of name and version neither the caller nor +// pyproject.toml supplied, and what pyproject.toml said about them, so the user +// can be told how to provide them. +type MissingMetadataError struct { + Fields []string + // Pyproject is the pyproject.toml that was read, or empty when there is none. + Pyproject string + // Dynamic holds the missing fields pyproject.toml lists in [project].dynamic, + // which a build backend computes and so cannot be read from the file. + Dynamic []string +} + +func (e *MissingMetadataError) Error() string { + return "package " + strings.Join(e.Fields, " and ") + " could not be determined" +} + +func (e *MissingMetadataError) Is(target error) bool { + return target == ErrMissingMetadata +} + // ResolveMetadata reads srcDir's pyproject.toml [project] table and applies the // non-empty fields of override on top, producing the metadata to pack with. It -// errors when neither source supplies a name or version. Requires always comes -// from pyproject.toml's dependencies. +// returns a *MissingMetadataError when neither source supplies a name or +// version. Requires always comes from pyproject.toml's dependencies. func ResolveMetadata(srcDir string, override Metadata) (*Metadata, error) { - proj, err := readPyProject(filepath.Join(srcDir, "pyproject.toml")) + path := filepath.Join(srcDir, "pyproject.toml") + proj, found, err := readPyProject(path) if err != nil { return nil, err } @@ -26,8 +48,23 @@ func ResolveMetadata(srcDir string, override Metadata) (*Metadata, error) { Summary: firstNonEmpty(override.Summary, proj.Description), Requires: proj.Dependencies, } - if meta.Name == "" || meta.Version == "" { - return nil, ErrMissingMetadata + missing := &MissingMetadataError{} + if found { + missing.Pyproject = path + } + for field, value := range map[string]string{"name": meta.Name, "version": meta.Version} { + if value != "" { + continue + } + missing.Fields = append(missing.Fields, field) + if slices.Contains(proj.Dynamic, field) { + missing.Dynamic = append(missing.Dynamic, field) + } + } + if len(missing.Fields) > 0 { + slices.Sort(missing.Fields) // "name" before "version", regardless of map order + slices.Sort(missing.Dynamic) + return nil, missing } return &meta, nil } @@ -37,17 +74,19 @@ type pyProject struct { Version string Description string Dependencies []string + Dynamic []string } -// readPyProject reads the [project] table from a pyproject.toml. A missing file -// is not an error; caller-supplied metadata may stand in for it. -func readPyProject(path string) (pyProject, error) { +// readPyProject reads the [project] table from a pyproject.toml and reports +// whether the file exists. A missing file is not an error; caller-supplied +// metadata may stand in for it. +func readPyProject(path string) (pyProject, bool, error) { data, err := os.ReadFile(path) if err != nil { if os.IsNotExist(err) { - return pyProject{}, nil + return pyProject{}, false, nil } - return pyProject{}, errs.Wrap(err, "could not read pyproject.toml") + return pyProject{}, false, errs.Wrap(err, "could not read %s", path) } var parsed struct { @@ -56,17 +95,19 @@ func readPyProject(path string) (pyProject, error) { Version string `toml:"version"` Description string `toml:"description"` Dependencies []string `toml:"dependencies"` + Dynamic []string `toml:"dynamic"` } `toml:"project"` } if err := toml.Unmarshal(data, &parsed); err != nil { - return pyProject{}, errs.Wrap(err, "could not parse pyproject.toml") + return pyProject{}, true, errs.Wrap(err, "could not parse %s", path) } return pyProject{ Name: parsed.Project.Name, Version: parsed.Project.Version, Description: parsed.Project.Description, Dependencies: parsed.Project.Dependencies, - }, nil + Dynamic: parsed.Project.Dynamic, + }, true, nil } func firstNonEmpty(values ...string) string { diff --git a/internal/python/wheel/metadata_test.go b/internal/python/wheel/metadata_test.go index 2147e88ddc..73af082023 100644 --- a/internal/python/wheel/metadata_test.go +++ b/internal/python/wheel/metadata_test.go @@ -4,6 +4,7 @@ import ( "errors" "os" "path/filepath" + "slices" "testing" ) @@ -53,9 +54,33 @@ func TestResolveMetadata(t *testing.T) { t.Run("missing name and version errors", func(t *testing.T) { dir := t.TempDir() // no pyproject.toml - if _, err := ResolveMetadata(dir, Metadata{Name: "only-name"}); !errors.Is(err, ErrMissingMetadata) { + _, err := ResolveMetadata(dir, Metadata{Name: "only-name"}) + if !errors.Is(err, ErrMissingMetadata) { t.Errorf("error = %v, want ErrMissingMetadata", err) } + var missing *MissingMetadataError + if !errors.As(err, &missing) { + t.Fatalf("error = %T, want *MissingMetadataError", err) + } + if !slices.Equal(missing.Fields, []string{"version"}) || missing.Pyproject != "" { + t.Errorf("got %+v, want only version missing and no pyproject.toml", missing) + } + }) + + t.Run("dynamic fields are reported", func(t *testing.T) { + dir := t.TempDir() + writePyproject(t, dir, "[project]\ndynamic = [\"version\"]\n") + _, err := ResolveMetadata(dir, Metadata{}) + var missing *MissingMetadataError + if !errors.As(err, &missing) { + t.Fatalf("error = %v, want *MissingMetadataError", err) + } + if !slices.Equal(missing.Fields, []string{"name", "version"}) || !slices.Equal(missing.Dynamic, []string{"version"}) { + t.Errorf("got %+v", missing) + } + if missing.Pyproject != filepath.Join(dir, "pyproject.toml") { + t.Errorf("Pyproject = %q", missing.Pyproject) + } }) } diff --git a/internal/runners/export/ingredientmeta.go b/internal/runners/export/ingredientmeta.go index 2dc33b3dfe..301bd2e3cc 100644 --- a/internal/runners/export/ingredientmeta.go +++ b/internal/runners/export/ingredientmeta.go @@ -5,6 +5,7 @@ import ( "github.com/ActiveState/cli/internal/errs" "github.com/ActiveState/cli/internal/locale" "github.com/ActiveState/cli/internal/runners/publish" + "gopkg.in/yaml.v3" ) type IngredientMetaParams struct { @@ -30,11 +31,15 @@ func (i *IngredientMeta) Run(params *IngredientMetaParams) error { return errs.Wrap(err, "Could not fetch ingredient meta data") } - b, err := meta.MarshalYaml(false) + // Plain yaml rather than meta.MarshalYaml, whose field help comments would end up in the exported file. + b, err := yaml.Marshal(meta) if err != nil { return errs.Wrap(err, "Could not marshal ingredient meta data") } - i.prime.Output().Print(string(b)) + // Written as-is: Print would word-wrap long lines and interpret [TAGS], producing a file --meta can't read. + if _, err := i.prime.Output().Config().OutWriter.Write(b); err != nil { + return errs.Wrap(err, "Could not write ingredient meta data") + } return nil } diff --git a/internal/runners/publish/build.go b/internal/runners/publish/build.go index 3912b05ca7..1152ae5389 100644 --- a/internal/runners/publish/build.go +++ b/internal/runners/publish/build.go @@ -3,8 +3,10 @@ package publish import ( "cmp" "context" + "errors" "os" "path/filepath" + "slices" "strings" "github.com/ActiveState/cli/internal/archiver" @@ -42,7 +44,11 @@ func (r *Runner) generateEncryptedArtifact(params *Params, metaFile request.Publ Summary: cmp.Or(params.Description, metaFile.Description), }) if err != nil { - return nil, nil, locale.WrapInputError(err, "err_publish_build_metadata", "Could not determine the ingredient name and version: {{.V0}}", errs.JoinMessage(err)) + var missing *wheel.MissingMetadataError + if errors.As(err, &missing) { + return nil, nil, missingMetadataError(missing, params.Build) + } + return nil, nil, locale.WrapInputError(err, "err_publish_build_metadata", "Could not read the ingredient metadata: {{.V0}}", errs.JoinMessage(err)) } // Fetch and validate the org key before building anything: a private publish @@ -80,6 +86,30 @@ func (r *Runner) generateEncryptedArtifact(params *Params, metaFile request.Publ return meta, cleanup, nil } +// missingMetadataError tells the user which of name and version are missing, why pyproject.toml did not supply them, +// and where to set them instead. +func missingMetadataError(missing *wheel.MissingMetadataError, srcDir string) error { + var lines []string + for _, field := range missing.Fields { + var reason string + switch { + case missing.Pyproject == "": + reason = locale.Tl("publish_metadata_no_pyproject", "there is no pyproject.toml in {{.V0}}", srcDir) + case slices.Contains(missing.Dynamic, field): + reason = locale.Tl("publish_metadata_dynamic", + "{{.V0}} lists it in [project].dynamic, so it is computed by a build backend and cannot be read from the file", + missing.Pyproject) + default: + reason = locale.Tl("publish_metadata_absent", "{{.V0}} has no '{{.V1}}' in its [project] table", missing.Pyproject, field) + } + lines = append(lines, locale.Tl("publish_metadata_missing_field", + " - [ACTIONABLE]{{.V0}}[/RESET]: {{.V1}}. Set it in pyproject.toml, with '[ACTIONABLE]--{{.V0}}[/RESET]', or as '[ACTIONABLE]{{.V0}}:[/RESET]' in a '[ACTIONABLE]--meta[/RESET]' file.", + field, reason)) + } + return locale.NewInputError("err_publish_build_metadata_missing", + "Could not determine the ingredient {{.V0}}:\n{{.V1}}", strings.Join(missing.Fields, " and "), strings.Join(lines, "\n")) +} + // requireOrgNamespace ensures ns belongs to the project owner's private org, so // an artifact encrypted under that org's key is published under that same org // and stays decryptable by its consumers. diff --git a/internal/runners/publish/build_test.go b/internal/runners/publish/build_test.go index 3bb54f1dc3..9bada6f744 100644 --- a/internal/runners/publish/build_test.go +++ b/internal/runners/publish/build_test.go @@ -193,3 +193,28 @@ func TestAddPrivateBuilderDependency(t *testing.T) { t.Errorf("private-builder was added twice: %+v", reqVars.Dependencies) } } + +func TestMissingMetadataError(t *testing.T) { + err := missingMetadataError(&wheel.MissingMetadataError{ + Fields: []string{"name", "version"}, + Pyproject: "src/pyproject.toml", + Dynamic: []string{"version"}, + }, "src") + msg := err.Error() + for _, want := range []string{ + "Could not determine the ingredient name and version", + "src/pyproject.toml has no 'name' in its [project] table", + "--name", + "src/pyproject.toml lists it in [project].dynamic", + "--version", + } { + if !strings.Contains(msg, want) { + t.Errorf("message does not contain %q:\n%s", want, msg) + } + } + + msg = missingMetadataError(&wheel.MissingMetadataError{Fields: []string{"version"}}, "src").Error() + if !strings.Contains(msg, "there is no pyproject.toml in src") { + t.Errorf("message does not explain the missing pyproject.toml:\n%s", msg) + } +} diff --git a/internal/runners/publish/meta.go b/internal/runners/publish/meta.go index 5f157da642..8c7f96396d 100644 --- a/internal/runners/publish/meta.go +++ b/internal/runners/publish/meta.go @@ -52,5 +52,6 @@ func FetchMeta(namespace, name, version string, auth *authentication.Auth) (*req if err := prepareEditRequest(ingredient, meta, true, auth); err != nil { return nil, errs.Wrap(err, "Could not read ingredient meta data") } + fillVersionRequirements(meta) return meta, nil } diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index 4ca7e621fa..dd7b62e6a4 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -251,7 +251,7 @@ func (r *Runner) Run(params *Params) error { if !r.out.Config().Interactive { return locale.NewInputError("err_uploadingredient_editor_not_supported", "Opening in editor is not supported in non-interactive mode") } - if err := r.OpenInEditor(&reqVars); err != nil { + if err := r.editRequest(&reqVars, params); err != nil { return err } } @@ -260,31 +260,47 @@ func (r *Runner) Run(params *Params) error { return locale.NewInputError("err_uploadingredient_namespace_required", "You have to supply the namespace when working outside of a project context") } - b, err := reqVars.MarshalYaml(false) - if err != nil { - return errs.Wrap(err, "Could not marshal publish variables") - } + // Until the user confirms, they can edit the prepared meta data or save it to a file and start over with it. + for { + if params.Build != "" { + addPrivateBuilderDependency(&reqVars) // again, in case it was removed in the editor + } + fillVersionRequirements(&reqVars) + + b, err := reqVars.MarshalYaml(false) + if err != nil { + return errs.Wrap(err, "Could not marshal publish variables") + } - cont, err := r.prompt.Confirm( - "", - locale.Tl("uploadingredient_confirm", `Prepared the following ingredient: + cont, err := r.prompt.Confirm( + "", + locale.Tl("uploadingredient_confirm", `Prepared the following ingredient: {{.V0}} -Do you want to publish this ingredient? +Do you want to publish this ingredient? Answer 'n' to edit it or save it to a file instead. `, string(b)), ptr.To(true), nil) - if err != nil { - return errs.Wrap(err, "Not confirmed") - } - if !cont { - return locale.NewInputError("uploadingredient_cancel", "Publish cancelled") + if err != nil { + return errs.Wrap(err, "Not confirmed") + } + if cont { + break + } + + saved, err := r.reviseRequest(&reqVars, params) + if err != nil { + return err + } + if saved { + return nil + } } r.out.Notice(locale.Tl("uploadingredient_uploading", "Publishing ingredient...")) publishResult, err := r.bp.Publish(reqVars, params.Filepath) if err != nil { - return locale.WrapError(err, "err_uploadingredient_publish", "Could not create publish request") + return locale.WrapError(err, "err_uploadingredient_publish", "Could not create publish request", errs.JoinMessage(err)) } if publishResult.Error != "" { @@ -491,6 +507,128 @@ func prepareEditRequest(ingredient *ParentIngredient, r *request.PublishVariable return nil } +// reviseRequest lets the user edit a request they did not confirm, or save it to a meta file to edit and publish later. +// It reports whether the request was saved, in which case nothing is published. +func (r *Runner) reviseRequest(reqVars *request.PublishVariables, params *Params) (bool, error) { + edit := locale.Tl("uploadingredient_choice_edit", "Edit it in my editor") + save := locale.Tl("uploadingredient_choice_save", "Save it to a meta file to edit and publish later") + cancel := locale.Tl("uploadingredient_choice_cancel", "Cancel") + choice, err := r.prompt.Select("", locale.Tl("uploadingredient_choice", "What would you like to do instead?"), + []string{edit, save, cancel}, &cancel, nil) + if err != nil { + return false, errs.Wrap(err, "Not selected") + } + + switch choice { + case edit: + return false, r.editRequest(reqVars, params) + case save: + path, err := r.saveMetaFile(reqVars) + if err != nil { + return false, err + } + r.out.Notice(locale.Tl("uploadingredient_saved", + "Saved the ingredient meta data to [ACTIONABLE]{{.V0}}[/RESET]. Edit it, then publish it with:\n\n [ACTIONABLE]{{.V1}}[/RESET]\n\n"+ + "The meta file takes precedence over pyproject.toml, so the dependencies it lists replace the ones there.", + path, rerunCommand(params, path))) + return true, nil + } + return false, locale.NewInputError("uploadingredient_cancel", "Publish cancelled") +} + +// editRequest opens the request in the user's editor. With --build or --edit, the name, namespace and version were +// already used to build the package or find the ingredient, so changing them here would publish something +// inconsistent: the edits are saved to a meta file instead, and the user is told to start over with it. +func (r *Runner) editRequest(reqVars *request.PublishVariables, params *Params) error { + before := *reqVars + if err := r.OpenInEditor(reqVars); err != nil { + return err + } + if params.Build == "" && !params.Edit { + return nil + } + if reqVars.Name == before.Name && reqVars.Namespace == before.Namespace && reqVars.Version == before.Version { + return nil + } + + path, err := r.saveMetaFile(reqVars) + if err != nil { + return err + } + return locale.NewInputError("err_uploadingredient_edit_identity_changed", + "The name, namespace or version can't be changed at this point, because they were already used to build the package "+ + "or find the ingredient. Your changes were saved to [ACTIONABLE]{{.V0}}[/RESET]. Publish them with:\n\n [ACTIONABLE]{{.V1}}[/RESET]", + path, rerunCommand(params, path)) +} + +// saveMetaFile writes the request to -.meta.yaml in the working directory, in the format --meta reads, +// and returns its path. An existing file is only replaced if the user agrees. +func (r *Runner) saveMetaFile(reqVars *request.PublishVariables) (string, error) { + path, err := filepath.Abs(reqVars.Name + "-" + reqVars.Version + ".meta.yaml") + if err != nil { + return "", errs.Wrap(err, "Could not determine meta file path") + } + if fileutils.TargetExists(path) { + overwrite, err := r.prompt.Confirm("", locale.Tl("uploadingredient_meta_overwrite", + "[ACTIONABLE]{{.V0}}[/RESET] already exists. Do you want to replace it?", path), ptr.To(false), nil) + if err != nil { + return "", errs.Wrap(err, "Not confirmed") + } + if !overwrite { + return "", locale.NewInputError("uploadingredient_cancel", "Publish cancelled") + } + } + + // Plain yaml rather than MarshalYaml, whose field help comments don't belong in a file the user keeps. + b, err := yaml.Marshal(reqVars) + if err != nil { + return "", errs.Wrap(err, "Could not marshal publish variables") + } + if err := fileutils.WriteFile(path, b); err != nil { + return "", locale.WrapExternalError(err, "err_uploadingredient_meta_write", "Could not write meta file: {{.V0}}", path) + } + return path, nil +} + +// rerunCommand is the publish command that picks up a saved meta file. Other flags are left out: their values are in +// the file already. +func rerunCommand(params *Params, metaPath string) string { + args := []string{"state", "publish"} + if params.Edit { + args = append(args, "--edit") + } + if params.Build != "" { + args = append(args, "--build", quoteArg(params.Build)) + } else if params.Filepath != "" { + args = append(args, quoteArg(params.Filepath)) + } + args = append(args, "--meta", quoteArg(metaPath)) + return strings.Join(args, " ") +} + +func quoteArg(arg string) string { + if strings.ContainsAny(arg, " \t") { + return `"` + arg + `"` + } + return arg +} + +// anyVersion is the version requirement the platform uses for a dependency or condition on any version. +const anyVersion = ">=0" + +// fillVersionRequirements gives every dependency and condition without a version requirement the any-version one. The +// platform rejects them otherwise, and they come about easily: a pyproject.toml dependency without a version range, a +// --depend flag without a version, or a condition inherited from a revision that has no requirements. +func fillVersionRequirements(r *request.PublishVariables) { + for i := range r.Dependencies { + dep := &r.Dependencies[i] + dep.VersionRequirements = cmp.Or(dep.VersionRequirements, anyVersion) + for j := range dep.Conditions { + dep.Conditions[j].VersionRequirements = cmp.Or(dep.Conditions[j].VersionRequirements, anyVersion) + } + } +} + func (r *Runner) OpenInEditor(pr *request.PublishVariables) error { // Prepare file for editing b, err := pr.MarshalYaml(true) @@ -520,10 +658,13 @@ func (r *Runner) OpenInEditor(pr *request.PublishVariables) error { return errs.Wrap(err, "Could not read file") } - // Write changes to request - if err := pr.UnmarshalYaml(eb); err != nil { - return locale.WrapError(err, "err_uploadingredient_publish", "Could not unmarshal publish request") + // Write changes to request. Unmarshal into a fresh value, so that fields removed in the editor are removed from the + // request too instead of keeping their previous values. + var edited request.PublishVariables + if err := edited.UnmarshalYaml(eb); err != nil { + return locale.WrapInputError(err, "err_uploadingredient_editor_yaml", "The edited file [ACTIONABLE]{{.V0}}[/RESET] is not valid: {{.V1}}", fn, err.Error()) } + *pr = edited return nil } diff --git a/internal/runners/publish/publish_test.go b/internal/runners/publish/publish_test.go index 14940dfdba..f188293fae 100644 --- a/internal/runners/publish/publish_test.go +++ b/internal/runners/publish/publish_test.go @@ -108,3 +108,46 @@ func TestCheckEditKeepsIdentity(t *testing.T) { }) } } + +func TestFillVersionRequirements(t *testing.T) { + r := &request.PublishVariables{Dependencies: []request.PublishVariableDep{ + { + Dependency: request.Dependency{Name: "requests", Namespace: "language/python"}, + Conditions: []request.Dependency{ + {Name: "Windows", Namespace: "kernel"}, + {Name: "python", Namespace: "language", VersionRequirements: "<3.11"}, + }, + }, + {Dependency: request.Dependency{Name: "six", Namespace: "language/python", VersionRequirements: ">=1.16"}}, + }} + fillVersionRequirements(r) + + if got := r.Dependencies[0].VersionRequirements; got != ">=0" { + t.Errorf("dependency without a version = %q, want >=0", got) + } + if got := r.Dependencies[0].Conditions[0].VersionRequirements; got != ">=0" { + t.Errorf("condition without a version = %q, want >=0", got) + } + if got := r.Dependencies[0].Conditions[1].VersionRequirements; got != "<3.11" { + t.Errorf("condition version changed to %q", got) + } + if got := r.Dependencies[1].VersionRequirements; got != ">=1.16" { + t.Errorf("dependency version changed to %q", got) + } +} + +func TestRerunCommand(t *testing.T) { + tests := []struct { + params *Params + want string + }{ + {&Params{Build: "./src", Filepath: "/tmp/built.tar.gz"}, "state publish --build ./src --meta m.yaml"}, + {&Params{Edit: true, Build: "my src"}, `state publish --edit --build "my src" --meta m.yaml`}, + {&Params{Filepath: "pkg.tar.gz", Name: "ignored"}, "state publish pkg.tar.gz --meta m.yaml"}, + } + for _, tt := range tests { + if got := rerunCommand(tt.params, "m.yaml"); got != tt.want { + t.Errorf("rerunCommand(%+v) = %q, want %q", tt.params, got, tt.want) + } + } +} diff --git a/internal/runners/publish/readme.md b/internal/runners/publish/readme.md index 1653291e1c..6b6e0e97b4 100644 --- a/internal/runners/publish/readme.md +++ b/internal/runners/publish/readme.md @@ -51,7 +51,8 @@ dependencies = [ ``` Each entry becomes a runtime dependency on the platform's Python package of that name. Version ranges carry over, with -`~=1.16` becoming `>=1.16,<2` and `==4.2.*` becoming `>=4.2,<4.3`. +`~=1.16` becoming `>=1.16,<2` and `==4.2.*` becoming `>=4.2,<4.3`. An entry without a range, such as `"click"`, accepts +any version (`>=0`). Platform and Python version markers become **conditions**, so the dependency is only installed where it applies: @@ -111,12 +112,23 @@ dependencies: namespace: builder versionRequirements: '>=0' -Do you want to publish this ingredient? (Y/n) +Do you want to publish this ingredient? Answer 'n' to edit it or save it to a file instead. (Y/n) ``` Check the dependency list here: it is exactly what will be published. The `private-builder` dependency is always added; it is what decrypts and installs the package, so leave it in place. +If something is wrong, answer `n` and choose one of: + +- **Edit it in my editor:** opens the meta data in `$EDITOR` (or the default program for `.yaml` files), then shows + the result for confirmation again. Name, namespace and version can't be changed this way, because the package was + already built with them: if you change them, your edits are saved to a meta file as below. +- **Save it to a meta file to edit and publish later:** writes `mypkg-1.0.0.meta.yaml` to the current directory and + prints the command to publish it, e.g. `state publish --build ./mypkg-src --meta mypkg-1.0.0.meta.yaml`. Edit the file, + then run that command. The meta file takes precedence over `pyproject.toml`, so its name, version and dependency list + are used instead. +- **Cancel:** nothing is published. + Flags override `pyproject.toml`: `--name`, `--version` and `--description` replace the values in it, and any `--depend`, `--depend-runtime`, `--depend-build` or `--depend-test` flag replaces its whole dependency list. @@ -194,6 +206,8 @@ is `name: python`, `namespace: language`, with a requirement such as `<3.11`. state publish --edit --build ./mypkg-src --meta meta.yaml ``` +The platform requires the source with every revision, so pass `--build` even when only the meta data changes. + Things to know about `--edit`: - **The meta file replaces the whole dependency list.** Anything you delete from it is removed from the ingredient. @@ -222,7 +236,7 @@ accident. | Message | What to do | |-------------------------------------------------------------------------|------------------------------------------------------------------------------------------------| -| `Could not determine the ingredient name and version` | Add `name` and `version` to `pyproject.toml`, or pass `--name` and `--version`. | +| `Could not determine the ingredient name and version` | The message lists each missing field and why. Add it to `pyproject.toml`'s `[project]` table, pass `--name`/`--version`, or set it in a `--meta` file. A version listed in `[project].dynamic` can't be read and must be given this way. | | `The '--build' flag requires a namespace under 'private/'` | Use a namespace under the organization that owns the project you are in. | | `org key does not belong to this project's organization` | The key service returned another organization's key. Check that you are in the right project. | | `No organization key service is configured` | Configure the `privateingredient.*` settings (see Before You Start). | diff --git a/test/integration/publish_int_test.go b/test/integration/publish_int_test.go index d125553263..1264eb56bd 100644 --- a/test/integration/publish_int_test.go +++ b/test/integration/publish_int_test.go @@ -450,6 +450,8 @@ authors: cp.SendLine("Y") } else { cp.SendLine("n") + cp.Expect("What would you like to do instead?") + cp.SendLine("") // the default choice: Cancel cp.Expect("Publish cancelled") return } From 26633395c82b207175b62445792cc5873febe6ff Mon Sep 17 00:00:00 2001 From: icanhasmath Date: Tue, 27 Oct 2026 14:45:06 -0500 Subject: [PATCH 2/5] Update echo to v4.16.1 and drop an unused publish error argument echo v4.15.4 has GHSA-cxf3-9h37-7rr8 (critical), GHSA-9969-6676-49h5, GHSA-f48p-c9v4-h7xv and GHSA-h7wr-284f-5qvc, which failed the Scan job. err_uploadingredient_publish is defined in en-us.yaml without a {{.V0}}, so the joined message passed to it was never shown. The wrapped error is already printed beneath it, so drop the argument and the changelog claim. --- changelog.md | 4 +- go.mod | 2 +- go.sum | 4 +- internal/runners/publish/publish.go | 2 +- .../github.com/labstack/echo/v4/CHANGELOG.md | 45 ++++ vendor/github.com/labstack/echo/v4/README.md | 18 +- vendor/github.com/labstack/echo/v4/bind.go | 39 +++- vendor/github.com/labstack/echo/v4/context.go | 97 +++++--- vendor/github.com/labstack/echo/v4/echo.go | 16 +- vendor/github.com/labstack/echo/v4/echo_fs.go | 75 +++++++ .../labstack/echo/v4/middleware/body_dump.go | 49 +++- .../labstack/echo/v4/middleware/body_limit.go | 56 ++--- .../labstack/echo/v4/middleware/decompress.go | 43 +++- .../echo/v4/middleware/method_override.go | 20 +- .../labstack/echo/v4/middleware/proxy.go | 142 ++++++------ .../labstack/echo/v4/middleware/secure.go | 3 +- .../labstack/echo/v4/middleware/slash.go | 30 +++ .../labstack/echo/v4/middleware/static.go | 93 +++++++- .../github.com/labstack/echo/v4/route_path.go | 104 +++++++++ vendor/github.com/labstack/echo/v4/router.go | 209 +++++++++++++----- vendor/github.com/labstack/echo/v4/scheme.go | 134 +++++++++++ vendor/modules.txt | 4 +- 22 files changed, 958 insertions(+), 231 deletions(-) create mode 100644 vendor/github.com/labstack/echo/v4/route_path.go create mode 100644 vendor/github.com/labstack/echo/v4/scheme.go diff --git a/changelog.md b/changelog.md index ce7dd80f61..f791b9f626 100644 --- a/changelog.md +++ b/changelog.md @@ -39,12 +39,14 @@ and this project adheres to - Dependencies and conditions without a version range (such as a bare `requests` in `pyproject.toml`) are now published as accepting any version instead of failing with "Expecting an OBJECT with a field named 'versionRequirements'". - Fields removed in the `state publish --editor` file are now removed from the publish instead of keeping their - previous values, and publish API errors are shown instead of ``. + previous values. ### Security - Updated `github.com/klauspost/compress` to v1.18.7 to remediate CVE-2026-63209 (GHSA-259r-337f-4rfw). - Updated to Go 1.26.10 and `golang.org/x/net` v0.60.0 to remediate 14 CVEs in the Go standard library and HTTP/2 handling. +- Updated `github.com/labstack/echo/v4` to v4.16.1 to remediate GHSA-cxf3-9h37-7rr8, GHSA-9969-6676-49h5, + GHSA-f48p-c9v4-h7xv and GHSA-h7wr-284f-5qvc. ## 0.48.1 diff --git a/go.mod b/go.mod index d2b63f4abb..91660e8763 100644 --- a/go.mod +++ b/go.mod @@ -34,7 +34,7 @@ require ( github.com/jarcoal/httpmock v1.0.3 github.com/jessevdk/go-flags v1.4.0 github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d - github.com/labstack/echo/v4 v4.15.4 + github.com/labstack/echo/v4 v4.16.1 github.com/mash/go-tempfile-suffix v0.0.0-20150731093933-48f0f8a3a5ab github.com/mattn/go-runewidth v0.0.15 github.com/nicksnyder/go-i18n v1.10.0 diff --git a/go.sum b/go.sum index 7dab0fdf09..0b1cf68135 100644 --- a/go.sum +++ b/go.sum @@ -499,8 +499,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.15.4 h1:DL45vVYa+BWE+XuW+zZNd9H0YEdZ80UAWJGcTVW4EVs= -github.com/labstack/echo/v4 v4.15.4/go.mod h1:CuMetKIRwsuO/qlAgMq+KTAalwGoB/h4tC+yPdrTj1g= +github.com/labstack/echo/v4 v4.16.1 h1:h05NuF1GvNu6jSAZxOv++AdjD/NJK+mZdGy0thiiuns= +github.com/labstack/echo/v4 v4.16.1/go.mod h1:V/EQaTVHYJH92QhbQe7kFQ4mjgcjxLQQUQtAN87zZdw= github.com/labstack/gommon v0.5.0 h1:6VSQ2NOzsnEJ5W6+84E0RbcaDDmgB6NIAzWCczTEe6c= github.com/labstack/gommon v0.5.0/go.mod h1:Rzlg7HHy1maLfzBYGg9NZcVuz1sA68HHhLjhcEllYE0= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= diff --git a/internal/runners/publish/publish.go b/internal/runners/publish/publish.go index dd7b62e6a4..f04d68bbab 100644 --- a/internal/runners/publish/publish.go +++ b/internal/runners/publish/publish.go @@ -300,7 +300,7 @@ Do you want to publish this ingredient? Answer 'n' to edit it or save it to a fi publishResult, err := r.bp.Publish(reqVars, params.Filepath) if err != nil { - return locale.WrapError(err, "err_uploadingredient_publish", "Could not create publish request", errs.JoinMessage(err)) + return locale.WrapError(err, "err_uploadingredient_publish", "Could not create publish request") } if publishResult.Error != "" { diff --git a/vendor/github.com/labstack/echo/v4/CHANGELOG.md b/vendor/github.com/labstack/echo/v4/CHANGELOG.md index 8388531e91..c8017d19d5 100644 --- a/vendor/github.com/labstack/echo/v4/CHANGELOG.md +++ b/vendor/github.com/labstack/echo/v4/CHANGELOG.md @@ -1,5 +1,50 @@ # Changelog +## v4.16.0 - 2026-09-27 + +**Security** + +This release fixes several security issues. Upgrading is recommended. Some fixes change behavior; read "Behavior changes to check before upgrading" below. + +* **Request scheme:** `Context.Scheme()` now uses the `X-Forwarded-Proto`, `X-Forwarded-Protocol`, `X-Forwarded-Ssl` and `X-Url-Scheme` headers only when the request comes directly from a loopback, link-local or private network address or a unix socket. Before this, any client could send `X-Forwarded-Proto: https` over plain HTTP and skip `HTTPSRedirect`. When `X-Forwarded-Proto` is present, only it is used (its last value), and the scheme is returned in lowercase. the new `Echo#SchemeExtractor` field selects the strategy: `ExtractSchemeFromHeaders(...TrustOption)` (default), `ExtractSchemeDirect()` or `LegacySchemeExtractor()`. The Secure middleware now sets HSTS based on `Context.Scheme()`. The Proxy middleware always sets `X-Forwarded-Proto` from `Context.Scheme()` and removes `X-Forwarded-Ssl`, `X-Forwarded-Protocol` and `X-Url-Scheme` before forwarding. [GHSA-2ffq-g2xg-c22p](https://github.com/labstack/echo/security/advisories/GHSA-2ffq-g2xg-c22p) +* **JSONP:** `Context.JSONP` and `Context.JSONPBlob` accept only a callback that is empty, a JavaScript identifier or a dot-separated path of identifiers (ASCII letters, digits, `_` and `$`). Any other callback returns a 400 Bad Request error that wraps the new `ErrInvalidJSONPCallback`, and nothing is written. JSONP responses now carry `X-Content-Type-Options: nosniff`. JSONP lets any website read the response with the user's cookies, so do not use it for data that needs authentication. [GHSA-h9g5-28mm-hx3g](https://github.com/labstack/echo/security/advisories/GHSA-h9g5-28mm-hx3g) +* **MethodOverride:** a POST can no longer be overridden to `GET`, `HEAD`, `OPTIONS`, `TRACE` or `CONNECT`. Before this, with the `MethodFromForm` or `MethodFromQuery` getter and MethodOverride registered with `Use` before the CSRF middleware, `_method=GET` skipped the CSRF check. Register MethodOverride with `Echo#Pre`. [GHSA-r7w9-592q-9vg4](https://github.com/labstack/echo/security/advisories/GHSA-r7w9-592q-9vg4) +* **Redirects:** the trailing slash middlewares and the static directory redirect percent-encode control characters in the redirect path. Before this, `/%09/evil.example/` redirected browsers to `evil.example`. [GHSA-v753-g4cw-jm48](https://github.com/labstack/echo/security/advisories/GHSA-v753-g4cw-jm48) +* **Static files:** with the default settings, the Static middleware resolves files from the same form of the path that the router matched, so `/admin%2Fsecret.txt` or `/%61dmin/secret.txt` can no longer reach a file under a guarded `/admin/*` route. [GHSA-375p-5qhx-8wq4](https://github.com/labstack/echo/security/advisories/GHSA-375p-5qhx-8wq4) The Static middleware and `StaticDirectoryHandler` (used by `Echo.Static`, `Echo.StaticFS`, `Group.Static` and `Group.StaticFS`) no longer serve paths with a `.`, `..` or empty segment, such as `/assets/../admin/secret.txt`, also after path unescaping. [GHSA-3pmx-cf9f-34xr](https://github.com/labstack/echo/security/advisories/GHSA-3pmx-cf9f-34xr) +* **Dependencies:** update `golang.org/x/text` to v0.40.0 ([GO-2026-5970](https://pkg.go.dev/vuln/GO-2026-5970)). + +**Client IP address (no code change in v4)** + +Without `Echo#IPExtractor`, `Context.RealIP()` in v4 trusts the `X-Forwarded-For` and `X-Real-IP` headers from any client, so the rate limiter can be bypassed and the Proxy middleware forwards a spoofed `X-Real-IP` ([GHSA-246p-cpwv-v3jq](https://github.com/labstack/echo/security/advisories/GHSA-246p-cpwv-v3jq), [GHSA-99jh-6h7p-pp36](https://github.com/labstack/echo/security/advisories/GHSA-99jh-6h7p-pp36)). Changing this default in v4 would put all clients behind a proxy into one rate-limit bucket, so v4 keeps it. Set an extractor that matches your deployment: +```go +e.IPExtractor = echo.ExtractIPDirect() // no proxy in front of the app +e.IPExtractor = echo.ExtractIPFromXFFHeader() // behind proxies in private networks that set X-Forwarded-For +// behind a proxy with public addresses (e.g. a CDN), also trust its ranges: +// e.IPExtractor = echo.ExtractIPFromXFFHeader(echo.TrustIPRange(cdnRange)) +``` +v5 uses the direct peer address by default since v5.1.0. + +**Behavior changes to check before upgrading** + +* **Proxies or load balancers with public IP addresses.** If a proxy connects to your app from a public (or `100.64.0.0/10`) address, its `X-Forwarded-Proto` is now ignored: `HTTPSRedirect` redirects in a loop and the Secure middleware stops sending HSTS. This affects, for example, Cloudflare, CloudFront and Azure Front Door connecting to a public origin, the GCP external HTTP(S) load balancer including GKE Ingress (`35.191.0.0/16`, `130.211.0.0/22`), and networks that use `100.64.0.0/10` (such as Alibaba Cloud SLB or EKS custom networking). Trust the proxy's address ranges: + ```go + _, gclb1, _ := net.ParseCIDR("35.191.0.0/16") + _, gclb2, _ := net.ParseCIDR("130.211.0.0/22") + e.SchemeExtractor = echo.ExtractSchemeFromHeaders(echo.TrustIPRange(gclb1), echo.TrustIPRange(gclb2)) + ``` + Proxies on the same host, in a private network (AWS ALB, in-cluster ingress controllers such as ingress-nginx or Traefik, most PaaS routers) or on a unix socket keep working without changes. `echo.LegacySchemeExtractor()` restores the old behavior but is not safe unless every request passes through a proxy that sets these headers. Serverless adapters or middleware that set `RemoteAddr` to the client's address also make `X-Forwarded-Proto` ignored (or, if they take it from a header, spoofable). +* **Trusted proxies must set `X-Forwarded-Proto`.** A proxy on a trusted address that passes the client's `X-Forwarded-Proto` through (for example nginx without `proxy_set_header X-Forwarded-Proto $scheme;`) still lets the client choose the scheme. An invalid `X-Forwarded-Proto` value now results in `http` instead of falling back to the other scheme headers. +* **Your own tests.** `httptest.NewRequest` sets `RemoteAddr` to `192.0.2.1:1234`, which is not trusted, so tests that set `X-Forwarded-Proto` now see `http`. Set `req.RemoteAddr = "10.0.0.1:1234"` or use `e.SchemeExtractor = echo.LegacySchemeExtractor()` in such tests. +* **Proxy middleware headers.** `X-Forwarded-Ssl`, `X-Forwarded-Protocol` and `X-Url-Scheme` are no longer forwarded to the upstream; `X-Forwarded-Proto` carries the scheme. +* **MethodOverride.** Overriding a POST to `GET` (for example with `X-HTTP-Method-Override: GET` to send a long query in a POST body) is no longer done; such requests keep the POST method. +* **Static files.** Paths with a double slash or dot segment (for example `/assets//app.js`) now return 404; in HTML5 mode the index is still served. The Static middleware no longer finds file names that the client sends with non-default escaping (for example `%2C`, `%40` or lowercase hex like `%c3%a9`) unless `StaticConfig.EnablePathUnescaping` is set; `Echo.Static` has behaved this way since v4.15.4. With `StaticConfig.EnablePathUnescaping` or `Echo#EnablePathUnescapingStaticFiles`, encoded dots (`%2e%2e`) no longer traverse directories, but encoded slashes are still decoded, so do not combine these options with route-based access control. +* **JSONP.** `Context.JSONP` returns an error for callbacks that are not JavaScript identifiers. + +**Documentation** + +* Static middleware: when registered with `Echo#Use` it runs before route and group middleware, so route guards do not protect the files it serves. + + ## v4.15.4 - 2026-06-15 **Security** diff --git a/vendor/github.com/labstack/echo/v4/README.md b/vendor/github.com/labstack/echo/v4/README.md index 5e52d1d4e8..49040fd7b0 100644 --- a/vendor/github.com/labstack/echo/v4/README.md +++ b/vendor/github.com/labstack/echo/v4/README.md @@ -36,6 +36,13 @@ Help and questions: [Github Discussions](https://github.com/labstack/echo/discus ## Sponsors + +
encore icon @@ -54,7 +61,15 @@ Click [here](https://github.com/sponsors/labstack) for more information on spons // go get github.com/labstack/echo/{version} go get github.com/labstack/echo/v4 ``` -Latest version of Echo supports last four Go major [releases](https://go.dev/doc/devel/release) and might work with older versions. +Echo v4.16.1 requires Go 1.26 or later so it can use patched `golang.org/x/` dependencies. Use a supported, patched Go toolchain when building applications. + +### Security defaults in v4.16.1 + +`BodyDump` captures at most 5 MiB of the request and 5 MiB of the response by default. The application and client still receive the complete bodies. Configure `BodyDumpConfig.MaxRequestBytes` and `MaxResponseBytes` to change the capture limits; `-1` disables a limit. + +`Decompress` limits decompressed request bodies to 100 MiB by default. Reading beyond the limit returns HTTP 413, including through JSON and XML binding. Configure `DecompressConfig.MaxDecompressedSize` to change the limit; `-1` disables it. Apply any smaller application body limit after decompression. + +The proxy now validates WebSocket upgrade responses, closes both WebSocket connections when either side disconnects, and removes incoming `Forwarded` headers. Backends should use the proxy's validated forwarding headers and enforce their own authorization. ### Example @@ -99,6 +114,7 @@ Following list of middleware is maintained by Echo team. |------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [github.com/labstack/echo-jwt](https://github.com/labstack/echo-jwt) | [JWT](https://github.com/golang-jwt/jwt) middleware | | [github.com/labstack/echo-contrib](https://github.com/labstack/echo-contrib) | [casbin](https://github.com/casbin/casbin), [gorilla/sessions](https://github.com/gorilla/sessions), [jaegertracing](https://github.com/uber/jaeger-client-go), [prometheus](https://github.com/prometheus/client_golang/), [pprof](https://pkg.go.dev/net/http/pprof), [zipkin](https://github.com/openzipkin/zipkin-go) middlewares | +| [github.com/labstack/echo-otel](https://github.com/labstack/echo-otel/tree/v4) | [OpenTelemetry](https://opentelemetry.io/) middleware for tracing and metrics (`github.com/labstack/echo-otel/v4`) | # Third-party middleware repositories diff --git a/vendor/github.com/labstack/echo/v4/bind.go b/vendor/github.com/labstack/echo/v4/bind.go index 1d4fe6f0a7..b9cffab09e 100644 --- a/vendor/github.com/labstack/echo/v4/bind.go +++ b/vendor/github.com/labstack/echo/v4/bind.go @@ -22,6 +22,13 @@ type Binder interface { } // DefaultBinder is the default implementation of the Binder interface. +// For path, query, header, and form binding, time.Time and *time.Time fields (not slices) +// may use a format tag: "date-time" uses the standard RFC3339 decoding, "date-time-local" +// accepts "2006-01-02T15:04:05" with optional fractional seconds, "date" accepts +// "2006-01-02", and other values specify Go time layouts. "date", "date-time-local", and +// layouts without a timezone give UTC times. JSON and XML decoding do not use this tag. +// HTML datetime-local inputs omit seconds by default, so bind them with a Go layout +// such as "2006-01-02T15:04" instead of "date-time-local". type DefaultBinder struct{} // BindUnmarshaler is the interface used to wrap the UnmarshalParam method. @@ -42,10 +49,11 @@ type bindMultipleUnmarshaler interface { // BindPathParams binds path params to bindable object // // Time format support: time.Time fields can use `format` tags to specify custom parsing layouts. -// Example: `param:"created" format:"2006-01-02T15:04"` for datetime-local format +// Example: `form:"created" format:"2006-01-02T15:04"` for HTML datetime-local input // Example: `param:"date" format:"2006-01-02"` for date format // Uses Go's standard time format reference time: Mon Jan 2 15:04:05 MST 2006 -// Works with form data, query parameters, and path parameters (not JSON body) +// OpenAPI format names "date-time", "date-time-local" and "date" are also supported (see DefaultBinder) +// Works with form data, query parameters, headers, and path parameters (not JSON body) // Falls back to default time.Time parsing if no format tag is specified func (b *DefaultBinder) BindPathParams(c Context, i interface{}) error { names := c.ParamNames() @@ -95,6 +103,10 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) { } case MIMEApplicationXML, MIMETextXML: if err = xml.NewDecoder(req.Body).Decode(i); err != nil { + var httpError *HTTPError + if errors.As(err, &httpError) { + return httpError + } if ute, ok := err.(*xml.UnsupportedTypeError); ok { return NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Unsupported type error: type=%v, error=%v", ute.Type, ute.Error())).SetInternal(err) } else if se, ok := err.(*xml.SyntaxError); ok { @@ -105,6 +117,10 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) { case MIMEApplicationForm: params, err := c.FormParams() if err != nil { + var httpError *HTTPError + if errors.As(err, &httpError) { + return httpError + } return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } if err = b.bindData(i, params, "form", nil); err != nil { @@ -113,6 +129,10 @@ func (b *DefaultBinder) BindBody(c Context, i interface{}) (err error) { case MIMEMultipartForm: params, err := c.MultipartForm() if err != nil { + var httpError *HTTPError + if errors.As(err, &httpError) { + return httpError + } return NewHTTPError(http.StatusBadRequest, err.Error()).SetInternal(err) } if err = b.bindData(i, params.Value, "form", params.File); err != nil { @@ -375,10 +395,19 @@ func unmarshalInputToField(valueKind reflect.Kind, val string, field reflect.Val fieldIValue := field.Addr().Interface() - // Handle time.Time with custom format tag - if formatTag != "" { + // date-time uses the same TextUnmarshaler as an untagged time.Time. + if formatTag != "" && formatTag != "date-time" { if _, isTime := fieldIValue.(*time.Time); isTime { - t, err := time.Parse(formatTag, val) + // OpenAPI dates and local date-times have no timezone. time.Parse assigns UTC, + // as it does for custom layouts without timezone information. + layout := formatTag + switch formatTag { + case "date": + layout = "2006-01-02" + case "date-time-local": + layout = "2006-01-02T15:04:05" + } + t, err := time.Parse(layout, val) if err != nil { return true, err } diff --git a/vendor/github.com/labstack/echo/v4/context.go b/vendor/github.com/labstack/echo/v4/context.go index 6500e5eef2..dd5f11c463 100644 --- a/vendor/github.com/labstack/echo/v4/context.go +++ b/vendor/github.com/labstack/echo/v4/context.go @@ -134,10 +134,17 @@ type Context interface { // JSONP sends a JSONP response with status code. It uses `callback` to construct // the JSONP payload. + // + // The callback must be empty, a JavaScript identifier or a dot-separated path of identifiers using only ASCII + // letters, digits, `_` and `$`. Otherwise nothing is written and a 400 Bad Request error wrapping + // ErrInvalidJSONPCallback is returned. The response has the `X-Content-Type-Options: nosniff` header. + // + // Any website can load a JSONP response with a