Repository: docker/buildx

Issue:
bake: Show variable types during variable list operation
### Description

The new typing feature introduced in #3167 should be part of the output of `--list variables`.

I assume this is desirable and already have an initial implementation, but there are enough questions that I thought I should open an issue first to hash it out.
1. For the (default) table view, it's generally the case that the structure/layout isn't guaranteed, but I didn't want to assume.  Are you okay with adding it to the table?  If so, can it go in the column that 'makes sense', or should it be last?  (I assumed that modifying the table layout was fine and that the JSON format is a non-issue)
2. I noticed that the 'value' field will *only* populate for values that are `string` or `bool` (with `number` being omitted).  Was this oversight or intentional?  (I assumed the former and now show numbers)
3. Is there any reason complex values should not be displayed?  (Since I assumed lack of `number` was not intentional, I assumed showing both numbers and complex types would be desirable).
4. Currently string values are not shown with quotes.  If we start showing numbers, it will be ambiguous whether a value that looks like a number was intended to be a string or an actual number..  Is that a concern?  (I've left strings unquoted, assuming strings are far more prevalent thus quotes would be unwelcome noise)
5. Same question, but for complex types (e.g., values for `list(string)` vs. `list(number)`).  (I've kept quotes in this case.)
6. I assumed a 'type' field should only be populated when an explicit type was provided, leaving it empty otherwise (i.e., not populating an inferred type).

For a visual example of all these put together, this is the output from a slightly doctored project using `v0.24.0`:
```
VARIABLE	VALUE	DESCRIPTION
CI		<null>	do not set; will always be 'true' in Jenkins
EXPORT_DIR	./out	for local file/tar exports
EXPORT_TARS		whether to export images as tarballs (for tests)
JAVA_VERSIONS		java LTS versions to build for
MAVEN_VERSIONS		maven versions to build for
PREFIX			
REGISTRY		registry to push to (for CI)
REPOSITORY		repository name (for CI)
TAG		latest	default tag to apply to all generated images
docker_gid		
flavor		debian	OS flavor
jenkins_gid		
jenkins_uid		
untyped_number		
untyped_string	99
```
and the output from my proposed/current changes:
```
VARIABLE	TYPE		VALUE		DESCRIPTION
CI		string		<null>		do not set; will always be 'true' in Jenkins
EXPORT_DIR	string		./out		for local file/tar exports
EXPORT_TARS	number		0		whether to export images as tarballs (for tests)
JAVA_VERSIONS	set of string	["17","21"]	java LTS versions to build for
MAVEN_VERSIONS	set of string	["3.9.8"]	maven versions to build for
PREFIX		string				
REGISTRY	string				registry to push to (for CI)
REPOSITORY	string				repository name (for CI)
TAG		string		latest		default tag to apply to all generated images
docker_gid	number		5004		
flavor		string		debian		OS flavor
jenkins_gid	number		597		
jenkins_uid	number		596		
untyped_number			99		
untyped_string			99
```

Let me know what you think, or if you'd rather just work off of an actual PR, I can do that as well.

Hints from issue comments:
@tonistiigi: Type column looks fine. I don't think we need quotes for strings if we have separate type field anyway. All types should attempt to show values.

> I assumed a 'type' field should only be populated when an explicit type was provided, leaving it empty otherwise (i.e., not populating an inferred type).

Yeah, makes sense.

Solution patch:
diff --git a/Dockerfile b/Dockerfile
index 03215fee..1ab4c8f3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -5,7 +5,7 @@ ARG ALPINE_VERSION=3.21
 ARG XX_VERSION=1.6.1
 
 # for testing
-ARG DOCKER_VERSION=28.1.1
+ARG DOCKER_VERSION=28.2.2
 ARG DOCKER_VERSION_ALT_27=27.5.1
 ARG DOCKER_VERSION_ALT_26=26.1.3
 ARG DOCKER_CLI_VERSION=${DOCKER_VERSION}
diff --git a/bake/hclparser/hclparser.go b/bake/hclparser/hclparser.go
index d8f13459..0cf38e14 100644
--- a/bake/hclparser/hclparser.go
+++ b/bake/hclparser/hclparser.go
@@ -38,6 +38,9 @@ type variable struct {
 	Validations []*variableValidation `json:"validation,omitempty" hcl:"validation,block"`
 	Body        hcl.Body              `json:"-" hcl:",body"`
 	Remain      hcl.Body              `json:"-" hcl:",remain"`
+
+	// the type described by Type if it was specified
+	constraint *cty.Type
 }
 
 type variableValidation struct {
@@ -296,6 +299,9 @@ func (p *parser) resolveValue(ectx *hcl.EvalContext, name string) (err error) {
 			return diags
 		}
 		typeSpecified = !varType.Equals(cty.DynamicPseudoType) || hcl.ExprAsKeyword(vr.Type) == "any"
+		if typeSpecified {
+			vr.constraint = &varType
+		}
 	}
 
 	if def == nil {
@@ -674,6 +680,7 @@ func (p *parser) validateVariables(vars map[string]*variable, ectx *hcl.EvalCont
 type Variable struct {
 	Name        string  `json:"name"`
 	Description string  `json:"description,omitempty"`
+	Type        string  `json:"type,omitempty"`
 	Value       *string `json:"value,omitempty"`
 }
 
@@ -803,13 +810,31 @@ func Parse(b hcl.Body, opt Opt, val any) (*ParseMeta, hcl.Diagnostics) {
 			Name:        p.vars[k].Name,
 			Description: p.vars[k].Description,
 		}
+		tc := p.vars[k].constraint
+		if tc != nil {
+			v.Type = tc.FriendlyNameForConstraint()
+		}
 		if vv := p.ectx.Variables[k]; !vv.IsNull() {
 			var s string
-			switch vv.Type() {
-			case cty.String:
-				s = vv.AsString()
-			case cty.Bool:
-				s = strconv.FormatBool(vv.True())
+			switch {
+			case tc != nil:
+				if bs, err := ctyjson.Marshal(vv, *tc); err == nil {
+					s = string(bs)
+					// untyped strings were always unquoted, so be consistent with typed strings as well
+					if tc.Equals(cty.String) {
+						s = strings.Trim(s, "\"")
+					}
+				}
+			case vv.Type().IsPrimitiveType():
+				// all primitives can convert to string, so error should never occur
+				if val, err := convert.Convert(vv, cty.String); err == nil {
+					s = val.AsString()
+				}
+			default:
+				// must be an (inferred) tuple or object
+				if bs, err := ctyjson.Marshal(vv, vv.Type()); err == nil {
+					s = string(bs)
+				}
 			}
 			v.Value = &s
 		}
diff --git a/commands/bake.go b/commands/bake.go
index e9636e2f..24e77761 100644
--- a/commands/bake.go
+++ b/commands/bake.go
@@ -663,7 +663,7 @@ func printVars(w io.Writer, format string, vars []*hclparser.Variable) error {
 	tw := tabwriter.NewWriter(w, 1, 8, 1, '\t', 0)
 	defer tw.Flush()
 
-	tw.Write([]byte("VARIABLE\tVALUE\tDESCRIPTION\n"))
+	tw.Write([]byte("VARIABLE\tTYPE\tVALUE\tDESCRIPTION\n"))
 
 	for _, v := range vars {
 		var value string
@@ -672,7 +672,7 @@ func printVars(w io.Writer, format string, vars []*hclparser.Variable) error {
 		} else {
 			value = "<null>"
 		}
-		fmt.Fprintf(tw, "%s\t%s\t%s\n", v.Name, value, v.Description)
+		fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", v.Name, v.Type, value, v.Description)
 	}
 	return nil
 }
diff --git a/docs/reference/buildx_bake.md b/docs/reference/buildx_bake.md
index 733a3eae..189866b2 100644
--- a/docs/reference/buildx_bake.md
+++ b/docs/reference/buildx_bake.md
@@ -198,12 +198,15 @@ To list variables:
 
 ```console
 $ docker buildx bake --list=variables
-VARIABLE      VALUE                DESCRIPTION
-REGISTRY      docker.io/username   Registry and namespace
-IMAGE_NAME    my-app               Image name
-GO_VERSION    <null>
+VARIABLE      TYPE      VALUE                DESCRIPTION
+REGISTRY      string    docker.io/username   Registry and namespace
+IMAGE_NAME    string    my-app               Image name
+GO_VERSION              <null>
+DEBUG         bool      false                Add debug symbols
 ```
 
+Variable types will be shown when set using the `type` property in the Bake file.
+
 By default, the output of `docker buildx bake --list` is presented in a table
 format. Alternatively, you can use a long-form CSV syntax and specify a
 `format` attribute to output the list in JSON.
diff --git a/go.mod b/go.mod
index de1b2f69..d9d16afb 100644
--- a/go.mod
+++ b/go.mod
@@ -16,9 +16,9 @@ require (
 	github.com/creack/pty v1.1.24
 	github.com/davecgh/go-spew v1.1.1
 	github.com/distribution/reference v0.6.0
-	github.com/docker/cli v28.2.1+incompatible
+	github.com/docker/cli v28.2.2+incompatible
 	github.com/docker/cli-docs-tool v0.10.0
-	github.com/docker/docker v28.2.1+incompatible
+	github.com/docker/docker v28.2.2+incompatible
 	github.com/docker/go-units v0.5.0
 	github.com/gofrs/flock v0.12.1
 	github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
diff --git a/go.sum b/go.sum
index ce752218..e4060098 100644
--- a/go.sum
+++ b/go.sum
@@ -108,15 +108,15 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU=
 github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v28.2.1+incompatible h1:AYyTcuwvhl9dXdyCiXlOGXiIqSNYzTmaDNpxIISPGsM=
-github.com/docker/cli v28.2.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A=
+github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/cli-docs-tool v0.10.0 h1:bOD6mKynPQgojQi3s2jgcUWGp/Ebqy1SeCr9VfKQLLU=
 github.com/docker/cli-docs-tool v0.10.0/go.mod h1:5EM5zPnT2E7yCLERZmrDA234Vwn09fzRHP4aX1qwp1U=
 github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
 github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
 github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
-github.com/docker/docker v28.2.1+incompatible h1:aTSWVTDStpHbnRu0xBcGoJEjRf5EQKt6nik6Vif8sWw=
-github.com/docker/docker v28.2.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
+github.com/docker/docker v28.2.2+incompatible h1:CjwRSksz8Yo4+RmQ339Dp/D2tGO5JxwYeqtMOEe0LDw=
+github.com/docker/docker v28.2.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
 github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
 github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c h1:lzqkGL9b3znc+ZUgi7FlLnqjQhcXxkNM/quxIjBVMD0=
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 8172161d..9a8e0339 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -221,7 +221,7 @@ github.com/davecgh/go-spew/spew
 # github.com/distribution/reference v0.6.0
 ## explicit; go 1.20
 github.com/distribution/reference
-# github.com/docker/cli v28.2.1+incompatible
+# github.com/docker/cli v28.2.2+incompatible
 ## explicit
 github.com/docker/cli/cli
 github.com/docker/cli/cli-plugins/metadata
@@ -257,7 +257,7 @@ github.com/docker/cli-docs-tool
 github.com/docker/cli-docs-tool/annotation
 # github.com/docker/distribution v2.8.3+incompatible
 ## explicit
-# github.com/docker/docker v28.2.1+incompatible
+# github.com/docker/docker v28.2.2+incompatible
 ## explicit
 github.com/docker/docker/api
 github.com/docker/docker/api/types



Test patch:
diff --git a/tests/bake.go b/tests/bake.go
index 7065726a..ca9902a4 100644
--- a/tests/bake.go
+++ b/tests/bake.go
@@ -74,6 +74,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
 	testBakeLoadPush,
 	testListTargets,
 	testListVariables,
+	testListTypedVariables,
 	testBakeCallCheck,
 	testBakeCallCheckFlag,
 	testBakeCallMetadata,
@@ -1737,7 +1738,93 @@ target "default" {
 	)
 	require.NoError(t, err, out)
 
-	require.Equal(t, "VARIABLE\tVALUE\tDESCRIPTION\nabc\t\t<null>\t\ndef\t\t\t\nfoo\t\tbar\tThis is foo", strings.TrimSpace(out))
+	require.Equal(t, "VARIABLE\tTYPE\tVALUE\tDESCRIPTION\nabc\t\t\t<null>\t\ndef\t\t\t\t\nfoo\t\t\tbar\tThis is foo", strings.TrimSpace(out))
+}
+
+func testListTypedVariables(t *testing.T, sb integration.Sandbox) {
+	bakefile := []byte(`
+variable "abc" {
+    type = string
+	default = "bar"
+	description = "This is abc"
+}
+variable "def" {
+    type = string
+    description = "simple type, no default"
+}
+variable "ghi" {
+    type = number
+    default = 99
+    description = "simple type w/ default"
+}
+variable "jkl" {
+    type = list(string)
+    default = ["hello"]
+    description = "collection with quoted strings"
+}
+variable "mno" {
+    type = list(number)
+    description = "collection, no default"
+}
+variable "pqr" {
+    type = tuple([number, string, bool])
+    default = [99, "99", true]
+}
+variable "stu" {
+    type = map(string)
+    default = {"foo": "bar"}
+}
+variable "vwx" {
+    type = set(bool)
+    default = null
+    description = "collection, null default"
+}
+
+// untyped, but previously didn't have its value output
+variable "wxy" {
+    default = ["foo"]
+    description = "inferred tuple"
+}
+// untyped, but previously didn't have its value output
+variable "xyz" {
+    default = {"foo": "bar"}
+    description = "inferred object"
+}
+// untyped, but previously didn't have its value output
+variable "yza" {
+    default = true
+    description = "inferred bool"
+}
+target "default" {
+}
+`)
+	dir := tmpdir(
+		t,
+		fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
+	)
+
+	out, err := bakeCmd(
+		sb,
+		withDir(dir),
+		withArgs("--list=variables"),
+	)
+	require.NoError(t, err, out)
+	require.Equal(t,
+		"VARIABLE\tTYPE\t\tVALUE\t\tDESCRIPTION\n"+
+			"abc\t\tstring\t\tbar\t\tThis is abc\n"+
+			"def\t\tstring\t\t<null>\t\tsimple type, no default\n"+
+			"ghi\t\tnumber\t\t99\t\tsimple type w/ default\n"+
+			"jkl\t\tlist of string\t[\"hello\"]\tcollection with quoted strings\n"+
+			"mno\t\tlist of number\t<null>\t\tcollection, no default\n"+
+			// the implementation for tuple's 'friendly name' is very basic
+			// and marked as TODO, so this may change/break at some point
+			"pqr\t\ttuple\t\t[99,\"99\",true]\t\n"+
+			"stu\t\tmap of string\t{\"foo\":\"bar\"}\t\n"+
+			"vwx\t\tset of bool\t<null>\t\tcollection, null default\n"+
+			"wxy\t\t\t\t[\"foo\"]\t\tinferred tuple\n"+
+			"xyz\t\t\t\t{\"foo\":\"bar\"}\tinferred object\n"+
+			"yza\t\t\t\ttrue\t\tinferred bool",
+		strings.TrimSpace(out))
 }
 
 func testBakeCallCheck(t *testing.T, sb integration.Sandbox) {