DEV Community

Amaresh Pelleti
Amaresh Pelleti

Posted on Originally published at devtoolhub.com

The terraform-docs GitHub Action: A Complete CI Setup Guide

Originally published on DevToolHub.

The terraform-docs GitHub Action generates a Markdown table of every input, output, and variable in a Terraform module, then commits it straight into your README.md on every pull request. The official docs page covers one example and stops there — no full input list, no config-file setup, and nothing about the one failure mode that catches almost everyone the first time they wire this into a real repo.

This covers what the official page skips: every input the action actually supports, the difference between fail-on-diff and auto-commit mode, how to document more than one module at once, and why the auto-commit step silently fails on pull requests from a fork.

What the terraform-docs GitHub Action Actually Sets Up

The action wraps the terraform-docs CLI and runs it inside your workflow instead of on a developer's laptop. On a pull_request trigger, it walks your working-dir, generates the docs, and either prints them, replaces the target file, or injects them between two HTML comment markers in your existing README.md.

That third mode — output-method: inject — is the default, and it's the one worth understanding first. It looks for <!-- BEGIN_TF_DOCS --> and <!-- END_TF_DOCS --> markers in the file. If they're there, the generated table replaces everything between them and leaves the rest of your README untouched. If the file doesn't have the markers yet, the action appends the generated block to the end. If the file doesn't exist at all, it creates one using the template input, which must itself contain both markers or the next run has nowhere to inject into.

Every terraform-docs GitHub Action Input, Not Just the One in the Docs

The official page's example sets four inputs and calls it done: working-dir, output-file, output-method, git-push. The action's actual README lists 18. Here's the full set:

Input Default What it does
working-dir . Comma-separated list of directories to generate docs for
output-file README.md File in the module directory where docs get written
output-format markdown table terraform-docs output format (ignored if config-file is set)
output-method inject print, replace, or inject
config-file disabled Name of a terraform-docs config file to use instead of individual inputs
atlantis-file disabled Parse an Atlantis config to find module directories automatically
find-dir disabled Run a find under this directory to discover .tf files
recursive false Update submodules recursively instead of just working-dir
recursive-path modules Submodule path to walk when recursive is true
fail-on-diff false Fail the job if generated docs differ from what's committed (ignored if git-push is set)
git-push false Commit and push the generated docs back to the branch
git-commit-message terraform-docs: automated action Commit message for the auto-push
git-push-user-name / git-push-user-email empty Defaults to the github-actions[bot] identity if left blank
git-push-sign-off false Add a Signed-off-by trailer to the commit
template HTML comment markers Used only when output-file doesn't already exist
indention 2 Markdown heading indent level, 1 through 5
args "" Extra flags passed straight through to the terraform-docs binary

fail-on-diff is explicitly ignored the moment git-push is set to true. And output-format does nothing once you set config-file, since the config file's own formatter key takes over.

Getting Auto-Commit Working: The checkout Step Most Setups Get Wrong

- uses: actions/checkout@v3
  with:
    ref: ${{ github.event.pull_request.head.ref }}
Enter fullscreen mode Exit fullscreen mode

Without that ref override, actions/checkout defaults to a detached-HEAD merge commit — a synthetic ref GitHub builds to preview the merge. You can commit to it, but the commit goes nowhere real. Set ref to github.event.pull_request.head.ref and checkout lands on the actual PR branch.

name: Generate terraform docs
on:
  - pull_request
jobs:
  docs:
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.ref }}
      - uses: terraform-docs/gh-actions@v1.4.1
        with:
          working-dir: .
          output-file: README.md
          output-method: inject
          git-push: "true"
Enter fullscreen mode Exit fullscreen mode

That permissions: contents: write block isn't in the action's own examples, but you need it — some orgs default GITHUB_TOKEN to read-only, and the push fails before it even reaches the fork-specific problem below.

Why terraform-docs GitHub Action Fails on Forked Pull Requests

GitHub's workflow-syntax docs are explicit: when a workflow runs on a pull_request "from a forked repository," the GITHUB_TOKEN's "permissions are adjusted to change any write permissions to read only" — regardless of your permissions: block. You "can use the permissions key to add and remove read permissions for forked repositories, but typically you can't grant write access."

Your workflow can have contents: write right there in the YAML, git-push set to true, and the checkout ref pointing at the exact right branch — none of it matters on a fork PR. The action generates the docs correctly, then fails on the push step with a 403.

Two real workarounds:

  • Switch to fail-on-diff for external PRs — you lose auto-commit, but the check still catches stale docs, and a human commits the fix.
  • Use pull_request_target instead of pull_request. It grants a full read/write token on fork PRs, but runs in the base branch's context — never check out head.ref from an untrusted fork with this event for anything beyond running terraform-docs itself.

fail-on-diff or Auto-Commit: Which Mode Should You Actually Use?

fail-on-diff fails the job if the generated output doesn't match README.md — nothing gets pushed, a human commits the fix. That's the safer default for a public or multi-contributor repo. Auto-commit (git-push: true) fits an internal repo with a small trusted team, where a docs-bot commit is a fair trade for never having a stale README. Since the two are mutually exclusive on the same job, decide per repo.

Documenting Every Module at Once

working-dir as a comma-separated list (working-dir: .,modules/vpc,modules/eks) works when you know every path already. recursive: true walks recursive-path (default modules) and picks up new submodules without touching the workflow file. atlantis-file reuses your existing atlantis.yaml project list. find-dir runs a plain find for .tf files — the least structured option, useful with no fixed module convention yet.

Using a Config File Instead of Piling On Inputs

Past four or five non-default inputs, a .terraform-docs.yml is easier to manage than a wall of with: keys:

formatter: "markdown table"
output:
  file: README.md
  mode: inject
  template: |-
    <!-- BEGIN_TF_DOCS -->
    {{ .Content }}
    <!-- END_TF_DOCS -->
sort:
  enabled: true
  by: name
Enter fullscreen mode Exit fullscreen mode

Set config-file: .terraform-docs.yml on the action, and output-format stops doing anything — the config file's formatter key wins.

Quick Summary:

  • 18 real inputs vs the 1 example shown on the official docs page
  • fail-on-diff and git-push are mutually exclusive on the same job
  • Auto-commit needs ref: ${{ github.event.pull_request.head.ref }} on checkout
  • Forked-repo PRs always get a read-only GITHUB_TOKEN — auto-commit silently fails there
  • recursive, atlantis-file, find-dir cover three different multi-module layouts

Full breakdown at DevToolHub.

Top comments (0)