Skip to content

[SPEC] terramate list --format json --include-modules — Terraform module references #2330

@mariux

Description

@mariux

SPEC: terramate list --format json --include-modules

Terraform/OpenTofu module references in JSON stack output. Follow-up to #2324.

Background

The JSON stack metadata output (#2324) covers stack definitions, dependencies, bundles, and components. Module references — the module blocks in .tf/.tofu files — were explicitly deferred because they require scanning Terraform HCL files and cannot fully resolve remote modules without terraform init.

This spec adds module reference scanning as an opt-in extension to the JSON output. It scans what's available on disk without requiring initialization.

CLI Interface

terramate list --format json --include-modules [other flags...]
  • --include-modules — opt-in: parses module blocks from .tf/.tofu files in each stack, resolves local module paths, and recursively scans nested local modules. Adds modules and all_module_paths fields to each stack object.
  • Requires --format json — errors if used without it.
  • Existing terramate list --format json behavior is unchanged when --include-modules is not specified.

Module Object

Within each stack object (when --include-modules is active):

{
  "modules": [
    {
      "name": "vpc",
      "source": "./modules/vpc",
      "type": "local",
      "resolved": true,
      "file": {
        "basename": "main.tf",
        "kind": "config",
        "line": 1
      },
      "path": {
        "absolute": "/modules/vpc",
        "relative": "../modules/vpc"
      },
      "modules": [
        {
          "name": "subnets",
          "source": "./subnets",
          "type": "local",
          "resolved": true,
          "file": {
            "basename": "main.tf",
            "kind": "config",
            "line": 1
          },
          "path": {
            "absolute": "/modules/vpc/subnets",
            "relative": "../../modules/vpc/subnets"
          },
          "modules": []
        }
      ]
    },
    {
      "name": "rds",
      "source": "hashicorp/rds/aws",
      "type": "remote",
      "resolved": false,
      "file": {
        "basename": "main.tf",
        "kind": "config",
        "line": 1
      }
    }
  ]
}

all_module_paths

A flat, deduplicated, sorted summary of all local module directory paths referenced by this stack — both direct modules and nested submodules. This is a convenience field that saves consumers from traversing the module tree.

{
  "all_module_paths": [
    {
      "path": {
        "absolute": "/modules/monitoring",
        "relative": "../../modules/monitoring"
      },
      "types": ["module"],
      "exists": false
    },
    {
      "path": {
        "absolute": "/modules/vpc",
        "relative": "../../modules/vpc"
      },
      "types": ["module"],
      "exists": true
    },
    {
      "path": {
        "absolute": "/modules/vpc/subnets",
        "relative": "../../modules/vpc/subnets"
      },
      "types": ["module", "submodule"],
      "exists": true
    }
  ]
}

all_module_paths fields

Field Type Description
path object Path object (same as #2324). relative is relative to the owning stack's directory. host included when --include-host-dir is active.
types string[] Sorted array of reference types. Values: "module" (directly referenced in stack's .tf files), "submodule" (referenced inside another module, not directly by the stack). A path can have multiple types.
exists bool Whether the directory exists on disk.

Behavior

  • Absent when --include-modules is not active
  • Absent when active but the stack has no local module references
  • Present when the stack has at least one local module reference (resolved or not)
  • Includes all local module paths regardless of whether they exist on disk — the exists field indicates filesystem status. Remote modules are excluded.
  • For unresolved local modules (exists: false), the path is computed from the raw source attribute relative to the declaring file's directory, even though the target doesn't exist on disk. This differs from the modules tree where path is absent for unresolved entries.
  • When override files exist for a module, override merging is applied: the effective source is determined by sorting entries by (file.basename, file.line), last override wins. Only the effective path is included — not all override variants.
  • Deduplicated by path.absolute — when the same directory is referenced as both a direct module and a submodule, they merge into one entry with combined types
  • Sorted by path.absolute
  • The modules and all_module_paths fields have independent presence rules

Note: Watch file paths from #2324's watch field are not included in all_module_paths. Consumers who need a combined list of all change-triggering paths can merge watch and all_module_paths themselves.

Field Reference

Module fields

Field Type Description
name string The module label from the module "name" {} block. Unique within each scope (enforced by Terraform).
source string The raw source attribute value as written in HCL. For nested modules, this is relative to the parent module's directory, not the stack — use path for resolved locations.
type string "local" for .//../ sources, "remote" for everything else
resolved bool true if the module source was found on disk and scanned
file object Source file info: basename (filename, e.g. "main.tf"), kind ("config" for regular .tf/.tofu files, "override" for *_override.tf/override.tf files), and line (1-based line number of the module block in the file)
path object Path object (same as #2324). Absent when resolved is false. relative is relative to the owning stack's directory — even for nested modules, relative is relative to the stack, not the parent module.
modules array Nested module references found inside this module. [] when resolved but has no sub-modules. Absent when resolved is false (children unknown).

Behavior details

Local modules (type: "local"):

  • Source starts with ./ or ../
  • Resolved by following the path relative to the file that contains the module block
  • resolved is true if the target directory exists on disk
  • resolved is false if the target directory doesn't exist (e.g. path typo, generated module not yet created)
  • When resolved, .tf/.tofu files in the target directory are scanned recursively for nested module blocks
  • Circular references are detected and scanning stops (no infinite loops)

Remote modules (type: "remote"):

  • Any source that doesn't start with ./ or ../ — includes registry (hashicorp/rds/aws), git (git::https://...), GitHub (github.com/...), S3, GCS, etc.
  • Always resolved: false in this spec
  • No path field (not on disk)
  • No modules field (children unknown without downloading)

Override files:

  • Terraform override files (override.tf, *_override.tf, and .tofu equivalents) can redefine a module's source attribute
  • An override must have a matching config entry — override-only modules are invalid in Terraform
  • Multiple overrides can exist for the same module name (across files or within the same file). Terraform compounds them: last override wins, ordered by (basename lexicographic, line number ascending).
  • Override file merging is not performed in the modules tree — all module blocks are parsed from each file independently and included as separate entries
  • Each module entry includes file.kind ("config" or "override") and file.line so consumers can determine override ordering
  • In all_module_paths, override merging is applied to produce a usable list: for each module name, the effective source is determined by sorting all entries by (file.basename, file.line) — if overrides exist, the last override's resolved path is used; otherwise the config entry's resolved path is used. When an override changes a module's source, the override's resolved submodule tree (if any) replaces the config entry's submodule tree entirely in all_module_paths.

path object:

Stack-level modules field:

Example: Full stack with modules

Note: Only showing the modules and all_module_paths fields. Other stack fields as defined in #2324.

{
  "id": "a1111111-1111-1111-1111-111111111111",
  "name": "infrastructure",
  "path": { "absolute": "/stacks/infra" },
  "modules": [
    {
      "name": "vpc",
      "source": "../../modules/vpc",
      "type": "local",
      "resolved": true,
      "file": {
        "basename": "main.tf",
        "kind": "config",
        "line": 1
      },
      "path": {
        "absolute": "/modules/vpc",
        "relative": "../../modules/vpc"
      },
      "modules": [
        {
          "name": "flow_logs",
          "source": "./flow-logs",
          "type": "local",
          "resolved": true,
          "file": {
            "basename": "main.tf",
            "kind": "config",
            "line": 1
          },
          "path": {
            "absolute": "/modules/vpc/flow-logs",
            "relative": "../../modules/vpc/flow-logs"
          },
          "modules": []
        }
      ]
    },
    {
      "name": "eks",
      "source": "terraform-aws-modules/eks/aws",
      "type": "remote",
      "resolved": false,
      "file": {
        "basename": "main.tf",
        "kind": "config",
        "line": 1
      }
    },
    {
      "name": "monitoring",
      "source": "../../modules/monitoring",
      "type": "local",
      "resolved": false,
      "file": {
        "basename": "main.tf",
        "kind": "config",
        "line": 1
      }
    },
    {
      "name": "vpc",
      "source": "../../modules/vpc-dev",
      "type": "local",
      "resolved": true,
      "file": {
        "basename": "dev_override.tf",
        "kind": "override",
        "line": 1
      },
      "path": {
        "absolute": "/modules/vpc-dev",
        "relative": "../../modules/vpc-dev"
      },
      "modules": []
    }
  ],
  "all_module_paths": [
    {
      "path": {
        "absolute": "/modules/monitoring",
        "relative": "../../modules/monitoring"
      },
      "types": ["module"],
      "exists": false
    },
    {
      "path": {
        "absolute": "/modules/vpc-dev",
        "relative": "../../modules/vpc-dev"
      },
      "types": ["module"],
      "exists": true
    }
  ]
}

Notes:

  • The monitoring module is type: "local" but resolved: false — the directory doesn't exist on disk. It appears in all_module_paths with exists: false (path computed from raw source).
  • The vpc module appears twice in modules — once from main.tf (kind: "config") and once from dev_override.tf (kind: "override") with a different source. Both entries are included since override merging is not performed in the modules tree.
  • In all_module_paths, override merging is applied: vpc resolves to /modules/vpc-dev (from dev_override.tf) not /modules/vpc (from main.tf). The config entry's /modules/vpc and its submodule /modules/vpc/flow-logs are not included because the override replaced the effective source.
  • eks is excluded from all_module_paths (remote).

Scope

In scope

  • Parsing module blocks from .tf/.tofu files in stacks
  • Classifying sources as local vs remote
  • Resolving local module paths and recursively scanning nested modules
  • Circular reference detection
  • Source file tracking with override identification (file.basename, file.kind)
  • all_module_paths convenience summary (flat list of all local module directory paths with types and existence)
  • --include-modules opt-in flag

Out of scope

  • .terraform/modules/modules.json scanning (resolving remote modules post-init) — future follow-up
  • Terraform override file merging (module blocks parsed independently per file)
  • Module version/constraint information (version attribute in module blocks)
  • Module input/output variable scanning
  • Terragrunt module references (handled separately via existing Terragrunt integration)
  • Module registry API queries
  • Any operation that requires network access or terraform init

Related Issues & PRs

Type # Title Disposition
ISSUE #723 Json stack list output Partially covered (module paths in JSON output)
ISSUE #2324 terramate list --format json spec Parent spec — this extends it

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions