Skip to content

Commit 2b0e649

Browse files
authored
Merge pull request #183 from chore/cloudflare-workers-migration
chore: migrate hosting from Netlify to Cloudflare Workers (static assets)
2 parents 6046e7c + 67498c1 commit 2b0e649

15 files changed

Lines changed: 1050 additions & 72 deletions

.github/labeler.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ ci:
2727
- any-glob-to-any-file:
2828
- '.github/**/*'
2929
- 'netlify.toml'
30+
- 'wrangler.jsonc'
3031

3132
# Dependencies
3233
dependencies:

.github/workflows/ci.yml

Lines changed: 2 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -138,71 +138,11 @@ jobs:
138138
path: dist/
139139
retention-days: 7
140140

141-
# Job 4: Deploy to Netlify
142-
deploy:
143-
name: Deploy to Netlify
144-
runs-on: ubuntu-latest
145-
needs: [quality, test, build] # Only deploy if all checks pass
146-
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
147-
permissions:
148-
contents: read
149-
pull-requests: write
150-
deployments: write
151-
statuses: write
152-
153-
steps:
154-
- name: Checkout code
155-
uses: actions/checkout@v6
156-
157-
- name: Setup pnpm
158-
uses: pnpm/action-setup@v4
159-
with:
160-
version: 9
161-
162-
- name: Setup Node.js
163-
uses: actions/setup-node@v6
164-
with:
165-
node-version: 24.11.1
166-
cache: 'pnpm'
167-
168-
- name: Install dependencies
169-
run: pnpm install --frozen-lockfile
170-
171-
- name: Build project
172-
run: pnpm build
173-
env:
174-
# embed branch name into Vite build so the frontend can read it at runtime
175-
VITE_GIT_BRANCH: ${{ github.ref_name }}
176-
# optional: clickable dev site URL to use in the badge
177-
VITE_DEV_SITE_URL: 'https://dev.bayanflow.com'
178-
179-
- name: Deploy to Netlify
180-
id: netlify
181-
uses: nwtgck/actions-netlify@v3.0
182-
with:
183-
publish-dir: './dist'
184-
production-branch: main
185-
github-token: ${{ secrets.GITHUB_TOKEN }}
186-
deploy-message: "Deploy from GitHub Actions (${{ github.ref_name }})"
187-
enable-pull-request-comment: false
188-
enable-commit-comment: false
189-
github-deployment-environment: ${{ github.ref == 'refs/heads/main' && 'production' || github.ref == 'refs/heads/develop' && 'beta' || 'preview' }}
190-
env:
191-
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
192-
# Use the dev site ID when building from develop, otherwise production site ID
193-
NETLIFY_SITE_ID: ${{ github.ref == 'refs/heads/develop' && secrets.NETLIFY_SITE_ID_DEV || secrets.NETLIFY_SITE_ID }}
194-
timeout-minutes: 10
195-
196-
- name: Output deployment URL
197-
if: always()
198-
run: |
199-
echo "Netlify deploy URL: ${{ steps.netlify.outputs.deploy-url }}"
200-
201-
# Job 5: All checks passed (optional but useful for branch protection)
141+
# Job 4: All checks passed (optional but useful for branch protection)
202142
all-checks-pass:
203143
name: All Checks Passed
204144
runs-on: ubuntu-latest
205-
needs: [quality, test, build, deploy]
145+
needs: [quality, test, build]
206146
if: always()
207147
permissions:
208148
contents: read
@@ -214,8 +154,4 @@ jobs:
214154
echo "One or more jobs failed"
215155
exit 1
216156
fi
217-
if [[ "${{ needs.deploy.result }}" != "success" && "${{ needs.deploy.result }}" != "skipped" ]]; then
218-
echo "Deploy job failed"
219-
exit 1
220-
fi
221157
echo "All checks passed successfully!"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Deploy (Cloudflare Workers)
2+
3+
on:
4+
workflow_run:
5+
workflows: [CI]
6+
types: [completed]
7+
branches: [main, develop]
8+
9+
concurrency:
10+
group: cloudflare-deploy-${{ github.event.workflow_run.head_branch }}
11+
cancel-in-progress: true
12+
13+
jobs:
14+
deploy:
15+
name: Deploy to Cloudflare Workers
16+
runs-on: ubuntu-latest
17+
if: >-
18+
github.event.workflow_run.conclusion == 'success' &&
19+
(github.event.workflow_run.head_branch == 'main' ||
20+
github.event.workflow_run.head_branch == 'develop')
21+
permissions:
22+
contents: read
23+
deployments: write
24+
25+
steps:
26+
- name: Checkout code
27+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
28+
with:
29+
ref: ${{ github.event.workflow_run.head_sha }}
30+
persist-credentials: false
31+
32+
- name: Setup pnpm
33+
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
34+
with:
35+
version: 9
36+
37+
- name: Setup Node.js
38+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
39+
with:
40+
node-version: 24.11.1
41+
cache: pnpm
42+
43+
- name: Install dependencies
44+
run: pnpm install --frozen-lockfile
45+
46+
- name: Build project
47+
run: pnpm build
48+
env:
49+
VITE_GIT_BRANCH: ${{ github.event.workflow_run.head_branch }}
50+
VITE_DEV_SITE_URL: https://dev.bayanflow.com
51+
52+
- name: Deploy to Cloudflare Workers
53+
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3
54+
with:
55+
apiToken: ${{ secrets.CF_API_TOKEN }}
56+
accountId: ${{ secrets.CF_ACCOUNT_ID }}
57+
packageManager: pnpm
58+
command: >-
59+
deploy --env
60+
${{ github.event.workflow_run.head_branch == 'main' && 'production' || 'staging' }}
61+
62+
- name: Output deployment URL
63+
env:
64+
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
65+
run: |
66+
if [[ "$HEAD_BRANCH" == "main" ]]; then
67+
echo "Production worker: https://bayan-flow.workers.dev"
68+
echo "Custom domain (after DNS cutover): https://bayanflow.com"
69+
else
70+
echo "Staging worker: https://bayan-flow-staging.workers.dev"
71+
echo "Custom domain (after DNS cutover): https://dev.bayanflow.com"
72+
fi
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
name: Preview (Cloudflare Workers)
2+
3+
on:
4+
pull_request:
5+
branches: [develop]
6+
types: [opened, synchronize, reopened]
7+
8+
concurrency:
9+
group: cloudflare-preview-${{ github.event.pull_request.number }}
10+
cancel-in-progress: true
11+
12+
jobs:
13+
preview:
14+
name: Upload PR preview
15+
runs-on: ubuntu-latest
16+
if: github.event.pull_request.head.repo.full_name == github.repository
17+
permissions:
18+
contents: read
19+
pull-requests: write
20+
21+
steps:
22+
- name: Checkout code
23+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
24+
with:
25+
ref: ${{ github.event.pull_request.head.sha }}
26+
persist-credentials: false
27+
28+
- name: Setup pnpm
29+
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
30+
with:
31+
version: 9
32+
33+
- name: Setup Node.js
34+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
35+
with:
36+
node-version: 24.11.1
37+
cache: pnpm
38+
39+
- name: Install dependencies
40+
run: pnpm install --frozen-lockfile
41+
42+
- name: Run ESLint
43+
run: pnpm lint
44+
45+
- name: Check code formatting
46+
run: pnpm format:check
47+
48+
- name: Run tests with coverage
49+
run: pnpm test:coverage
50+
env:
51+
NODE_OPTIONS: --max-old-space-size=4096
52+
53+
- name: Build project
54+
run: pnpm build
55+
env:
56+
VITE_GIT_BRANCH: ${{ github.head_ref }}
57+
VITE_DEV_SITE_URL: https://dev.bayanflow.com
58+
59+
- name: Upload preview version
60+
id: deploy
61+
uses: cloudflare/wrangler-action@9acf94ace14e7dc412b076f2c5c20b8ce93c79cd # v3
62+
with:
63+
apiToken: ${{ secrets.CF_API_TOKEN }}
64+
accountId: ${{ secrets.CF_ACCOUNT_ID }}
65+
packageManager: pnpm
66+
command: >-
67+
versions upload --env staging
68+
--preview-alias pr-${{ github.event.pull_request.number }}
69+
--tag pr-${{ github.event.pull_request.number }}
70+
--message "PR #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}"
71+
72+
- name: Comment PR with preview URL
73+
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
74+
env:
75+
DEPLOY_OUTPUT: ${{ steps.deploy.outputs['command-output'] }}
76+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
77+
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
78+
with:
79+
script: |
80+
const output = process.env.DEPLOY_OUTPUT || '';
81+
const urlRegex = /Version Preview Alias URL:\s+(https:\/\/[^\s]+)/;
82+
const match = output.match(urlRegex);
83+
const prNumber = context.payload.pull_request.number;
84+
const fallbackUrl = `https://pr-${prNumber}-bayan-flow-staging.workers.dev`;
85+
const previewUrl = match ? match[1] : fallbackUrl;
86+
const runUrl = process.env.RUN_URL;
87+
const headSha = process.env.HEAD_SHA;
88+
const qrUrl = `https://quickchart.io/qr?size=200&text=${encodeURIComponent(previewUrl)}`;
89+
const marker = '<!-- cloudflare-preview:bayan-flow-staging -->';
90+
const body = [
91+
'### <span aria-hidden="true">✅</span> Cloudflare Workers preview for *bayan-flow-staging* ready!',
92+
'',
93+
'| Name | Link |',
94+
'|:-:|------------------------|',
95+
`| <span aria-hidden="true">🔨</span> Latest commit | ${headSha} |`,
96+
`| <span aria-hidden="true">🔍</span> Latest deploy log | ${runUrl} |`,
97+
`| <span aria-hidden="true">😎</span> Deploy Preview | [${previewUrl}](${previewUrl}) |`,
98+
`| <span aria-hidden="true">📱</span> Preview on mobile | <details><summary> Toggle QR Code... </summary><br /><br />![QR Code](${qrUrl})<br /><br />_Use your smartphone camera to open QR code link._</details> |`,
99+
'',
100+
'---',
101+
marker,
102+
`_Preview alias \`pr-${prNumber}\` on the staging worker. Updates automatically with new commits._`,
103+
].join('\n');
104+
105+
const comments = await github.paginate(github.rest.issues.listComments, {
106+
owner: context.repo.owner,
107+
repo: context.repo.repo,
108+
issue_number: context.issue.number,
109+
per_page: 100,
110+
});
111+
const existing = comments.find(
112+
(comment) =>
113+
comment.body?.includes(marker) ||
114+
comment.body?.includes('<!-- cloudflare-preview -->')
115+
);
116+
117+
if (existing) {
118+
await github.rest.issues.updateComment({
119+
owner: context.repo.owner,
120+
repo: context.repo.repo,
121+
comment_id: existing.id,
122+
body,
123+
});
124+
} else {
125+
await github.rest.issues.createComment({
126+
owner: context.repo.owner,
127+
repo: context.repo.repo,
128+
issue_number: context.issue.number,
129+
body,
130+
});
131+
}

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ coverage/
2626
*.sln
2727
*.sw?
2828
.tmp
29+
.wrangler

index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
hostname === 'localhost' ||
2929
hostname === '127.0.0.1' ||
3030
hostname.indexOf('deploy-preview') !== -1 ||
31+
hostname.slice(-12) === '.workers.dev' ||
3132
(hostname.slice(-12) === '.netlify.app' &&
3233
hostname !== 'bayanflow.com' &&
3334
hostname !== 'www.bayanflow.com');
@@ -49,6 +50,7 @@
4950
if (
5051
hostname === 'dev.bayanflow.com' ||
5152
hostname === 'dev-bayanflow.netlify.app' ||
53+
hostname.slice(-12) === '.workers.dev' ||
5254
hostname.includes('deploy-preview')
5355
) {
5456
// Develop

netlify.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Kept for rollback during Netlify → Cloudflare migration.
2+
# CI no longer deploys here; remove this file after Netlify projects are deleted (~2 weeks post cutover).
3+
14
[build]
25
command = "pnpm build"
36
publish = "dist"
@@ -15,7 +18,7 @@
1518
X-Frame-Options = "DENY"
1619
Referrer-Policy = "strict-origin-when-cross-origin"
1720
Permissions-Policy = "camera=(), microphone=(), geolocation=()"
18-
Content-Security-Policy = "default-src 'self'; script-src 'self' https://cloud.umami.is 'unsafe-inline'; connect-src 'self' https://cloud.umami.is https://api.github.com https://cdn.jsdelivr.net; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; worker-src 'self' blob:; frame-src https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'"
21+
Content-Security-Policy = "default-src 'self'; script-src 'self' https://cloud.umami.is 'unsafe-inline'; connect-src 'self' https://cloud.umami.is https://api.github.com https://cdn.jsdelivr.net; img-src 'self' data: blob: https://api.producthunt.com; style-src 'self' 'unsafe-inline'; font-src 'self'; worker-src 'self' blob:; frame-src https://www.youtube-nocookie.com; object-src 'none'; base-uri 'self'; form-action 'self'"
1922

2023
# SPA routing: rewrite all non-file requests to index.html
2124
[[redirects]]

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@
4747
"test:run": "vitest run",
4848
"test:coverage": "vitest run --coverage",
4949
"generate:export-sfx": "node scripts/render-tone-export-sfx.mjs",
50-
"fetch:youtube-thumbnails": "node scripts/fetch-youtube-thumbnails.mjs"
50+
"fetch:youtube-thumbnails": "node scripts/fetch-youtube-thumbnails.mjs",
51+
"deploy:cf:staging": "wrangler deploy --env staging",
52+
"deploy:cf:production": "wrangler deploy --env production",
53+
"preview:cf:upload": "wrangler versions upload --env staging"
5154
},
5255
"dependencies": {
5356
"@fontsource-variable/inter": "^5.2.8",
@@ -91,7 +94,8 @@
9194
"prettier": "^3.7.4",
9295
"tailwindcss": "^4.1.17",
9396
"vite": "npm:rolldown-vite@7.2.10",
94-
"vitest": "^3.2.6"
97+
"vitest": "^3.2.6",
98+
"wrangler": "^4.104.0"
9599
},
96100
"pnpm": {
97101
"overrides": {

0 commit comments

Comments
 (0)