Created
April 10, 2026 12:38
-
-
Save r5b9/583a1e657850915ee5aa25900ebe3fd7 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Plan Terraform Changes | |
| on: | |
| workflow_call: | |
| inputs: | |
| environment_name: | |
| required: true | |
| type: string | |
| working_directory: | |
| required: true | |
| type: string | |
| jobs: | |
| plan: | |
| runs-on: ubuntu-latest | |
| # Run all subsequent commands in the specified directory | |
| defaults: | |
| run: | |
| working-directory: ${{ inputs.working_directory }} | |
| environment: | |
| name: ${{ inputs.environment_name }} | |
| # Grant permissions for the OIDC token | |
| permissions: | |
| id-token: write | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Resolve Terraform Version | |
| id: tfver | |
| uses: ./.github/actions/resolve-terraform-version | |
| - name: Setup SSH key for private modules | |
| uses: webfactory/ssh-agent@v0.9.0 | |
| with: | |
| ssh-private-key: ${{ secrets.TERRAFORM_MODULES_SSH_KEY }} | |
| - name: Configure AWS Credentials | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| # Use the role ARN from the environment secret | |
| role-to-assume: ${{ secrets.TERRAFORM_ROLE }} | |
| aws-region: ${{ vars.AWS_REGION }} | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@v3 | |
| with: | |
| terraform_version: ${{ steps.tfver.outputs.version }} | |
| terraform_wrapper: false | |
| - name: Debug Inputs | |
| run: | | |
| echo "Received Environment Name: ${{ inputs.environment_name }}" | |
| echo "Received Working Directory: ${{ inputs.working_directory }}" | |
| - name: Terraform Init | |
| # Use an absolute path to the backend config at the repo root | |
| run: terraform init -upgrade -backend-config=${{ github.workspace }}/backend-${{ inputs.environment_name }}.hcl | |
| - name: Terraform Plan | |
| id: plan | |
| continue-on-error: true # Allow the workflow to continue on non-zero exit codes | |
| # The var-file is relative to the working directory | |
| run: | | |
| set +e # Temporarily disable exit-on-error | |
| terraform plan -var-file=./${{ inputs.environment_name }}.tfvars -no-color -lock=false -detailed-exitcode -out=tfplan > plan.log 2>&1 | |
| PLAN_EXIT_CODE=$? | |
| set -e # Re-enable exit-on-error for the rest of the script | |
| echo "Terraform plan completed with exit code ${PLAN_EXIT_CODE}" | |
| cat plan.log | |
| echo "exit_code=${PLAN_EXIT_CODE}" >> $GITHUB_OUTPUT | |
| # Exit with 0 for success (0) or changes (2), but fail for errors (1) | |
| [[ "${PLAN_EXIT_CODE}" == "1" ]] && exit 1 || exit 0 | |
| - name: Terraform Plan Exit Code | |
| run: echo "Terraform plan exited with code ${{ steps.plan.outputs.exit_code }}" | |
| - name: Prepare plan output for comment | |
| id: prep-comment | |
| if: always() | |
| run: | | |
| if [[ "${{ steps.plan.outputs.exit_code }}" != '1' ]]; then | |
| terraform show -no-color tfplan > output | |
| terraform show -json tfplan > plan.json | |
| else | |
| cat plan.log > output | |
| fi | |
| - name: Check for Destructive Changes | |
| id: check_destructive_changes | |
| if: steps.plan.outputs.exit_code == '2' | |
| run: | | |
| REPLACED_RESOURCES=$(jq -r '[.resource_changes[] | select(.change.actions | contains(["create"]) and contains(["delete"])) | .address] | map("-/+ " + .) | join("\n")' plan.json) | |
| DELETED_RESOURCES=$(jq -r '[.resource_changes[] | select(.change.actions == ["delete"]) | .address] | map("- " + .) | join("\n")' plan.json) | |
| MESSAGE_ITEMS="" | |
| if [[ -n "$REPLACED_RESOURCES" ]]; then | |
| MESSAGE_ITEMS+="${REPLACED_RESOURCES}" | |
| fi | |
| if [[ -n "$DELETED_RESOURCES" ]]; then | |
| if [[ -n "$MESSAGE_ITEMS" ]]; then | |
| MESSAGE_ITEMS+=$'\n' | |
| fi | |
| MESSAGE_ITEMS+="${DELETED_RESOURCES}" | |
| fi | |
| if [[ -n "$REPLACED_RESOURCES" ]]; then | |
| echo "has_destructive_changes=true" >> "$GITHUB_OUTPUT" | |
| echo "message=The following resources will be recreated (destroyed and created):" >> "$GITHUB_OUTPUT" | |
| echo "message_type=error" >> "$GITHUB_OUTPUT" | |
| echo "message_items<<EOF" >> "$GITHUB_OUTPUT" | |
| echo -e "$MESSAGE_ITEMS" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| elif [[ -n "$DELETED_RESOURCES" ]]; then | |
| echo "has_destructive_changes=false" >> "$GITHUB_OUTPUT" | |
| echo "message=The following resources will be deleted:" >> "$GITHUB_OUTPUT" | |
| echo "message_type=warning" >> "$GITHUB_OUTPUT" | |
| echo "message_items<<EOF" >> "$GITHUB_OUTPUT" | |
| echo -e "$MESSAGE_ITEMS" >> "$GITHUB_OUTPUT" | |
| echo "EOF" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Set Success Tip | |
| id: success_tip | |
| if: steps.plan.outputs.exit_code == '2' && steps.check_destructive_changes.outputs.has_destructive_changes != 'true' | |
| run: | | |
| echo "message_type=tip" >> "$GITHUB_OUTPUT" | |
| echo "message=Plan successful with non-destructive changes." >> "$GITHUB_OUTPUT" | |
| - name: Set No Changes Note | |
| id: no_changes_note | |
| if: steps.plan.outputs.exit_code == '0' | |
| run: | | |
| echo "message_type=note" >> "$GITHUB_OUTPUT" | |
| echo "message=No changes detected in the plan." >> "$GITHUB_OUTPUT" | |
| - name: Create Plan Comment Body | |
| uses: ./.github/actions/pr-comment | |
| if: always() | |
| with: | |
| path: ${{ inputs.working_directory }} | |
| environment: ${{ inputs.environment_name }} | |
| title: "Terraform Plan" | |
| input_file: output | |
| output_file: pr-comment | |
| # Conditionally set the message and items | |
| message: ${{ steps.check_destructive_changes.outputs.message || steps.no_changes_note.outputs.message || steps.success_tip.outputs.message }} | |
| message_type: ${{ steps.check_destructive_changes.outputs.message_type || steps.no_changes_note.outputs.message_type || steps.success_tip.outputs.message_type }} | |
| message_items: ${{ steps.check_destructive_changes.outputs.message_items }} | |
| - name: Ensure Plan Header (for quoting) | |
| if: always() | |
| run: | | |
| set +e | |
| if [ ! -f pr-comment ]; then | |
| echo "pr-comment file missing (earlier step may have failed); creating empty placeholder."; | |
| touch pr-comment | |
| fi | |
| if ! grep -Eq '^### Terraform Plan for ' pr-comment 2>/dev/null; then | |
| echo "Prepending canonical header line (repo prefix removed)"; | |
| TMP=$(mktemp) | |
| echo "### Terraform Plan for ${{ inputs.working_directory }}/${{ inputs.environment_name }}" > "$TMP" | |
| cat pr-comment >> "$TMP" | |
| mv "$TMP" pr-comment | |
| else | |
| echo "Header already present; no action needed."; | |
| fi | |
| exit 0 | |
| - name: Add destructive-changes label | |
| if: steps.plan.outputs.exit_code == '2' && (steps.check_destructive_changes.outputs.message_type == 'error' || steps.check_destructive_changes.outputs.message_type == 'warning') | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const LABEL_NAME = 'destructive-changes'; | |
| const LABEL_COLOR = 'b60205'; // red | |
| // Ensure label exists | |
| try { | |
| await github.rest.issues.createLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| name: LABEL_NAME, | |
| color: LABEL_COLOR, | |
| description: 'Terraform plan includes deletions or replacements' | |
| }); | |
| } catch (error) { | |
| if (error.status !== 422) throw error; // already exists | |
| } | |
| // Add label to PR | |
| await github.rest.issues.addLabels({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| labels: [LABEL_NAME] | |
| }); | |
| - name: Remove destructive-changes label when safe | |
| if: steps.plan.outputs.exit_code == '0' || (steps.plan.outputs.exit_code == '2' && steps.check_destructive_changes.outputs.message_type != 'error' && steps.check_destructive_changes.outputs.message_type != 'warning') | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const LABEL_NAME = 'destructive-changes'; | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| name: LABEL_NAME, | |
| }); | |
| } catch (error) { | |
| if (error.status !== 404) throw error; // ignore if label not present | |
| } | |
| - name: Comment PR | |
| uses: thollander/actions-comment-pull-request@v3 | |
| if: always() | |
| with: | |
| file-path: pr-comment | |
| comment-tag: terraform-plan-${{ inputs.working_directory }}-${{ inputs.environment_name }} | |
| - name: Fail on Destructive Changes | |
| if: steps.check_destructive_changes.outputs.has_destructive_changes == 'true' | |
| run: | | |
| echo "::warning::Terraform plan includes destructive changes (replacements). Proceeding with plan so it can be reviewed and approved before apply." | |
| - name: Sanitize artifact name | |
| id: sanitize | |
| uses: ./.github/actions/sanitize-name | |
| with: | |
| value: ${{ inputs.working_directory }} | |
| - name: Save Artifact | |
| id: save-artifact | |
| uses: actions/upload-artifact@v4 | |
| if: steps.plan.outputs.exit_code == '2' | |
| with: | |
| name: ${{ steps.sanitize.outputs.safe_name }}-${{ inputs.environment_name }}-tfplan | |
| path: ${{ inputs.working_directory }}/tfplan | |
| retention-days: 7 | |
| if-no-files-found: error |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment