From ae77521094dff93d8b719d03f07a61d958363010 Mon Sep 17 00:00:00 2001 From: Damien Arnodo Date: Sun, 7 Dec 2025 14:10:00 +0000 Subject: [PATCH] feat(ci): add Terraform CI/CD workflow Implements automated Terraform pipeline with: - Validation: fmt check, tflint, terraform validate - Plan: on all PRs and pushes, with PR comments - Apply: automatic on push to dev (when changes detected) Runner: self-hosted (Docker) with Tailscale access to PVE01 Backend: Scaleway S3 Object Storage Closes #3 --- .gitea/workflows/terraform.yml | 212 +++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 .gitea/workflows/terraform.yml diff --git a/.gitea/workflows/terraform.yml b/.gitea/workflows/terraform.yml new file mode 100644 index 0000000..b9ac653 --- /dev/null +++ b/.gitea/workflows/terraform.yml @@ -0,0 +1,212 @@ +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