Skip to content

Conversation

@timsolovev
Copy link

@timsolovev timsolovev commented Oct 3, 2025

Which issue(s) this PR fixes:

Fixes #2069, Fixes #723
Supersedes #2073

What this PR does / why we need it:

This PR introduces several new non-breaking features to terramate list command:

  • Multiple output formats via --format flag:

    • text (default): Traditional line-by-line stack listing
    • json: JSON output with complete stack metadata and direct execution dependencies
    • dot: GraphViz DOT format for visual dependency graph generation
  • Customizable labels via -l/--label flag:

    • stack.dir (default): Display stack directory paths
    • stack.id: Display stack IDs
    • stack.name: Display stack names (not available with --format=json due to possible name duplication).

JSON Format

The recommendation in #2069 suggested very minimalist map-of-lists structure, but since #723 requested a more exhaustive JSON structure, I went with something like this:

{
"A/B1/C": {
    "dir": "A",
    "id": "a1111111-1111-1111-1111-111111111111",
    "name": "A",
    "description": "",
    "tags": [],
    "dependencies": ["A/B1", "D"],
    "reason": "",
    "is_changed": false
  }
}

In combination with --label stack.id it will use stack ids as references:

"c1111111-1111-1111-1111-111111111111": {
    "dir": "A/B1/C",
    "id": "c1111111-1111-1111-1111-111111111111",
    "name": "C",
    "description": "",
    "tags": [],
    "dependencies": [
      "b1111111-1111-1111-1111-111111111111",
      "d1111111-1111-1111-1111-111111111111"
    ],
    "reason": "",
    "is_changed": false
  }

Dot format

Then I realised that the stack aggregation for JSON format can be useful for Dot graph generation. experimental run-graph always graphed only explicit dependencies of the stacks (i.e. before/after) and suffered from stack.name collisions. This PR addresses both these issues, but since the recursive implementation of dot formatting of terramate experimental run-graph is too divergent from JSON stack aggregation, it was essentially re-implemented and (as expected) produces deeper execution graph that also cover the dir hierarchy.

Comparison on the example project described below. Before:

terramate experimental run-graph --label stack.dir graphviz (1)
digraph  {

        n1[label="/A"];
        n2[label="/A/B1"];
        n3[label="/A/B1/C"];
        n5[label="/A/B2"];
        n4[label="/D"];
        n4->n3;
        n4->n5;

}

After:

terramate list --format dot graphviz
digraph  {

	n1[label="A"];
	n2[label="A/B1"];
	n3[label="A/B1/C"];
	n5[label="A/B2"];
	n4[label="D"];
	n1->n2;
	n1->n5;
	n2->n3;
	n4->n3;
	n4->n5;

}

Example

Consider a simple Terramate project:

/tmp/terramate-test
├── A
│   ├── B1
│   │   ├── C
│   │   │   └── stack.tm.hcl
│   │   └── stack.tm.hcl
│   ├── B2
│   │   └── stack.tm.hcl
│   └── stack.tm.hcl
└── D
    └── stack.tm.hcl

6 directories, 5 files

The explicit dependencies are introduced with before in D/stack.tm.hcl:

stack {
  id          = "d1111111-1111-1111-1111-111111111111"
  before      = ["../A/B2"]
}

and after in C A/B1/C/stack.tm.hcl:

stack {
  id    = "c1111111-1111-1111-1111-111111111111"
  after = ["../../../D"]
}

The outputs for all possible combinations of --format and --label are provided below.

terramate list - equivalent to terramate list --format text --label stack.dir
A
A/B1
A/B1/C
A/B2
D
terramate list --format json
{
  "A": {
    "dir": "A",
    "id": "a1111111-1111-1111-1111-111111111111",
    "name": "A",
    "description": "",
    "tags": [],
    "dependencies": [],
    "reason": "",
    "is_changed": false
  },
  "A/B1": {
    "dir": "A/B1",
    "id": "b1111111-1111-1111-1111-111111111111",
    "name": "B1",
    "description": "",
    "tags": [],
    "dependencies": [
      "A"
    ],
    "reason": "",
    "is_changed": false
  },
  "A/B1/C": {
    "dir": "A/B1/C",
    "id": "c1111111-1111-1111-1111-111111111111",
    "name": "C",
    "description": "",
    "tags": [],
    "dependencies": [
      "A/B1",
      "D"
    ],
    "reason": "",
    "is_changed": false
  },
  "A/B2": {
    "dir": "A/B2",
    "id": "b2222222-2222-2222-2222-222222222222",
    "name": "B2",
    "description": "",
    "tags": [],
    "dependencies": [
      "A",
      "D"
    ],
    "reason": "",
    "is_changed": false
  },
  "D": {
    "dir": "D",
    "id": "d1111111-1111-1111-1111-111111111111",
    "name": "D",
    "description": "",
    "tags": [],
    "dependencies": [],
    "reason": "",
    "is_changed": false
  }
}
terramate list --format dot
digraph  {

	n1[label="A"];
	n2[label="A/B1"];
	n3[label="A/B1/C"];
	n5[label="A/B2"];
	n4[label="D"];
	n1->n2;
	n1->n5;
	n2->n3;
	n4->n3;
	n4->n5;

}
terramate list --label stack.id
a1111111-1111-1111-1111-111111111111
b1111111-1111-1111-1111-111111111111
c1111111-1111-1111-1111-111111111111
b2222222-2222-2222-2222-222222222222
d1111111-1111-1111-1111-111111111111
terramate list --label stack.name
A
B1
C
B2
D
terramate list --label stack.dir
A
A/B1
A/B1/C
A/B2
D
terramate list --format json --label stack.id
{
  "a1111111-1111-1111-1111-111111111111": {
    "dir": "A",
    "id": "a1111111-1111-1111-1111-111111111111",
    "name": "A",
    "description": "",
    "tags": [],
    "dependencies": [],
    "reason": "",
    "is_changed": false
  },
  "b1111111-1111-1111-1111-111111111111": {
    "dir": "A/B1",
    "id": "b1111111-1111-1111-1111-111111111111",
    "name": "B1",
    "description": "",
    "tags": [],
    "dependencies": [
      "a1111111-1111-1111-1111-111111111111"
    ],
    "reason": "",
    "is_changed": false
  },
  "b2222222-2222-2222-2222-222222222222": {
    "dir": "A/B2",
    "id": "b2222222-2222-2222-2222-222222222222",
    "name": "B2",
    "description": "",
    "tags": [],
    "dependencies": [
      "a1111111-1111-1111-1111-111111111111",
      "d1111111-1111-1111-1111-111111111111"
    ],
    "reason": "",
    "is_changed": false
  },
  "c1111111-1111-1111-1111-111111111111": {
    "dir": "A/B1/C",
    "id": "c1111111-1111-1111-1111-111111111111",
    "name": "C",
    "description": "",
    "tags": [],
    "dependencies": [
      "b1111111-1111-1111-1111-111111111111",
      "d1111111-1111-1111-1111-111111111111"
    ],
    "reason": "",
    "is_changed": false
  },
  "d1111111-1111-1111-1111-111111111111": {
    "dir": "D",
    "id": "d1111111-1111-1111-1111-111111111111",
    "name": "D",
    "description": "",
    "tags": [],
    "dependencies": [],
    "reason": "",
    "is_changed": false
  }
}
terramate list --format json --label stack.name
Error: --format json cannot be used with --label stack.name (stack names are not guaranteed to be unique)
terramate list --format json --label stack.dir
{
  "A": {
    "dir": "A",
    "id": "a1111111-1111-1111-1111-111111111111",
    "name": "A",
    "description": "",
    "tags": [],
    "dependencies": [],
    "reason": "",
    "is_changed": false
  },
  "A/B1": {
    "dir": "A/B1",
    "id": "b1111111-1111-1111-1111-111111111111",
    "name": "B1",
    "description": "",
    "tags": [],
    "dependencies": [
      "A"
    ],
    "reason": "",
    "is_changed": false
  },
  "A/B1/C": {
    "dir": "A/B1/C",
    "id": "c1111111-1111-1111-1111-111111111111",
    "name": "C",
    "description": "",
    "tags": [],
    "dependencies": [
      "A/B1",
      "D"
    ],
    "reason": "",
    "is_changed": false
  },
  "A/B2": {
    "dir": "A/B2",
    "id": "b2222222-2222-2222-2222-222222222222",
    "name": "B2",
    "description": "",
    "tags": [],
    "dependencies": [
      "A",
      "D"
    ],
    "reason": "",
    "is_changed": false
  },
  "D": {
    "dir": "D",
    "id": "d1111111-1111-1111-1111-111111111111",
    "name": "D",
    "description": "",
    "tags": [],
    "dependencies": [],
    "reason": "",
    "is_changed": false
  }
}
terramate list --format dot --label stack.id
digraph  {

	n1[label="a1111111-1111-1111-1111-111111111111"];
	n2[label="b1111111-1111-1111-1111-111111111111"];
	n3[label="c1111111-1111-1111-1111-111111111111"];
	n5[label="b2222222-2222-2222-2222-222222222222"];
	n4[label="d1111111-1111-1111-1111-111111111111"];
	n1->n2;
	n1->n5;
	n2->n3;
	n4->n3;
	n4->n5;

}
terramate list --format dot --label stack.name
digraph  {

	n1[label="A"];
	n2[label="B1"];
	n3[label="C"];
	n5[label="B2"];
	n4[label="D"];
	n1->n2;
	n1->n5;
	n2->n3;
	n4->n3;
	n4->n5;

}
terramate list --format dot --label stack.dir
digraph  {

	n1[label="A"];
	n2[label="A/B1"];
	n3[label="A/B1/C"];
	n5[label="A/B2"];
	n4[label="D"];
	n1->n2;
	n1->n5;
	n2->n3;
	n4->n3;
	n4->n5;

}

Special notes for your reviewer:

Since this PR turned out to be bigger than I thought, I'm certainly open to reworking or completely discarding some of the features.

Since #2069 mentioned that returning dependency edge nodes would be a sensible approach, I took the liberty of introducing a new method DirectAncestorsOf() in dag.go to implement transitive reduction of ancestors.

It is still missing a few tests, but I wanted to get your feedback before proceeding.

Does this PR introduce a user-facing change?

This PR introduces explicit formatting options for terraform list through --format {text,json,dot} and --label stack.{id,name,dir} parameters. It does not introduce breaking changes to the previous behaviour of terraform list.


Note

Adds selectable output formats (text/json/dot) and labeling (stack.id/name/dir) to terramate list, including JSON metadata and DOT graphs with direct dependencies.

  • List command (commands/stack/list):
    • New flags: --format {text,json,dot} and --label stack.{id,name,dir}; blocks --format json with --label stack.name.
    • Outputs:
      • text: now uses selected label; --why prints <label> - <reason>.
      • json: emits map[label] -> stack metadata (dir, id, name, description, tags, dependencies, reason, is_changed).
      • dot: emits GraphViz graph of stacks with edges for direct dependencies.
    • Implementation:
      • Build DAG from stacks; compute direct ancestors for dependencies; shared metadata builder.
      • Adds StackInfo, label resolution, and printers wiring.
  • DAG (run/dag/dag.go):
    • Adds DirectAncestorsOf() with transitive reduction and helper to find reachable ancestors.
  • UI/CLI:
    • cli_spec.go: adds list flags --format and --label.
    • cli_handler.go: wires flags to list spec and telemetry; passes printers.
  • Testing:
    • Adds e2e tests validating JSON output structure and direct dependencies across multiple graph shapes.
  • Test sandbox:
    • Supports setting stack name in generated test configs.

Written by Cursor Bugbot for commit 39da876. This will update automatically on new commits. Configure here.

@timsolovev timsolovev changed the title feat: list --format {json,dot} feat: list --format {text,json,dot} --label stack.{id,name,dir} Oct 4, 2025
@timsolovev timsolovev force-pushed the timsolovev-list-json branch from b5e73d6 to 3ab99f5 Compare October 4, 2025 13:37
@timsolovev timsolovev marked this pull request as ready for review October 6, 2025 12:27
@timsolovev timsolovev requested a review from a team as a code owner October 6, 2025 12:27
@timsolovev timsolovev force-pushed the timsolovev-list-json branch from 46f424a to 0aa3955 Compare October 6, 2025 12:43
cursor[bot]

This comment was marked as outdated.

@soerenmartius
Copy link
Contributor

Hey @timsolovev, thanks for your Pull Request - we really like your suggested changes. While @snakster will review the PR in detail, I wanted to ask if you would be willing to sign our Contributor license agreement? if so, please send me an email to [email protected] so I can initiate the process. Thank you

@mariux
Copy link
Contributor

mariux commented Oct 7, 2025

@timsolovev thanks a lot for providing this PR. We love it.

We see some conflicts with planned features that we would need to address here. Nothing fundamental, mainly user facing format and naming topics.

Let me align with the team and compile a list of high level feedback points. FYI, I am not the one to give feedback on code but on the product side of things here ;)

@timsolovev
Copy link
Author

@soerenmartius I will gladly sign the contributor agreement after this PR is preliminarily approved.

@soerenmartius
Copy link
Contributor

@soerenmartius I will gladly sign the contributor agreement after this PR is preliminarily approved.

fanstastic - thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] terramate list --run-order to include specific ordering [FEATURE] Json stack list output

4 participants