| name: "Terraform: CI Pipelines Infrastructure" | |
| on: | |
| pull_request: | |
| paths: ["buildkite/terraform/**"] | |
| push: | |
| branches: ["master"] | |
| paths: ["buildkite/terraform/**"] | |
| # go/github-security: Explicitly set minimum permissions | |
| permissions: | |
| contents: read # Required to check out the code | |
| pull-requests: write # Required to post plan output as a comment | |
| jobs: | |
| # Job 1: Detect which organization folders changed | |
| detect-changes: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| orgs: ${{ steps.process-orgs.outputs.orgs }} | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Get Changed Files | |
| id: changed-files | |
| uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c # v45 | |
| with: | |
| files: buildkite/terraform/** | |
| - name: Extract Orgs | |
| id: process-orgs | |
| shell: bash | |
| env: | |
| # go/github-security: Pass action outputs via env vars, not inline ${{ }}, | |
| # to prevent shell metacharacter evaluation (script injection). | |
| ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }} | |
| ALL_DELETED_FILES: ${{ steps.changed-files.outputs.deleted_files }} | |
| run: | | |
| CHANGED="$ALL_CHANGED_FILES" | |
| DELETED="$ALL_DELETED_FILES" | |
| if [ -z "$CHANGED" ] && [ -z "$DELETED" ]; then | |
| ORGS="[]" | |
| else | |
| CHANGED_FILES="$CHANGED $DELETED" | |
| # Pull Specific Orgs -> Cut Org Name -> Sort Unique -> JSON Array | |
| ORGS=$(echo "$CHANGED_FILES" | tr ' ' '\n' | \ | |
| grep -E -o "^buildkite/terraform/(bazel|bazel-trusted|bazel-testing)/" | cut -d/ -f3 | sort -u | \ | |
| jq -R -s -c 'split("\n") | map(select(length > 0))') | |
| fi | |
| echo "DEBUG:ORGS changed are: $ORGS" | |
| echo "orgs=${ORGS:-[]}" >> "$GITHUB_OUTPUT" | |
| # Job 2: Terraform Execution | |
| terraform: | |
| needs: detect-changes | |
| if: needs.detect-changes.outputs.orgs != '[]' | |
| runs-on: ubuntu-latest | |
| # go/github-security: Scope id-token to this job only. The detect-changes job | |
| # processes untrusted PR filenames; confining OIDC token access here prevents | |
| # credential exfiltration via script injection in that job. | |
| permissions: | |
| contents: read | |
| id-token: write | |
| pull-requests: write | |
| issues: write | |
| # Lock per organization. Cancels old runs on PRs (saves time), but queues pushes (safe apply). | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ matrix.org }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| # Use environment for secret isolation and manual approvals | |
| environment: ${{ matrix.org }} | |
| strategy: | |
| fail-fast: false # If one org fails, don't stop the others | |
| matrix: | |
| org: ${{ fromJSON(needs.detect-changes.outputs.orgs) }} | |
| defaults: | |
| run: | |
| working-directory: ./buildkite/terraform/${{ matrix.org }} | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| #TODO: change this back to WIP once the provider request is approved | |
| - name: Authenticate to Google Cloud | |
| uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0 | |
| with: | |
| credentials_json: '${{ secrets.GCP_SA_KEY }}' | |
| - name: Setup Terraform | |
| uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2 | |
| with: | |
| terraform_wrapper: false | |
| terraform_version: "1.9.5" | |
| - name: Terraform Init | |
| run: terraform init | |
| - name: Terraform Plan | |
| if: github.event_name == 'pull_request' | |
| id: plan | |
| env: | |
| TF_VAR_buildkite_api_token: ${{ secrets.BUILDKITE_API_TOKEN }} | |
| run: | | |
| terraform plan -no-color -input=false -out=tfplan | |
| terraform show -no-color tfplan > plan.txt | |
| - name: Post Plan to PR | |
| if: github.event_name == 'pull_request' | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| let planOutput = "Plan file not found"; | |
| try { | |
| planOutput = fs.readFileSync(`buildkite/terraform/${{ matrix.org }}/plan.txt`, 'utf8'); | |
| } catch (e) { | |
| planOutput = "Error reading plan file: " + e.message; | |
| } | |
| // Regex to match "Plan: X to add, Y to change, Z to destroy." | |
| const planRegex = /Plan:\s+(\d+)\s+to add,\s+(\d+)\s+to change,\s+(\d+)\s+to destroy\./; | |
| const match = planOutput.match(planRegex); | |
| let summary = `#### Terraform Plan for \`${{ matrix.org }}\` 📖`; | |
| if (match) { | |
| const [_, toAdd, toChange, toDestroy] = match; | |
| summary += `\nPlan: ${toAdd} to add, ${toChange} to change, ${toDestroy} to destroy.`; | |
| } else if (planOutput.includes("No changes.")) { | |
| summary += `\nNo changes. Your infrastructure matches the configuration.`; | |
| } | |
| const maxLength = 64000; | |
| let truncatedPlan = planOutput; | |
| if (planOutput.length > maxLength) { | |
| const msg = "\n... (truncated due to length. Read full plan in 'Terraform Plan' step of GitHub Actions run) ..."; | |
| truncatedPlan = planOutput.substring(0, maxLength) + msg; | |
| } | |
| const output = `${summary}\n<details><summary>Show Plan</summary>\n\n\`\`\`terraform\n${truncatedPlan}\n\`\`\`\n\n</details>`; | |
| github.rest.issues.listComments({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo | |
| }).then(({ data: comments }) => { | |
| const botComment = comments.find(comment => comment.body.includes(`#### Terraform Plan for \`${{ matrix.org }}\``)); | |
| if (botComment) { | |
| return github.rest.issues.updateComment({ | |
| comment_id: botComment.id, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: output | |
| }); | |
| } else { | |
| return github.rest.issues.createComment({ | |
| issue_number: context.issue.number, | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| body: output | |
| }); | |
| } | |
| }); | |
| - name: Terraform Apply | |
| if: github.ref == 'refs/heads/master' && github.event_name == 'push' | |
| env: | |
| TF_VAR_buildkite_api_token: ${{ secrets.BUILDKITE_API_TOKEN }} | |
| run: terraform apply -auto-approve -input=false |