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 }}
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"
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-difffor external PRs — you lose auto-commit, but the check still catches stale docs, and a human commits the fix. -
Use
pull_request_targetinstead ofpull_request. It grants a full read/write token on fork PRs, but runs in the base branch's context — never check outhead.reffrom an untrusted fork with this event for anything beyond runningterraform-docsitself.
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
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-diffandgit-pushare 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-dircover three different multi-module layouts
Full breakdown at DevToolHub.
Top comments (0)