feat(push): add --output json/yaml/table flag to helm push#32009
feat(push): add --output json/yaml/table flag to helm push#32009
Conversation
There was a problem hiding this comment.
Pull request overview
Adds machine-readable output support to helm push by plumbing a structured push result (*registry.PushResult) through the pusher/action layers and formatting it in the CLI as table/JSON/YAML.
Changes:
- Update pusher/uploader/action push APIs to return
(*registry.PushResult, error)instead of plain text / error-only. - Add
--outputsupport tohelm pushand implement table/JSON/YAML formatting for push results. - Adjust OCI pusher tests for the new return signature.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/uploader/chart_uploader.go | Propagates *registry.PushResult from the selected pusher. |
| pkg/pusher/pusher.go | Changes Pusher interface to return *registry.PushResult. |
| pkg/pusher/ocipusher.go | Returns the registry.Client.Push() result instead of only an error. |
| pkg/pusher/ocipusher_test.go | Updates tests to ignore the new result value when asserting errors. |
| pkg/cmd/push.go | Adds --output flag wiring and formats push result as table/JSON/YAML. |
| pkg/action/push.go | Changes Push.Run() to return *registry.PushResult and adjusts output handling. |
Comments suppressed due to low confidence (1)
pkg/action/push.go:100
WithPushOptWritersetsp.out, butRun()now hard-codesChartUploader.Outtoio.Discardand never usesp.out. This makes the option effectively dead and can change where push informational output/warnings are emitted (and can also contribute to duplicated output if the registry client still writes its own messages). Consider either wiringp.outthrough to whatever component is responsible for emitting push info, or removingWithPushOptWriter/Push.outentirely to avoid misleading API surface.
func (p *Push) Run(chartRef string, remote string) (*registry.PushResult, error) {
c := uploader.ChartUploader{
Out: io.Discard,
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
pusher.WithPlainHTTP(p.plainHTTP),
},
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| result, err := client.Run(chartRef, remote) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| fmt.Fprint(out, output) | ||
| return nil | ||
| writer := &pushWriter{ | ||
| result: pushResult{ | ||
| Ref: result.Ref, | ||
| Digest: result.Manifest.Digest, | ||
| }, | ||
| } | ||
| return o.outfmt.Write(out, writer) | ||
| }, |
There was a problem hiding this comment.
The command now always prints a formatted result to out, but the registry client used by the pusher also prints "Pushed:" / "Digest:" to its own writer inside registry.Client.Push() (and newRegistryClient configures that writer as os.Stderr). In a terminal, this will typically result in duplicated output for the default table format. Consider suppressing registry-client push output when --output table is used (or conversely, for table output rely solely on the registry client’s messages), while keeping stderr clean for machine-readable --output json|yaml.
pkg/cmd/push.go
Outdated
| action.WithPlainHTTP(o.plainHTTP), | ||
| action.WithPushOptWriter(out)) |
There was a problem hiding this comment.
action.WithPushOptWriter(out) is still passed when constructing the push action, but Push.Run() no longer uses that writer. This is confusing for readers and suggests output routing is configurable when it currently isn't. Either remove this option usage here or restore using the writer in the action so the flag has an effect.
| action.WithPlainHTTP(o.plainHTTP), | |
| action.WithPushOptWriter(out)) | |
| action.WithPlainHTTP(o.plainHTTP)) |
69abd22 to
a5ebd5f
Compare
a5ebd5f to
0b439c0
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
pkg/action/push.go:105
WithPushOptWritersetsp.out, butPush.Run()only passes that writer touploader.ChartUploader.Out.ChartUploader.UploadTo()currently never usesOut, so this option doesn’t actually control any push output and can’t be used to redirect/suppress registry client messages. Either wireoutthrough to where output is emitted (e.g., registry clientClientOptWriter) or remove/rename this option to avoid a misleading API.
func (p *Push) Run(chartRef string, remote string) (*registry.PushResult, error) {
out := p.out
if out == nil {
out = io.Discard
}
c := uploader.ChartUploader{
Out: out,
Pushers: pusher.All(p.Settings),
Options: []pusher.Option{
pusher.WithTLSClientConfig(p.certFile, p.keyFile, p.caFile),
pusher.WithInsecureSkipTLSVerify(p.insecureSkipTLSVerify),
pusher.WithPlainHTTP(p.plainHTTP),
},
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -104,5 +138,7 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command { | |||
| f.StringVar(&o.username, "username", "", "chart repository username where to locate the requested chart") | |||
| f.StringVar(&o.password, "password", "", "chart repository password where to locate the requested chart") | |||
|
|
|||
| bindOutputFlag(cmd, &o.outfmt) | |||
|
|
|||
| return cmd | |||
There was a problem hiding this comment.
New behavior adds JSON/YAML output for helm push, but there are no command-level tests asserting the emitted formats (unlike other commands in this package that use --output golden tests). Adding tests for table/json/yaml (and ensuring the default output remains unchanged) would help prevent regressions, especially around stderr/stdout interactions.
0b439c0 to
0ee6823
Compare
Add support for --output flag to 'helm push' command for machine-readable output formats (JSON and YAML). This enables programmatic consumption of push results and integration with tools like cosign for artifact signing. Changes: - Modified Pusher interface to return (*registry.PushResult, error) - Updated OCIPusher.Push() and push() to return PushResult - Updated action.Push.Run() to return (*registry.PushResult, error) - Added output formatting to push command (table/json/yaml) - Created pushResult struct with Ref and Digest fields - Implemented pushWriter with WriteTable/WriteJSON/WriteYAML methods - Updated test fixtures to handle new return signature The default table format maintains backward compatibility with existing plain-text output style. Fixes helm#11735 Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
0ee6823 to
0b9946a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func (p *Push) Run(chartRef string, remote string) (*registry.PushResult, error) { | ||
| c := uploader.ChartUploader{ | ||
| Out: &out, | ||
| Out: io.Discard, | ||
| Pushers: pusher.All(p.Settings), | ||
| Options: []pusher.Option{ |
There was a problem hiding this comment.
Push.Run no longer uses p.out (set via WithPushOptWriter) and always builds the uploader with Out: io.Discard. That makes WithPushOptWriter effectively a no-op and can mislead API consumers/tests. Consider either wiring p.out through to the underlying registry client's writer (or uploader warnings) or removing/deprecating WithPushOptWriter and the unused out field/tests to avoid dead API surface.
pkg/cmd/push.go
Outdated
| // WriteTable is a no-op for push: the registry client already prints | ||
| // "Pushed:" and "Digest:" to stderr, so we avoid duplicating that output. | ||
| func (w *pushWriter) WriteTable(_ io.Writer) error { | ||
| return nil |
There was a problem hiding this comment.
pushWriter.WriteTable being a no-op means --output table produces no structured output on the command's out stream; the only visible result comes from registry client side-effects to stderr. Since --output advertises selectable formats, consider emitting a minimal human-readable table/lines (ref + digest) in WriteTable and, if needed, suppressing the registry client's own printing when --output is not table to prevent mixed/duplicated output.
| // WriteTable is a no-op for push: the registry client already prints | |
| // "Pushed:" and "Digest:" to stderr, so we avoid duplicating that output. | |
| func (w *pushWriter) WriteTable(_ io.Writer) error { | |
| return nil | |
| // WriteTable writes a minimal human-readable push result to the provided output. | |
| func (w *pushWriter) WriteTable(out io.Writer) error { | |
| _, err := fmt.Fprintf(out, "REF\t%s\nDIGEST\t%s\n", w.result.Ref, w.result.Digest) | |
| return err |
cmd/push: implement WriteTable to emit ref and digest The WithPushOptWriter option and the out field on Push were never wired to any meaningful output path — ChartUploader.Out is not read by UploadTo() and the registry client manages its own writer. Remove them to avoid dead API surface. WriteTable now writes a tab-aligned REF/DIGEST result to the command's stdout stream. The registry client continues to write its own progress output to stderr, so there is no duplication. Signed-off-by: Mentigen <mentigen@mentigen.ru> Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
3602cda to
5a093e9
Compare
Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pkg/cmd/push.go
Outdated
| // WriteTable writes a minimal human-readable push result to the provided output. | ||
| // The registry client prints progress to stderr; this writes the structured | ||
| // result (ref + digest) to the command's stdout stream. | ||
| func (w *pushWriter) WriteTable(out io.Writer) error { | ||
| tw := tabwriter.NewWriter(out, 0, 0, 2, ' ', 0) | ||
| fmt.Fprintf(tw, "REF\t%s\n", w.result.Ref) | ||
| fmt.Fprintf(tw, "DIGEST\t%s\n", w.result.Digest) | ||
| return tw.Flush() |
There was a problem hiding this comment.
The default table output produced here ("REF"/"DIGEST") does not match the existing push summary strings emitted by the registry client ("Pushed: …" / "Digest: …" in pkg/registry/client.go:746-747). If the goal is backward-compatible human output, consider aligning WriteTable to the established wording/format, or explicitly documenting that the default output format changes.
| result, err := client.Run(chartRef, remote) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| fmt.Fprint(out, output) | ||
| return nil | ||
| writer := &pushWriter{ | ||
| result: pushResult{ | ||
| Ref: result.Ref, | ||
| Digest: result.Manifest.Digest, | ||
| }, | ||
| } | ||
| return o.outfmt.Write(out, writer) | ||
| }, |
There was a problem hiding this comment.
helm.sh/helm/v4/pkg/registry.Client.Push already writes a push summary ("Pushed:" and "Digest:") to the registry client's configured writer (currently os.Stderr in pkg/cmd/root.go). With the additional structured output written here, a successful push will produce duplicated summary information across stderr/stdout. Consider suppressing the registry client's summary output for helm push (e.g., configure the registry client writer to io.Discard for this command, or add an option to registry.Push/client to disable the final summary lines) so that stdout is the single source of truth for --output consumers.
| writer := &pushWriter{ | ||
| result: pushResult{ | ||
| Ref: result.Ref, | ||
| Digest: result.Manifest.Digest, | ||
| }, | ||
| } | ||
| return o.outfmt.Write(out, writer) |
There was a problem hiding this comment.
The new --output behavior is only unit-tested at the pushWriter level; there’s no command-level test asserting that helm push ... --output json|yaml|table wires through correctly and produces the expected stdout (similar to other commands’ golden tests like pkg/cmd/get_metadata_test.go). Adding an integration-style cmd test would help prevent regressions in flag binding and output shape.
pkg/pusher/pusher.go
Outdated
| // Push file content by url string, returning the push result and any error | ||
| Push(chartRef, url string, options ...Option) (*registry.PushResult, error) | ||
| } | ||
|
|
There was a problem hiding this comment.
Changing the exported pusher.Pusher interface method signature is a breaking API change for any external implementations of Pusher. If this package is intended as an extension point, consider preserving the old Push(...) error via a new interface (or an adapter layer) and introducing a new method/interface for returning *registry.PushResult, to avoid forcing downstream updates in a minor release.
| // Push file content by url string, returning the push result and any error | |
| Push(chartRef, url string, options ...Option) (*registry.PushResult, error) | |
| } | |
| // Push file content by url string, returning any error encountered. | |
| Push(chartRef, url string, options ...Option) error | |
| } | |
| // ResultPusher is an optional extension interface for pushers that can also | |
| // return a registry push result without breaking existing Pusher implementations. | |
| type ResultPusher interface { | |
| // PushWithResult pushes file content by url string, returning the push result | |
| // and any error encountered. | |
| PushWithResult(chartRef, url string, options ...Option) (*registry.PushResult, error) | |
| } | |
| // Push performs a push using the provided pusher. If the pusher also supports | |
| // ResultPusher, the push result will be returned. Otherwise, this falls back to | |
| // the legacy Push behavior and returns a nil result on success. | |
| func Push(pusher Pusher, chartRef, url string, options ...Option) (*registry.PushResult, error) { | |
| if resultPusher, ok := pusher.(ResultPusher); ok { | |
| return resultPusher.PushWithResult(chartRef, url, options...) | |
| } | |
| if err := pusher.Push(chartRef, url, options...); err != nil { | |
| return nil, err | |
| } | |
| return nil, nil | |
| } |
| func (p *Push) Run(chartRef string, remote string) (string, error) { | ||
| var out strings.Builder | ||
|
|
||
| func (p *Push) Run(chartRef string, remote string) (*registry.PushResult, error) { |
There was a problem hiding this comment.
action.Push.Run changed from returning (string, error) to (*registry.PushResult, error), which is a breaking change for any callers using the action package as a library. If compatibility for existing consumers matters, consider adding a new method (e.g., RunWithResult) or keeping the old signature and returning the previous string form via the new result.
| func (p *Push) Run(chartRef string, remote string) (*registry.PushResult, error) { | |
| func (p *Push) Run(chartRef string, remote string) (string, error) { | |
| result, err := p.RunWithResult(chartRef, remote) | |
| if err != nil { | |
| return "", err | |
| } | |
| return result.Ref, nil | |
| } | |
| // RunWithResult executes 'helm push' against the given chart archive and returns the full push result. | |
| func (p *Push) RunWithResult(chartRef string, remote string) (*registry.PushResult, error) { |
Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
Address Copilot review feedback: - WriteTable now uses "Pushed:"/"Digest:" labels consistent with the registry client's built-in output (pkg/registry/client.go:746-747), so the default --output table experience is familiar to existing users - Add TestPushOutputFlagCompletion to verify the --output flag is properly registered and offers json/yaml/table completions - Document that Pusher.Push and action.Push.Run signature changes are intentional breaking changes in the Helm v4 major release Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Ref: result.Ref, | ||
| Digest: result.Manifest.Digest, | ||
| }, | ||
| } |
There was a problem hiding this comment.
The OCI registry client used here (created by newRegistryClient) writes its own push summary ("Pushed:"/"Digest:" and the underscore warning) to its configured writer (currently os.Stderr via registry.ClientOptWriter). Since this command also formats and writes the result, users will see duplicated table output and --output json|yaml will still produce extra human-readable lines on stderr. Consider constructing the registry client with an io.Discard writer for helm push (e.g., make newRegistryClient accept a writer override) and, if needed, emit only the underscore warning separately so structured output stays clean.
| } | |
| } | |
| outputFlag := "" | |
| if flag := cmd.Flag("output"); flag != nil { | |
| outputFlag = flag.Value.String() | |
| } | |
| if outputFlag == "" || outputFlag == "table" { | |
| return nil | |
| } |
Add an optional io.Writer parameter to newRegistryClient (and the internal newDefaultRegistryClient / newRegistryClientWithTLS helpers) so callers can control where registry client output goes. All existing callers are unaffected (default remains os.Stderr). For helm push, pass io.Discard so that the registry client's built-in "Pushed:"/"Digest:" lines are suppressed. The --output writer (WriteTable / WriteJSON / WriteYAML) is the single source of truth for push result output, preventing duplication on the terminal. Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
pkg/cmd/push.go
Outdated
| // Suppress the registry client's built-in "Pushed:"/"Digest:" lines; | ||
| // the --output writer (WriteTable/WriteJSON/WriteYAML) is the single | ||
| // source of structured push output for this command. | ||
| registryClient, err := newRegistryClient( | ||
| o.certFile, o.keyFile, o.caFile, o.insecureSkipTLSVerify, o.plainHTTP, o.username, o.password, | ||
| io.Discard, | ||
| ) |
There was a problem hiding this comment.
Passing io.Discard into newRegistryClient suppresses all registry client output, not just the built-in "Pushed:"/"Digest:" summary lines. This also drops important warnings emitted by the registry client (e.g., the underscore warning in pkg/registry/client.go that can affect image references). Consider keeping warnings on stderr while suppressing only the summary lines (e.g., add an option to the registry client to disable summary printing, or wrap the writer to filter only the two summary prefixes and forward everything else to cmd.ErrOrStderr()).
pkg/cmd/root.go
Outdated
| w ...io.Writer, | ||
| ) (*registry.Client, error) { | ||
| out := io.Writer(os.Stderr) | ||
| if len(w) > 0 { |
There was a problem hiding this comment.
newRegistryClient accepts an optional writer override via a variadic parameter, but it doesn't guard against callers passing a nil writer (e.g., newRegistryClient(..., nil)), which would later cause panics when the registry client writes output. Consider treating a nil override the same as "not provided" (default to os.Stderr) or explicitly returning an error for nil writers.
| if len(w) > 0 { | |
| if len(w) > 0 && w[0] != nil { |
| if registry.IsOCI(remote) { | ||
| // Don't use the default registry client if tls options are set. | ||
| c.Options = append(c.Options, pusher.WithRegistryClient(p.cfg.RegistryClient)) | ||
| } |
There was a problem hiding this comment.
Push.Run can panic if it is constructed without WithPushConfig: when remote is OCI, it unconditionally dereferences p.cfg.RegistryClient even if p.cfg is nil. Since pkg/action is a public API and NewPushWithOpts doesn't enforce cfg, consider returning a clear error when p.cfg is nil (or changing the constructor to require a *Configuration) to avoid a nil-pointer crash.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| // newRegistryClient creates a registry client. The optional w parameter | ||
| // overrides where the registry client writes its output (default: os.Stderr). | ||
| // Pass io.Discard to suppress the client's built-in push/pull summary lines. |
There was a problem hiding this comment.
The doc comment suggests passing io.Discard only suppresses the registry client's built-in push/pull summary lines, but in practice it will discard all registry client output (including underscore warnings, etc.). Consider rewording to reflect that io.Discard suppresses all output, or documenting the expectation that callers should provide a filtering writer when they want to suppress only specific lines.
| // Pass io.Discard to suppress the client's built-in push/pull summary lines. | |
| // Pass io.Discard to suppress all output written by the registry client to | |
| // this writer. To suppress only specific lines, provide a filtering writer. |
| if p.cfg == nil { | ||
| return nil, errors.New("missing action configuration: use WithPushConfig when constructing Push") | ||
| } | ||
| // Don't use the default registry client if tls options are set. |
There was a problem hiding this comment.
This comment appears inaccurate: the code always injects p.cfg.RegistryClient for OCI remotes, not only when TLS options are set. Please update or remove the comment to match the current behavior so future readers don't assume the registry client selection is conditional on TLS flags.
| // Don't use the default registry client if tls options are set. | |
| // Use the configured registry client for OCI remotes. |
- root.go: guard against nil writer in newRegistryClient variadic parameter to prevent panic on explicit nil pass - push.go (cmd): replace io.Discard with suppressSummaryWriter that forwards warnings/errors to stderr while silently dropping the registry client's built-in "Pushed:"/"Digest:" summary lines - push.go (action): return a clear error when Run() is called on an OCI remote without WithPushConfig, preventing nil-pointer panic on p.cfg.RegistryClient dereference Signed-off-by: Ilya Kislitsyn <kis-ilya-a@yandex.ru> Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
Fixes revive lint: unnecessary-format. Signed-off-by: Ilya Kislitsyn <kis-ilya-a@yandex.ru> Signed-off-by: Ilya Kiselev <kis-ilya-a@yandex.ru>
051a978 to
2ce8dbd
Compare
|
Promptless prepared a documentation update related to this change. Triggered by this PR Updated the OCI registries topic documentation with a new "Machine-readable output" subsection explaining how to use the Review at helm/helm-www#2060 |
Summary
Add support for --output flag to 'helm push' command for machine-readable output formats (JSON and YAML). This enables programmatic consumption of push results and integration with tools like cosign for artifact signing.
Changes
The default table format maintains backward compatibility with existing plain-text output style.
Test plan
go test ./pkg/cmd/... ./pkg/action/... ./pkg/pusher/...to ensure all tests passhelm push mychart oci://registry/repo --output json{"ref":"...","digest":"..."}