Skip to content

retailnext/tf-report-action

Repository files navigation

tf-report-action

Reports OpenTofu/Terraform workflow status as PR comments or status issues with rich plan and apply detail.

Features

  • Rich plan diffs — attribute-level changes with inline character diffs, grouped by module
  • Apply reports — shows only actually-changed resources with per-resource outcomes, diagnostics, and resolved computed values when post-apply state is available
  • Progressive enrichment — structured plan → raw text fallback → general workflow table, progressively enriched with whatever data is available
  • PR comments — automatically posts and updates comments on pull requests with workspace-based deduplication
  • Status issues — creates and maintains status issues for non-PR workflows (push to main, scheduled runs)
  • Auto-discovery — automatically identifies init, validate, plan, show-plan, apply, and state steps from the workflow context
  • Limit-aware — intelligently truncates output to fit within GitHub's 65,536 character limit while preserving the most important information
  • Zero runtime dependencies — uses only Node.js built-in modules

Example Output

The action posts a Markdown comment to your PR showing plan details with attribute-level diffs grouped by module. Click to expand:

production Plan: 3 to add, 2 to replace, 2 to destroy

Plan Summary

Action Resource Count
➕ Add null_resource 3
Add 3
± Replace null_resource 1
random_string 1
Replace 2
🗑️ Destroy null_resource 2
Destroy 2

Resource Changes

📦 Module: root

± null_resource root
null_resource.root
Attribute Before After
id 4740972482117666024 (known after apply)
triggers.stage 0 10

📦 Module: module.naming

± random_string suffix
module.naming.random_string.suffix
Attribute Before After
id 0aura6xr (known after apply)
keepers.prefix fixture fixture-updated
result 0aura6xr (known after apply)

📦 Module: module.parent["2"].module.child

🗑️ null_resource item[0]
module.parent["2"].module.child.null_resource.item[0]
Attribute Before After
id 2081773518097729371 2081773518097729371
triggers.index 0 0
triggers.label 2 2
🗑️ null_resource item[1]
module.parent["2"].module.child.null_resource.item[1]
Attribute Before After
id 8179146176311086127 8179146176311086127
triggers.index 1 1
triggers.label 2 2

📦 Module: module.parent["3"].module.child

null_resource item[0]
module.parent["3"].module.child.null_resource.item[0]
Attribute Before After
id (known after apply)
triggers.index 0
triggers.label 3
null_resource item[1]
module.parent["3"].module.child.null_resource.item[1]
Attribute Before After
id (known after apply)
triggers.index 1
triggers.label 3
null_resource item[2]
module.parent["3"].module.child.null_resource.item[2]
Attribute Before After
id (known after apply)
triggers.index 2
triggers.label 3

Outputs

Output Action Before After
generated_name 🔧 fixture-0aura6xr (known after apply)
root_id 🔧 4740972482117666024 (known after apply)

Quick Start

name: OpenTofu Plan

on:
  pull_request:

permissions:
  contents: read
  pull-requests: write

jobs:
  plan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up OpenTofu
        uses: opentofu/setup-opentofu@v2
        with:
          # The wrapper is disabled because it interferes with signal forwarding
          # (opentofu/setup-opentofu#41), detailed exitcodes (opentofu/setup-opentofu#42),
          # and is generally discouraged when using retailnext/exec-action.
          tofu_wrapper: false

      - name: Init
        id: init
        uses: retailnext/exec-action@main
        with:
          command: tofu init -json

      - name: Validate
        id: validate
        uses: retailnext/exec-action@main
        with:
          command: tofu validate -json

      - name: Plan
        id: plan
        uses: retailnext/exec-action@main
        with:
          command: tofu plan -json -detailed-exitcode -out=tfplan
          success_exit_codes: "0,2"

      - name: Show Plan
        id: show-plan
        uses: retailnext/exec-action@main
        with:
          command: tofu show -json tfplan

      - name: Report
        if: always()
        uses: retailnext/tf-report-action@main
        with:
          steps: ${{ toJSON(steps) }}
          github-token: ${{ github.token }}

Usage Examples

Comprehensive Workflow (PR Plan + Merge Apply)

A single workflow that plans on pull requests and applies on merge to main. All steps use -json where available for the richest output. The workspace input identifies the report for deduplication.

On PRs, the action posts a comment with the plan. On push to main, it creates or updates a status issue with the apply result.

name: Infrastructure

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  pull-requests: write
  issues: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up OpenTofu
        uses: opentofu/setup-opentofu@v2
        with:
          # The wrapper is disabled because it interferes with signal forwarding
          # (opentofu/setup-opentofu#41), detailed exitcodes (opentofu/setup-opentofu#42),
          # and is generally discouraged when using retailnext/exec-action.
          tofu_wrapper: false

      - name: Init
        id: init
        uses: retailnext/exec-action@main
        with:
          command: tofu init -json

      - name: Validate
        id: validate
        uses: retailnext/exec-action@main
        with:
          command: tofu validate -json

      - name: Plan
        id: plan
        uses: retailnext/exec-action@main
        with:
          command: tofu plan -json -detailed-exitcode -out=tfplan
          success_exit_codes: "0,2"

      - name: Show Plan
        id: show-plan
        uses: retailnext/exec-action@main
        with:
          command: tofu show -json tfplan

      - name: Apply
        id: apply
        if: github.event_name == 'push'
        uses: retailnext/exec-action@main
        with:
          command: tofu apply -json tfplan

      - name: State
        id: state
        if: steps.apply.outcome != 'skipped'
        uses: retailnext/exec-action@main
        with:
          command: tofu state pull
          # Hide outputs to avoid leaking sensitive state values in the
          # GitHub Actions log. The state is still available to the report
          # action via the stdout_file output.
          hide_outputs: true

      - name: Report
        if: always()
        uses: retailnext/tf-report-action@main
        with:
          steps: ${{ toJSON(steps) }}
          workspace: production
          github-token: ${{ github.token }}

Multiple Workspaces via Matrix

When managing multiple workspaces from the same repository, use a matrix to run each workspace in its own working directory. Each workspace gets a separate PR comment, identified by its deduplication marker.

name: Infrastructure

on:
  pull_request:

permissions:
  contents: read
  pull-requests: write

jobs:
  plan:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        workspace:
          - name: staging
            directory: envs/staging
          - name: production
            directory: envs/production
    defaults:
      run:
        working-directory: ${{ matrix.workspace.directory }}
    steps:
      - uses: actions/checkout@v4

      - name: Set up OpenTofu
        uses: opentofu/setup-opentofu@v2
        with:
          # The wrapper is disabled because it interferes with signal forwarding
          # (opentofu/setup-opentofu#41), detailed exitcodes (opentofu/setup-opentofu#42),
          # and is generally discouraged when using retailnext/exec-action.
          tofu_wrapper: false

      - name: Init
        id: init
        uses: retailnext/exec-action@main
        with:
          command: tofu init -json
          working_directory: ${{ matrix.workspace.directory }}

      - name: Plan
        id: plan
        uses: retailnext/exec-action@main
        with:
          command: tofu plan -json -detailed-exitcode -out=tfplan
          success_exit_codes: "0,2"
          working_directory: ${{ matrix.workspace.directory }}

      - name: Show Plan
        id: show-plan
        uses: retailnext/exec-action@main
        with:
          command: tofu show -json tfplan
          working_directory: ${{ matrix.workspace.directory }}

      - name: Report
        if: always()
        uses: retailnext/tf-report-action@main
        with:
          steps: ${{ toJSON(steps) }}
          workspace: ${{ matrix.workspace.name }}
          github-token: ${{ github.token }}

Reporting Non-IaC Workflows with target-step

The action can report on any workflow, not just Terraform/OpenTofu. The target-step input focuses the report on a specific step — if that step is skipped or fails, the report prominently surfaces the failure.

When no recognized IaC steps are found, the action renders a general workflow step status table showing each step's outcome, exit code, and duration.

name: Database Migration

on:
  push:
    branches: [main]

permissions:
  contents: read
  issues: write

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Migrations
        id: migrate
        uses: retailnext/exec-action@main
        with:
          command: npm run db:migrate

      - name: Verify Schema
        id: verify
        uses: retailnext/exec-action@main
        with:
          command: npm run db:verify

      - name: Report
        if: always()
        uses: retailnext/tf-report-action@main
        with:
          steps: ${{ toJSON(steps) }}
          target-step: migrate
          workspace: db-migrations
          github-token: ${{ github.token }}

Inputs

Input Required Default Description
steps Yes JSON string of workflow steps (use ${{ toJSON(steps) }})
workspace No GITHUB_WORKFLOW/GITHUB_JOB Workspace name for comment title, status issue title, and deduplication marker
target-step No Step ID to focus the report on; skipped/failed status is prominently reported
github-token Yes GitHub token for API calls
init-step-id No init Step ID for the init step (override when your workflow uses non-default IDs)
validate-step-id No validate Step ID for the validate step
plan-step-id No plan Step ID for the plan step
show-plan-step-id No show-plan Step ID for the show-plan step
apply-step-id No apply Step ID for the apply step
state-step-id No state Step ID for the state step (post-apply state for resolving computed values)

How It Works

Auto-Discovery

The action automatically identifies IaC workflow steps by their step IDs. It looks for steps matching the configured step IDs (defaulting to init, validate, plan, show-plan, apply, and state) and determines which operations were performed.

Progressive Enrichment

The action progressively enriches the report with whatever data is available:

  1. Tier 1 — Structured report: When a show-plan step provides plan JSON, the action renders a full report with attribute-level diffs, module grouping, and inline character highlighting. Adding -json to plan and apply steps further enriches the report with diagnostics, drift detection, and per-resource apply outcomes. When a state step provides post-apply state JSON (via state pull), attribute values that were unknown at plan time (such as provider-generated IDs) are resolved to their actual values in the apply report. Without the state step, these values are shown as "(value not in plan)".
  2. Tier 3 — Raw text fallback: When no show-plan JSON is available but plan or apply steps ran, the action formats their raw output with JSON Lines parsing where possible.
  3. Tier 4 — Workflow table: When no recognized IaC steps are found, the action renders a general workflow step status table. This is the mode used when reporting non-IaC workflows via target-step.

Comment Lifecycle

Each report includes an HTML comment marker for deduplication:

<!-- tf-report-action:"WORKSPACE_NAME" -->
  • PR context: Old comments with the same workspace marker are deleted before posting the new one, ensuring the latest status appears at the bottom of the conversation.
  • Non-PR context: The action searches for an existing issue with the marker and updates it, or creates a new one if none exists.

Only bot-authored comments are deleted — human comments are never touched, even if they contain the marker text.

Permissions

Context Required Permission
Pull request pull-requests: write
Non-PR (status issues) issues: write
Both PR and push triggers Both pull-requests: write and issues: write

The contents: read permission is always needed to check out the repository.

Size Limits

GitHub enforces a 65,536 character limit on comment and issue bodies. The action automatically manages output within this limit:

  • The rendering engine uses a compositor that progressively degrades sections (full → compact → omit) to fit within the limit
  • The footer (logs link, timestamp) is reserved from the budget before rendering
  • A 512-character safety margin prevents edge cases from exceeding the limit

Development

# Install dependencies
npm ci

# Run all linters
npm run lint

# Type check
npm run typecheck

# Run tests
npm run test

# Run tests with coverage
npm run test:coverage:ci

# Run integration tests with coverage
npm run test:integration:coverage

# Bundle for distribution
npm run bundle

# Format code
npm run format

# Full CI pipeline
npm run ci

Attribution

Output format inspired by tfplan2md by oocx, used under the MIT License.

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors