From ae77521094dff93d8b719d03f07a61d958363010 Mon Sep 17 00:00:00 2001 From: Damien Arnodo Date: Sun, 7 Dec 2025 14:10:00 +0000 Subject: [PATCH 1/3] 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 -- 2.54.0 From 2f396fd882c47d990da90d4ce1c1dbebe8610dd0 Mon Sep 17 00:00:00 2001 From: Damien Arnodo Date: Mon, 8 Dec 2025 08:12:14 +0000 Subject: [PATCH 2/3] fix(ci): use custom terraform-ci container image - Use gitea.arnodo.fr/damien/terraform-ci:latest (Terraform 1.5.7 + TFLint) - Remove setup-terraform and setup-tflint actions (already in image) - Replace actions/github-script with curl + Gitea API for PR comments - Use gitea.* context variables instead of github.* - Fix plan exit code handling (0=no changes, 2=changes, 1=error) Refs #3 --- .gitea/workflows/terraform.yml | 128 +++++++++++++++------------------ 1 file changed, 58 insertions(+), 70 deletions(-) diff --git a/.gitea/workflows/terraform.yml b/.gitea/workflows/terraform.yml index b9ac653..69f8e2e 100644 --- a/.gitea/workflows/terraform.yml +++ b/.gitea/workflows/terraform.yml @@ -29,28 +29,18 @@ jobs: validate: name: Validate runs-on: self-hosted + container: + image: gitea.arnodo.fr/damien/terraform-ci:latest 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 @@ -89,6 +79,8 @@ jobs: plan: name: Plan runs-on: self-hosted + container: + image: gitea.arnodo.fr/damien/terraform-ci:latest needs: validate outputs: plan_exitcode: ${{ steps.plan.outputs.exitcode }} @@ -96,11 +88,6 @@ jobs: - 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 }} @@ -108,16 +95,24 @@ jobs: - name: Terraform Plan id: plan run: | + set +e terraform plan -input=false -no-color -detailed-exitcode -out=tfplan 2>&1 | tee plan_output.txt - echo "exitcode=$?" >> $GITHUB_OUTPUT + EXITCODE=$? + echo "exitcode=${EXITCODE}" >> $GITHUB_OUTPUT + # Exit 0 for no changes, exit 2 for changes - both are OK + if [ $EXITCODE -eq 1 ]; then + exit 1 + fi + exit 0 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 + path: | + ${{ env.TF_WORKING_DIR }}/tfplan + ${{ env.TF_WORKING_DIR }}/plan_output.txt retention-days: 7 - name: Plan Summary @@ -125,81 +120,74 @@ jobs: run: | echo "## Terraform Plan" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY + echo "**Exit code:** \`${{ steps.plan.outputs.exitcode }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`0\` = No changes" >> $GITHUB_STEP_SUMMARY + echo "- \`2\` = Changes detected" >> $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 + if: gitea.event_name == 'pull_request' + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + PLAN_OUTPUT=$(cat plan_output.txt | head -c 60000) + COMMENT_BODY=$(cat < + Click to expand plan output + + \`\`\`hcl + ${PLAN_OUTPUT} + \`\`\` + + + EOF + ) + + # Post comment via Gitea API + curl -s -X POST \ + -H "Authorization: token ${GITEA_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg body "$COMMENT_BODY" '{body: $body}')" \ + "${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/issues/${{ gitea.event.pull_request.number }}/comments" + working-directory: ${{ env.TF_WORKING_DIR }} # =========================================================================== - # Apply - Runs on push to dev OR after PR merge + # Apply - Runs on push to dev only (when changes detected) # =========================================================================== apply: name: Apply runs-on: self-hosted + container: + image: gitea.arnodo.fr/damien/terraform-ci:latest needs: plan if: | - (github.event_name == 'push' && github.ref == 'refs/heads/dev') && + gitea.event_name == 'push' && + gitea.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 Init + run: terraform init -input=false + working-directory: ${{ env.TF_WORKING_DIR }} + - name: Terraform Apply run: terraform apply -input=false -auto-approve tfplan working-directory: ${{ env.TF_WORKING_DIR }} -- 2.54.0 From 3acc0c367902e717acecdd4272acf07f1f85031c Mon Sep 17 00:00:00 2001 From: Damien Arnodo Date: Mon, 8 Dec 2025 08:21:37 +0000 Subject: [PATCH 3/3] fix(ci): rename GITEA_TOKEN to GIT_TOKEN GITEA_TOKEN is a reserved name in Gitea Actions Refs #3 --- .gitea/workflows/terraform.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/terraform.yml b/.gitea/workflows/terraform.yml index 69f8e2e..04e18b2 100644 --- a/.gitea/workflows/terraform.yml +++ b/.gitea/workflows/terraform.yml @@ -132,7 +132,7 @@ jobs: - name: Comment PR with Plan if: gitea.event_name == 'pull_request' env: - GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + GIT_TOKEN: ${{ secrets.GIT_TOKEN }} run: | PLAN_OUTPUT=$(cat plan_output.txt | head -c 60000) COMMENT_BODY=$(cat <