├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── entrypoint.sh └── images ├── fmt-output.png ├── init-output.png ├── plan-output.png └── validate-output.png /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEs 2 | .idea 3 | .vscode 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v1.5.0 4 | 5 | - Bump to Terraform v1.0.6 internally (only affects `fmt`) 6 | - Fix Terraform v1 `plan` output truncation 7 | 8 | ## v1.4.0 9 | 10 | - Bump to Terraform v0.15.0 internally (only affects `fmt`) 11 | - Change the way `plan`s are truncated after introduction of new horizontal break in TF v0.15.0 12 | - Add `validate` comment handling 13 | - Update readme 14 | 15 | ## v1.3.0 16 | 17 | - Bump to Terraform v0.14.9 internally (only affects `fmt`) 18 | - Fix output truncation in Terraform v0.14 and above 19 | 20 | ## v1.2.0 21 | 22 | - Bump to Terraform v0.14.5 internally (only affects `fmt`) 23 | - Change to leave `fmt` output as-is 24 | - Add colourisation to `plan` diffs where there are changes (on by default, controlled with `HIGHLIGHT_CHANGES` environment variable) 25 | - Update readme 26 | 27 | ## v1.1.0 28 | 29 | - Adds better parsing for Terraform v0.14 30 | 31 | ## v1.0.0 32 | 33 | - Initial release. 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM hashicorp/terraform:1.0.6 2 | 3 | LABEL repository="https://github.com/robburger/terraform-pr-commenter" \ 4 | homepage="https://github.com/robburger/terraform-pr-commenter" \ 5 | maintainer="Rob Burger" \ 6 | com.github.actions.name="Terraform PR Commenter" \ 7 | com.github.actions.description="Adds opinionated comments to a PR from Terraform fmt/init/plan output" \ 8 | com.github.actions.icon="git-pull-request" \ 9 | com.github.actions.color="purple" 10 | 11 | RUN apk add --no-cache -q \ 12 | bash \ 13 | curl \ 14 | jq 15 | 16 | ADD entrypoint.sh /entrypoint.sh 17 | RUN chmod +x /entrypoint.sh 18 | 19 | ENTRYPOINT ["/entrypoint.sh"] 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Rob Burger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform PR Commenter 2 | 3 | Adds opinionated comments to PR's based on Terraform `fmt`, `init`, `plan` and `validate` outputs. 4 | 5 | ## Summary 6 | 7 | This Docker-based GitHub Action is designed to work in tandem with [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) with the **wrapper enabled**, taking the output from a `fmt`, `init`, `plan` or `validate`, formatting it and adding it to a pull request. Any previous comments from this Action are removed to keep the PR timeline clean. 8 | 9 | > The `terraform_wrapper` needs to be set to `true` (which is already the default) for the `hashicorp/setup-terraform` step as it enables the capturing of `stdout`, `stderr` and the `exitcode`. 10 | 11 | Support (for now) is [limited to Linux](https://help.github.com/en/actions/creating-actions/about-actions#types-of-actions) as Docker-based GitHub Actions can only be used on Linux runners. 12 | 13 | ## Usage 14 | 15 | This action can only be run after a Terraform `fmt`, `init`, `plan` or `validate` has completed, and the output has been captured. Terraform rarely writes to `stdout` and `stderr` in the same action, so we concatenate the `commenter_input`: 16 | 17 | ```yaml 18 | - uses: robburger/terraform-pr-commenter@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | commenter_type: fmt/init/plan/validate # Choose one 23 | commenter_input: ${{ format('{0}{1}', steps.step_id.outputs.stdout, steps.step_id.outputs.stderr) }} 24 | commenter_exitcode: ${{ steps.step_id.outputs.exitcode }} 25 | ``` 26 | 27 | ### Inputs 28 | 29 | | Name | Requirement | Description | 30 | | -------------------- | ----------- | ----------------------------------------------------------------- | 31 | | `commenter_type` | _required_ | The type of comment. Options: [`fmt`, `init`, `plan`, `validate`] | 32 | | `commenter_input` | _required_ | The comment to post from a previous step output. | 33 | | `commenter_exitcode` | _required_ | The exit code from a previous step output. | 34 | 35 | ### Environment Variables 36 | 37 | | Name | Requirement | Description | 38 | | ------------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | 39 | | `GITHUB_TOKEN` | _required_ | Used to execute API calls. The `${{ secrets.GITHUB_TOKEN }}` already has permissions, but if you're using your own token, ensure it has the `repo` scope. | 40 | | `TF_WORKSPACE` | _optional_ | Default: `default`. This is used to separate multiple comments on a pull request in a matrix run. | 41 | | `EXPAND_SUMMARY_DETAILS` | _optional_ | Default: `true`. This controls whether the comment output is collapsed or not. | 42 | | `HIGHLIGHT_CHANGES` | _optional_ | Default: `true`. This switches `~` to `!` in `plan` diffs to highlight Terraform changes in orange. Set to `false` to disable. | 43 | 44 | All of these environment variables can be set at `job` or `step` level. For example, you could collapse all outputs but expand on a `plan`: 45 | 46 | ```yaml 47 | jobs: 48 | terraform: 49 | name: 'Terraform' 50 | runs-on: ubuntu-latest 51 | env: 52 | EXPAND_SUMMARY_DETAILS: 'false' # All steps will have this environment variable 53 | steps: 54 | - name: Checkout 55 | uses: actions/checkout@v2 56 | ... 57 | - name: Post Plan 58 | uses: robburger/terraform-pr-commenter@v1 59 | env: 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | EXPAND_SUMMARY_DETAILS: 'true' # Override global environment variable; expand details just for this step 62 | with: 63 | commenter_type: plan 64 | commenter_input: ${{ format('{0}{1}', steps.plan.outputs.stdout, steps.plan.outputs.stderr) }} 65 | commenter_exitcode: ${{ steps.plan.outputs.exitcode }} 66 | ... 67 | ``` 68 | 69 | ## Examples 70 | 71 | Single workspace build, full example: 72 | 73 | ```yaml 74 | name: 'Terraform' 75 | 76 | on: 77 | pull_request: 78 | push: 79 | branches: 80 | - master 81 | 82 | jobs: 83 | terraform: 84 | name: 'Terraform' 85 | runs-on: ubuntu-latest 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | TF_IN_AUTOMATION: true 89 | steps: 90 | - name: Checkout 91 | uses: actions/checkout@v2 92 | 93 | - name: Setup Terraform 94 | uses: hashicorp/setup-terraform@v1 95 | with: 96 | cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} 97 | terraform_version: 0.15.0 98 | 99 | - name: Terraform Format 100 | id: fmt 101 | run: terraform fmt -check -recursive 102 | continue-on-error: true 103 | 104 | - name: Post Format 105 | if: always() && github.ref != 'refs/heads/master' && (steps.fmt.outcome == 'success' || steps.fmt.outcome == 'failure') 106 | uses: robburger/terraform-pr-commenter@v1 107 | with: 108 | commenter_type: fmt 109 | commenter_input: ${{ format('{0}{1}', steps.fmt.outputs.stdout, steps.fmt.outputs.stderr) }} 110 | commenter_exitcode: ${{ steps.fmt.outputs.exitcode }} 111 | 112 | - name: Terraform Init 113 | id: init 114 | run: terraform init 115 | 116 | - name: Post Init 117 | if: always() && github.ref != 'refs/heads/master' && (steps.init.outcome == 'success' || steps.init.outcome == 'failure') 118 | uses: robburger/terraform-pr-commenter@v1 119 | with: 120 | commenter_type: init 121 | commenter_input: ${{ format('{0}{1}', steps.init.outputs.stdout, steps.init.outputs.stderr) }} 122 | commenter_exitcode: ${{ steps.init.outputs.exitcode }} 123 | 124 | - name: Terraform Validate 125 | id: validate 126 | run: terraform validate 127 | 128 | - name: Post Validate 129 | if: always() && github.ref != 'refs/heads/master' && (steps.validate.outcome == 'success' || steps.validate.outcome == 'failure') 130 | uses: robburger/terraform-pr-commenter@v1 131 | with: 132 | commenter_type: validate 133 | commenter_input: ${{ format('{0}{1}', steps.validate.outputs.stdout, steps.validate.outputs.stderr) }} 134 | commenter_exitcode: ${{ steps.validate.outputs.exitcode }} 135 | 136 | - name: Terraform Plan 137 | id: plan 138 | run: terraform plan -out workspace.plan 139 | 140 | - name: Post Plan 141 | if: always() && github.ref != 'refs/heads/master' && (steps.plan.outcome == 'success' || steps.plan.outcome == 'failure') 142 | uses: robburger/terraform-pr-commenter@v1 143 | with: 144 | commenter_type: plan 145 | commenter_input: ${{ format('{0}{1}', steps.plan.outputs.stdout, steps.plan.outputs.stderr) }} 146 | commenter_exitcode: ${{ steps.plan.outputs.exitcode }} 147 | 148 | - name: Terraform Apply 149 | id: apply 150 | if: github.ref == 'refs/heads/master' && github.event_name == 'push' 151 | run: terraform apply workspace.plan 152 | ``` 153 | 154 | Multi-workspace matrix/parallel build: 155 | 156 | ```yaml 157 | ... 158 | jobs: 159 | terraform: 160 | name: 'Terraform' 161 | runs-on: ubuntu-latest 162 | strategy: 163 | matrix: 164 | workspace: [audit, staging] 165 | env: 166 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 167 | TF_IN_AUTOMATION: true 168 | TF_WORKSPACE: ${{ matrix['workspace'] }} 169 | steps: 170 | - name: Checkout 171 | uses: actions/checkout@v2 172 | 173 | - name: Setup Terraform 174 | uses: hashicorp/setup-terraform@v1 175 | with: 176 | cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }} 177 | terraform_version: 0.15.0 178 | 179 | - name: Terraform Init - ${{ matrix['workspace'] }} 180 | id: init 181 | run: terraform init 182 | 183 | - name: Post Init - ${{ matrix['workspace'] }} 184 | if: always() && github.ref != 'refs/heads/master' && (steps.init.outcome == 'success' || steps.init.outcome == 'failure') 185 | uses: robburger/terraform-pr-commenter@v1 186 | with: 187 | commenter_type: init 188 | commenter_input: ${{ format('{0}{1}', steps.init.outputs.stdout, steps.init.outputs.stderr) }} 189 | commenter_exitcode: ${{ steps.init.outputs.exitcode }} 190 | 191 | - name: Terraform Plan - ${{ matrix['workspace'] }} 192 | id: plan 193 | run: terraform plan -out ${{ matrix['workspace'] }}.plan 194 | 195 | - name: Post Plan - ${{ matrix['workspace'] }} 196 | if: always() && github.ref != 'refs/heads/master' && (steps.plan.outcome == 'success' || steps.plan.outcome == 'failure') 197 | uses: robburger/terraform-pr-commenter@v1 198 | with: 199 | commenter_type: plan 200 | commenter_input: ${{ format('{0}{1}', steps.plan.outputs.stdout, steps.plan.outputs.stderr) }} 201 | commenter_exitcode: ${{ steps.plan.outputs.exitcode }} 202 | ... 203 | ``` 204 | 205 | "What's the crazy-looking `if:` doing there?" Good question! It's broken into 3 logic groups separated by `&&`, so all need to return `true` for the step to run: 206 | 207 | 1. `always()` - ensures that the step is run regardless of the outcome in any previous steps. i.e. We don't want the build to quit after the previous step before we can write a PR comment with the failure reason. 208 | 2. `github.ref != 'refs/heads/master'` - prevents the step running on a `master` branch. PR comments are not possible when there's no PR! 209 | 3. `(steps.step_id.outcome == 'success' || steps.step_id.outcome == 'failure')` - ensures that this step only runs when `step_id` has either a `success` or `failed` outcome. 210 | 211 | In English: "Always run this step, but only on a pull request and only when the previous step succeeds or fails...and then stop the build." 212 | 213 | ## Screenshots 214 | 215 | ### `fmt` 216 | 217 | ![fmt](images/fmt-output.png) 218 | 219 | ### `init` 220 | 221 | ![fmt](images/init-output.png) 222 | 223 | ### `plan` 224 | 225 | ![fmt](images/plan-output.png) 226 | 227 | ### `validate` 228 | 229 | ![fmt](images/validate-output.png) 230 | 231 | ## Troubleshooting & Contributing 232 | 233 | Feel free to head over to the [Issues](https://github.com/robburger/terraform-pr-commenter/issues) tab to see if the issue you're having has already been reported. If not, [open a new one](https://github.com/robburger/terraform-pr-commenter/issues/new) and be sure to include as much relevant information as possible, including code-samples, and a description of what you expect to be happening. 234 | 235 | ## License 236 | 237 | [MIT](LICENSE) 238 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Terraform PR Commenter' 2 | description: 'Adds opinionated comments to a PR from Terraform fmt/init/plan output' 3 | author: 'Rob Burger' 4 | branding: 5 | icon: 'git-pull-request' 6 | color: 'purple' 7 | inputs: 8 | commenter_type: 9 | description: 'The type of comment. Options: [fmt, init, plan]' 10 | required: true 11 | commenter_input: 12 | description: 'The comment to post from a previous step output' 13 | required: true 14 | commenter_exitcode: 15 | description: 'The exit code from a previous step output' 16 | required: true 17 | runs: 18 | using: 'docker' 19 | image: 'Dockerfile' 20 | args: 21 | - ${{ inputs.commenter_type }} 22 | - ${{ inputs.commenter_input }} 23 | - ${{ inputs.commenter_exitcode }} 24 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############# 4 | # Validations 5 | ############# 6 | PR_NUMBER=$(jq -r ".pull_request.number" "$GITHUB_EVENT_PATH") 7 | if [[ "$PR_NUMBER" == "null" ]]; then 8 | echo "This isn't a PR." 9 | exit 0 10 | fi 11 | 12 | if [[ -z "$GITHUB_TOKEN" ]]; then 13 | echo "GITHUB_TOKEN environment variable missing." 14 | exit 1 15 | fi 16 | 17 | if [[ -z $3 ]]; then 18 | echo "There must be an exit code from a previous step." 19 | exit 1 20 | fi 21 | 22 | if [[ ! "$1" =~ ^(fmt|init|plan|validate)$ ]]; then 23 | echo -e "Unsupported command \"$1\". Valid commands are \"fmt\", \"init\", \"plan\", \"validate\"." 24 | exit 1 25 | fi 26 | 27 | ################## 28 | # Shared Variables 29 | ################## 30 | # Arg 1 is command 31 | COMMAND=$1 32 | # Arg 2 is input. We strip ANSI colours. 33 | INPUT=$(echo "$2" | sed 's/\x1b\[[0-9;]*m//g') 34 | # Arg 3 is the Terraform CLI exit code 35 | EXIT_CODE=$3 36 | 37 | # Read TF_WORKSPACE environment variable or use "default" 38 | WORKSPACE=${TF_WORKSPACE:-default} 39 | 40 | # Read EXPAND_SUMMARY_DETAILS environment variable or use "true" 41 | if [[ ${EXPAND_SUMMARY_DETAILS:-true} == "true" ]]; then 42 | DETAILS_STATE=" open" 43 | else 44 | DETAILS_STATE="" 45 | fi 46 | 47 | # Read HIGHLIGHT_CHANGES environment variable or use "true" 48 | COLOURISE=${HIGHLIGHT_CHANGES:-true} 49 | 50 | ACCEPT_HEADER="Accept: application/vnd.github.v3+json" 51 | AUTH_HEADER="Authorization: token $GITHUB_TOKEN" 52 | CONTENT_HEADER="Content-Type: application/json" 53 | 54 | PR_COMMENTS_URL=$(jq -r ".pull_request.comments_url" "$GITHUB_EVENT_PATH") 55 | PR_COMMENT_URI=$(jq -r ".repository.issue_comment_url" "$GITHUB_EVENT_PATH" | sed "s|{/number}||g") 56 | 57 | ############## 58 | # Handler: fmt 59 | ############## 60 | if [[ $COMMAND == 'fmt' ]]; then 61 | # Look for an existing fmt PR comment and delete 62 | echo -e "\033[34;1mINFO:\033[0m Looking for an existing fmt PR comment." 63 | PR_COMMENT_ID=$(curl -sS -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENTS_URL" | jq '.[] | select(.body|test ("### Terraform `fmt` Failed")) | .id') 64 | if [ "$PR_COMMENT_ID" ]; then 65 | echo -e "\033[34;1mINFO:\033[0m Found existing fmt PR comment: $PR_COMMENT_ID. Deleting." 66 | PR_COMMENT_URL="$PR_COMMENT_URI/$PR_COMMENT_ID" 67 | curl -sS -X DELETE -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENT_URL" > /dev/null 68 | else 69 | echo -e "\033[34;1mINFO:\033[0m No existing fmt PR comment found." 70 | fi 71 | 72 | # Exit Code: 0 73 | # Meaning: All files formatted correctly. 74 | # Actions: Exit. 75 | if [[ $EXIT_CODE -eq 0 ]]; then 76 | echo -e "\033[34;1mINFO:\033[0m Terraform fmt completed with no errors. Continuing." 77 | 78 | exit 0 79 | fi 80 | 81 | # Exit Code: 1, 2 82 | # Meaning: 1 = Malformed Terraform CLI command. 2 = Terraform parse error. 83 | # Actions: Build PR comment. 84 | if [[ $EXIT_CODE -eq 1 || $EXIT_CODE -eq 2 ]]; then 85 | PR_COMMENT="### Terraform \`fmt\` Failed 86 | Show Output 87 | 88 | \`\`\` 89 | $INPUT 90 | \`\`\` 91 | " 92 | fi 93 | 94 | # Exit Code: 3 95 | # Meaning: One or more files are incorrectly formatted. 96 | # Actions: Iterate over all files and build diff-based PR comment. 97 | if [[ $EXIT_CODE -eq 3 ]]; then 98 | ALL_FILES_DIFF="" 99 | for file in $INPUT; do 100 | THIS_FILE_DIFF=$(terraform fmt -no-color -write=false -diff "$file") 101 | ALL_FILES_DIFF="$ALL_FILES_DIFF 102 | $file 103 | 104 | \`\`\`diff 105 | $THIS_FILE_DIFF 106 | \`\`\` 107 | " 108 | done 109 | 110 | PR_COMMENT="### Terraform \`fmt\` Failed 111 | $ALL_FILES_DIFF" 112 | fi 113 | 114 | # Add fmt failure comment to PR. 115 | PR_PAYLOAD=$(echo '{}' | jq --arg body "$PR_COMMENT" '.body = $body') 116 | echo -e "\033[34;1mINFO:\033[0m Adding fmt failure comment to PR." 117 | curl -sS -X POST -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -H "$CONTENT_HEADER" -d "$PR_PAYLOAD" -L "$PR_COMMENTS_URL" > /dev/null 118 | 119 | exit 0 120 | fi 121 | 122 | ############### 123 | # Handler: init 124 | ############### 125 | if [[ $COMMAND == 'init' ]]; then 126 | # Look for an existing init PR comment and delete 127 | echo -e "\033[34;1mINFO:\033[0m Looking for an existing init PR comment." 128 | PR_COMMENT_ID=$(curl -sS -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENTS_URL" | jq '.[] | select(.body|test ("### Terraform `init` Failed")) | .id') 129 | if [ "$PR_COMMENT_ID" ]; then 130 | echo -e "\033[34;1mINFO:\033[0m Found existing init PR comment: $PR_COMMENT_ID. Deleting." 131 | PR_COMMENT_URL="$PR_COMMENT_URI/$PR_COMMENT_ID" 132 | curl -sS -X DELETE -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENT_URL" > /dev/null 133 | else 134 | echo -e "\033[34;1mINFO:\033[0m No existing init PR comment found." 135 | fi 136 | 137 | # Exit Code: 0 138 | # Meaning: Terraform successfully initialized. 139 | # Actions: Exit. 140 | if [[ $EXIT_CODE -eq 0 ]]; then 141 | echo -e "\033[34;1mINFO:\033[0m Terraform init completed with no errors. Continuing." 142 | 143 | exit 0 144 | fi 145 | 146 | # Exit Code: 1 147 | # Meaning: Terraform initialize failed or malformed Terraform CLI command. 148 | # Actions: Build PR comment. 149 | if [[ $EXIT_CODE -eq 1 ]]; then 150 | PR_COMMENT="### Terraform \`init\` Failed 151 | Show Output 152 | 153 | \`\`\` 154 | $INPUT 155 | \`\`\` 156 | " 157 | fi 158 | 159 | # Add init failure comment to PR. 160 | PR_PAYLOAD=$(echo '{}' | jq --arg body "$PR_COMMENT" '.body = $body') 161 | echo -e "\033[34;1mINFO:\033[0m Adding init failure comment to PR." 162 | curl -sS -X POST -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -H "$CONTENT_HEADER" -d "$PR_PAYLOAD" -L "$PR_COMMENTS_URL" > /dev/null 163 | 164 | exit 0 165 | fi 166 | 167 | ############### 168 | # Handler: plan 169 | ############### 170 | if [[ $COMMAND == 'plan' ]]; then 171 | # Look for an existing plan PR comment and delete 172 | echo -e "\033[34;1mINFO:\033[0m Looking for an existing plan PR comment." 173 | PR_COMMENT_ID=$(curl -sS -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENTS_URL" | jq '.[] | select(.body|test ("### Terraform `plan` .* for Workspace: `'"$WORKSPACE"'`")) | .id') 174 | if [ "$PR_COMMENT_ID" ]; then 175 | echo -e "\033[34;1mINFO:\033[0m Found existing plan PR comment: $PR_COMMENT_ID. Deleting." 176 | PR_COMMENT_URL="$PR_COMMENT_URI/$PR_COMMENT_ID" 177 | curl -sS -X DELETE -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENT_URL" > /dev/null 178 | else 179 | echo -e "\033[34;1mINFO:\033[0m No existing plan PR comment found." 180 | fi 181 | 182 | # Exit Code: 0, 2 183 | # Meaning: 0 = Terraform plan succeeded with no changes. 2 = Terraform plan succeeded with changes. 184 | # Actions: Strip out the refresh section, ignore everything after the 72 dashes, format, colourise and build PR comment. 185 | if [[ $EXIT_CODE -eq 0 || $EXIT_CODE -eq 2 ]]; then 186 | CLEAN_PLAN=$(echo "$INPUT" | sed -r '/^(An execution plan has been generated and is shown below.|Terraform used the selected providers to generate the following execution|No changes. Infrastructure is up-to-date.|No changes. Your infrastructure matches the configuration.|Note: Objects have changed outside of Terraform)$/,$!d') # Strip refresh section 187 | CLEAN_PLAN=$(echo "$CLEAN_PLAN" | sed -r '/Plan: /q') # Ignore everything after plan summary 188 | CLEAN_PLAN=${CLEAN_PLAN::65300} # GitHub has a 65535-char comment limit - truncate plan, leaving space for comment wrapper 189 | CLEAN_PLAN=$(echo "$CLEAN_PLAN" | sed -r 's/^([[:blank:]]*)([-+~])/\2\1/g') # Move any diff characters to start of line 190 | if [[ $COLOURISE == 'true' ]]; then 191 | CLEAN_PLAN=$(echo "$CLEAN_PLAN" | sed -r 's/^~/!/g') # Replace ~ with ! to colourise the diff in GitHub comments 192 | fi 193 | PR_COMMENT="### Terraform \`plan\` Succeeded for Workspace: \`$WORKSPACE\` 194 | Show Output 195 | 196 | \`\`\`diff 197 | $CLEAN_PLAN 198 | \`\`\` 199 | " 200 | fi 201 | 202 | # Exit Code: 1 203 | # Meaning: Terraform plan failed. 204 | # Actions: Build PR comment. 205 | if [[ $EXIT_CODE -eq 1 ]]; then 206 | PR_COMMENT="### Terraform \`plan\` Failed for Workspace: \`$WORKSPACE\` 207 | Show Output 208 | 209 | \`\`\` 210 | $INPUT 211 | \`\`\` 212 | " 213 | fi 214 | 215 | # Add plan comment to PR. 216 | PR_PAYLOAD=$(echo '{}' | jq --arg body "$PR_COMMENT" '.body = $body') 217 | echo -e "\033[34;1mINFO:\033[0m Adding plan comment to PR." 218 | curl -sS -X POST -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -H "$CONTENT_HEADER" -d "$PR_PAYLOAD" -L "$PR_COMMENTS_URL" > /dev/null 219 | 220 | exit 0 221 | fi 222 | 223 | ################### 224 | # Handler: validate 225 | ################### 226 | if [[ $COMMAND == 'validate' ]]; then 227 | # Look for an existing validate PR comment and delete 228 | echo -e "\033[34;1mINFO:\033[0m Looking for an existing validate PR comment." 229 | PR_COMMENT_ID=$(curl -sS -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENTS_URL" | jq '.[] | select(.body|test ("### Terraform `validate` Failed")) | .id') 230 | if [ "$PR_COMMENT_ID" ]; then 231 | echo -e "\033[34;1mINFO:\033[0m Found existing validate PR comment: $PR_COMMENT_ID. Deleting." 232 | PR_COMMENT_URL="$PR_COMMENT_URI/$PR_COMMENT_ID" 233 | curl -sS -X DELETE -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -L "$PR_COMMENT_URL" > /dev/null 234 | else 235 | echo -e "\033[34;1mINFO:\033[0m No existing validate PR comment found." 236 | fi 237 | 238 | # Exit Code: 0 239 | # Meaning: Terraform successfully validated. 240 | # Actions: Exit. 241 | if [[ $EXIT_CODE -eq 0 ]]; then 242 | echo -e "\033[34;1mINFO:\033[0m Terraform validate completed with no errors. Continuing." 243 | 244 | exit 0 245 | fi 246 | 247 | # Exit Code: 1 248 | # Meaning: Terraform validate failed or malformed Terraform CLI command. 249 | # Actions: Build PR comment. 250 | if [[ $EXIT_CODE -eq 1 ]]; then 251 | PR_COMMENT="### Terraform \`validate\` Failed 252 | Show Output 253 | 254 | \`\`\` 255 | $INPUT 256 | \`\`\` 257 | " 258 | fi 259 | 260 | # Add validate failure comment to PR. 261 | PR_PAYLOAD=$(echo '{}' | jq --arg body "$PR_COMMENT" '.body = $body') 262 | echo -e "\033[34;1mINFO:\033[0m Adding validate failure comment to PR." 263 | curl -sS -X POST -H "$AUTH_HEADER" -H "$ACCEPT_HEADER" -H "$CONTENT_HEADER" -d "$PR_PAYLOAD" -L "$PR_COMMENTS_URL" > /dev/null 264 | 265 | exit 0 266 | fi 267 | -------------------------------------------------------------------------------- /images/fmt-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robburger/terraform-pr-commenter/72c6e45eced6641488a6cf3ff104b7b9bda9c66c/images/fmt-output.png -------------------------------------------------------------------------------- /images/init-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robburger/terraform-pr-commenter/72c6e45eced6641488a6cf3ff104b7b9bda9c66c/images/init-output.png -------------------------------------------------------------------------------- /images/plan-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robburger/terraform-pr-commenter/72c6e45eced6641488a6cf3ff104b7b9bda9c66c/images/plan-output.png -------------------------------------------------------------------------------- /images/validate-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robburger/terraform-pr-commenter/72c6e45eced6641488a6cf3ff104b7b9bda9c66c/images/validate-output.png --------------------------------------------------------------------------------