From abbeddd7b136717a6b115d95db31065d2cc7b8ce Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 3 Mar 2026 16:07:06 +0100 Subject: [PATCH 1/3] commands/history: rename var that shadowed type Signed-off-by: Sebastiaan van Stijn --- commands/history/ls.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/commands/history/ls.go b/commands/history/ls.go index 9153d8fca094..1ce8d78a864e 100644 --- a/commands/history/ls.go +++ b/commands/history/ls.go @@ -51,7 +51,7 @@ func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error { return err } - queryOptions := &queryOptions{} + queryOpts := &queryOptions{} if opts.local { wd, err := os.Getwd() @@ -69,11 +69,11 @@ func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error { if err != nil { return errors.Wrapf(err, "could not get remote URL for local filter") } - queryOptions.Filters = append(queryOptions.Filters, fmt.Sprintf("repository=%s", remote)) + queryOpts.Filters = append(queryOpts.Filters, fmt.Sprintf("repository=%s", remote)) } - queryOptions.Filters = append(queryOptions.Filters, opts.filters...) + queryOpts.Filters = append(queryOpts.Filters, opts.filters...) - out, err := queryRecords(ctx, "", nodes, queryOptions) + out, err := queryRecords(ctx, "", nodes, queryOpts) if err != nil { return err } From 008142ef39d24cdaf52aa6723d9bafe6d20946cf Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 3 Mar 2026 16:02:10 +0100 Subject: [PATCH 2/3] history ls: fix '--format' flag to accept go templates Before this PR, attempting to use a format template would always fail; ```bash docker buildx history ls --no-trunc --format 'table {{.Status}}\t{{.NumTotalSteps}}\t{{.Duration}}\t{{.Name}}' ERROR: template parsing error: template: :1:14: executing "" at <.NumTotalSteps>: can't evaluate field NumTotalSteps in type *history.lsContext ``` With this PR, formatting works (mostly); the formatting templates don't match the JSON response (which can be confusing), and not all fields have a table defined (which can result in `` used). ```bash docker buildx history ls --no-trunc --format 'table {{.Status}}\t{{.NumTotalSteps}}\t{{.Duration}}\t{{.Name}}' STATUS DURATION NAME Completed 16 17.1s buildx (binaries) Completed 12 19.9s buildkit/hack/dockerfiles/vendor.Dockerfile (update) Completed 123 28.7s docker (dev-base) Completed 26 7.9s Completed 12 2m 41s cli/dockerfiles/Dockerfile.vendor (update) ``` Or a slightly more adventurous; ```bash docker buildx history ls --no-trunc --format '{{printf "{\"status\":%s,\"steps\":%s,\"duration\":%s,\"name\":%s}" (json .Status) (json .NumTotalSteps) (json .Duration) (json .Name)}}' {"status":"Completed","steps":16,"duration":"17.1s","name":"buildx (binaries)"} {"status":"Completed","steps":12,"duration":"19.9s","name":"buildkit/hack/dockerfiles/vendor.Dockerfile (update)"} {"status":"Completed","steps":123,"duration":"28.7s","name":"docker (dev-base)"} {"status":"Completed","steps":26,"duration":"7.9s","name":""} {"status":"Completed","steps":12,"duration":"2m 41s","name":"cli/dockerfiles/Dockerfile.vendor (update)"} {"status":"Error","steps":11,"duration":"3m 14s","name":"cli/dockerfiles/Dockerfile.vendor (update)"} ``` The flag description has been updated to align with other `--format` flags; ```bash docker buildx history ls --help Usage: docker buildx history ls [OPTIONS] List build records Options: --builder string Override the configured builder instance -D, --debug Enable debug logging --filter stringArray Provide filter values (e.g., "status=error") --format string Format output using a custom template: 'table': Print output in table format with column headers (default) 'table TEMPLATE': Print output in table format using the given Go template 'json': Print in JSON format 'TEMPLATE': Print output using the given Go template. Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates (default "table") --local List records for current repository only --no-trunc Don't truncate output ``` Signed-off-by: Sebastiaan van Stijn --- commands/history/ls.go | 49 +++++++++++++++-------------- commands/history/utils.go | 2 +- docs/reference/buildx_history_ls.md | 16 +++++----- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/commands/history/ls.go b/commands/history/ls.go index 1ce8d78a864e..11b8902117f6 100644 --- a/commands/history/ls.go +++ b/commands/history/ls.go @@ -18,6 +18,7 @@ import ( "github.com/docker/cli/cli" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/formatter" + cliflags "github.com/docker/cli/cli/flags" "github.com/docker/go-units" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -85,7 +86,7 @@ func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error { for i, rec := range out { st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref) - rec.name = BuildName(rec.FrontendAttrs, st) + rec.Name = BuildName(rec.FrontendAttrs, st) out[i] = rec } @@ -108,7 +109,7 @@ func lsCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command { } flags := cmd.Flags() - flags.StringVar(&options.format, "format", formatter.TableFormatKey, "Format the output") + flags.StringVar(&options.format, "format", formatter.TableFormatKey, cliflags.FormatHelp) flags.BoolVar(&options.noTrunc, "no-trunc", false, "Don't truncate output") flags.StringArrayVar(&options.filters, "filter", nil, `Provide filter values (e.g., "status=error")`) flags.BoolVar(&options.local, "local", false, "List records for current repository only") @@ -144,10 +145,10 @@ func lsPrint(dockerCli command.Cli, records []historyRecord, in lsOptions) error render := func(format func(subContext formatter.SubContext) error) error { for _, r := range records { if err := format(&lsContext{ - format: formatter.Format(in.format), - isTerm: term, - trunc: !in.noTrunc, - record: &r, + format: formatter.Format(in.format), + isTerm: term, + trunc: !in.noTrunc, + historyRecord: &r, }); err != nil { return err } @@ -177,7 +178,7 @@ type lsContext struct { isTerm bool trunc bool format formatter.Format - record *historyRecord + *historyRecord } func (c *lsContext) MarshalJSON() ([]byte, error) { @@ -185,27 +186,27 @@ func (c *lsContext) MarshalJSON() ([]byte, error) { "ref": c.FullRef(), "name": c.Name(), "status": c.Status(), - "created_at": c.record.CreatedAt.AsTime().Format(time.RFC3339Nano), - "total_steps": c.record.NumTotalSteps, - "completed_steps": c.record.NumCompletedSteps, - "cached_steps": c.record.NumCachedSteps, + "created_at": c.historyRecord.CreatedAt.AsTime().Format(time.RFC3339Nano), + "total_steps": c.historyRecord.NumTotalSteps, + "completed_steps": c.historyRecord.NumCompletedSteps, + "cached_steps": c.historyRecord.NumCachedSteps, } - if c.record.CompletedAt != nil { - m["completed_at"] = c.record.CompletedAt.AsTime().Format(time.RFC3339Nano) + if c.historyRecord.CompletedAt != nil { + m["completed_at"] = c.historyRecord.CompletedAt.AsTime().Format(time.RFC3339Nano) } return json.Marshal(m) } func (c *lsContext) Ref() string { - return c.record.Ref + return c.historyRecord.Ref } func (c *lsContext) FullRef() string { - return fmt.Sprintf("%s/%s/%s", c.record.node.Builder, c.record.node.Name, c.record.Ref) + return fmt.Sprintf("%s/%s/%s", c.historyRecord.node.Builder, c.historyRecord.node.Name, c.historyRecord.Ref) } func (c *lsContext) Name() string { - name := c.record.name + name := c.historyRecord.Name if c.trunc && c.format.IsTable() { return trimBeginning(name, 36) } @@ -213,8 +214,8 @@ func (c *lsContext) Name() string { } func (c *lsContext) Status() string { - if c.record.CompletedAt != nil { - if c.record.Error != nil { + if c.historyRecord.CompletedAt != nil { + if c.historyRecord.Error != nil { return "Error" } return "Completed" @@ -223,20 +224,20 @@ func (c *lsContext) Status() string { } func (c *lsContext) CreatedAt() string { - return units.HumanDuration(time.Since(c.record.CreatedAt.AsTime())) + " ago" + return units.HumanDuration(time.Since(c.historyRecord.CreatedAt.AsTime())) + " ago" } func (c *lsContext) Duration() string { - lastTime := c.record.currentTimestamp - if c.record.CompletedAt != nil { - tm := c.record.CompletedAt.AsTime() + lastTime := c.historyRecord.currentTimestamp + if c.historyRecord.CompletedAt != nil { + tm := c.historyRecord.CompletedAt.AsTime() lastTime = &tm } if lastTime == nil { return "" } - v := formatDuration(lastTime.Sub(c.record.CreatedAt.AsTime())) - if c.record.CompletedAt == nil { + v := formatDuration(lastTime.Sub(c.historyRecord.CreatedAt.AsTime())) + if c.historyRecord.CompletedAt == nil { v += "+" } return v diff --git a/commands/history/utils.go b/commands/history/utils.go index beab15a22a77..6c7aa2f35909 100644 --- a/commands/history/utils.go +++ b/commands/history/utils.go @@ -120,7 +120,7 @@ type historyRecord struct { *controlapi.BuildHistoryRecord currentTimestamp *time.Time node *builder.Node - name string + Name string } type queryOptions struct { diff --git a/docs/reference/buildx_history_ls.md b/docs/reference/buildx_history_ls.md index 9f27d238df12..75bae00f4e07 100644 --- a/docs/reference/buildx_history_ls.md +++ b/docs/reference/buildx_history_ls.md @@ -9,14 +9,14 @@ List build records ### Options -| Name | Type | Default | Description | -|:--------------------------|:--------------|:--------|:---------------------------------------------| -| `--builder` | `string` | | Override the configured builder instance | -| `-D`, `--debug` | `bool` | | Enable debug logging | -| [`--filter`](#filter) | `stringArray` | | Provide filter values (e.g., `status=error`) | -| [`--format`](#format) | `string` | `table` | Format the output | -| [`--local`](#local) | `bool` | | List records for current repository only | -| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | +| Name | Type | Default | Description | +|:--------------------------|:--------------|:--------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--builder` | `string` | | Override the configured builder instance | +| `-D`, `--debug` | `bool` | | Enable debug logging | +| [`--filter`](#filter) | `stringArray` | | Provide filter values (e.g., `status=error`) | +| [`--format`](#format) | `string` | `table` | Format output using a custom template:
'table': Print output in table format with column headers (default)
'table TEMPLATE': Print output in table format using the given Go template
'json': Print in JSON format
'TEMPLATE': Print output using the given Go template.
Refer to https://docs.docker.com/go/formatting/ for more information about formatting output with templates | +| [`--local`](#local) | `bool` | | List records for current repository only | +| [`--no-trunc`](#no-trunc) | `bool` | | Don't truncate output | From db5f3fc38ff20add582e3a842ce5936311eed5d4 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Tue, 3 Mar 2026 18:06:25 +0100 Subject: [PATCH 3/3] fix: F1008: could remove embedded field (staticcheck) Signed-off-by: Sebastiaan van Stijn --- commands/history/ls.go | 54 +++++++++++++++++++-------------------- commands/history/utils.go | 2 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/commands/history/ls.go b/commands/history/ls.go index 11b8902117f6..c79d73740058 100644 --- a/commands/history/ls.go +++ b/commands/history/ls.go @@ -32,7 +32,7 @@ const ( lsHeaderDuration = "DURATION" lsHeaderLink = "" - lsDefaultTableFormat = "table {{.Ref}}\t{{.Name}}\t{{.Status}}\t{{.CreatedAt}}\t{{.Duration}}\t{{.Link}}" + lsDefaultTableFormat = "table {{.BuildID}}\t{{.Name}}\t{{.Status}}\t{{.Created}}\t{{.Duration}}\t{{.Link}}" headerKeyTimestamp = "buildkit-current-timestamp" ) @@ -86,7 +86,7 @@ func runLs(ctx context.Context, dockerCli command.Cli, opts lsOptions) error { for i, rec := range out { st, _ := ls.ReadRef(rec.node.Builder, rec.node.Name, rec.Ref) - rec.Name = BuildName(rec.FrontendAttrs, st) + rec.name = BuildName(rec.FrontendAttrs, st) out[i] = rec } @@ -161,12 +161,12 @@ func lsPrint(dockerCli command.Cli, records []historyRecord, in lsOptions) error trunc: !in.noTrunc, } lsCtx.Header = formatter.SubHeaderContext{ - "Ref": lsHeaderBuildID, - "Name": lsHeaderName, - "Status": lsHeaderStatus, - "CreatedAt": lsHeaderCreated, - "Duration": lsHeaderDuration, - "Link": lsHeaderLink, + "BuildID": lsHeaderBuildID, + "Name": lsHeaderName, + "Status": lsHeaderStatus, + "Created": lsHeaderCreated, + "Duration": lsHeaderDuration, + "Link": lsHeaderLink, } return ctx.Write(&lsCtx, render) @@ -186,27 +186,27 @@ func (c *lsContext) MarshalJSON() ([]byte, error) { "ref": c.FullRef(), "name": c.Name(), "status": c.Status(), - "created_at": c.historyRecord.CreatedAt.AsTime().Format(time.RFC3339Nano), - "total_steps": c.historyRecord.NumTotalSteps, - "completed_steps": c.historyRecord.NumCompletedSteps, - "cached_steps": c.historyRecord.NumCachedSteps, + "created_at": c.CreatedAt.AsTime().Format(time.RFC3339Nano), + "total_steps": c.NumTotalSteps, + "completed_steps": c.NumCompletedSteps, + "cached_steps": c.NumCachedSteps, } - if c.historyRecord.CompletedAt != nil { - m["completed_at"] = c.historyRecord.CompletedAt.AsTime().Format(time.RFC3339Nano) + if c.CompletedAt != nil { + m["completed_at"] = c.CompletedAt.AsTime().Format(time.RFC3339Nano) } return json.Marshal(m) } -func (c *lsContext) Ref() string { - return c.historyRecord.Ref +func (c *lsContext) BuildID() string { + return c.Ref } func (c *lsContext) FullRef() string { - return fmt.Sprintf("%s/%s/%s", c.historyRecord.node.Builder, c.historyRecord.node.Name, c.historyRecord.Ref) + return fmt.Sprintf("%s/%s/%s", c.node.Builder, c.node.Name, c.Ref) } func (c *lsContext) Name() string { - name := c.historyRecord.Name + name := c.name if c.trunc && c.format.IsTable() { return trimBeginning(name, 36) } @@ -214,8 +214,8 @@ func (c *lsContext) Name() string { } func (c *lsContext) Status() string { - if c.historyRecord.CompletedAt != nil { - if c.historyRecord.Error != nil { + if c.CompletedAt != nil { + if c.Error != nil { return "Error" } return "Completed" @@ -223,21 +223,21 @@ func (c *lsContext) Status() string { return "Running" } -func (c *lsContext) CreatedAt() string { - return units.HumanDuration(time.Since(c.historyRecord.CreatedAt.AsTime())) + " ago" +func (c *lsContext) Created() string { + return units.HumanDuration(time.Since(c.CreatedAt.AsTime())) + " ago" } func (c *lsContext) Duration() string { - lastTime := c.historyRecord.currentTimestamp - if c.historyRecord.CompletedAt != nil { - tm := c.historyRecord.CompletedAt.AsTime() + lastTime := c.currentTimestamp + if c.CompletedAt != nil { + tm := c.CompletedAt.AsTime() lastTime = &tm } if lastTime == nil { return "" } - v := formatDuration(lastTime.Sub(c.historyRecord.CreatedAt.AsTime())) - if c.historyRecord.CompletedAt == nil { + v := formatDuration(lastTime.Sub(c.CreatedAt.AsTime())) + if c.CompletedAt == nil { v += "+" } return v diff --git a/commands/history/utils.go b/commands/history/utils.go index 6c7aa2f35909..beab15a22a77 100644 --- a/commands/history/utils.go +++ b/commands/history/utils.go @@ -120,7 +120,7 @@ type historyRecord struct { *controlapi.BuildHistoryRecord currentTimestamp *time.Time node *builder.Node - Name string + name string } type queryOptions struct {