name: Terraform CI/CD on: push: branches: - dev paths: - 'terraform/**' pull_request: branches: - dev paths: - 'terraform/**' env: TF_WORKING_DIR: terraform/prod # Backend S3 (Scaleway) AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} # Terraform variables TF_VAR_proxmox_url: ${{ secrets.PROXMOX_URL }} TF_VAR_proxmox_api_token: ${{ secrets.PROXMOX_API_TOKEN }} TF_VAR_tailscale_auth_key: ${{ secrets.TAILSCALE_AUTH_KEY }} jobs: # =========================================================================== # Validation - Runs on all pushes and PRs # =========================================================================== validate: name: Validate runs-on: self-hosted steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.9" - name: Setup TFLint uses: terraform-linters/setup-tflint@v4 - name: Terraform Format Check id: fmt run: terraform fmt -check -recursive -diff working-directory: ${{ env.TF_WORKING_DIR }} continue-on-error: true - name: TFLint Init run: tflint --init working-directory: ${{ env.TF_WORKING_DIR }} - name: TFLint id: lint run: tflint --recursive --format compact working-directory: ${{ env.TF_WORKING_DIR }} continue-on-error: true - name: Terraform Init id: init run: terraform init -input=false working-directory: ${{ env.TF_WORKING_DIR }} - name: Terraform Validate id: validate run: terraform validate -no-color working-directory: ${{ env.TF_WORKING_DIR }} - name: Validation Summary if: always() run: | echo "## Validation Results" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY echo "| Format | ${{ steps.fmt.outcome == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY echo "| TFLint | ${{ steps.lint.outcome == 'success' && '✅' || '⚠️' }} |" >> $GITHUB_STEP_SUMMARY echo "| Init | ${{ steps.init.outcome == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY echo "| Validate | ${{ steps.validate.outcome == 'success' && '✅' || '❌' }} |" >> $GITHUB_STEP_SUMMARY - name: Check Validation Status if: steps.fmt.outcome == 'failure' || steps.init.outcome == 'failure' || steps.validate.outcome == 'failure' run: exit 1 # =========================================================================== # Plan - Runs on PRs and pushes to dev # =========================================================================== plan: name: Plan runs-on: self-hosted needs: validate outputs: plan_exitcode: ${{ steps.plan.outputs.exitcode }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.9" - name: Terraform Init run: terraform init -input=false working-directory: ${{ env.TF_WORKING_DIR }} - name: Terraform Plan id: plan run: | terraform plan -input=false -no-color -detailed-exitcode -out=tfplan 2>&1 | tee plan_output.txt echo "exitcode=$?" >> $GITHUB_OUTPUT working-directory: ${{ env.TF_WORKING_DIR }} continue-on-error: true - name: Upload Plan uses: actions/upload-artifact@v4 with: name: tfplan path: ${{ env.TF_WORKING_DIR }}/tfplan retention-days: 7 - name: Plan Summary if: always() run: | echo "## Terraform Plan" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY cat plan_output.txt >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY working-directory: ${{ env.TF_WORKING_DIR }} - name: Comment PR with Plan if: github.event_name == 'pull_request' uses: actions/github-script@v7 with: script: | const fs = require('fs'); const plan = fs.readFileSync('${{ env.TF_WORKING_DIR }}/plan_output.txt', 'utf8'); const maxLength = 60000; const truncated = plan.length > maxLength ? plan.substring(0, maxLength) + '\n... (truncated)' : plan; const body = `## 🔍 Terraform Plan
Click to expand \`\`\`hcl ${truncated} \`\`\`
**Exit code:** \`${{ steps.plan.outputs.exitcode }}\` - \`0\` = No changes - \`1\` = Error - \`2\` = Changes detected `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: body }); - name: Check Plan Status if: steps.plan.outputs.exitcode == '1' run: exit 1 # =========================================================================== # Apply - Runs on push to dev OR after PR merge # =========================================================================== apply: name: Apply runs-on: self-hosted needs: plan if: | (github.event_name == 'push' && github.ref == 'refs/heads/dev') && needs.plan.outputs.plan_exitcode == '2' environment: production steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Terraform uses: hashicorp/setup-terraform@v3 with: terraform_version: "1.9" - name: Terraform Init run: terraform init -input=false working-directory: ${{ env.TF_WORKING_DIR }} - name: Download Plan uses: actions/download-artifact@v4 with: name: tfplan path: ${{ env.TF_WORKING_DIR }} - name: Terraform Apply run: terraform apply -input=false -auto-approve tfplan working-directory: ${{ env.TF_WORKING_DIR }} - name: Apply Summary if: always() run: | echo "## ✅ Terraform Apply Complete" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "Infrastructure has been updated successfully." >> $GITHUB_STEP_SUMMARY