diff --git a/.github/workflows/terraform-root-apply.yaml b/.github/workflows/terraform-root-apply.yaml
index 5acc481..4eb28b6 100644
--- a/.github/workflows/terraform-root-apply.yaml
+++ b/.github/workflows/terraform-root-apply.yaml
@@ -3,11 +3,10 @@ name: 'Terraform Apply'
on:
workflow_call:
inputs:
- workflow:
- description: 'Which workflow step to run.'
- required: false
+ root:
+ description: 'The root module directory to apply.'
+ required: true
type: string
- default: ''
runs-on:
description: 'Agent selection string.'
required: false
@@ -21,6 +20,10 @@ on:
description: 'The AWS region to assume IAM in for Terraform operations.'
required: true
type: string
+ environment:
+ description: 'GitHub environment for approval gates (must have required reviewers configured).'
+ required: true
+ type: string
tailscale:
description: 'Should the job connect to Tailscale.'
required: false
@@ -48,36 +51,115 @@ permissions:
contents: read
jobs:
- terraform-apply:
- name: 'Terraform Apply'
+ plan:
+ name: 'Plan: ${{ inputs.root }}'
defaults:
run:
shell: bash
- working-directory: './${{ inputs.workflow }}'
+ working-directory: './${{ inputs.root }}'
runs-on: ${{ inputs.runs-on }}
env:
TF_IN_AUTOMATION: true
steps:
- - name: Sanity check
+ - name: Sanity Check
if: github.ref != 'refs/heads/main'
- run: echo 'Not running against 'main' branch, exiting.' && exit 1
+ run: echo "Not running against 'main' branch, exiting." && exit 1
+
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Read Terraform Version
+ run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v5
+ with:
+ role-to-assume: ${{ inputs.aws-assume-role-arn }}
+ aws-region: ${{ inputs.aws-assume-role-region }}
+
+ - name: Setup Terraform
+ uses: hashicorp/setup-terraform@v3.1.2
+ with:
+ terraform_version: ${{ env.TF_VERSION }}
+ cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
+ terraform_wrapper: false
+
+ - name: Initialize Terraform
+ run: terraform init
+
+ - name: Tailscale
+ if: inputs.tailscale == true
+ uses: tailscale/github-action@v4
+ with:
+ oauth-client-id: ${{ secrets.tailscale-client-id }}
+ oauth-secret: ${{ secrets.tailscale-secret }}
+ tags: ${{ secrets.tailscale-tags }}
+ version: 1.82.5
+
+ - name: Terraform Plan
+ run: |
+ ${{ secrets.terraform-env-vars }} terraform plan -input=false -out=tfplan 2>&1 | tee plan-output.txt
+
+ - name: Generate Plan Text
+ run: terraform show tfplan -no-color > plan-readable.txt
+
+ - name: Show Plan in Workflow Summary
+ run: |
+ echo "## Terraform Plan: \`${{ inputs.root }}\`" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo '```hcl' >> $GITHUB_STEP_SUMMARY
+ cat plan-readable.txt >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
+
+ - name: Upload Plan Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: tfplan-${{ inputs.root }}
+ path: |
+ ./${{ inputs.root }}/tfplan
+ ./${{ inputs.root }}/plan-readable.txt
+ retention-days: 1
+
+ apply:
+ name: 'Apply: ${{ inputs.root }}'
+ needs: plan
+ runs-on: ${{ inputs.runs-on }}
+ environment: ${{ inputs.environment }}
+ defaults:
+ run:
+ shell: bash
+ working-directory: './${{ inputs.root }}'
+ env:
+ TF_IN_AUTOMATION: true
+ steps:
- name: Checkout
uses: actions/checkout@v6
+
- name: Read Terraform Version
run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
+
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ inputs.aws-assume-role-arn }}
aws-region: ${{ inputs.aws-assume-role-region }}
+
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3.1.2
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
terraform_wrapper: false
+
- name: Initialize Terraform
run: terraform init
+
+ - name: Download Plan Artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: tfplan-${{ inputs.root }}
+ path: ./${{ inputs.root }}
+
- name: Tailscale
if: inputs.tailscale == true
uses: tailscale/github-action@v4
@@ -86,5 +168,6 @@ jobs:
oauth-secret: ${{ secrets.tailscale-secret }}
tags: ${{ secrets.tailscale-tags }}
version: 1.82.5
+
- name: Terraform Apply
- run: ${{ secrets.terraform-env-vars }} terraform apply -auto-approve -input=false
+ run: ${{ secrets.terraform-env-vars }} terraform apply -input=false tfplan
diff --git a/.github/workflows/terraform-root-ci.yaml b/.github/workflows/terraform-root-ci.yaml
new file mode 100644
index 0000000..b2dc38c
--- /dev/null
+++ b/.github/workflows/terraform-root-ci.yaml
@@ -0,0 +1,301 @@
+name: 'Terraform CI'
+
+on:
+ workflow_call:
+ inputs:
+ roots:
+ description: 'JSON array of all root module directories to monitor (e.g. ["feature-a", "networking", "compute"]).'
+ required: true
+ type: string
+ runs-on:
+ description: 'Agent selection string.'
+ required: false
+ default: 'ubuntu-latest'
+ type: string
+ aws-assume-role-arn:
+ description: 'The AWS Role ARN to assume for Terraform operations.'
+ required: true
+ type: string
+ aws-assume-role-region:
+ description: 'The AWS region to assume IAM in for Terraform operations.'
+ required: true
+ type: string
+ tailscale:
+ description: 'Should the job connect to Tailscale.'
+ required: false
+ type: boolean
+ default: false
+ ai-summary:
+ description: 'Generate an AI summary of the plan using GitHub Models.'
+ required: false
+ type: boolean
+ default: false
+ secrets:
+ terraform-registry-token:
+ description: 'Terraform registry token to authorize Terraform operations.'
+ required: false
+ terraform-env-vars:
+ description: 'Environment variables to inject into Terraform operations.'
+ required: false
+ tailscale-client-id:
+ description: 'Your Tailscale OAuth Client ID.'
+ required: false
+ tailscale-secret:
+ description: 'Your Tailscale OAuth Client Secret.'
+ required: false
+ tailscale-tags:
+ description: 'Comma separated list of Tags to be applied to nodes. The OAuth client must have permission to apply these tags.'
+ required: false
+
+permissions:
+ id-token: write
+ contents: read
+ pull-requests: write
+ models: read
+
+jobs:
+ detect-changes:
+ name: 'Detect Changed Roots'
+ runs-on: ${{ inputs.runs-on }}
+ outputs:
+ changed: ${{ steps.filter.outputs.changed }}
+ has_changes: ${{ steps.filter.outputs.has_changes }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+
+ - name: Detect Changed Roots
+ id: filter
+ run: |
+ ROOTS='${{ inputs.roots }}'
+ CHANGED=()
+
+ # Determine diff base
+ if [ "${{ github.event_name }}" = "pull_request" ]; then
+ BASE="${{ github.event.pull_request.base.sha }}"
+ HEAD="${{ github.event.pull_request.head.sha }}"
+ else
+ BASE="${{ github.event.before }}"
+ HEAD="${{ github.sha }}"
+ fi
+
+ # Get list of changed files
+ CHANGED_FILES=$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || git diff --name-only HEAD~1 HEAD)
+
+ # Check each root for changes
+ for root in $(echo "$ROOTS" | jq -r '.[]'); do
+ if echo "$CHANGED_FILES" | grep -q "^${root}/"; then
+ CHANGED+=("\"${root}\"")
+ fi
+ done
+
+ # Build JSON output
+ if [ ${#CHANGED[@]} -eq 0 ]; then
+ echo "changed=[]" >> $GITHUB_OUTPUT
+ echo "has_changes=false" >> $GITHUB_OUTPUT
+ else
+ CHANGED_JSON=$(printf '%s\n' "${CHANGED[@]}" | jq -s -c '.')
+ echo "changed=${CHANGED_JSON}" >> $GITHUB_OUTPUT
+ echo "has_changes=true" >> $GITHUB_OUTPUT
+ fi
+
+ echo "### Changed Roots" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ if [ ${#CHANGED[@]} -eq 0 ]; then
+ echo "No Terraform roots changed." >> $GITHUB_STEP_SUMMARY
+ else
+ for root in $(echo "$ROOTS" | jq -r '.[]'); do
+ if echo "$CHANGED_FILES" | grep -q "^${root}/"; then
+ echo "- ✅ \`${root}\`" >> $GITHUB_STEP_SUMMARY
+ else
+ echo "- ⏭️ \`${root}\` (no changes)" >> $GITHUB_STEP_SUMMARY
+ fi
+ done
+ fi
+
+ lint:
+ name: 'Lint: ${{ matrix.root }}'
+ needs: detect-changes
+ if: needs.detect-changes.outputs.has_changes == 'true'
+ runs-on: ${{ inputs.runs-on }}
+ strategy:
+ fail-fast: false
+ matrix:
+ root: ${{ fromJson(needs.detect-changes.outputs.changed) }}
+ defaults:
+ run:
+ shell: bash
+ working-directory: './${{ matrix.root }}'
+ env:
+ TF_IN_AUTOMATION: true
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Read Terraform Version
+ run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v5
+ with:
+ role-to-assume: ${{ inputs.aws-assume-role-arn }}
+ aws-region: ${{ inputs.aws-assume-role-region }}
+
+ - name: Setup Terraform
+ uses: hashicorp/setup-terraform@v3.1.2
+ with:
+ terraform_version: ${{ env.TF_VERSION }}
+ cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
+
+ - name: Check Terraform Format
+ run: terraform fmt -check
+
+ - name: Initialize and Validate
+ run: terraform init && terraform validate
+
+ - name: Setup TFLint
+ uses: terraform-linters/setup-tflint@v6
+
+ - name: Check Terraform Lint
+ run: tflint --init && tflint -f compact
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+
+ plan:
+ name: 'Plan: ${{ matrix.root }}'
+ needs: [detect-changes, lint]
+ if: needs.detect-changes.outputs.has_changes == 'true'
+ runs-on: ${{ inputs.runs-on }}
+ strategy:
+ fail-fast: false
+ matrix:
+ root: ${{ fromJson(needs.detect-changes.outputs.changed) }}
+ defaults:
+ run:
+ shell: bash
+ working-directory: './${{ matrix.root }}'
+ env:
+ TF_IN_AUTOMATION: true
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+
+ - name: Read Terraform Version
+ run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
+
+ - name: Configure AWS Credentials
+ uses: aws-actions/configure-aws-credentials@v5
+ with:
+ role-to-assume: ${{ inputs.aws-assume-role-arn }}
+ aws-region: ${{ inputs.aws-assume-role-region }}
+
+ - name: Setup Terraform
+ uses: hashicorp/setup-terraform@v3.1.2
+ with:
+ terraform_version: ${{ env.TF_VERSION }}
+ cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
+ terraform_wrapper: false
+
+ - name: Initialize Terraform
+ run: terraform init
+
+ - name: Tailscale
+ if: inputs.tailscale == true
+ uses: tailscale/github-action@v4
+ with:
+ oauth-client-id: ${{ secrets.tailscale-client-id }}
+ oauth-secret: ${{ secrets.tailscale-secret }}
+ tags: ${{ secrets.tailscale-tags }}
+ version: 1.82.5
+
+ - name: Terraform Plan
+ id: plan
+ run: |
+ ${{ secrets.terraform-env-vars }} terraform plan -input=false -out=tfplan 2>&1 | tee plan-output.txt
+ echo "exit_code=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
+
+ - name: Generate Plan Text
+ if: always()
+ run: terraform show tfplan -no-color > plan-readable.txt 2>/dev/null || true
+
+ - name: Upload Plan Artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: tfplan-${{ matrix.root }}
+ path: |
+ ./${{ matrix.root }}/tfplan
+ ./${{ matrix.root }}/plan-readable.txt
+ ./${{ matrix.root }}/plan-output.txt
+ retention-days: 7
+
+ - name: AI Summary
+ if: inputs.ai-summary == true && github.event_name == 'pull_request'
+ id: ai-summary
+ uses: actions/ai-inference@v1
+ with:
+ prompt: |
+ You are a Terraform expert reviewing infrastructure changes. Summarize the following Terraform plan in a concise, human-readable way. Focus on:
+ - What resources are being created, changed, or destroyed
+ - Any potentially dangerous changes (destroys, replacements, security group changes)
+ - A risk assessment (low/medium/high)
+
+ Keep the summary brief and actionable. Use markdown formatting.
+
+ Plan output:
+ ```
+ $(cat plan-readable.txt | head -500)
+ ```
+ model: openai/gpt-4o-mini
+
+ - name: Post PR Comment
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ github.token }}
+ script: |
+ const fs = require('fs');
+ const root = '${{ matrix.root }}';
+ const planOutput = fs.readFileSync(`${root}/plan-readable.txt`, 'utf8');
+ const aiSummary = `${{ steps.ai-summary.outputs.response }}` || '';
+
+ const maxLength = 60000;
+ const truncated = planOutput.length > maxLength
+ ? planOutput.substring(0, maxLength) + '\n\n... (truncated, see artifact for full plan)'
+ : planOutput;
+
+ let body = `## Terraform Plan: \`${root}\`\n\n`;
+
+ if (aiSummary) {
+ body += `### 🤖 AI Summary\n\n${aiSummary}\n\n---\n\n`;
+ }
+
+ body += `\nPlan output (click to expand)
\n\n\`\`\`hcl\n${truncated}\n\`\`\`\n\n \n\n`;
+ body += `📦 Full plan available as [workflow artifact](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`;
+
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const marker = `## Terraform Plan: \`${root}\``;
+ const existing = comments.find(c => c.body.startsWith(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body: body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: body,
+ });
+ }
diff --git a/.github/workflows/terraform-root-continuous-integration.yaml b/.github/workflows/terraform-root-continuous-integration.yaml
deleted file mode 100644
index e68320a..0000000
--- a/.github/workflows/terraform-root-continuous-integration.yaml
+++ /dev/null
@@ -1,65 +0,0 @@
-name: 'Continuous Integration'
-
-on:
- workflow_call:
- inputs:
- runs-on:
- description: 'Agent selection string.'
- required: false
- default: 'ubuntu-latest'
- type: string
- aws-assume-role-arn:
- description: 'The AWS Role ARN to assume for Terraform operations.'
- required: true
- type: string
- aws-assume-role-region:
- description: 'The AWS region to assume IAM in for Terraform operations.'
- required: true
- type: string
- secrets:
- terraform-registry-token:
- description: 'Terraform registry token to authorize Terraform operations.'
- required: false
-
-permissions:
- id-token: write
- contents: read
-
-jobs:
- continuous-integration:
- name: 'Continuous Integration'
- runs-on: ${{ inputs.runs-on }}
- env:
- TF_IN_AUTOMATION: true
- steps:
- - name: Checkout
- uses: actions/checkout@v6
- - name: Read Terraform Version
- run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
- - name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v5
- with:
- role-to-assume: ${{ inputs.aws-assume-role-arn }}
- aws-region: ${{ inputs.aws-assume-role-region }}
- - name: Setup Terraform
- uses: hashicorp/setup-terraform@v3.1.2
- with:
- terraform_version: ${{ env.TF_VERSION }}
- cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
- - name: Check Terraform Format
- run: terraform fmt -check
- - name: Check Terraform Syntax
- run: terraform init && terraform validate
- - name: Setup TFLint
- uses: terraform-linters/setup-tflint@v6
- - name: Check Terraform Lint
- run: tflint --init && tflint -f compact
- env:
- GITHUB_TOKEN: ${{ github.token }}
- - name: Check Terraform Security (TFsec)
- uses: aquasecurity/tfsec-action@v1.0.3
- - name: Check Terraform Security (Checkov)
- uses: bridgecrewio/checkov-action@v12
- with:
- directory: .
- framework: terraform
diff --git a/.github/workflows/terraform-root-destroy.yaml b/.github/workflows/terraform-root-destroy.yaml
index 89d754d..28f1769 100644
--- a/.github/workflows/terraform-root-destroy.yaml
+++ b/.github/workflows/terraform-root-destroy.yaml
@@ -3,6 +3,10 @@ name: 'Terraform Destroy'
on:
workflow_call:
inputs:
+ root:
+ description: 'The root module directory to destroy.'
+ required: true
+ type: string
runs-on:
description: 'Agent selection string.'
required: false
@@ -21,6 +25,10 @@ on:
description: 'The AWS region to assume IAM in for Terraform operations.'
required: true
type: string
+ environment:
+ description: 'GitHub environment for approval gates.'
+ required: true
+ type: string
tailscale:
description: 'Should the job connect to Tailscale.'
required: false
@@ -49,31 +57,42 @@ permissions:
jobs:
terraform-destroy:
- name: 'Terraform Destroy'
+ name: 'Destroy: ${{ inputs.root }}'
runs-on: ${{ inputs.runs-on }}
+ environment: ${{ inputs.environment }}
+ defaults:
+ run:
+ shell: bash
+ working-directory: './${{ inputs.root }}'
env:
TF_IN_AUTOMATION: true
steps:
- - name: Sanity check
+ - name: Sanity Check
if: github.ref != 'refs/heads/main' || inputs.confirm != 'true'
- run: echo 'Not running against 'main' branch or the action has not been confirmed, exiting.' && exit 1
+ run: echo "Not running against 'main' branch or the action has not been confirmed, exiting." && exit 1
+
- name: Checkout
uses: actions/checkout@v6
+
- name: Read Terraform Version
run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
+
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ inputs.aws-assume-role-arn }}
aws-region: ${{ inputs.aws-assume-role-region }}
+
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3.1.2
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
terraform_wrapper: false
+
- name: Initialize Terraform
run: terraform init
+
- name: Tailscale
if: inputs.tailscale == true
uses: tailscale/github-action@v4
@@ -81,5 +100,7 @@ jobs:
oauth-client-id: ${{ secrets.tailscale-client-id }}
oauth-secret: ${{ secrets.tailscale-secret }}
tags: ${{ secrets.tailscale-tags }}
+ version: 1.82.5
+
- name: Terraform Destroy
run: ${{ secrets.terraform-env-vars }} terraform destroy -auto-approve -input=false
diff --git a/.github/workflows/terraform-root-plan.yaml b/.github/workflows/terraform-root-plan.yaml
index f45e95e..eee1505 100644
--- a/.github/workflows/terraform-root-plan.yaml
+++ b/.github/workflows/terraform-root-plan.yaml
@@ -3,11 +3,10 @@ name: 'Terraform Plan'
on:
workflow_call:
inputs:
- workflow:
- description: 'Which workflow step to run.'
- required: false
+ root:
+ description: 'The root module directory to plan.'
+ required: true
type: string
- default: ''
runs-on:
description: 'Agent selection string.'
required: false
@@ -26,6 +25,11 @@ on:
required: false
type: boolean
default: false
+ ai-summary:
+ description: 'Generate an AI summary of the plan using GitHub Models.'
+ required: false
+ type: boolean
+ default: false
secrets:
terraform-registry-token:
description: 'Terraform registry token to authorize Terraform operations.'
@@ -46,35 +50,42 @@ on:
permissions:
id-token: write
contents: read
+ pull-requests: write
+ models: read
jobs:
terraform-plan:
- name: 'Terraform Plan'
+ name: 'Plan: ${{ inputs.root }}'
defaults:
run:
shell: bash
- working-directory: './${{ inputs.workflow }}'
+ working-directory: './${{ inputs.root }}'
runs-on: ${{ inputs.runs-on }}
env:
TF_IN_AUTOMATION: true
steps:
- name: Checkout
uses: actions/checkout@v6
+
- name: Read Terraform Version
run: echo "TF_VERSION=$(cat .terraform-version)" >> $GITHUB_ENV
+
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ inputs.aws-assume-role-arn }}
aws-region: ${{ inputs.aws-assume-role-region }}
+
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3.1.2
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.terraform-registry-token }}
terraform_wrapper: false
+
- name: Initialize Terraform
run: terraform init
+
- name: Tailscale
if: inputs.tailscale == true
uses: tailscale/github-action@v4
@@ -83,5 +94,95 @@ jobs:
oauth-secret: ${{ secrets.tailscale-secret }}
tags: ${{ secrets.tailscale-tags }}
version: 1.82.5
+
- name: Terraform Plan
- run: ${{ secrets.terraform-env-vars }} terraform plan -input=false
+ id: plan
+ run: |
+ ${{ secrets.terraform-env-vars }} terraform plan -input=false -out=tfplan 2>&1 | tee plan-output.txt
+ echo "exit_code=${PIPESTATUS[0]}" >> $GITHUB_OUTPUT
+
+ - name: Generate Plan Text
+ if: always()
+ run: terraform show tfplan -no-color > plan-readable.txt 2>/dev/null || true
+
+ - name: Upload Plan Artifact
+ if: always()
+ uses: actions/upload-artifact@v4
+ with:
+ name: tfplan-${{ inputs.root }}
+ path: |
+ ./${{ inputs.root }}/tfplan
+ ./${{ inputs.root }}/plan-readable.txt
+ ./${{ inputs.root }}/plan-output.txt
+ retention-days: 7
+
+ - name: AI Summary
+ if: inputs.ai-summary == true && github.event_name == 'pull_request'
+ id: ai-summary
+ uses: actions/ai-inference@v1
+ with:
+ prompt: |
+ You are a Terraform expert reviewing infrastructure changes. Summarize the following Terraform plan in a concise, human-readable way. Focus on:
+ - What resources are being created, changed, or destroyed
+ - Any potentially dangerous changes (destroys, replacements, security group changes)
+ - A risk assessment (low/medium/high)
+
+ Keep the summary brief and actionable. Use markdown formatting.
+
+ Plan output:
+ ```
+ $(cat plan-readable.txt | head -500)
+ ```
+ model: openai/gpt-4o-mini
+
+ - name: Post PR Comment
+ if: github.event_name == 'pull_request'
+ uses: actions/github-script@v7
+ with:
+ github-token: ${{ github.token }}
+ script: |
+ const fs = require('fs');
+ const root = '${{ inputs.root }}';
+ const planOutput = fs.readFileSync(`${root}/plan-readable.txt`, 'utf8');
+ const aiSummary = `${{ steps.ai-summary.outputs.response }}` || '';
+
+ // Truncate plan if too long for a comment
+ const maxLength = 60000;
+ const truncated = planOutput.length > maxLength
+ ? planOutput.substring(0, maxLength) + '\n\n... (truncated, see artifact for full plan)'
+ : planOutput;
+
+ let body = `## Terraform Plan: \`${root}\`\n\n`;
+
+ if (aiSummary) {
+ body += `### 🤖 AI Summary\n\n${aiSummary}\n\n---\n\n`;
+ }
+
+ body += `\nPlan output (click to expand)
\n\n\`\`\`hcl\n${truncated}\n\`\`\`\n\n \n\n`;
+ body += `📦 Full plan available as [workflow artifact](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`;
+
+ // Find existing comment for this root
+ const { data: comments } = await github.rest.issues.listComments({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ });
+
+ const marker = `## Terraform Plan: \`${root}\``;
+ const existing = comments.find(c => c.body.startsWith(marker));
+
+ if (existing) {
+ await github.rest.issues.updateComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: existing.id,
+ body: body,
+ });
+ } else {
+ await github.rest.issues.createComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ body: body,
+ });
+ }