GitHub Actions - Automated Terraform-docs
Tags: githubactions, automation, devops, terraform
Published: 2023-09-16 22:30:36

Earlier this year I wrote about the challenges I faced [creating a Terraform module](https://dev.to/coleheard/writing-a-terraform-module-3md0). I mentioned then that I was leveraging [terraform-docs](https://terraform-docs.io/) and [GitHub Actions](https://github.com/features/actions) to automate documentation, but a full workflow walkthrough was out of that post's scope. 

**This post, however, is entirely focused on automated documentation.** 

If you're looking for an easy way to save time, enforce documentation formatting standards, or ensure documentation is kept up-to-date - [my autodoc workflow](https://github.com/ColeHeard/githubactions-pipeline-gallery/blob/main/samples/automation/autodoc.yaml) is a great place to start.

---

{%- # TOC start -%}

- **[The Workflow](#the-workflow)**
   * [Repo clone](#repo-clone)
- **[Generating Documentation](#generating-documentation)**
   * [Action arguments](#action-arguments)
   * [Markdown formatting](#markdown-formatting)
   * [Resulting output](#resulting-output)
- **[Premerge Review](#premerge-review)**
   * [Echoing contents](#echoing-contents)
     + [Escape characters](#escape-characters)
     + [Multiline string](#multiline-strings)
   * [Pull request comment](#pull-request-comment)
- **[Wrapping Up](#wrapping-up)**

{%- # TOC end -%}

---

## The Workflow
The workflow starts by defining a name, a start condition, and a host that will execute the job.
 
```yaml
name: "Autodoc Workflow - Terraform-docs"

on:
  pull_request:

jobs:
  tfdocs:
    runs-on: ubuntu-latest
```
On a pull request, GitHub Actions will provision a Ubuntu VM with the latest OS version considered ["stable"](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources) by GitHub.

> Note: The `-latest` runner images are the latest stable images that GitHub provides, and might not be the most recent version of the operating system available from the operating system vendor.

### Repo clone
Now that the job has been defined, the [first step](https://github.com/actions/checkout) will clone the repository down to the runner. More specifically, it will pull the PR head from [Actions context](https://docs.github.com/en/actions/learn-github-actions/contexts).

```yaml
    steps:
    - name: Pull request checkout
      uses: actions/checkout@v3
      id: checkout
      with:
        ref: ${{ github.event.pull_request.head.ref }}
```

With a local copy of the code now available, the runner can update the documentation.

---

## Generating documentation
This step uses the [terraform-docs action](https://github.com/terraform-docs/gh-actions) to update the README.md. 
```yaml
    - name: README.md generation
      uses: terraform-docs/gh-actions@main
      id: tfdocs
      with:
        config-file: terraform-docs.yaml
        working-dir: .
        output-file: README.md
        output-method: inject
        git-push: "true"
```
If you'd prefer to install the CLI tool on the runner and execute the commands directly from the shell, other [installation options](https://terraform-docs.io/user-guide/installation/) exist. 

### Action arguments
The terraform-docs action accepts many [arguments](https://github.com/terraform-docs/gh-actions/blob/main/README.md), but the five below are used by this workflow. 
- **config-file** accepts the [terraform-docs config file](https://terraform-docs.io/user-guide/configuration).
- **working-dir** specifies the location of the terraform files.
- **output-file** defines the name of the generated/updated doc.
- **output-method** accepts print, replace, or inject. If other content (e.g. content not generated by terraform-docs) exists within the README.md file, you'll want to use **inject**. Inject updates the existing file and places the newly created documentation between the hardcoded delimiters `<!-- BEGIN_TF_DOCS -->` and `<!-- END_TF_DOCS -->`.
- **git-push** is set to "true" and adds the newly updated document to the pull request.

### Markdown formatting
GitHub understands [markdown](https://www.markdownguide.org/) formatting in readme files, posts, and comments. The terraform-docs.yaml file specifies markdown output to take advantage of this. 
```yaml
formatter: "markdown table"
```
You can find the full example [terraform-docs.yaml file](https://github.com/ColeHeard/example-module-hcl/blob/main/terraform-docs.yaml) here.

### Resulting output
This is what the [README.md file](https://github.com/ColeHeard/example-module-hcl#readme) looks like after the pull request is merged:
![Readme file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/frmsjeyj0ikkmdig6pjc.png)

---

## Premerge Review
Now that the README.md has been updated, approvers will want to review the changes prior to merging branches. 

The next two steps will post updated content to pull request review thread for the approver's convenience. 

### Echoing contents
The output step echoes the file content into a standard GitHub environmental variable, [**github_env**](https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables).

```yaml
    - name: Output README.md
      id: output
      run: |
        echo 'readme<<EOF' >> $GITHUB_ENV
        echo "$(<README.md)" >> $GITHUB_ENV
        echo 'EOF' >> $GITHUB_ENV
```

#### Escape characters
Streaming output of the file into a variable will strip the content of ["escape characters"](https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html), including **newline**. To preserve these special characters, **$(<README.md)** is wrapped in double quotes. 

```sh
echo "$(<README.md)"
```

#### Multiline strings
Action's requirements surrounding environmental variables restrict the use of [multiline strings](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings). "EOF" (end of file) is used as the delimiter to circumvent this restriction. 
```sh
echo 'readme<<EOF' >> $GITHUB_ENV
echo "$(<README.md)" >> $GITHUB_ENV
echo 'EOF' >> $GITHUB_ENV
```

The updated markdown content can now be used in the next step.

### Pull request comment
The last step in the workflow creates a comment on the pull request thread in GitHub with the [script action](https://github.com/actions/github-script).

[A fine-grained token](https://github.blog/2022-10-18-introducing-fine-grained-personal-access-tokens-for-github/) grants access to the repository. It is stored as the secret **GH_TOKEN**.

```yaml
    - name: Pull request comment
      id: comment
      uses: actions/github-script@v6
      with:
        github-token: ${{ secrets.GH_TOKEN }}
        script: |
          const output = `Terraform-docs has updated the README.md. 

          ${process.env.readme}`
          github.rest.issues.createComment({
            issue_number: context.issue.number,
            owner: context.repo.owner,
            repo: context.repo.repo,
            body: output
          })
```

The markdown stored in the previous step is referenced with **${process.env.readme}**. 

After the job is completed, this is what the comment looks like:

![Pull request comment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sruk2v3xce5tds31ed1j.png)
---

## Wrapping up
The job is complete and the documentation has been updated! If you'd like to test this workflow yourself, fork and modify my [example module](https://github.com/ColeHeard/example-module-hcl) - Configure the repository permissions, modify a new branch, and kick off a pull request to see it in action.

If you're interested in learning more about terraform-docs, checkout their [user guide](https://terraform-docs.io/user-guide/introduction/) or drop by their [slack](https://slack.terraform-docs.io/).

Thanks for taking the time read this post!

![Ralph Wiggum - Goodbye](https://www.reactiongifs.com/wp-content/uploads/2013/07/ralph-wave.gif)