π Official Website
Convert Terraform plan JSON files into human-readable Markdown reports.
NOTE: This project was developed 100% with GitHub Copilot to explore how far AI-assisted development can go. All code and specifications were generated with AI support.
Terraform plans are notoriously difficult to review in pull requests:
- Wall of text output - Raw
terraform planoutput is verbose and hard to scan - No structure - Changes aren't grouped logically, making it difficult to understand impact
- Cryptic JSON -
terraform show -jsonprovides complete data but is unreadable for humans - Index-based diffs - Changes to lists show as confusing index modifications (e.g.,
firewall_rule[2]deleted,firewall_rule[1]modified) - Lost context - Hard to see the big picture: "What's actually changing and why?"
tfplan2md solves this by generating clean, readable Markdown reports that:
- β Structure changes logically - Group by module, summarize by action type
- β Show semantic diffs - See which firewall rules or NSG rules were added/removed, not index changes
- β Highlight key changes - One-line summaries show what changed in each resource at a glance
- β Format for readability - Collapsible sections, tables, and syntax highlighting make review efficient
- β Render natively - GitHub and Azure DevOps display reports beautifully in PR comments
Result: Reviewers can quickly understand infrastructure changes, catch potential issues, and approve confidently.
- Release documentation - Attach plan reports to release notes for audit trails
- Compliance audits - Generate human-readable change documentation for compliance reviews
- Team communication - Share infrastructure changes with stakeholders who don't read Terraform code
- CI/CD integration - Automatically post plan summaries to PRs, Slack, or Teams
- π Convert Terraform plans to Markdown - Generate clean, readable reports from
terraform show -jsonoutput - π Static analysis integration - Display security and quality findings from Checkov, Trivy, TFLint, and Semgrep (SARIF 2.1.0 format) directly in reports
- β Validated markdown output - Comprehensive testing ensures GitHub/Azure DevOps compatibility
- π Sensitive value masking - Sensitive values are masked by default for security
- π Customizable templates - Use Scriban templates for custom report formats
- π³ Minimal Docker image - 14.7MB AOT-compiled native binary for fast deployments and minimal attack surface
- π Module grouping - Resource changes are grouped by module and rendered as module sections
- π Readable Azure Resource IDs - Long Azure IDs are automatically formatted as readable scopes with values in code (e.g., Key Vault
kvin resource grouprg) - π¨ Semantic icons - Visual icons for values: π for IPs, π for ports, π¨/π for protocols, β /β for booleans, π€/π₯/π» for principals, π‘οΈ for roles, π for identifiers, π§ for emails
- π Resource summaries - Each resource change shows a concise one-line summary for quick scanning
- π Replacement reasons - Resources being replaced show which attributes forced the replacement
- π§ Specialized templates - Custom rendering for complex resources (Azure Firewall rules, NSG rules, Azure DevOps variable groups, Azure AD resources)
- π Azure API documentation links - Reliable links to Microsoft Learn REST API documentation for 92 Azure resource types (AzAPI provider)
docker pull oocx/tfplan2md:latestThe Docker image is a 14.7MB AOT-compiled native binary built from scratch for optimal security and performance. It includes a comprehensive demo at /examples/comprehensive-demo/ showcasing all features.
Requires .NET 10 SDK.
git clone https://github.com/oocx/tfplan2md.git
cd tfplan2md
dotnet buildterraform show -json plan.tfplan | docker run -i oocx/tfplan2md# Using Docker with mounted volume
docker run -v $(pwd):/data oocx/tfplan2md /data/plan.json
# Or with .NET
dotnet run --project src/Oocx.TfPlan2Md -- plan.jsonterraform show -json plan.tfplan | docker run -i -v $(pwd):/data oocx/tfplan2md --output /data/plan.mdterraform show -json plan.tfplan | docker run -i oocx/tfplan2md --template summary| Option | Description |
|---|---|
--output, -o <file> |
Write output to a file instead of stdout |
--template, -t <name|file> |
Use a built-in template by name (default, summary) or a custom Scriban template file |
--report-title <text> |
Override the level-1 heading in the generated report |
--render-target <github|azuredevops> |
Target platform for rendering: github (simple diff) or azuredevops (inline diff, default) |
--principal-mapping, --principals, -p <file> |
Map Azure principal IDs to names using a JSON file |
--code-analysis-results <pattern> |
SARIF file pattern for static analysis findings (can be specified multiple times) |
--code-analysis-minimum-level <level> |
Minimum severity to display (critical, high, medium, low, informational) |
--fail-on-static-code-analysis-errors <level> |
Exit with code 10 when findings at or above this level exist |
--show-unchanged-values |
Include unchanged attribute values in tables (hidden by default) |
--show-sensitive |
Show sensitive values unmasked |
--hide-metadata |
Suppress tfplan2md version and generation timestamp from report header |
--debug |
Append diagnostic information to the report for troubleshooting |
--help, -h |
Display help information |
--version, -v |
Display version information |
The --render-target flag controls platform-specific rendering behavior. Attributes with newlines or over 100 characters are automatically moved to a collapsible <details> section below the main attribute table:
azuredevops(default, alias:azdo): Styled HTML with line-by-line and character-level diff highlighting. Optimized for Azure DevOps PR comments (GitHub strips styles but content remains readable).github: Traditional diff format with+/-markers. Fully portable and works on both GitHub and Azure DevOps.
Example:
terraform show -json plan.tfplan | tfplan2md --render-target githubMigration note: The --large-value-format flag has been deprecated and replaced by --render-target. Use --render-target azuredevops for inline-diff behavior or --render-target github for simple-diff behavior.
When troubleshooting issues or verifying tfplan2md's behavior, enable debug mode to append diagnostic information to the report:
# Enable debug output
terraform show -json plan.tfplan | tfplan2md --debug
# With principal mapping
tfplan2md --debug --principal-mapping principals.json plan.json -o report.mdDebug information is added as a "Debug Information" section at the end of the report and includes:
- Principal mapping diagnostics: Load status, principal type counts, and failed ID resolutions with context showing which resource referenced each missing ID
- Enhanced error diagnostics (when principal mapping fails):
- File and directory existence checks
- Specific error type (FileNotFound, JsonParseError, DirectoryNotFound, AccessDenied)
- Line and column numbers for JSON syntax errors
- Docker-specific troubleshooting guidance
- Actionable solutions based on the error type
- Template resolution: Which templates (custom, built-in, or default) were used for each resource type
This helps diagnose principal mapping failures, Docker volume mount issues, and understand template selection behavior.
The --principal-mapping file can include principals plus Azure metadata (subscriptions, management groups, tenants, roles).
The new sections use an array-of-objects format; existing principal-only files remain supported.
{
"users": [
{ "id": "user-guid", "displayName": "Jane Doe" }
],
"groups": [
{ "id": "group-guid", "displayName": "DevOps Team" }
],
"servicePrincipals": [
{ "id": "sp-guid", "displayName": "CI/CD Pipeline" }
],
"subscriptions": [
{ "id": "d1828a48-fced-4ea2-b2ec-4b9623f327fd", "displayName": "Production" }
],
"managementGroups": [
{ "id": "mg-production", "displayName": "Production Workloads" }
],
"tenants": [
{ "id": "tenant-guid", "displayName": "Contoso Corp" }
],
"roles": [
{ "id": "custom-role-guid", "displayName": "Custom Deployment Role" }
]
}Use the Azure CLI to export the new mapping sections (each command returns the array-of-objects format):
# Principals
az ad user list --all --query "[].{id:id,displayName:displayName}" -o json
az ad group list --query "[].{id:id,displayName:displayName}" -o json
az ad sp list --all --query "[].{id:id,displayName:displayName}" -o json
# Subscriptions
az account list --query "[].{id:id,displayName:name}" -o json
# Management groups
az account management-group list --query "[].{id:name,displayName:displayName}" -o json
# Tenants
az account tenant list --query "[].{id:tenantId,displayName:displayName}" -o json
# Custom roles
az role definition list --custom-role-only true --query "[].{id:name,displayName:roleName}" -o jsonUse scripts/validate-azure-cli-commands.sh to validate the commands in your environment.
When using Docker, you need to mount the principals.json file into the container:
# Mount from current directory
docker run -v $(pwd):/data oocx/tfplan2md \
--principal-mapping /data/principals.json \
/data/plan.json --output /data/plan.md
# Mount as read-only to specific path
docker run \
-v $(pwd)/plan.json:/data/plan.json:ro \
-v $(pwd)/principals.json:/app/principals.json:ro \
oocx/tfplan2md --principal-mapping /app/principals.json /data/plan.json
# With debug output
docker run -v $(pwd):/data oocx/tfplan2md --debug \
--principal-mapping /data/principals.json \
/data/plan.json --output /data/plan.mdRender existing tfplan2md reports to HTML with GitHub- or Azure-DevOps-like output using the standalone tool in src/tools/Oocx.TfPlan2Md.HtmlRenderer:
dotnet run --project src/tools/Oocx.TfPlan2Md.HtmlRenderer -- \
--input artifacts/comprehensive-demo.md \
--flavor github
dotnet run --project src/tools/Oocx.TfPlan2Md.HtmlRenderer -- \
--input artifacts/comprehensive-demo.md \
--flavor azdo \
--template src/tools/Oocx.TfPlan2Md.HtmlRenderer/templates/azdo-wrapper.html \
--output artifacts/comprehensive-demo.azdo.htmlGenerate PNG or JPEG screenshots from HTML using Playwright in src/tools/Oocx.TfPlan2Md.ScreenshotGenerator. Install the browser once after build:
pwsh src/tools/Oocx.TfPlan2Md.ScreenshotGenerator/bin/Debug/net10.0/playwright.ps1 install chromium --with-depsAutomated screenshot generation (recommended for website):
Use scripts/generate-screenshot.sh to automate the full workflow (plan β markdown β HTML β screenshots with all variants):
scripts/generate-screenshot.sh \
--plan examples/firewall-with-static-analysis/plan.json \
--output-prefix firewall-example \
--selector "details:has(summary:has-text('azurerm_firewall'))" \
--thumbnail-width 580 --thumbnail-height 400 \
--lightbox-width 1200 --lightbox-height 900 \
--render-target azdo \
--open-details-selector "details"This generates 12 screenshot files (thumbnail/lightbox Γ light/dark Γ 1x/2x DPI).
Manual usage examples (formats: png default, jpeg; WebP deferred):
# Default viewport (1920x1080), output derived from input name
dotnet run --project src/tools/Oocx.TfPlan2Md.ScreenshotGenerator -- \
--input artifacts/comprehensive-demo.github.html
# Custom viewport
dotnet run --project src/tools/Oocx.TfPlan2Md.ScreenshotGenerator -- \
--input artifacts/comprehensive-demo.github.html \
--output artifacts/screenshot-1280x720.png \
--width 1280 \
--height 720
# Full-page capture
dotnet run --project src/tools/Oocx.TfPlan2Md.ScreenshotGenerator -- \
--input artifacts/comprehensive-demo.github.html \
--output artifacts/full-report.png \
--full-page
# JPEG with quality
dotnet run --project src/tools/Oocx.TfPlan2Md.ScreenshotGenerator -- \
--input artifacts/comprehensive-demo.github.html \
--output artifacts/screenshot.jpg \
--quality 85
# Capture specific resource by Terraform address
dotnet run --project src/tools/Oocx.TfPlan2Md.ScreenshotGenerator -- \
--input artifacts/comprehensive-demo.github.html \
--output artifacts/resource.png \
--target-terraform-resource-id "azurerm_storage_account.example"
# Capture by selector with expanded details
dotnet run --project src/tools/Oocx.TfPlan2Md.ScreenshotGenerator -- \
--input artifacts/comprehensive-demo.github.html \
--output artifacts/firewall.png \
--target-selector "details:has(summary:has-text('azurerm_firewall'))" \
--open-details "details"Generate terminal-style output that mirrors terraform show for creating "before tfplan2md" examples. The default output includes ANSI color; add --no-color for plain text.
# Colored output
dotnet run --project src/tools/Oocx.TfPlan2Md.TerraformShowRenderer -- \
--input src/tests/Oocx.TfPlan2Md.Tests/TestData/TerraformShow/plan1.json \
--output artifacts/terraform-show-plan1.txt
# Plain text (no ANSI)
dotnet run --project src/tools/Oocx.TfPlan2Md.TerraformShowRenderer -- \
--input src/tests/Oocx.TfPlan2Md.Tests/TestData/TerraformShow/plan1.json \
--no-color \
--output artifacts/terraform-show-plan1.nocolor.txtAll generated markdown is automatically validated and linted for correct formatting. Special characters in resource names and attribute values are properly escaped to ensure tables and headings render correctly on GitHub and Azure DevOps.
# Terraform Plan Report
Generated by tfplan2md 0.30.0 (a1b2c3d) on 2026-01-03 14:23:15 UTC | Terraform 1.14.0
## Summary
| Action | Count | Resource Types |
|--------|-------|----------------|
| β Add | 3 | 1 azurerm_resource_group<br/>2 azurerm_storage_account |
| π Change | 1 | 1 azurerm_key_vault |
| β»οΈ Replace | 1 | 1 azuredevops_git_repository |
| β Destroy | 1 | 1 azurerm_virtual_network |
| **Total** | **6** | |
## Resource Changes
### Module: root
#### β azurerm_resource_group.main
**Summary:** `example-rg` (`westeurope`)
<details>
| Attribute | Value |
|-----------|-------|
| location | `westeurope` |
| name | π `example-rg` |
</details>
#### π azurerm_storage_account.logs
**Summary:** `stlogs` | Changed: custom_data, tags.environment
<details>
| Attribute | Before | After |
|-----------|--------|-------|
| tags.environment | `dev` | `production` |
</details>
<details>
<summary>Large values: custom_data (5 lines, 2 changed)</summary>
##### **custom_data:**
<pre style="font-family: monospace; line-height: 1.5;"><code>#!/bin/bash
<span style="background-color: #fff5f5; border-left: 3px solid #d73a49; color: #24292e; display: block; padding-left: 8px; margin-left: -4px;">echo "Installing<span style="background-color: #ffc0c0; color: #24292e;"> v1.0</span>"</span>
<span style="background-color: #f0fff4; border-left: 3px solid #28a745; color: #24292e; display: block; padding-left: 8px; margin-left: -4px;">echo "Installing<span style="background-color: #acf2bd; color: #24292e;"> v2.0</span>"</span>
apt-get update
apt-get install -y nginx
</code></pre>
</details>A comprehensive demo is available in the Docker image and the repository:
# View the demo report (Docker)
docker run --rm oocx/tfplan2md /examples/comprehensive-demo/plan.json \
--principals /examples/comprehensive-demo/demo-principals.json
# View the demo locally
dotnet run --project src/Oocx.TfPlan2Md/Oocx.TfPlan2Md.csproj -- \
examples/comprehensive-demo/plan.json \
--principals examples/comprehensive-demo/demo-principals.jsonThe demo includes:
- Module grouping (root, module.network, module.security, nested modules)
- All action types (create, update, replace, delete, no-op)
- Firewall rule semantic diffing
- Network security group rule semantic diffing
- Role assignments with principal mapping
- Sensitive value handling
- Complex nested attributes
See examples/comprehensive-demo/README.md for details.
Create custom Scriban templates for your own report format. Templates focus on layout and presentation, with all value formatting handled by C# helpers for consistency.
docker run -i -v $(pwd):/data oocx/tfplan2md --template /data/my-template.sbn < plan.jsonBuilt-in templates:
default(implicit when not specified): Full report with resource changessummary: Compact summary with Terraform version, plan timestamp, and action counts only
See Scriban documentation for template syntax and docs/features.md for available helper functions.
For complex resources like firewall rule collections, tfplan2md provides resource-specific templates that show semantic diffs instead of confusing index-based changes. The default renderer (used by the CLI) applies resource-specific templates automatically when a matching template is available; the global default template is used as a fallback.
Currently supported:
azapi_resource- Flattens JSON body into dot-notation tables with before/after comparison for updates; includes reliable documentation links to Microsoft Learn for 92 Azure resource types across 37 servicesazurerm_firewall_application_rule_collection- Shows application firewall rules with FQDNs, protocols (HTTP/HTTPS/MSSQL), and source addressesazurerm_firewall_network_rule_collection- Shows network firewall rules with protocols, ports, and IP addressesazurerm_network_security_group- Shows security rule changes with semantic diffingazurerm_role_assignment- Displays human-readable role names, scopes, and principal informationazuredevops_variable_group- Shows all variables (regular and secret) with metadata, hiding only secret values
Example output for a firewall rule update:
### π azurerm_firewall_network_rule_collection.web_tier
**Collection:** `web-tier-rules` | **Priority:** 100 | **Action:** Allow
#### Rule Changes
| | Rule Name | Protocols | Source Addresses | Destination Addresses | Destination Ports |
|---|-----------|-----------|------------------|----------------------|-------------------|
| β | allow-dns | UDP | 10.0.1.0/24, 10.0.2.0/24 | 168.63.129.16 | 53 |
| π | allow-http | TCP | 10.0.1.0/24, 10.0.3.0/24 | * | 80 |
| β | allow-ssh-old | TCP | 10.0.0.0/8 | 10.0.2.0/24 | 22 |
| βΊοΈ | allow-https | TCP | 10.0.1.0/24 | * | 443 |See docs/features/001-resource-specific-templates/specification.md for creating custom resource templates.
Templates have access to:
terraform_version- Terraform version stringformat_version- Plan format versiontimestamp- Plan generation timestamp (RFC3339 format), if availablesummary- Summary object with action details:to_add,to_change,to_destroy,to_replace,no_op- Each is anActionSummaryobject containing:count- Number of resources for this actionbreakdown- Array ofResourceTypeBreakdownobjects, each withtype(resource type name) andcount(number of that type)
total- Total number of resources with changes
changes- List of resource changes withaddress,type,action,action_symbol,attribute_changesmodule_changes- Resource changes grouped by module
Notes: Attribute tables now vary depending on the resource change action:
- create resources show a 2-column table (
Attribute | Value) containing the after values. - delete resources show a 2-column table (
Attribute | Value) containing the before values. - update and replace resources show a 3-column table (
Attribute | Before | After).
This makes create/delete outputs more concise and avoids empty columns when a side is missing.
- .NET 10 SDK
- Docker (for container builds and integration tests)
- Git
# Clone the repository
git clone https://github.com/oocx/tfplan2md.git
cd tfplan2md
# Restore tools (including Husky for git hooks)
dotnet tool restore
# Install git hooks
dotnet husky install
# Build and test
dotnet build
dotnet test
Tests use **TUnit** with **AwesomeAssertions** for fluent, readable assertions.
### Coverage Helpers
Use the helper scripts to summarize coverage from Cobertura output:
```bash
# Print overall line/branch coverage
scripts/coverage-summary.sh
# List lowest branch coverage classes (default 30, can pass a count)
scripts/coverage-low-branches.sh 20
### Pre-commit Hooks
This project uses [Husky.Net](https://github.com/alirezanet/Husky.Net) for git hooks:
- **pre-commit**: Runs `dotnet format --verify-no-changes` and `dotnet build` (enforces code style and quality metrics)
- **commit-msg**: Validates commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) format
**Code quality checks:** The build enforces cyclomatic complexity (β€15), maintainability index (β₯20), and line length (β€160 characters). See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
### Docker Build
```bash
docker build -t tfplan2md .
See CONTRIBUTING.md for guidelines on:
- Branch naming conventions
- Commit message format (Conventional Commits)
- Pull request process
- Code style requirements
This project uses GitHub Actions for continuous integration and deployment:
| Workflow | Trigger | Purpose |
|---|---|---|
| PR Validation | Pull requests to main |
Format check, build, test, coverage enforcement, vulnerability scan |
| Coverage Data | Push to main |
Publish coverage badge + history to coverage-data branch |
| CI | Push to main |
Auto-version with Versionize when Docker-relevant files change |
| Release | Version tags (v*) |
Create GitHub Release, build and push Docker image |
Code coverage is automatically collected and enforced on every pull request:
- Coverage badge: The
badge in the README shows current line coverage percentage
- Coverage thresholds: PRs must maintain or improve code coverage (currently 84.48% line coverage and 72.80% branch coverage)
- Coverage history: Historical coverage data is published to the
coverage-databranch at docs/coverage/history.json - Coverage reports: Detailed HTML coverage reports are available as workflow artifacts
- Maintainer override: PRs can bypass coverage requirements using the
coverage-overridelabel when justified
Versioning is automated using Conventional Commits:
feat:commits bump the minor versionfix:commits bump the patch versionBREAKING CHANGEor!bumps the major version
Mathias Raacke develops software professionally since 2000 and uses .net and c# since 2003. He currently works at Diamant Software as part of the Platform-Team that provides Azure Landingzones for the Diamant Software SaaS solution. The Diamant Software Azure platform is developed with 100% IaC and Terraform. Before he moved to the Platform Team, he has been working as software-architect at Diamant since 2012. In the past, Mathias used to work as independent trainer and consultant for .NET development and software architecture, and he developed the WPF localization addin NLocalize for Visual Studio with his own former company Neovelop GmbH.
I'm GitHub Copilot, the AI pair programmer that helped write 100% of this project's code, tests, and documentation. I work as an intelligent coding assistant, providing context-aware suggestions, generating implementations from specifications, and helping maintain code quality throughout the development lifecycle.
For this project, we use a multi-model approach to leverage different AI strengths:
- Claude Sonnet 4.5 - Primary model for requirements engineering, code review, and technical writing
- GPT-5.2-Codex - Latest Codex model for C# code generation, .NET patterns, and development tasks
- Claude Opus 4.5 - Reserved for difficult problems and edge cases where other models struggled
- GPT-5.2 - General-purpose reasoning, architectural decisions, and complex problem-solving
- Gemini 3 Flash - Fast iteration for task planning, release management, and UAT testing
This hybrid approach combines the best capabilities of each model, selecting the right tool for each type of work while maintaining high code quality and development velocity.
MIT

