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
- 🔮 Known-after-apply visibility - Computed attributes (
after_unknown) are shown in reports with(known after apply)labels, including configuration references (e.g.,azuread_group.admins) when available, instead of being silently omitted - 🔧 Specialized templates - Custom rendering for complex resources (Azure Firewall rules, NSG rules, Azure DevOps build definitions and variable groups, Azure AD resources, and inline parent-child tables for memberships and Azure network resources)
- 📚 Azure API documentation links - Reliable links to Microsoft Learn REST API documentation for 92 Azure resource types (AzAPI provider)
- 🔇 Case-insensitive Azure ID filter - Azure resource ID attribute changes that differ only in casing (ARM API quirk) are suppressed by default (
--ignore-azure-id-case-changes), reducing noise in reports
tfplan2md is distributed in multiple ways to suit different environments:
Install tfplan2md using Homebrew:
brew tap oocx/tfplan2md
brew install tfplan2mdTo upgrade to the latest version:
brew upgrade tfplan2mdSupported Platforms:
- macOS ARM64 (Apple Silicon - M1/M2/M3)
- Linux x64 (including WSL)
Recommended for:
- macOS and Linux users who prefer package manager installation
- Development environments where Homebrew is already installed
- Users who want automatic update notifications via
brew outdated
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.
Recommended for:
- Containerized environments
- CI/CD pipelines with Docker support
- Users who prefer isolated, reproducible builds
- Alpine Linux or musl-based systems
Available starting with the next release
Download pre-built binaries for your platform from GitHub Releases:
| Platform | Architecture | Archive | Notes |
|---|---|---|---|
| Linux | x64 | tfplan2md_VERSION_linux-x64.tar.gz |
Ubuntu 24.04+, Debian 13+, RHEL 10+ (glibc 2.39) |
| Linux | ARM64 | tfplan2md_VERSION_linux-arm64.tar.gz |
Ubuntu 24.04+, Debian 13+, RHEL 10+ (glibc 2.39) |
| Windows | x64 | tfplan2md_VERSION_windows-x64.zip |
Windows 10+ (x64) |
| macOS | Apple Silicon | tfplan2md_VERSION_macos-arm64.tar.gz |
macOS 11+ (M1/M2/M3) |
-
Download the binary for your platform:
VERSION="1.x.x" # Replace with desired version PLATFORM="linux-x64" # Choose: linux-x64, linux-arm64, windows-x64, macos-arm64 # Linux/macOS (tar.gz) wget https://github.com/oocx/tfplan2md/releases/download/v${VERSION}/tfplan2md_${VERSION}_${PLATFORM}.tar.gz # Windows (zip) - use PowerShell # Invoke-WebRequest -Uri "https://github.com/oocx/tfplan2md/releases/download/v${VERSION}/tfplan2md_${VERSION}_windows-x64.zip" -OutFile "tfplan2md_${VERSION}_windows-x64.zip"
-
Download and verify checksums:
# Download consolidated checksums wget https://github.com/oocx/tfplan2md/releases/download/v${VERSION}/SHA256SUMS # Verify (Linux/macOS) sha256sum -c SHA256SUMS --ignore-missing # Verify (Windows PowerShell) # $expectedHash = (Get-Content SHA256SUMS | Select-String "windows-x64").Line.Split()[0] # $actualHash = (Get-FileHash tfplan2md_${VERSION}_windows-x64.zip).Hash.ToLower() # if ($expectedHash -eq $actualHash) { "OK" } else { "FAILED" }
Expected output:
tfplan2md_VERSION_PLATFORM.tar.gz: OK -
Extract and run:
# Linux/macOS tar -xzf tfplan2md_${VERSION}_${PLATFORM}.tar.gz ./tfplan2md --help terraform show -json plan.tfplan | ./tfplan2md > plan.md # Windows (PowerShell) # Expand-Archive tfplan2md_${VERSION}_windows-x64.zip # .\tfplan2md.exe --help
Linux:
- glibc 2.39 or newer (binaries built on Ubuntu 24.04)
- No .NET runtime required (self-contained NativeAOT)
- Supported distributions:
- Ubuntu 24.04 LTS or newer
- Debian 13 (Trixie) or newer
- RHEL 10 or newer
- Other glibc-based distributions with glibc 2.39+
- Note: For Alpine Linux or musl-based systems, use the Docker image
Windows:
- Windows 10 version 1607 or newer (x64)
- No .NET runtime required (self-contained NativeAOT)
- Note: Windows ARM64 builds are not currently available. Use x64 binary (runs via emulation) or Docker image.
macOS:
- macOS 11 (Big Sur) or newer for Apple Silicon builds
- No .NET runtime required (self-contained NativeAOT)
- Note: Intel (x64) builds are not currently available. Use Docker image or build from source.
Recommended for:
- Closed/air-gapped systems where Docker images cannot be pulled
- High compliance or regulatory environments
- Environments without container runtime or Homebrew
- Local development and testing without Docker overhead
- Windows users (Homebrew not available on native Windows)
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) |
--details <auto|open|closed> |
Control resource details display: auto (expand resources with findings, default), open (expand all), closed (collapse all) |
--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) |
--ignore-azure-id-case-changes |
Suppress attribute change rows where before/after values are Azure resource IDs that differ only in casing (see below) |
--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 collapsible "Debug Information" section at the end of the report (collapsed by default to reduce visual clutter) 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 --details option controls whether resource details blocks are initially expanded or collapsed in the generated markdown report. This is particularly useful for large plans or when focusing on resources with code analysis findings:
# Collapse all resources by default (clean, scannable view)
terraform show -json plan.tfplan | tfplan2md --details closed
# Expand all resources by default
terraform show -json plan.tfplan | tfplan2md --details open
# Auto mode: expand only resources with code analysis findings (default)
terraform show -json plan.tfplan | tfplan2md --details auto \
--code-analysis-results checkov-results.sarifDisplay Modes:
auto(default): Automatically expand resources that have code analysis findings attached, collapse others. This helps draw attention to security and quality issues while keeping the report clean.open: Expand all resource details blocks. Useful for small plans or when you want to review everything in detail.closed: Collapse all resource details blocks. Provides a clean overview where you can selectively expand resources of interest.
Users can always manually expand or collapse details blocks by clicking the summary line, regardless of the initial state. The debug information block (enabled with --debug) is always collapsed regardless of the --details setting.
The Azure ARM API occasionally returns resource IDs with different capitalisation on successive reads (for example, /subscriptions/ABC123/resourceGroups/my-rg versus /subscriptions/abc123/resourceGroups/my-rg). Terraform detects these as changes, and tfplan2md faithfully reports them — which can create noise for reviewers who need to focus on real infrastructure changes.
This filtering is enabled by default. The --ignore-azure-id-case-changes flag explicitly requests this behaviour (same as the default). To see all changes including Azure ID casing differences, this feature currently requires a custom template or running an older version.
# Suppress Azure ID casing noise (default behaviour, flag is optional)
tfplan2md plan.json --ignore-azure-id-case-changesExample: A Terraform plan reports two changes on an azurerm_role_assignment:
| Attribute | Before | After |
|---|---|---|
scope |
/subscriptions/ABC123/resourceGroups/my-rg |
/subscriptions/abc123/resourceGroups/my-rg |
role_definition_id |
/subscriptions/ABC123/providers/Microsoft.Authorization/roleDefinitions/… |
/subscriptions/abc123/providers/… |
display_name |
My App |
My Application |
By default (with --ignore-azure-id-case-changes), only the display_name row is shown — the two Azure ID casing rows are suppressed.
Important notes:
- Only Azure resource ID attribute values (subscription, resource group, resource, or management group scope) are subject to the filter. Plain display names, numeric values, boolean values, and null values are never suppressed.
- The filter applies to
azurermresources only. Resources from other providers (e.g.,azapi,aws) are unaffected even if their attribute values look like Azure resource IDs. - Rows suppressed by
--ignore-azure-id-case-changesremain hidden even when--show-unchanged-valuesis also active — casing-only Azure ID changes take precedence. - The summary counts (resources to add, change, destroy) are not affected by this flag.
The --principal-mapping file can include Azure AD principals, Azure metadata (subscriptions, management groups, tenants, roles), and Azure DevOps entities (users, groups, projects).
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" }
],
"azdoUsers": {
"4a2c5e2b-3b4f-4e6f-8a9b-1c2d3e4f5a6b": "John Smith",
"7f8e9d0c-1b2a-3c4d-5e6f-7a8b9c0d1e2f": "Alice Johnson"
},
"azdoGroups": {
"vssgp.Uy0xLTktMTU1MTM...": "Platform Team",
"aadgp.Uy0.ReleaseManagers": "Release Managers"
},
"azdoProjects": {
"8f7e6d5c-4b3a-2c1d-0e9f-8a7b6c5d4e3f": "Infrastructure Project",
"1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d": "Application Platform"
},
"azdoRepositories": {
"a1b2c3d4-e5f6-7a8b-9c0d-1e2f3a4b5c6d": "Infrastructure Repo",
"f9e8d7c6-b5a4-3210-fedc-ba9876543210": "Web Application Repo"
}
}Azure DevOps sections (optional):
azdoUsers: Map Azure DevOps user GUIDs to display namesazdoGroups: Map Azure DevOps group descriptors to display names (supports long descriptors)azdoProjects: Map Azure DevOps project GUIDs to display namesazdoRepositories: Map Azure DevOps repository GUIDs to display names
Azure DevOps entities are automatically resolved in group memberships, team rosters, project references, and repository attributes. Repositories display as 🗃️ DisplayName [GUID] when mapped. Branch/ref attributes (e.g., default_branch, branch_name) are shown with the ⎇ icon for improved visual scanning.
Use the Azure CLI to export the Azure AD and Azure infrastructure mapping sections (each command returns the array-of-objects format):
# Principals (Azure AD)
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 jsonAzure DevOps sections must be created manually as the Azure DevOps CLI does not provide direct export commands for this format. Collect user GUIDs, group descriptors, project GUIDs, and repository GUIDs from your Azure DevOps organization and add them to the azdoUsers, azdoGroups, azdoProjects, and azdoRepositories sections in the JSON file.
Use scripts/validate-azure-cli-commands.sh to validate the Azure CLI 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-release-screenshots.sh for release notes - it generates a single 580×400 screenshot optimized for release documentation:
scripts/generate-release-screenshots.sh \
--plan examples/firewall-with-static-analysis/plan.json \
--output-prefix feature-065-icons \
--output-dir docs/features/043-tenant-display-mapping \
--selector "details:has(summary:has-text('azurerm_role_assignment'))" \
--render-target githubThis generates a single PNG file at the specified size (default 580×400) in light mode.
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, forget)
- 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 servicesazapi_update_resource- Applies intelligent attribute grouping and array rendering to partial Azure resource updates; includes Azure API documentation linksazurerm_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_build_definition- Shows pipeline variables, triggers, repository configuration, schedules, and jobs as structured tables with secret variable protectionazuredevops_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

