terraform-stack-plan

module
v0.25.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 11, 2026 License: Apache-2.0

README

tfstackplan

Your monorepo just opened 8 Terraform plans on this one PR. Your reviewer has gone very quiet.

An engineer at dusk, cold coffee in hand, a '127 files changed' sign glowing in the window
He's reviewing the networking stack.

tfstackplan turns the N plan.json files a monorepo spits out per PR into a single comment a human can actually read — then, if you're brave, gates the apply behind a button someone has to consciously press.

Terramate makes a Terraform monorepo runnable. tfstackplan makes it reviewable, governable, and safe inside GitHub's PR workflow. It closes the four gaps a raw terramate run in CI leaves wide open — and it has four faces, one per gap:

Face The gap it closes In one line
render Review N unreadable plan.json → one scannable PR comment
serve Governance no approval story → live gates + privilege-backed apply
run Observability CI logs are a black hole → live DAG UI, per-stack logs, check runs
state Management cross-stack refactors are scary → declarative state moves

Terramate runs your stacks. GitHub shrugs and posts the logs. terraform apply does exactly what you told it to, which is the problem. tfstackplan is the adult in the room.

render is fully standalone — point it at a directory of plans and you get a comment. You never have to touch run / serve / state. The other three layer a control plane on top, while Terraform keeps executing in your CI under your identities.

New here? Start with the guide — it reads in order, from why this exists to every flag there is.


What it looks like

A run over eight stacks with classification on, rendered the way GitHub shows it in a PR comment: a scannable summary table, then a per-stack drill-down. Each resource is its own row inside an indented blockquote bar, so it's always clear which stack and resource you're reading. Small changes show expanded; big ones collapse to a row you click to open.

Terraform plan — nonprod (8 stacks changed)
Stack Add Change Destroy Replace Categories
platform/nonprod 0 4 0 0 🔐 iam
service-projects/app-dev 4 3 0 0 ✅ safe
data/warehouse 0 0 6 0 💣 destructive
networking/shared-vpc 0 5 0 2 💣 destructive
observability/grafana 5 6 0 0 ✅ safe
📁 platform/nonprod · 🔐 iam · 4 change
✏️ google_project_iam_member.data_engineers
    1 changed
~ role = "roles/viewer" → "roles/editor"
📁 data/warehouse · 💣 destructive · 6 destroy
➖ google_bigquery_dataset.legacy_events
    2 attrs
- location = "us-central1"
- name     = "legacy_events"

That whole block is live Markdown, not a screenshot — it renders natively on GitHub and you can paste it anywhere. The first line of the real output is an invisible HTML-comment marker (<!-- tfstackplan:nonprod -->) that CI uses to upsert one comment per environment. The full anatomy — folding rules, aligned diffs, structured-value rendering, state-op rows — lives in the render chapter.

More worked outputs (big plans, the byte budget kicking in, state ops) are in examples/.


60-second quickstart

render is offline and standalone — no terraform, no posting, no config required. Give it a directory of plans laid out to mirror your stack tree.

# 1. install
go install github.com/Fluent-Health/terraform-stack-plan/cmd/tfstackplan@latest

# 2. for each stack, capture a plan as JSON next to where it lives
terraform plan -out plan.bin && terraform show -json plan.bin > out/<stack>/tfplan.json

# 3. render every stack under out/ into one comment
tfstackplan render --plans-dir out/ \
  --title  "Terraform plan — nonprod" \
  --marker tfstackplan:nonprod \
  --output report.md

report.md is the comment you saw above. With no config, classification is off and the tool degrades gracefully — add a .tfstackplan.hcl when you want categories, links, and budgets. Then have your CI post report.md; render writes Markdown, your CI does the posting.

The full path from here — classification, then the control plane — is laid out in the guide.


The four faces

render — the review gap. A pure, offline renderer: many plan.json files in, one reviewer-friendly Markdown comment out, with optional classification. It never runs terraform and never posts. Use it entirely on its own.

run — the observability gap. The CI driver. It wraps your terramate script run, detects the changed stacks, runs plan/apply/verify, renders + classifies in-process, and reports the execution lifecycle to the control plane — so a CI run stops being a wall of interleaved logs.

serve — the governance gap. The control plane: a live dependency-DAG UI, approval gates, one GitHub check run per environment, and SSE-tailed per-stack logs.

terraform apply across 8 stacks, straight from CI, with no one watching, is a perfectly reasonable thing to do. serve is for the other 100% of cases.

state — the management gap. Declarative cross-stack Terraform state moves, applied as part of the normal apply — so refactoring resources between stacks stops being a hand-run, fingers-crossed operation.


Where do I go now?

The docs read as a sequence — start at the index — but if you know what you want:


Install

go install github.com/Fluent-Health/terraform-stack-plan/cmd/tfstackplan@latest
# or
go build -o tfstackplan ./cmd/tfstackplan

Prebuilt binaries (linux/darwin · amd64/arm64) are on the Releases page, which also ships a multi-arch, distroless Cloud Run container (entrypoint serve). The binary is fully static — pure-Go SQLite, no cgo — and embeds its assets, so the image needs no runtime files.

This repo is also its own asdf plugin (hook scripts in bin/, no separate plugin repo):

asdf plugin add tfstackplan https://github.com/Fluent-Health/terraform-stack-plan.git
asdf install tfstackplan latest      # or a pinned version
asdf set tfstackplan latest          # writes .tool-versions

Full install detail — toolchain notes, container, deployment — is in Reference → Install & deploy.


Agentic Workflows, Extensions & Workspace Skills

tfstackplan publishes a comprehensive, cross-runtime Workspace Agent Skill directly in this repository under skills/tfstackplan/SKILL.md. This skill governs the entire lifecycle and CLI surface of tfstackplan (lint, plan, state refactoring, watch status, claims, and applies), instructing future coding agents on how to operate this codebase with maximum safety and TDD discipline.

Loading in Claude Code (Marketplace & Plugins)

We package tfstackplan as a standard Claude Code plugin. You can load this plugin directly from GitHub or subscribe to our marketplace feed:

  1. Install directly as a Claude Plugin:
    /plugin add https://github.com/Fluent-Health/terraform-stack-plan
    
  2. Add as a Marketplace Catalog:
    /plugin marketplace add https://raw.githubusercontent.com/Fluent-Health/terraform-stack-plan/main/.claude-plugin/marketplace.json
    

Claude Code will automatically detect .claude-plugin/plugin.json and load the configured ./skills on demand when your session context matches the skill's frontmatter description, executing these instructions natively via the Claude Skill tool.

Loading in Gemini CLI

You can install this repository's agent skill and extensions globally or link them locally during development:

  1. Install globally as a Gemini Extension:
    gemini extensions install https://github.com/Fluent-Health/terraform-stack-plan
    
  2. Link the skill locally:
    gemini skills link ./skills/tfstackplan
    

Once loaded, Gemini CLI will automatically trigger and follow this skill whenever you ask the model to validate, plan, or restructure Terraform state.

Manual / Cross-Runtime Loading

Both Claude Code and Gemini CLI support the open Agent Skills standard, enabling cross-runtime interoperability:

  1. Create the global skill directory:
    mkdir -p ~/.agents/skills/tfstackplan
    
  2. Symlink or copy the skill file:
    ln -s "$(pwd)/skills/tfstackplan/SKILL.md" ~/.agents/skills/tfstackplan/SKILL.md
    

When not to use this

Honesty up front saves everyone a wasted afternoon:

  • One root module, one plan per PR. If terraform plan already produces a single readable diff, you don't need a multi-plan renderer. Reach for tfplan2md or terraform-plan-summary.
  • You want the tool to run terraform for you. render doesn't; its inputs are pre-existing plan.json files. (run does drive Terraform via Terramate — that's a different face.)
  • You want it to post comments for you. render writes Markdown; your CI posts it. (serve posts its own check runs, but that's the control plane, not the renderer.)
  • You need static-analysis rollups (Checkov / Trivy / SARIF). Not today; possibly later.

Yes, it's another Terraform tool. We're as surprised as you are. The difference is the multi-plan part — taking the many plans a monorepo emits per PR and making them one reviewable, gate-able, observable thing:

  • tfplan2md — single-plan Markdown renderer; the inspiration for the per-stack diff style.
  • Atlantis — PR-comment-driven Terraform CI; renders per-project, tied to its own workflow.
  • terraform-plan-summary — single-plan summary table; no multi-plan support.

Contributing

Contributions welcome — see CONTRIBUTING.md. For security issues, follow SECURITY.md rather than opening a public issue.

License

Licensed under the Apache License, Version 2.0 — see LICENSE.


Built and maintained by Fluent Health.

Directories

Path Synopsis
cmd
tfstackplan command
Command tfstackplan merges many Terraform plan.json files into one marker-keyed markdown report for a PR comment.
Command tfstackplan merges many Terraform plan.json files into one marker-keyed markdown report for a PR comment.
internal
ansi
Package ansi strips ANSI terminal escape sequences from captured command output, for the surfaces that cannot render them (the outcome-classification regex and the GitHub check-run markdown).
Package ansi strips ANSI terminal escape sequences from captured command output, for the surfaces that cannot render them (the outcome-classification regex and the GitHub check-run markdown).
api
Package api provides primitives to interact with the openapi HTTP API.
Package api provides primitives to interact with the openapi HTTP API.
approval
Package approval defines the approval-gate backend abstraction.
Package approval defines the approval-gate backend abstraction.
approval/gcppam
Package gcppam implements the approval.Backend over GCP Privileged Access Manager (PAM).
Package gcppam implements the approval.Backend over GCP Privileged Access Manager (PAM).
claims
Package claims is the pure event-sourced domain core for the apply-lock claim ledger.
Package claims is the pure event-sourced domain core for the apply-lock claim ledger.
classify
Package classify applies an ordered list of rules to a parsed stack and returns the set of matching categories.
Package classify applies an ordered list of rules to a parsed stack and returns the set of matching categories.
codes
Package codes is the closed registry of stable, machine-readable problem codes.
Package codes is the closed registry of stable, machine-readable problem codes.
config
Package config loads and validates the HCL policy file (.tfstackplan.hcl), resolving classification presets+rules into a single ordered rule list while preserving source declaration order.
Package config loads and validates the HCL policy file (.tfstackplan.hcl), resolving classification presets+rules into a single ordered rule list while preserving source declaration order.
differ
Package differ turns a single attribute's before/after values into an ordered list of render variants (preferred → minimal).
Package differ turns a single attribute's before/after values into an ordered list of render variants (preferred → minimal).
domain
Package domain holds the canonical value types shared across the render pipeline, the runner→server wire protocol, and persistence.
Package domain holds the canonical value types shared across the render pipeline, the runner→server wire protocol, and persistence.
events
Package events defines the typed, versioned protocol the CI runner speaks to the server: the lifecycle of a multi-stack Terraform execution (init → phase → per-stack updates → finalize) plus the apply-time gate exchange.
Package events defines the typed, versioned protocol the CI runner speaks to the server: the lifecycle of a multi-stack Terraform execution (init → phase → per-stack updates → finalize) plus the apply-time gate exchange.
eventsourcing
Package eventsourcing is a generic, domain-agnostic decider host over store.EventStore: Load (snapshot + replay-tail via Evolve) and Append (encode + optimistic append + snapshot), parameterized per aggregate by a Decider value.
Package eventsourcing is a generic, domain-agnostic decider host over store.EventStore: Load (snapshot + replay-tail via Evolve) and Append (encode + optimistic append + snapshot), parameterized per aggregate by a Decider value.
executor
Package executor abstracts the CI muscle the control plane drives: serve receives the webhooks and asks a Backend to start/cancel/probe builds.
Package executor abstracts the CI muscle the control plane drives: serve receives the webhooks and asks a Backend to start/cancel/probe builds.
fit
Package fit deterministically reduces a model.Report so its rendered size fits a byte budget, degrading the largest attribute diffs first, then falling back through a report-level terminal cascade.
Package fit deterministically reduces a model.Report so its rendered size fits a byte budget, degrading the largest attribute diffs first, then falling back through a report-level terminal cascade.
gauth
Package gauth obtains Google-signed OIDC ID tokens for authenticating to the tfstackplan control plane, replacing the shared-secret HS256 scheme.
Package gauth obtains Google-signed OIDC ID tokens for authenticating to the tfstackplan control plane, replacing the shared-secret HS256 scheme.
gauth/gauthtest
Package gauthtest provides a fake Google OIDC issuer for offline tests of the gauth mint/verify paths — no network, no credentials, no test users.
Package gauthtest provides a fake Google OIDC issuer for offline tests of the gauth mint/verify paths — no network, no credentials, no test users.
links
Package links resolves URL templates with {placeholder} substitution.
Package links resolves URL templates with {placeholder} substitution.
model
Package model holds the shared, budget-agnostic intermediate types that flow through the gather → fit → render pipeline.
Package model holds the shared, budget-agnostic intermediate types that flow through the gather → fit → render pipeline.
plan
Package plan parses a Terraform plan JSON document into a RawStack: reduced action counts plus the set of affected attributes for each change action.
Package plan parses a Terraform plan JSON document into a RawStack: reduced action counts plus the set of affected attributes for each change action.
plandir
Package plandir discovers per-stack Terraform plan JSON files under a single directory.
Package plandir discovers per-stack Terraform plan JSON files under a single directory.
presets
Package presets provides built-in named classification rule bundles that a repo can opt into without hand-writing regexes.
Package presets provides built-in named classification rule bundles that a repo can opt into without hand-writing regexes.
reconcile
Package reconcile is the pure functional core for the server's gate/grant and execution-lifecycle state.
Package reconcile is the pure functional core for the server's gate/grant and execution-lifecycle state.
render
Package render turns a (possibly fit-reduced) model.Report into the final markdown document.
Package render turns a (possibly fit-reduced) model.Report into the final markdown document.
runner
Package runner is the CI-side driver: it invokes the same terramate scripts a human runs, captures per-stack output, renders + classifies in-process, and reports progress to the control-plane server over the typed events protocol.
Package runner is the CI-side driver: it invokes the same terramate scripts a human runs, captures per-stack output, renders + classifies in-process, and reports progress to the control-plane server over the typed events protocol.
server
Package server is the control-plane HTTP service: it records multi-stack Terraform executions, drives one GitHub check run per environment, and projects approval-gate state — all as a pure function of its SQLite store.
Package server is the control-plane HTTP service: it records multi-stack Terraform executions, drives one GitHub check run per environment, and projects approval-gate state — all as a pure function of its SQLite store.
source
Package source indexes a stack's Terraform source so a plan resource can be linked to the file:line where it is declared.
Package source indexes a stack's Terraform source so a plan resource can be linked to the file:line where it is declared.
statemove
Package statemove validates declared Terraform resource moves against plan data and generates/maintains the native `moved {}` shim files that effect them (SP1: within one stack).
Package statemove validates declared Terraform resource moves against plan data and generates/maintains the native `moved {}` shim files that effect them (SP1: within one stack).
statemoves
Package statemoves loads the optional --state-moves manifest: per stack, the addresses being moved IN by an external cross-state move (infra's state-mover).
Package statemoves loads the optional --state-moves manifest: per stack, the addresses being moved IN by an external cross-state move (infra's state-mover).
store
Package store is the server's SQLite persistence layer: executions, their stack/edge subgraph, and per-(class,target) approval gate state.
Package store is the server's SQLite persistence layer: executions, their stack/edge subgraph, and per-(class,target) approval gate state.
store/migrations
Package migrations embeds the goose SQL migrations for the server store.
Package migrations embeds the goose SQL migrations for the server store.
ui
Package ui is the central UI service: a single pane of glass over the tier serves.
Package ui is the central UI service: a single pane of glass over the tier serves.
uiapi
Package uiapi holds the generated OpenAPI artifacts for the central UI's backend API (api/ui.openapi.yaml): the std-http router and models.
Package uiapi holds the generated OpenAPI artifacts for the central UI's backend API (api/ui.openapi.yaml): the std-http router and models.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL