pez-infra/.github/workflows/validate-terraform.yml
Rasmus Wejlgaard d2ec024e6a Give Terraform CI tailnet access for the Proxmox provider
The bpg/proxmox provider has to reach london-a's API and node, which only
live on the tailnet, during plan and apply. Add a setup-tailnet composite
action (Tailscale via the CI OAuth client + the deploy SSH key in an agent)
and use it in the terraform plan/apply and validate workflows. Pin the
provider's node SSH address to london-a's Tailscale IP so it isn't reached
via the API-reported LAN address.
2026-06-21 18:20:02 +01:00

131 lines
4.7 KiB
YAML

name: Validate Terraform
on:
pull_request:
paths:
- "terraform/**"
- ".github/workflows/validate-terraform.yml"
permissions:
contents: read
pull-requests: write
# Requires these repository secrets:
# AGE_SECRET_KEY — age private key for SOPS decryption
#
# Dependabot PRs run with no access to these secrets and a read-only token,
# so they take a lightweight, secret-free path (init + validate, no plan/
# comment). Provider-version bumps are still resolved and validated.
jobs:
plan:
name: tofu plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-tofu
# --- Dependabot: secret-free validation -------------------------------
- name: Validate (no secrets)
if: github.actor == 'dependabot[bot]'
working-directory: terraform/
run: |
# secrets.yaml is decrypted from SOPS at plan time and can't be
# produced here, so stub the keys the config reads (kept in sync by
# deriving them from the actual secrets["..."] references).
# Stub values must satisfy provider config validators: hcloud
# requires a 64-char token, and Grafana's fleet_management_auth
# must look like `username:password`.
stub64=$(printf 'stub%.0s' {1..16})
grep -rhoE 'secrets\["[^"]+"\]' . \
| sed -E 's/.*secrets\["([^"]+)"\].*/\1/' \
| sort -u \
| while read -r key; do
case "$key" in
*_auth) echo "$key: \"stub:stub\"" ;;
*) echo "$key: \"$stub64\"" ;;
esac
done > secrets.yaml
tofu init -backend=false
tofu validate
# --- Human PRs: full plan against real backend ------------------------
- name: Decrypt secrets
if: github.actor != 'dependabot[bot]'
uses: ./.github/actions/sops-decrypt
with:
age-key: ${{ secrets.AGE_SECRET_KEY }}
- name: Set backend credentials
if: github.actor != 'dependabot[bot]'
uses: ./.github/actions/tofu-backend-creds
# Proxmox (bpg) provider reaches london-a over the tailnet during plan.
- name: Set up tailnet + SSH
if: github.actor != 'dependabot[bot]'
uses: ./.github/actions/setup-tailnet
with:
tailscale-client-id: ${{ secrets.TAILSCALE_CLIENT_ID }}
tailscale-audience: ${{ secrets.TAILSCALE_AUDIENCE }}
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: tofu init
if: github.actor != 'dependabot[bot]'
working-directory: terraform/
run: tofu init
- name: tofu validate
if: github.actor != 'dependabot[bot]'
working-directory: terraform/
run: tofu validate
- name: tofu plan
id: plan
if: github.actor != 'dependabot[bot]'
working-directory: terraform/
continue-on-error: true
run: |
set -o pipefail
tofu plan -no-color 2>&1 | tee plan_output.txt
- name: Post plan as PR comment
if: github.actor != 'dependabot[bot]'
uses: actions/github-script@v9
with:
script: |
const fs = require('fs');
const raw = fs.readFileSync('terraform/plan_output.txt', 'utf8');
const filtered = raw.split('\n').filter(l => !l.includes(': Refreshing state...')).join('\n');
const truncated = filtered.length > 65000
? filtered.slice(0, 65000) + '\n\n...(output truncated)'
: filtered;
const outcome = '${{ steps.plan.outcome }}';
const header = outcome === 'failure' ? '## Terraform Plan — FAILED' : '## Terraform Plan';
const body = `${header}\n\`\`\`\n${truncated}\n\`\`\``;
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const existing = comments.find(c => c.body.startsWith('## Terraform Plan'));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
- name: Fail if plan failed
if: github.actor != 'dependabot[bot]' && steps.plan.outcome == 'failure'
run: exit 1