Skip to content

Commit cb7be2f

Browse files
committed
Add GHA workflow to integrate and deploy OpenToFu plan
Signed-off-by: Benoit Donneaux <ben@tergology.com>
1 parent 3de89d5 commit cb7be2f

File tree

2 files changed

+233
-0
lines changed

2 files changed

+233
-0
lines changed

.github/workflows/_tf.yml

+200
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Re-usable workflow to continuously integrate and deploy OpenToFu plan
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
tf_version:
7+
description: 'Version of OpenToFu runtime to use'
8+
required: false
9+
type: string
10+
default: '1.9.0'
11+
tf_dir:
12+
description: 'Path to the OpenToFu plan to use'
13+
required: false
14+
type: string
15+
default: './'
16+
gh_runner_version:
17+
description: 'Version of the GitHub runner to use'
18+
required: false
19+
type: string
20+
default: 'ubuntu-22.04'
21+
auto_comment:
22+
description: 'Enable automatic comment on GitHub pull request'
23+
required: false
24+
type: boolean
25+
default: true
26+
apply_on_branch:
27+
description: 'Automaticaly apply plan when on a specific branch'
28+
required: false
29+
type: string
30+
default: ''
31+
aws_default_region:
32+
description: 'AWS default region'
33+
required: false
34+
type: string
35+
default: 'eu-central-1'
36+
secrets:
37+
aws_access_key_id:
38+
description: 'AWS access key id'
39+
required: false
40+
aws_secret_access_key:
41+
description: 'AWS secret access key'
42+
required: false
43+
hcloud_token:
44+
description: 'API token for Hetzner Cloud'
45+
required: false
46+
47+
jobs:
48+
tf:
49+
name: OpenToFu
50+
runs-on: ${{ inputs.gh_runner_version }}
51+
defaults:
52+
run:
53+
working-directory: ${{ inputs.tf_dir }}
54+
env:
55+
AWS_ACCESS_KEY_ID: ${{ secrets.aws_access_key_id }}
56+
AWS_SECRET_ACCESS_KEY: ${{ secrets.aws_secret_access_key }}
57+
AWS_DEFAULT_REGION: ${{ inputs.aws_default_region }}
58+
TF_VAR_hcloud_token: ${{ secrets.hcloud_token }}
59+
permissions:
60+
pull-requests: write
61+
contents: read
62+
63+
steps:
64+
- name: Checkout
65+
id: checkout
66+
uses: actions/checkout@v4
67+
68+
- name: Setup
69+
id: setup
70+
uses: opentofu/setup-opentofu@v1
71+
with:
72+
tofu_version: ${{ inputs.tf_version }}
73+
74+
- name: Format
75+
id: fmt
76+
run: tofu fmt -no-color -check -diff
77+
78+
- name: Init
79+
id: init
80+
run: tofu init -no-color -input=false
81+
82+
- name: Validate
83+
id: validate
84+
run: tofu validate -no-color
85+
86+
- name: Plan
87+
id: plan
88+
run: tofu plan -no-color -input=false -compact-warnings -out tf_plan.out
89+
90+
- name: Post Setup
91+
id: post_setup
92+
# Only to avoid wrapper to mess up outputs in later steps
93+
uses: opentofu/setup-opentofu@v1
94+
with:
95+
tofu_version: ${{ inputs.tf_version }}
96+
tofu_wrapper: false
97+
98+
- name: Verify
99+
id: verify
100+
run: |
101+
# Process the plan to verify the presence of some data
102+
# which can be used later to make additional checks
103+
tofu show -no-color tf_plan.out > tf_plan.log 2> >(tee tf_plan.err >&2) && ret=0 || ret=$?
104+
# Export the plan in json too
105+
tofu show -json tf_plan.out > tf_plan.json
106+
# Extract current state from the plan for later comparison
107+
unzip tf_plan.out tfstate
108+
# Extract data from temp files and export them as outputs for next steps
109+
# - changes made, if any
110+
echo "changes<<tf_verify_changes" >> $GITHUB_OUTPUT
111+
awk '/the following actions/,0' tf_plan.log >> $GITHUB_OUTPUT
112+
echo "tf_verify_changes" >> $GITHUB_OUTPUT
113+
# - summary of the change, if any
114+
echo "summary<<tf_verify_summary" >> $GITHUB_OUTPUT
115+
awk '/(Plan: |No changes. )/,1' tf_plan.log | sed -e 's/Plan: /change(s): /' >> $GITHUB_OUTPUT
116+
echo "tf_verify_summary" >> $GITHUB_OUTPUT
117+
# - stderr describing errors, if any
118+
echo "stderr<<tf_verify_stderr" >> $GITHUB_OUTPUT
119+
cat tf_plan.err >> $GITHUB_OUTPUT
120+
echo "tf_verify_stderr" >> $GITHUB_OUTPUT
121+
# Exit with failure if any
122+
exit $ret
123+
124+
- name: Comment
125+
id: update
126+
if: ${{ always() && github.event_name == 'pull_request' && inputs.auto_comment }}
127+
uses: actions/github-script@v7
128+
with:
129+
github-token: ${{ github.token }}
130+
script: |
131+
// 1. Retrieve existing bot comments for the PR
132+
const { data: comments } = await github.rest.issues.listComments({
133+
owner: context.repo.owner,
134+
repo: context.repo.repo,
135+
issue_number: context.issue.number,
136+
})
137+
const botComment = comments.find(comment => {
138+
return comment.user.type === 'Bot' && comment.body.includes('pr-auto-comment-${{ inputs.tf_dir }}')
139+
})
140+
141+
// 2. Prepare format of the comment, using toJSON to escape any special char
142+
const changes = [
143+
${{ toJSON(steps.verify.outputs.changes) }},
144+
]
145+
const errors = [
146+
${{ toJSON(steps.fmt.outputs.stdout) }},
147+
${{ toJSON(steps.init.outputs.stderr) }},
148+
${{ toJSON(steps.validate.outputs.stderr) }},
149+
${{ toJSON(steps.plan.outputs.stderr) }},
150+
${{ toJSON(steps.verify.outputs.stderr) }},
151+
]
152+
const output = `
153+
<!-- pr-auto-comment-${{ inputs.tf_dir }} -->
154+
### ${{ github.workflow }}
155+
| Step | Outcome |
156+
| ---- | ------- |
157+
| :pencil2: **Format** | \`${{ steps.fmt.outcome }}\` |
158+
| :wrench: **Init** ️| \`${{ steps.init.outcome }}\` |
159+
| :mag: **Validate** | \`${{ steps.validate.outcome }}\` |
160+
| :page_facing_up: **Plan** | \`${{ steps.plan.outcome }}\` |
161+
| :passport_control: **Verify** | \`${{ steps.verify.outcome }}\` |
162+
| :point_right: **Result** | ${{ ( steps.plan.outcome == 'success' && steps.verify.outcome == 'success' && steps.verify.outputs.summary ) || 'with error(s) - see below' }} |
163+
164+
<details><summary>show change(s)</summary>
165+
166+
\`\`\`
167+
${ changes.filter(function(entry) { return /\S/.test(entry); }).join('\n') }
168+
\`\`\`
169+
170+
</details>
171+
172+
<details><summary>show error(s)</summary>
173+
174+
\`\`\`
175+
${ errors.filter(function(entry) { return /\S/.test(entry); }).join('\n') }
176+
\`\`\`
177+
178+
</details>
179+
180+
*Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*
181+
*Workflow: \`${{ github.workflow_ref }}\`*`;
182+
183+
// 3. If we have a comment, update it, otherwise create a new one
184+
const comment_data = {
185+
owner: context.repo.owner,
186+
repo: context.repo.repo,
187+
body: output
188+
}
189+
if (botComment) {
190+
comment_data.comment_id = botComment.id
191+
github.rest.issues.updateComment(comment_data)
192+
} else {
193+
comment_data.issue_number = context.issue.number
194+
github.rest.issues.createComment(comment_data)
195+
}
196+
197+
- name: Apply
198+
id: apply
199+
if: ${{ inputs.apply_on_branch != '' && github.ref == format('refs/heads/{0}', inputs.apply_on_branch) }}
200+
run: tofu apply -no-color -input=false -auto-approve tf_plan.out

.github/workflows/tf-core.yml

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Workflow to continuously integrate and deploy OpenToFu plan for
2+
# the core resources of Tahoe-LAFS (e.g.: DNS, self-hosted vps, ...)
3+
name: ToFu - core
4+
concurrency: tf-core_state
5+
6+
on:
7+
push:
8+
branches:
9+
- main
10+
paths:
11+
- '.github/workflows/_tf.yml'
12+
- '.github/workflows/tf-core.yml'
13+
- 'tf/core/**'
14+
pull_request:
15+
paths:
16+
- '.github/workflows/_tf.yml'
17+
- '.github/workflows/tf-core.yml'
18+
- 'tf/core/**'
19+
workflow_dispatch:
20+
21+
jobs:
22+
call-workflow-passing-data:
23+
# Call the re-usable ToFu workflow
24+
uses: ./.github/workflows/_tf.yml
25+
with:
26+
tf_version: '1.9.0'
27+
tf_dir: tf/core
28+
apply_on_branch: 'main'
29+
aws_default_region: 'eu-central-1'
30+
secrets:
31+
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY_ID }}
32+
aws_secret_access_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
33+
hcloud_token: ${{ secrets.HCLOUD_TOKEN }}

0 commit comments

Comments
 (0)