Skip to content

Commit 5bb0ce0

Browse files
committed
feat: add Cloudflare Workers IaC and deployment workflows
1 parent 5a5b38f commit 5bb0ce0

18 files changed

Lines changed: 2055 additions & 92 deletions
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
name: Cloudflare Worker Preview Deploy
2+
3+
on:
4+
pull_request:
5+
6+
concurrency:
7+
group: cloudflare-worker-preview-${{ github.event.pull_request.number }}
8+
cancel-in-progress: true
9+
10+
jobs:
11+
deploy:
12+
if: github.event.pull_request.head.repo.full_name == github.repository
13+
runs-on: ubuntu-latest
14+
permissions:
15+
contents: read
16+
pull-requests: write
17+
steps:
18+
- name: Checkout repository
19+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
20+
with:
21+
persist-credentials: false
22+
23+
- name: Prepare preview metadata
24+
id: metadata
25+
shell: bash
26+
run: |
27+
preview_message="$(git log -1 --pretty=%s)"
28+
preview_message="$(printf '%s' "$preview_message" | head -c 100)"
29+
30+
{
31+
echo 'preview_message<<EOF'
32+
echo "$preview_message"
33+
echo EOF
34+
} >> "$GITHUB_OUTPUT"
35+
36+
- name: Setup node
37+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
38+
with:
39+
node-version-file: '.node-version'
40+
package-manager-cache: false
41+
42+
- name: Install dependencies
43+
run: npm ci
44+
45+
- name: Build app
46+
run: npm run build
47+
48+
- name: Upload Worker preview
49+
id: deploy
50+
uses: cloudflare/wrangler-action@da0e0dfe58b7a431659754fdf3f186c529afbe65 # v3.14.1
51+
with:
52+
apiToken: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }}
53+
accountId: ${{ secrets.TF_VAR_ACCOUNT_ID }}
54+
command: >
55+
versions upload
56+
-c dist/wrangler.json
57+
--preview-alias pr-${{ github.event.pull_request.number }}
58+
--message "${{ steps.metadata.outputs.preview_message }}"
59+
60+
- name: Resolve preview URL
61+
id: preview
62+
env:
63+
DEPLOYMENT_URL: ${{ steps.deploy.outputs.deployment-url }}
64+
COMMAND_OUTPUT: ${{ steps.deploy.outputs.command-output }}
65+
PR_NUMBER: ${{ github.event.pull_request.number }}
66+
shell: bash
67+
run: |
68+
alias="pr-${PR_NUMBER}"
69+
preview_url=""
70+
alias_url_pattern="^https?://${alias}-[^[:space:]]+$"
71+
72+
if printf '%s\n' "$DEPLOYMENT_URL" | grep -Eq "$alias_url_pattern"; then
73+
preview_url="$DEPLOYMENT_URL"
74+
else
75+
preview_url="$(printf '%s\n' "$COMMAND_OUTPUT" | grep -Eo "https?://${alias}-[^[:space:]\"'<>)]+" | head -n 1 || true)"
76+
fi
77+
78+
if ! printf '%s\n' "$preview_url" | grep -Eq "$alias_url_pattern"; then
79+
echo "Failed to resolve aliased Worker preview URL." >&2
80+
exit 1
81+
fi
82+
83+
{
84+
echo "preview_alias=${alias}"
85+
echo "preview_url=${preview_url}"
86+
} >> "$GITHUB_OUTPUT"
87+
88+
- name: Write deployment summary
89+
env:
90+
PREVIEW_ALIAS: ${{ steps.preview.outputs.preview_alias }}
91+
PREVIEW_URL: ${{ steps.preview.outputs.preview_url }}
92+
shell: bash
93+
run: |
94+
{
95+
echo "## Cloudflare Worker Preview"
96+
echo
97+
echo "- Alias: \`${PREVIEW_ALIAS}\`"
98+
echo "- Preview URL: ${PREVIEW_URL}"
99+
} >> "$GITHUB_STEP_SUMMARY"
100+
101+
- name: Publish preview URL
102+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
103+
env:
104+
MARKER: '<!-- cloudflare-worker-preview -->'
105+
PREVIEW_URL: ${{ steps.preview.outputs.preview_url }}
106+
with:
107+
github-token: ${{ secrets.GITHUB_TOKEN }}
108+
script: |
109+
const marker = process.env.MARKER;
110+
const previewUrl = process.env.PREVIEW_URL;
111+
if (!previewUrl) {
112+
core.setFailed("Missing preview URL from Cloudflare deploy step.");
113+
return;
114+
}
115+
116+
const body = `${marker}\nWorker preview: ${previewUrl}`;
117+
const { owner, repo } = context.repo;
118+
const issue_number = context.issue.number;
119+
const comments = await github.paginate(github.rest.issues.listComments, {
120+
owner,
121+
repo,
122+
issue_number,
123+
});
124+
const existing = comments.find(
125+
(comment) => comment.user.type === "Bot" && comment.body.includes(marker),
126+
);
127+
128+
if (existing) {
129+
await github.rest.issues.updateComment({
130+
owner,
131+
repo,
132+
comment_id: existing.id,
133+
body,
134+
});
135+
return;
136+
}
137+
138+
await github.rest.issues.createComment({
139+
owner,
140+
repo,
141+
issue_number,
142+
body,
143+
});
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
name: Cloudflare Infra
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'infra/web/**'
7+
- '.github/workflows/cloudflare-worker-prod.yml'
8+
push:
9+
branches:
10+
- dev
11+
workflow_dispatch:
12+
13+
concurrency:
14+
group: cloudflare-infra
15+
cancel-in-progress: false
16+
17+
jobs:
18+
plan:
19+
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
env:
24+
CLOUDFLARE_API_TOKEN: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }}
25+
TF_VAR_account_id: ${{ secrets.TF_VAR_ACCOUNT_ID }}
26+
TF_VAR_zone_id: ${{ secrets.TF_VAR_ZONE_ID }}
27+
TF_HTTP_ADDRESS: ${{ secrets.TF_HTTP_ADDRESS }}
28+
TF_HTTP_LOCK_ADDRESS: ${{ secrets.TF_HTTP_LOCK_ADDRESS }}
29+
TF_HTTP_UNLOCK_ADDRESS: ${{ secrets.TF_HTTP_UNLOCK_ADDRESS }}
30+
TF_HTTP_USERNAME: ${{ secrets.TF_HTTP_USERNAME }}
31+
TF_HTTP_PASSWORD: ${{ secrets.TF_HTTP_PASSWORD }}
32+
TF_HTTP_LOCK_METHOD: 'POST'
33+
TF_HTTP_UNLOCK_METHOD: 'DELETE'
34+
TF_HTTP_RETRY_WAIT_MIN: '5'
35+
defaults:
36+
run:
37+
working-directory: infra/web
38+
steps:
39+
- name: Checkout repository
40+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
41+
with:
42+
persist-credentials: false
43+
44+
- name: Prepare deployment metadata
45+
shell: bash
46+
run: |
47+
workers_message="$(git log -1 --pretty=%s)"
48+
workers_message="$(printf '%s' "$workers_message" | head -c 100)"
49+
printf 'TF_VAR_workers_message=%s\n' "$workers_message" >> "$GITHUB_ENV"
50+
51+
- name: Setup node
52+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
53+
with:
54+
node-version-file: '.node-version'
55+
cache: 'npm'
56+
57+
- name: Install dependencies
58+
working-directory: .
59+
run: npm ci
60+
61+
- name: Build app
62+
working-directory: .
63+
run: npm run build
64+
65+
- name: Setup OpenTofu
66+
uses: opentofu/setup-opentofu@9d84900f3238fab8cd84ce47d658d25dd008be2f # v1.0.8
67+
with:
68+
tofu_version: 1.11.5
69+
70+
- name: Check formatting
71+
run: tofu fmt -check
72+
73+
- name: Initialize OpenTofu
74+
run: tofu init -input=false -reconfigure
75+
76+
- name: Validate configuration
77+
run: tofu validate -no-color
78+
79+
- name: Plan infrastructure
80+
run: tofu plan -input=false -lock=false -no-color
81+
82+
apply:
83+
if: (github.event_name == 'push' && github.ref_name == 'dev') || github.event_name == 'workflow_dispatch'
84+
runs-on: ubuntu-latest
85+
permissions:
86+
contents: read
87+
env:
88+
CLOUDFLARE_API_TOKEN: ${{ secrets.TF_CLOUDFLARE_API_TOKEN }}
89+
TF_VAR_account_id: ${{ secrets.TF_VAR_ACCOUNT_ID }}
90+
TF_VAR_zone_id: ${{ secrets.TF_VAR_ZONE_ID }}
91+
TF_HTTP_ADDRESS: ${{ secrets.TF_HTTP_ADDRESS }}
92+
TF_HTTP_LOCK_ADDRESS: ${{ secrets.TF_HTTP_LOCK_ADDRESS }}
93+
TF_HTTP_UNLOCK_ADDRESS: ${{ secrets.TF_HTTP_UNLOCK_ADDRESS }}
94+
TF_HTTP_USERNAME: ${{ secrets.TF_HTTP_USERNAME }}
95+
TF_HTTP_PASSWORD: ${{ secrets.TF_HTTP_PASSWORD }}
96+
TF_HTTP_LOCK_METHOD: 'POST'
97+
TF_HTTP_UNLOCK_METHOD: 'DELETE'
98+
TF_HTTP_RETRY_WAIT_MIN: '5'
99+
defaults:
100+
run:
101+
working-directory: infra/web
102+
steps:
103+
- name: Checkout repository
104+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
105+
with:
106+
persist-credentials: false
107+
108+
- name: Prepare deployment metadata
109+
shell: bash
110+
run: |
111+
workers_message="$(git log -1 --pretty=%s)"
112+
workers_message="$(printf '%s' "$workers_message" | head -c 100)"
113+
printf 'TF_VAR_workers_message=%s\n' "$workers_message" >> "$GITHUB_ENV"
114+
115+
- name: Setup node
116+
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
117+
with:
118+
node-version-file: '.node-version'
119+
cache: 'npm'
120+
121+
- name: Install dependencies
122+
working-directory: .
123+
run: npm ci
124+
125+
- name: Build app
126+
working-directory: .
127+
run: npm run build
128+
129+
- name: Setup OpenTofu
130+
uses: opentofu/setup-opentofu@9d84900f3238fab8cd84ce47d658d25dd008be2f # v1.0.8
131+
with:
132+
tofu_version: 1.11.5
133+
134+
- name: Check formatting
135+
run: tofu fmt -check
136+
137+
- name: Initialize OpenTofu
138+
run: tofu init -input=false -reconfigure
139+
140+
- name: Validate configuration
141+
run: tofu validate -no-color
142+
143+
- name: Plan infrastructure
144+
run: tofu plan -input=false -no-color
145+
146+
- name: Apply infrastructure
147+
run: tofu apply -input=false -auto-approve

.gitignore

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,15 @@ devAssets
66
.DS_Store
77
.idea
88

9-
.env
9+
.env
10+
11+
.wrangler
12+
.dev.vars*
13+
.terraform/
14+
*.tfstate
15+
*.tfstate.*
16+
*.tfvars
17+
!*.tfvars.example
18+
*.tfbackend
19+
!*.tfbackend.example
20+
crash.log

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,7 @@ To build the app:
7474
```sh
7575
npm run build # Compiles the app into the dist/ directory
7676
```
77+
78+
## Deployment and infrastructure
79+
Deployment workflows and infrastructure details live in
80+
[`infra/README.md`](infra/README.md).

0 commit comments

Comments
 (0)