Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b59721f
feat(gha): download + run binary with given dirs
allejo Feb 26, 2026
0d568db
feat(gha): add opt-in commiting functionality
allejo Feb 26, 2026
bf14b8e
feat(gha): add fail-on-diff mode
allejo Feb 26, 2026
eb47028
feat(gha): save JSON representation of each processed module as output
allejo Feb 26, 2026
d490b58
feat(gha): treat fail_on_diff and commit as booleans
allejo Feb 26, 2026
74540ea
docs(gha): update README with new action documentation
allejo Feb 26, 2026
3bab149
feat(gha): support * and ** for directory paths
allejo Feb 27, 2026
3a8db2a
feat(gha): download latest pre-release if no stable
allejo Feb 27, 2026
65a30eb
docs: update actions/checkout version in README
allejo Mar 3, 2026
3336954
fix(gha): address Copilot review feedback
allejo Mar 26, 2026
0e68c54
fix(gha): implement unused commit_branch arg
allejo Mar 27, 2026
18d8f09
feat(gha): add format + linting jobs to workflows
allejo Mar 27, 2026
dc2dff7
chore(gha): address first batch of linting errors
allejo Mar 27, 2026
5c77eca
docs: format README + address linter warnings
allejo Mar 27, 2026
4f6f6b7
fix(gha): don't fail silently on GH API calls
allejo Mar 27, 2026
488bf12
docs: show GH template syntax for token in arg table
allejo Mar 27, 2026
feac1c4
feat(gha): apply PR feedback from Copilot
allejo Mar 27, 2026
8e98cb2
feat(gha): use GH_TOKEN for gh/git operations
allejo Mar 27, 2026
55df454
fix(ci/cd): setup Go before formatting step
allejo Mar 27, 2026
03faae3
feat(gha): enable Bash debug when runner.debug is true
allejo Mar 27, 2026
9e9a271
feat(gha): don't silence gh errors
allejo Mar 27, 2026
13b0969
fix(gha): don't kill workflow when 'gh' command fails
allejo Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 134 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ This project includes a rudimentary CLI tool that reads a Terraform module folde
The tool accepts a single argument specifying the path to a Terraform module folder. It will read the module folder using Terraform Docs, parse the variable definitions using this library, and write the output to a `README.md` file in the module folder.

```bash
./tfdocs-extra /path/to/TerraformModules/aws/route53
./tfdocs-extras /path/to/TerraformModules/aws/route53
```

The README requires specific markers to identify where to insert the generated documentation. The generated markdown will be inserted between the following markers:
Expand All @@ -165,6 +165,139 @@ The README requires specific markers to identify where to insert the generated d
<!-- TFDOCS_EXTRAS_END -->
```

### JSON Output

Pass the `-json` flag to print a JSON representation of the parsed module manifest to stdout instead of updating the README. This is useful for piping the output into other tools or for debugging.

```bash
./tfdocs-extras -json /path/to/TerraformModules/aws/route53
```

## Usage as a GitHub Action

This repository is also published as a GitHub Action. It automatically downloads the appropriate binary for the runner's OS and architecture, scans the specified directories for `README.md` files containing both the `TFDOCS_EXTRAS_START`/`TFDOCS_EXTRAS_END` markers, and processes each one.

Each entry in `directories` is either an explicit path or a glob pattern:

| Pattern | Behavior |
|---|---|
| `./modules/aws/vpc` | Process this single directory. |
| `./modules/aws/*` | Process every direct subdirectory of `aws/`. |
| `./modules/**` | Recursively process all subdirectories. |

Directories that do not contain a `README.md` with both the `TFDOCS_EXTRAS_START`/`TFDOCS_EXTRAS_END` markers are silently skipped.

```yaml
- uses: FriendsOfTerraform/tfdocs-extras@main
with:
directories: |
./modules/aws/vpc
./modules/aws/s3
./modules/gcp/*
./modules/azure/**
```

### Inputs

| Input | Required | Default | Description |
|---|---|---|---|
| `directories` | Yes | | Newline-separated list of Terraform module directories to process. Supports `*` and `**` glob patterns. |
| `version` | No | `latest` | Version of tfdocs-extras to download (e.g. `v0.1.0`). |
| `token` | No | `github.token` | GitHub token used to download the release binary. |
Comment thread
allejo marked this conversation as resolved.
Outdated
| `commit` | No | `false` (boolean) | Commit any README.md changes after processing. Mutually exclusive with `fail_on_diff`. |
| `commit_message` | No | `chore: update tfdocs-extras documentation` | Commit message. Only used when `commit` is `true`. |
| `commit_author` | No | `github-actions[bot] <github-actions[bot]@users.noreply.github.com>` | Commit author in `Name <email>` format. Only used when `commit` is `true`. |
| `commit_branch` | No | Current branch | Branch to push the commit to. Only used when `commit` is `true`. |
| `fail_on_diff` | No | `false` (boolean) | Exit with a non-zero status if any README.md files were modified. Mutually exclusive with `commit`. |
| `json_output_file` | No | | Path to write the aggregated JSON output. Use this for large outputs that exceed GitHub Actions' output size limit. |

### Outputs

| Output | Description |
|---|---|
| `result` | JSON array of module manifests, one object per processed directory. Only set if the total size is below ~1MB (GitHub Actions output limit). For larger outputs, use the `json_output_file` input to write to a file instead. |

### Examples

#### Auto-commit updated documentation

Automatically regenerate and commit documentation whenever Terraform files change on the main branch.

```yaml
on:
push:
branches: [main]
paths: ['**.tf']

jobs:
docs:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: FriendsOfTerraform/tfdocs-extras@main
with:
directories: |
./modules/aws/vpc
./modules/aws/s3
commit: true
commit_author: 'github-actions[bot] <github-actions[bot]@users.noreply.github.com>'
```

#### Enforce up-to-date documentation in pull requests

Fail the CI check if a pull request contains Terraform changes without updated documentation.

```yaml
on:
pull_request:
paths: ['**.tf']

jobs:
docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: FriendsOfTerraform/tfdocs-extras@main
with:
directories: |
./modules/aws/vpc
./modules/aws/s3
./modules/azure/*
./modules/vault/*
fail_on_diff: true
```

#### Use the JSON output in a downstream step

```yaml
- uses: actions/checkout@v6
- uses: FriendsOfTerraform/tfdocs-extras@main
id: tfdocs
with:
directories: ./modules/aws/vpc
- run: echo '${{ steps.tfdocs.outputs.result }}'
```

#### Write JSON output to a file for large module sets

When processing many modules or modules with large manifests, the aggregated JSON may exceed GitHub Actions' output size limit (~1MB). Use `json_output_file` to write the output to a file instead.

```yaml
- uses: actions/checkout@v6
- uses: FriendsOfTerraform/tfdocs-extras@main
with:
directories: |
./modules/**
json_output_file: ./tfdocs-manifests.json
- name: Upload manifest
uses: actions/upload-artifact@v4
with:
name: tfdocs-manifests
path: ./tfdocs-manifests.json
```

## Documentation Specification

> [!IMPORTANT]
Expand Down
242 changes: 242 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
name: 'tfdocs-extras'
description: 'Generate Terraform object() type documentation in README.md files using tfdocs-extras'
author: 'Vladimir "allejo" Jimenez'

branding:
icon: 'book-open'
color: 'purple'

inputs:
directories:
description: |
Newline-separated list of Terraform module directories to process. Each directory
will be checked for a README.md containing TFDOCS_EXTRAS_START/TFDOCS_EXTRAS_END
markers and processed if found.
Comment thread
allejo marked this conversation as resolved.
required: true
version:
description: 'Version of tfdocs-extras to download (e.g. "v0.1.0"). Defaults to the latest release.'
required: false
default: 'latest'
token:
description: 'GitHub token used to download the release binary and avoid API rate limits.'
required: false
default: ${{ github.token }}
Comment thread
allejo marked this conversation as resolved.
Outdated
commit:
description: 'Commit any README.md changes after processing.'
required: false
default: 'false'
commit_message:
description: 'Commit message to use when commit is true.'
required: false
default: 'chore: update tfdocs-extras documentation'
commit_author:
description: 'Author identity for the commit in "Name <email>" format. Only used when commit is true.'
required: false
default: 'github-actions[bot] <github-actions[bot]@users.noreply.github.com>'
commit_branch:
description: 'Branch to push the commit to. Only used when commit is true. Defaults to the current branch.'
required: false
default: ''
Comment thread
allejo marked this conversation as resolved.
Outdated
fail_on_diff:
description: 'Exit with a non-zero status if any README.md files were modified. Mutually exclusive with commit.'
required: false
default: 'false'
json_output_file:
description: 'Path to write the aggregated JSON output. If not specified, JSON is only written to the step output (subject to size limits).'
required: false
default: ''

outputs:
result:
description: 'JSON array of module manifests, one object per processed directory.'
value: ${{ steps.collect-json.outputs.result }}

runs:
using: composite
steps:
- name: Validate inputs
shell: bash
run: |
if ${{ fromJSON(inputs.commit) }} && ${{ fromJSON(inputs.fail_on_diff) }}; then
echo "::error::commit and fail_on_diff are mutually exclusive"
exit 1
fi

- name: Download tfdocs-extras
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
Comment thread
allejo marked this conversation as resolved.
Outdated
VERSION: ${{ inputs.version }}
run: |
# Detect OS
case "${{ runner.os }}" in
Linux) BINARY_OS="linux" ;;
macOS) BINARY_OS="darwin" ;;
Windows) BINARY_OS="windows" ;;
*) echo "::error::Unsupported OS: ${{ runner.os }}"; exit 1 ;;
esac

# Detect architecture
case "${{ runner.arch }}" in
X64) BINARY_ARCH="amd64" ;;
ARM64) BINARY_ARCH="arm64" ;;
*) echo "::error::Unsupported architecture: ${{ runner.arch }}"; exit 1 ;;
esac

# Resolve 'latest' to the actual version tag
if [ "$VERSION" = "latest" ]; then
# Prefer the latest stable release; gh release view only returns a
# release marked "latest" on GitHub, which is never a pre-release.
VERSION=$(gh release view \
--repo FriendsOfTerraform/tfdocs-extras \
--json tagName \
--jq '.tagName' 2>/dev/null) || true

if [ -z "$VERSION" ]; then
# No stable release exists; fall back to the most recent pre-release.
echo "No stable release found, checking for pre-releases..."
VERSION=$(gh release list \
--repo FriendsOfTerraform/tfdocs-extras \
--json tagName,isPrerelease \
--jq '[.[] | select(.isPrerelease)] | first | .tagName')
Comment thread
allejo marked this conversation as resolved.
Outdated

if [ -z "$VERSION" ] || [ "$VERSION" = "null" ]; then
echo "::error::No stable release or pre-release found"
exit 1
fi
echo "Resolved latest pre-release: $VERSION"
else
echo "Resolved latest stable release: $VERSION"
fi
fi

# Build binary name matching the release asset naming convention
EXT=""
[ "$BINARY_OS" = "windows" ] && EXT=".exe"
BINARY_NAME="tfdocs-extras-${VERSION}-${BINARY_OS}-${BINARY_ARCH}${EXT}"
DEST="${RUNNER_TEMP}/tfdocs-extras${EXT}"

echo "Downloading $BINARY_NAME..."
gh release download "$VERSION" \
--repo FriendsOfTerraform/tfdocs-extras \
--pattern "$BINARY_NAME" \
--output "$DEST"
chmod +x "$DEST"

echo "TFDOCS_EXTRAS_BIN=$DEST" >> "$GITHUB_ENV"

- name: Process README.md files
id: collect-json
shell: bash
env:
DIRECTORIES: ${{ inputs.directories }}
JSON_OUTPUT_FILE: ${{ inputs.json_output_file }}
run: |
# globstar enables ** for recursive matching; nullglob silently drops
# patterns that match nothing rather than treating them as literals
shopt -s globstar nullglob

MARKER_START="<!-- TFDOCS_EXTRAS_START -->"
MARKER_END="<!-- TFDOCS_EXTRAS_END -->"
PROCESSED=0
JSON_ITEMS=""

while IFS= read -r pattern; do
# Skip blank/whitespace-only lines
[ -z "$(echo "$pattern" | tr -d '[:space:]')" ] && continue

# Expand the pattern into an array so paths with spaces are preserved
dirs=( $pattern )
Comment thread
allejo marked this conversation as resolved.
Outdated

for dir in "${dirs[@]}"; do
[ -d "$dir" ] || continue

readme="${dir}/README.md"
[ -f "$readme" ] || continue
grep -qF "$MARKER_START" "$readme" || continue
grep -qF "$MARKER_END" "$readme" || continue
Comment thread
allejo marked this conversation as resolved.
Outdated

echo "Processing: $dir"
"$TFDOCS_EXTRAS_BIN" "$dir"

JSON=$("$TFDOCS_EXTRAS_BIN" -json "$dir")
Comment thread
allejo marked this conversation as resolved.
Outdated
[ -n "$JSON_ITEMS" ] && JSON_ITEMS+=","
JSON_ITEMS+="$JSON"

PROCESSED=$((PROCESSED + 1))
done
done <<< "$DIRECTORIES"

echo "Done: processed $PROCESSED director(ies)"
Comment thread
allejo marked this conversation as resolved.
Outdated

# Build the final JSON array
FINAL_JSON="[${JSON_ITEMS}]"

# Write to file if path is specified
if [ -n "$JSON_OUTPUT_FILE" ]; then
echo "Writing JSON output to: $JSON_OUTPUT_FILE"
Comment thread
allejo marked this conversation as resolved.
echo "$FINAL_JSON" > "$JSON_OUTPUT_FILE"
fi

# GitHub Actions outputs have a size limit of ~1MB. Only set the output
# if the JSON is below this threshold.
JSON_SIZE=${#FINAL_JSON}
MAX_OUTPUT_SIZE=1000000 # ~1MB

if [ $JSON_SIZE -lt $MAX_OUTPUT_SIZE ]; then
{
echo 'result<<EOF'
echo "$FINAL_JSON"
echo 'EOF'
} >> "$GITHUB_OUTPUT"
else
echo "::warning::JSON output size ($JSON_SIZE bytes) exceeds GitHub Actions output limit (~1MB). Output not set. Use json_output_file input to write to a file instead."
fi

- name: Commit changes
if: ${{ fromJSON(inputs.commit) }}
shell: bash
env:
COMMIT_MESSAGE: ${{ inputs.commit_message }}
COMMIT_AUTHOR: ${{ inputs.commit_author }}
COMMIT_BRANCH: ${{ inputs.commit_branch }}
run: |
if git diff --quiet; then
echo "No changes to commit"
exit 0
fi

AUTHOR_NAME="${COMMIT_AUTHOR% <*}"
AUTHOR_EMAIL="${COMMIT_AUTHOR#*<}"; AUTHOR_EMAIL="${AUTHOR_EMAIL%>}"
git config user.name "$AUTHOR_NAME"
git config user.email "$AUTHOR_EMAIL"
git add -u
git commit -m "$COMMIT_MESSAGE"

if [ -n "$COMMIT_BRANCH" ]; then
BRANCH="$COMMIT_BRANCH"
else
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
Comment thread
allejo marked this conversation as resolved.
if [ "$BRANCH" = "HEAD" ]; then
# Detached HEAD; try to derive a branch name from GitHub context
if [ -n "${{ github.head_ref }}" ]; then
BRANCH="${{ github.head_ref }}"
elif [ -n "${{ github.ref_name }}" ]; then
BRANCH="${{ github.ref_name }}"
else
echo "::error::Cannot determine branch to push to (detached HEAD and no commit_branch, github.head_ref, or github.ref_name)"
exit 1
fi
fi
fi

git push origin "$BRANCH"
Comment thread
allejo marked this conversation as resolved.

- name: Check for diff
if: ${{ fromJSON(inputs.fail_on_diff) }}
shell: bash
run: |
if ! git diff --quiet; then
echo "::error::README.md files have uncommitted changes after processing"
exit 1
fi
Loading