Skip to content

Commit 2cbb365

Browse files
committed
Add tofu plan and validate workflows for pull requests
Split into two workflows: - validate.yaml: runs on pull_request (no secrets needed), does tofu init -backend=false + tofu validate for immediate feedback on syntax/config errors. - plan.yaml: runs on pull_request_target with a required "plan" environment gate, so a maintainer must approve before secrets (AWS creds, GitHub App token) are exposed to the PR's code. Posts the plan output as a PR comment. The environment gate mitigates the Pwn Request risk inherent in pull_request_target + checkout PR head: external data sources and provider binaries execute during plan, so untrusted code must not run without human review of the diff. Assisted-by: Claude Code <noreply@anthropic.com> Signed-off-by: Riccardo Piccoli <rpiccoli@redhat.com>
1 parent 568e268 commit 2cbb365

2 files changed

Lines changed: 151 additions & 0 deletions

File tree

.github/workflows/plan.yaml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
name: Plan
2+
3+
on:
4+
pull_request_target:
5+
paths:
6+
- "**/*.tf"
7+
- "**/*.csv"
8+
9+
permissions:
10+
contents: read
11+
pull-requests: write
12+
13+
concurrency:
14+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
plan:
19+
runs-on: ubuntu-latest
20+
environment: plan
21+
env:
22+
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
23+
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
24+
CONFIG_APP_CLIENT_ID: Iv23lipEOAvwk5QqNUie
25+
TF_CLI_ARGS: "-no-color"
26+
TF_IN_AUTOMATION: "true"
27+
steps:
28+
- name: Checkout PR
29+
uses: actions/checkout@v7
30+
with:
31+
ref: ${{ github.event.pull_request.head.sha }}
32+
33+
- name: Generate a token
34+
id: generate-token
35+
uses: actions/create-github-app-token@v3
36+
with:
37+
client-id: ${{ env.CONFIG_APP_CLIENT_ID }}
38+
private-key: ${{ secrets.CONFIG_APP_SECRET }}
39+
owner: ${{ github.repository_owner }}
40+
41+
- name: Setup OpenTofu
42+
uses: opentofu/setup-opentofu@v2
43+
with:
44+
tofu_wrapper: false
45+
46+
- name: TF init
47+
run: tofu init
48+
49+
- name: TF Validate
50+
run: tofu validate
51+
52+
- name: TF Plan
53+
id: plan
54+
run: |
55+
set +e
56+
tofu plan -detailed-exitcode 2>&1 | tee plan_output.txt
57+
exitcode=${PIPESTATUS[0]}
58+
echo "exitcode=$exitcode" >> "$GITHUB_OUTPUT"
59+
if [ "$exitcode" -eq 1 ]; then
60+
exit 1
61+
fi
62+
env:
63+
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
64+
65+
- name: Comment plan on PR
66+
if: always() && steps.plan.outcome != 'skipped'
67+
uses: actions/github-script@v7
68+
with:
69+
script: |
70+
const fs = require('fs');
71+
const raw = fs.readFileSync('plan_output.txt', 'utf8');
72+
const plan = raw.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
73+
const exitcode = '${{ steps.plan.outputs.exitcode }}';
74+
75+
let summary;
76+
if (exitcode === '0') {
77+
summary = 'No changes. Infrastructure is up-to-date.';
78+
} else if (exitcode === '2') {
79+
summary = 'Changes detected. Review the plan below.';
80+
} else {
81+
summary = 'Plan failed. See details below.';
82+
}
83+
84+
const marker = '<!-- tofu-plan -->';
85+
const body = [
86+
marker,
87+
`### OpenTofu Plan`,
88+
'',
89+
summary,
90+
'',
91+
'<details><summary>Show plan output</summary>',
92+
'',
93+
`<pre>${plan.substring(0, 60000)}</pre>`,
94+
'',
95+
'</details>',
96+
'',
97+
`*Commit: ${context.payload.pull_request.head.sha}*`,
98+
].join('\n');
99+
100+
const { data: comments } = await github.rest.issues.listComments({
101+
owner: context.repo.owner,
102+
repo: context.repo.repo,
103+
issue_number: context.issue.number,
104+
});
105+
const existing = comments.find(c => c.body.includes(marker));
106+
107+
if (existing) {
108+
await github.rest.issues.updateComment({
109+
owner: context.repo.owner,
110+
repo: context.repo.repo,
111+
comment_id: existing.id,
112+
body,
113+
});
114+
} else {
115+
await github.rest.issues.createComment({
116+
owner: context.repo.owner,
117+
repo: context.repo.repo,
118+
issue_number: context.issue.number,
119+
body,
120+
});
121+
}

.github/workflows/validate.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Validate
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "**/*.tf"
7+
- "**/*.csv"
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
validate:
14+
runs-on: ubuntu-latest
15+
env:
16+
TF_CLI_ARGS: "-no-color"
17+
TF_IN_AUTOMATION: "true"
18+
steps:
19+
- uses: actions/checkout@v7
20+
21+
- name: Setup OpenTofu
22+
uses: opentofu/setup-opentofu@v2
23+
with:
24+
tofu_wrapper: false
25+
26+
- name: TF init (no backend)
27+
run: tofu init -backend=false
28+
29+
- name: TF Validate
30+
run: tofu validate

0 commit comments

Comments
 (0)