feat(ops): replace Cloudflare Pages with Workers for frontend hosting
CI / test (push) Successful in 2m55s
CI / docker (push) Successful in 46s

This commit is contained in:
zwitschi committed 2026-08-16 08:17:23 +02:00
1 parent 9ac1a8b79d
commit 9e56ff7859
16 files changed
+214 -283

No files matched your search

-57
View File
@@ -1,57 +0,0 @@
name: Deploy to Cloudflare Pages
on:
workflow_run:
workflows: [CI]
types: [completed]
workflow_dispatch: {}
jobs:
deploy:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: cd frontend && npm ci
- name: Build
run: cd frontend && npm run build
- name: Generate deployment manifest
run: |
cd frontend/dist
form_args="-F manifest={"
for file in $(find . -type f ! -name manifest.json | sed 's|^\./||'); do
hash=$(sha256sum "$file" | awk '{print $1}')
form_args="$form_args\"$file\":\"$hash\","
done
form_args="${form_args%,}}" # remove trailing comma and close JSON
echo "Form arguments: $form_args"
echo "FORM_ARGS=$form_args" >> $GITHUB_ENV
- name: Deploy to Cloudflare Pages
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
cd frontend/dist
form_args="$FORM_ARGS" # use form arguments generated in previous step
# Get short commit hash for deployment metadata
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null | head -1 || echo "Gitea Actions deploy")
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/linearvoid/deployments" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-F "commit_hash=${COMMIT_HASH}" \
-F "commit_message=${COMMIT_MSG}" \
-F "commit_dirty=false" \
$form_args
+34
View File
@@ -0,0 +1,34 @@
name: Deploy to Cloudflare Workers
on:
workflow_run:
workflows: [CI]
types: [completed]
workflow_dispatch: {}
jobs:
deploy:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: cd frontend && npm ci
- name: Build
run: cd frontend && npm run build
- name: Deploy to Cloudflare Workers
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
cd frontend
npx wrangler deploy
+1 -1
View File
@@ -109,7 +109,7 @@
- Total: 45 frontend tests passing (2 test files).
- Added coverage reporting: `vitest.config.ts` with v8 provider, thresholds at 20/15/15/20 (statements/branches/functions/lines).
- Added `test:coverage` script to `package.json`.
- Fixed CI: bumped Node from 20 to 22 in `ci.yml` and `pages-deploy.yml` (jsdom@30 requires Node 22+). Added `engines` to `package.json`.
- Fixed CI: bumped Node from 20 to 22 in `ci.yml` and `workers-deploy.yml` (jsdom@30 requires Node 22+). Added `engines` to `package.json`.
- Updated AGENTS.md: added Required tracking files table with 13 docs and update cadence.
- All Go vet, Go tests, frontend test:coverage passing.
- CORS on media.linearvoid.tv remains the only streaming pipeline gap (Cloudflare R2 config, not code).
+5 -5
View File
@@ -6,8 +6,8 @@
### Phase 5: complete manual Cloudflare and external-service setup
- [ ] Apply Cloudflare Pages, Tiered Cache, WAF, rate limiting, and HTTPS/HSTS settings in the dashboard
- [ ] Cloudflare Pages: link custom domain and cut over frontend hosting
- [ ] Apply Cloudflare Workers, Tiered Cache, WAF, rate limiting, and HTTPS/HSTS settings in the dashboard
- [ ] Cloudflare Workers: link custom domain and cut over frontend hosting
- [ ] Enable Smart Tiered Cache; validate `Cf-Cache-Status: HIT` from upper tier
- [ ] WAF: enable OWASP Managed Ruleset (Simulate → review 48h → Block), enable Bot Fight Mode
- [ ] Rate limiting: `/api/admin/login` + `/api/admin/*` → 10 req/10s per IP, Challenge action
@@ -58,10 +58,10 @@
## Cloudflare Configuration
### DNS & Pages
### DNS & Workers
- [ ] Cloudflare Pages: link custom domain (manual)
- [ ] Create Cloudflare Pages project and cut over frontend hosting (manual)
- [ ] Cloudflare Workers: link custom domain (manual)
- [ ] Create Cloudflare Worker and cut over frontend hosting (manual)
### Security & TLS
+1 -1
View File
@@ -187,7 +187,7 @@ frontend/
Everything needed to deploy and configure the stack (Docker, Nginx, Cloudflare rules, future Terraform).
- Cloudflare
- [worker.js](infrastructure/cloudflare/worker.js) — adds immutable cache headers to HLS segments
- [worker.js](infrastructure/cloudflare/worker.js) — Cloudflare Worker serving the static SPA and adding immutable cache headers to HLS segments
- Nginx
- [Dockerfile](infrastructure/nginx/Dockerfile) — builds Nginx with custom config
- [nginx.conf](infrastructure/nginx/nginx.conf) — reverse proxy, static SPA, HLS delivery
+23 -22
View File
@@ -14,6 +14,7 @@
- [Target Topology](#target-topology)
- [Prerequisites](#prerequisites)
- [Environment File](#environment-file)
- [Gitea Secrets](#gitea-secrets)
- [Pull-and-Run Deployment](#pull-and-run-deployment)
- [First-Time or Recovery Provisioning](#first-time-or-recovery-provisioning)
- [Health Verification](#health-verification)
@@ -93,12 +94,12 @@ Readiness is not complete until the checklist above and the manual evidence trac
Three Gitea Actions workflows drive the full pipeline:
| Workflow | Trigger | Purpose |
| ------------------ | ---------------- | ---------------------------------------------------------------------------------------- |
| `ci.yml` | Push to `main` | Lint, build frontend, vet Go, build & push Docker images (`:latest`, `:main`) |
| `cd.yml` | Push to `main` | Deploy to test lab (`192.168.88.40:2375`) — runs **in parallel** with CI |
| `release.yml` | Tag push `*.*.*` | Build & push versioned Docker images (`:X.Y.Z`, `:X.Y`, `:latest`), create Gitea Release |
| `pages-deploy.yml` | Push to `main` | Deploy frontend to Cloudflare Pages (see `docs/operations/gitea-cloudflare-pages.md`) |
| Workflow | Trigger | Purpose |
| -------------------- | ----------------- | ----------------------------------------------------------------------------------------- |
| `ci.yml` | Push to `main` | Lint, build frontend, vet Go, build & push Docker images (`:latest`, `:main`) |
| `cd.yml` | Push to `main` | Deploy to test lab (`192.168.88.40:2375`) — runs **in parallel** with CI |
| `release.yml` | Tag push `*.*.*` | Build & push versioned Docker images (`:X.Y.Z`, `:X.Y`, `:latest`), create Gitea Release |
| `workers-deploy.yml` | After CI succeeds | Deploy frontend to Cloudflare Workers (see `docs/operations/gitea-cloudflare-workers.md`) |
CI and CD run simultaneously on every push to `main`. The deploy pulls whatever `:latest` images are available in the registry at that moment.
@@ -146,7 +147,7 @@ Use `.env` with at least:
- `R2_ENDPOINT` — S3-compatible endpoint URL
- `R2_REGION` — set to `auto`
**Cloudflare (optional, for Workers/Pages):**
**Cloudflare (optional, for Workers):**
- `CLOUDFLARE_API_TOKEN` / `CLOUDFLARE_ACCOUNT_ID`
- `MEDIA_DOMAIN` — split media domain (e.g. `media.linearvoid.tv`)
@@ -157,22 +158,22 @@ See `.env.example` and `SETUP.md` for full variable reference.
The CD workflow (`cd.yml`) injects sensitive values from Gitea secrets into the `.env` file at deploy time. The following secrets must be configured in the Gitea repository:
| Secret Name | Description |
| ------------------------- | ------------------------------------ |
| `DATABASE_URL` | PostgreSQL connection string |
| `ADMIN_USERNAME` | Admin dashboard username |
| `ADMIN_PASSWORD` | Admin dashboard password |
| `R2_ACCESS_KEY_ID` | Cloudflare R2 access key |
| `R2_SECRET_ACCESS_KEY` | Cloudflare R2 secret key |
| `R2_BUCKET_NAME` | R2 bucket name |
| `R2_ENDPOINT` | R2 S3-compatible endpoint URL |
| `R2_ADMIN_ACCESS_KEY_ID` | R2 admin access key (optional) |
| Secret Name | Description |
| ---------------------------- | --------------------------------- |
| `DATABASE_URL` | PostgreSQL connection string |
| `ADMIN_USERNAME` | Admin dashboard username |
| `ADMIN_PASSWORD` | Admin dashboard password |
| `R2_ACCESS_KEY_ID` | Cloudflare R2 access key |
| `R2_SECRET_ACCESS_KEY` | Cloudflare R2 secret key |
| `R2_BUCKET_NAME` | R2 bucket name |
| `R2_ENDPOINT` | R2 S3-compatible endpoint URL |
| `R2_ADMIN_ACCESS_KEY_ID` | R2 admin access key (optional) |
| `R2_ADMIN_SECRET_ACCESS_KEY` | R2 admin secret key (optional) |
| `R2_ADMIN_ENDPOINT` | R2 admin endpoint (optional) |
| `CLOUDFLARE_API_TOKEN` | Cloudflare API token (optional) |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID (optional) |
| `CI_REGISTRY_USER` | Gitea container registry username |
| `CI_REGISTRY_TOKEN` | Gitea container registry token |
| `R2_ADMIN_ENDPOINT` | R2 admin endpoint (optional) |
| `CLOUDFLARE_API_TOKEN` | Cloudflare API token (optional) |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID (optional) |
| `CI_REGISTRY_USER` | Gitea container registry username |
| `CI_REGISTRY_TOKEN` | Gitea container registry token |
## Pull-and-Run Deployment
+1 -1
View File
@@ -9,7 +9,7 @@ This folder contains the documentation for the Linear Void project. The document
## Infrastructure
- Cloudflare
- [Cache Rules](./infrastructure/cloudflare/cache-rules.md) - Cache rules for Cloudflare Workers and Pages.
- [Cache Rules](./infrastructure/cloudflare/cache-rules.md) - Cache rules for Cloudflare Workers.
- [Account Troubleshooting](./infrastructure/cloudflare/cloudflare-account-troubleshooting.md) - Troubleshooting Cloudflare account issues.
- Monitoring
- [Prometheus CURL](./infrastructure/monitoring/prometheus_curl.md) - Prometheus metrics endpoint testing with curl and jq. Doocumentation of available metrics and their meanings.
+10 -15
View File
@@ -188,14 +188,14 @@ flowchart LR
## Cloudflare
Use this section for full edge setup (DNS, Pages, cache, security, and monitoring).
Use this section for full edge setup (DNS, Workers, cache, security, and monitoring).
Execution tracker: `docs/operations/cloudflare-uptimerobot-checklist.md`.
### 1. DNS Setup
1. Add apex record for `linearvoid.tv` pointing to Cloudflare Pages target.
2. Add `www` CNAME to Cloudflare Pages and configure redirect to `https://linearvoid.tv`.
1. Add apex record for `linearvoid.tv` pointing to the Cloudflare Worker.
2. Add `www` CNAME to the Cloudflare Worker and configure redirect to `https://linearvoid.tv`.
3. Add `media.linearvoid.tv` for split-domain HLS delivery.
4. Keep all records proxied (orange cloud) unless troubleshooting requires DNS-only mode.
@@ -230,26 +230,21 @@ Configure Cloudflare Cache Rules:
Reference: [cache-rules](../docs/infrastructure/cloudflare/cache-rules.md).
### 4. Cloudflare Pages
### 4. Cloudflare Workers
Also see [the automated Gitea Actions deployment guide](./operations/gitea-cloudflare-pages.md).
Also see [the automated Gitea Actions deployment guide](./operations/gitea-cloudflare-workers.md).
Manual setup:
1. Connect Gitea repository to Cloudflare Pages.
2. Build command:
1. Create a Cloudflare Worker for the static SPA (Workers & Pages → Create → Worker).
2. Build the frontend:
```bash
cd frontend && npm run build
```
3. Output directory:
```text
frontend/dist
```
4. Configure production domain to `linearvoid.tv`.
3. Bind the build output (`frontend/dist`) as Worker static assets.
4. Configure production domain `linearvoid.tv` on the Worker.
### 5. Tiered Cache
@@ -306,7 +301,7 @@ Create external monitors:
### 10. Verification Checklist
1. DNS resolves for `linearvoid.tv`, `www.linearvoid.tv`, and `media.linearvoid.tv`.
2. Pages serves frontend for apex and `www` redirects correctly.
2. Workers serve frontend for apex and `www` redirects correctly.
3. HLS segments are cacheable long-term, manifests refresh every ~2s.
4. Admin paths are rate-limited and WAF events are visible.
5. HTTPS and WSS both succeed with valid certificates.
+10 -10
View File
@@ -164,7 +164,7 @@ flowchart TD
subgraph "Cloudflare Global Edge"
CDN[Cloudflare CDN<br/>Cache Rules + WAF + TLS]
Pages[Cloudflare Pages<br/>Static SPA Hosting]
Workers[Cloudflare Workers<br/>Static SPA Hosting]
R2[Cloudflare R2<br/>HLS Segments + Source MP4s<br/>enam location hint]
end
@@ -177,17 +177,17 @@ flowchart TD
Admins -->|HTTPS - Schedule/Playlist| Playout
Playout -->|S3-compatible Upload| R2
R2 -->|Cached HLS| CDN
Viewers -->|HTTPS| Pages
Viewers -->|HTTPS| Workers
Viewers -->|HLS Requests| CDN
Viewers -->|WSS Chat| Chat
Chat --> VPS
VPS -->|Proxy / Static| Pages
VPS -->|Proxy / Static| Workers
CDN -->|Origin Pull on Miss| R2
```
**External Interfaces**:
- **Cloudflare**: CDN (cache rules for `.m4s` and `.ts` vs `.m3u8`), R2 (object storage, $0 egress), Pages (static hosting), WAF/TLS.
- **Cloudflare**: CDN (cache rules for `.m4s` and `.ts` vs `.m3u8`), R2 (object storage, $0 egress), Workers (static hosting), WAF/TLS.
- **Hetzner VPS**: Compute for playout and chat (low-cost US-East with good peering).
- **Browsers**: Native HLS (`<video>` + hls.js) and WebSocket APIs.
- **Source**: Pre-normalized MP4 files uploaded to R2.
@@ -233,7 +233,7 @@ graph TD
subgraph Cloudflare
CDN[CDN + Cache Rules<br/>.m4s and .ts immutable long TTL<br/>.m3u8 short TTL]
R2[R2 - HLS + Source]
Pages[Pages - Static SPA]
Workers[Workers - Static SPA]
end
VPS[VPS Hetzner Ashburn]
Playout[FFmpeg + MediaMTX/SRS<br/>Playlist → HLS]
@@ -244,7 +244,7 @@ graph TD
Admin --> Playout
Playout --> R2
R2 --> CDN
Viewer --> Pages
Viewer --> Workers
Viewer --> CDN
Viewer --> Chat
Chat --> VPS
@@ -274,7 +274,7 @@ graph TB
subgraph "Cloudflare Edge"
CF[CDN + WAF<br/>Cache Rules]
R2[R2 Bucket<br/>media + HLS]
PAGES[Pages<br/>SPA hosting]
WORKERS[Workers<br/>SPA hosting]
end
subgraph "Docker Host (Hetzner CX22)"
@@ -461,7 +461,7 @@ flowchart LR
### Infrastructure Level 1
**Overview Diagram**: See Mermaid in Building Block section (VPS + Cloudflare R2/Pages/CDN).
**Overview Diagram**: See Mermaid in Building Block section (VPS + Cloudflare R2/Workers/CDN).
**Motivation**: Low-cost US-East origin with global edge caching. Hetzner provides excellent price/performance and included traffic; Cloudflare eliminates egress fees.
@@ -470,7 +470,7 @@ flowchart LR
**Mapping of Building Blocks to Infrastructure**:
- Playout + Chat + Nginx → Hetzner Cloud CX22 (Ashburn, VA).
- HLS storage, caching, static SPA → Cloudflare R2 + CDN + Pages (custom domains, `enam` location hint).
- HLS storage, caching, static SPA → Cloudflare R2 + CDN + Workers (custom domains, `enam` location hint).
- All public traffic routed through Cloudflare.
### Infrastructure Level 2
@@ -482,7 +482,7 @@ flowchart LR
- R2 bucket for media.
- Cache Rules (`.m4s` and `.ts`: Cache Everything, immutable, long TTL; `.m3u8`: short TTL/must-revalidate).
- WAF, rate limiting, Tiered Cache.
- Pages for SPA hosting.
- Workers for SPA hosting.
Future evolution: Docker Compose, managed DB, secondary regions (detailed in subsequent ADRs).
+2 -2
View File
@@ -69,7 +69,7 @@ Adopt a **self-managed US-East playout + Cloudflare R2 + edge-cached HLS** archi
### 3. Frontend
- Static SPA (HTML + custom CSS + vanilla JS) hosted on Cloudflare Pages or same VPS/Nginx.
- Static SPA (HTML + custom CSS + vanilla JS) hosted on Cloudflare Workers or same VPS/Nginx.
- Video player: `hls.js` (Chromium/Firefox) or native HLS (Safari). Single live `master.m3u8`.
- Embedded chat sidebar with room switcher, private DMs, now-playing overlay.
@@ -82,7 +82,7 @@ Adopt a **self-managed US-East playout + Cloudflare R2 + edge-cached HLS** archi
### 5. Infrastructure Summary & Cost Model
- **Primary**: Hetzner Ashburn VPS (playout + chat + Nginx) + Cloudflare R2 + Pages.
- **Primary**: Hetzner Ashburn VPS (playout + chat + Nginx) + Cloudflare R2 + Workers.
- **Monitoring**: MediaMTX/SRS dashboard, UptimeRobot, systemd logs, Cloudflare analytics.
- **Backup**: Weekly R2/S3-compatible snapshots of playlist, DB, config.
- **Fallback**: Emergency slate/loop playlist, automated restart.
@@ -2,7 +2,7 @@
Reference for Cloudflare account and API token troubleshooting.
Use `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects` to verify the account ID and API token are valid.
Use `wrangler whoami` to verify the account ID and API token are valid, or query the Workers API at `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/workers/scripts`.
## Validate Cloudflare account and API token
@@ -12,13 +12,13 @@ Set-Location -Path "E:\Documents\02-Projects\07-Mentoring\Corey\linearvoid"
Get-Content .env | Select-String 'CLOUDFLARE_API_TOKEN'
Get-Content .env | Select-String 'CLOUDFLARE_ACCOUNT_ID'
# Test API token with curl
# Test API token with wrangler
npx wrangler whoami
# Or verify with the Workers API directly
$env:CLOUDFLARE_API_TOKEN = (Get-Content .env | Select-String 'CLOUDFLARE_API_TOKEN' | ForEach-Object { $_.Line.Split('=')[1].Trim() })
$env:CLOUDFLARE_ACCOUNT_ID = (Get-Content .env | Select-String 'CLOUDFLARE_ACCOUNT_ID' | ForEach-Object { $_.Line.Split('=')[1].Trim() })
# List all Pages projects for the account
curl -X GET "https://api.cloudflare.com/client/v4/accounts/$env:CLOUDFLARE_ACCOUNT_ID/pages/projects" -H "Authorization: Bearer $env:CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json"
# Create a new Pages project (if needed)
# curl -X POST "https://api.cloudflare.com/client/v4/accounts/$env:CLOUDFLARE_ACCOUNT_ID/pages/projects" -H "Authorization: Bearer $env:CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json" -d '{"name":"linearvoid","production_branch":"main"}'
# List all Workers for the account
curl -X GET "https://api.cloudflare.com/client/v4/accounts/$env:CLOUDFLARE_ACCOUNT_ID/workers/scripts" -H "Authorization: Bearer $env:CLOUDFLARE_API_TOKEN" -H "Content-Type: application/json"
```
@@ -5,7 +5,7 @@ Purpose: execute and record all manual dashboard tasks that cannot be automated
Recommended execution order:
1. Bind `media.linearvoid.tv` and confirm the HLS origin is separate from the app origin.
2. Enable Pages, Tiered Cache, and cache behavior for the media zone.
2. Enable Workers, Tiered Cache, and cache behavior for the media zone.
3. Apply WAF, bot, rate limiting, HTTPS, and HSTS settings.
4. Verify monitor targets and alert delivery in UptimeRobot.
5. Capture evidence and fill in the sign-off block.
@@ -15,18 +15,18 @@ Recommended execution order:
- [ ] Pending
- [x] Done
## 1) Cloudflare Pages Link and Build
## 1) Cloudflare Workers Deploy and Custom Domain
- [ ] Connect repo to Cloudflare Pages project
- [ ] Create a Cloudflare Worker for the SPA and bind the build output
- [ ] Build command set to: `cd frontend && npm run build`
- [ ] Output directory set to: `frontend/dist`
- [ ] Production domain attached to `linearvoid.tv`
Evidence to capture:
- Screenshot of Pages deployment settings
- Screenshot of Worker deployment settings
- Deployment URL and latest deployment timestamp
- Confirmed production build output path in the Pages project
- Confirmed production build output path in the Worker
## 2) Tiered Cache Enablement and Validation
@@ -125,7 +125,7 @@ Evidence to capture:
Completion summary:
- Pages build URL:
- Worker build URL:
- Cache validation result:
- WAF/rate-limit status:
- HTTPS/HSTS status:
@@ -1,10 +1,10 @@
# Cloudflare Pages & Workers Setup — Linear Void
# Cloudflare Workers Setup — Linear Void
Linear Void uses two Cloudflare products:
Linear Void uses Cloudflare Workers for two purposes:
| Product | Purpose | What |
| ----------- | ------------------ | ------------------------------------------------------------------------------------ |
| **Pages** | Static SPA hosting | Vite-built frontend (`frontend/dist`) deployed via Git integration or Direct Upload |
| **Workers** | Static SPA hosting | Vite-built frontend (`frontend/dist`) served as Worker static assets |
| **Workers** | Edge cache logic | `infrastructure/cloudflare/worker.js` — adds immutable cache headers to HLS segments |
Both are managed under **Workers & Pages** in the Cloudflare dashboard.
@@ -15,7 +15,6 @@ Both are managed under **Workers & Pages** in the Cloudflare dashboard.
| ------------------------ | ------------------------------------------------------------------------------------------------------------------- |
| Workers & Pages overview | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers-and-pages` |
| Worker: linearvoid | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers/services/view/linearvoid/production/settings` |
| Pages project | `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/pages/view/linearvoid` |
| API tokens | <https://dash.cloudflare.com/profile/api-tokens> |
## Worker: `linearvoid` (edge cache headers)
@@ -30,17 +29,19 @@ npx wrangler deploy infrastructure/cloudflare/worker.js
Or paste into the Workers dashboard at the URL above (Quick Edit).
## Pages: Static frontend
## Worker: static frontend
### Direct Upload via Gitea Actions
Serve the Vite-built SPA (`frontend/dist`) as Worker static assets:
Use `.gitea/workflows/pages-deploy.yml` to build in Gitea Actions and upload via the Cloudflare API.
1. Build the frontend: `cd frontend && npm run build`.
2. Bind `frontend/dist` to the Worker as static assets (Workers Static Assets / `assets` in `wrangler.toml`).
3. Attach the `linearvoid.tv` custom domain to the Worker.
See [gitea-cloudflare-pages.md](./gitea-cloudflare-pages.md) for the workflow.
See [gitea-cloudflare-workers.md](./gitea-cloudflare-workers.md) for the automated deployment workflow.
### Variables and secrets
On Cloudflare pages, set the following environment variables (needed to run the frontend)
On the Worker, set the following environment variables (needed to run the frontend):
| Type | Name | Value |
| --------- | -------------------------------------- | ----------------------------------------------- |
@@ -59,11 +60,11 @@ On Cloudflare pages, set the following environment variables (needed to run the
## Custom domains
- `linearvoid.tv` → Pages project
- `linearvoid.tv` → Worker (static SPA)
- `www.linearvoid.tv` → redirect to `linearvoid.tv`
- `media.linearvoid.tv` → R2 bucket (separate; not Pages)
- `media.linearvoid.tv` → R2 bucket (separate; not a Worker)
## Related
- [Gitea to Cloudflare Pages deployment guide](./gitea-cloudflare-pages.md)
- [Gitea to Cloudflare Workers deployment guide](./gitea-cloudflare-workers.md)
- [Cloudflare & UptimeRobot checklist](./cloudflare-uptimerobot-checklist.md)
-144
View File
@@ -1,144 +0,0 @@
# Gitea to Cloudflare Pages — Linear Void
## Goal
Deploy the Linear Void frontend (Vite + TypeScript SPA) from Gitea to Cloudflare Pages using Gitea Actions.
## Prerequisites
> **Important:** The Pages project must exist before the API deploy call works. You'll get error 8000007 ("Project not found") if you skip this step.
1. **Cloudflare Pages project** created first:
- Dashboard → Workers & Pages → Create → Pages → Direct Upload
- Name it `linearvoid` (or a different name if `linearvoid` is already taken by a Worker — see note below)
- See [Cloudflare Pages Setup](./cloudflare-pages-setup.md) for full details.
2. **Project name**: note the exact name (e.g., `linearvoid`).
3. **API token** with Pages permissions:
- Create at <https://dash.cloudflare.com/profile/api-tokens>
- Permission: `Account → Cloudflare Pages → Edit`
- Store in Gitea → Settings → Secrets as `CLOUDFLARE_API_TOKEN`
4. **Account ID** from Cloudflare dashboard overview → store as `CLOUDFLARE_ACCOUNT_ID`
> **Naming conflict:** If a Worker already exists with the name `linearvoid`, the Pages project must use a different name (e.g., `linearvoid-site`). Update the `projectName` in the workflow accordingly. Workers and Pages share a namespace under "Workers & Pages".
## Project-specific values
| Setting | Value |
| ------------------ | ---------------------------------------------------------- |
| Framework | Vite + TypeScript |
| Build command | `cd frontend && npm install && npm run build` |
| Output directory | `frontend/dist` |
| Node version | 20 |
| Multi-page entries | `index.html`, `admin/index.html`, `admin/login/index.html` |
## Gitea Actions workflow
Create `.gitea/workflows/pages-deploy.yml`:
```yaml
name: Deploy to Cloudflare Pages
on:
push:
branches: ["main"]
workflow_dispatch: {}
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: cd frontend && npm ci
- name: Build
run: cd frontend && npm run build
- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: linearvoid
directory: frontend/dist
gitHubToken: ${{ secrets.GITEA_TOKEN }}
```
If the `cloudflare/pages-action@v1` action is unavailable from Gitea, use the direct Cloudflare Pages API (multipart/form-data with manifest):
Snippet to generate a manifest in format `-F manifest='{"index.html": "abc123", "style.css": "def456"}'`
```bash
# Build manifest form argument from all files in frontend/dist
form_args="-F manifest={"
for file in $(find frontend/dist -type f | sed 's|^frontend/dist/||'); do
hash=$(sha256sum "frontend/dist/$file" | awk '{print $1}')
form_args="$form_args\"$file\":\"$hash\","
done
form_args="${form_args%,}}" # remove trailing comma and close JSON
```
```yaml
- name: Generate deployment manifest
run: |
cd frontend/dist
form_args="-F manifest={"
for file in $(find . -type f ! -name manifest.json | sed 's|^\./||'); do
hash=$(sha256sum "$file" | awk '{print $1}')
form_args="$form_args\"$file\":\"$hash\","
done
form_args="${form_args%,}}" # remove trailing comma and close JSON
echo "Form arguments: $form_args"
echo "FORM_ARGS=$form_args" >> $GITHUB_ENV
- name: Deploy to Cloudflare Pages
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
cd frontend/dist
form_args="$FORM_ARGS" # use form arguments generated in previous step
COMMIT_HASH=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
curl -sS -X POST \
"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/linearvoid/deployments" \
-H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \
-F "commit_hash=${COMMIT_HASH}" \
$form_args
```
This matches the Cloudflare Pages Direct Upload API: `Content-Type: multipart/form-data`, each file as a separate form field, and a `manifest` JSON mapping filenames to SHA-256 hashes.
## Secrets to add in Gitea
| Secret | Value |
| ----------------------- | ------------------------------------ |
| `CLOUDFLARE_API_TOKEN` | API token with Pages:Edit permission |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID from dashboard |
## Verify deployment
1. Push to `main` or trigger manually from Gitea Actions.
2. Check **Gitea Actions** → job logs for the deploy step.
3. Check **Cloudflare Pages** → Deployments for the new deployment.
4. Visit the `.pages.dev` preview URL, then promote to `linearvoid.tv` when ready.
## Notes
- The existing `ci.yml` already builds the frontend; the Pages deploy workflow is additive and independent.
- Multi-page SPA (`vite.config.ts`) produces three entry points — Cloudflare Pages serves them correctly.
- For SPA fallback routing, add a `frontend/public/_redirects` file with `/* /index.html 200` before building.
- If deployments fail with 403, verify the API token has `Account → Cloudflare Pages → Edit` permission.
- If you get error **8000007** ("Project not found"), the Pages project hasn't been created yet. Create it via the Cloudflare dashboard (Workers & Pages → Create → Pages → Direct Upload) first, then re-run.
- If `linearvoid` is already taken by a Worker, use a different project name like `linearvoid-site` and update the `projectName` and `PROJECT_NAME` values in the workflow.
## Related resources
- Cloudflare Workers dashboard: `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers/services/view/linearvoid/production/settings` — for managing the `infrastructure/cloudflare/worker.js` deployment
- Cloudflare Pages project: `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/pages/view/linearvoid` — deployment history and custom domain settings
@@ -0,0 +1,95 @@
# Gitea to Cloudflare Workers — Linear Void
## Goal
Deploy the Linear Void frontend (Vite + TypeScript SPA) from Gitea to Cloudflare Workers using Gitea Actions and Wrangler.
## Prerequisites
1. **Cloudflare Worker** for the SPA:
- Dashboard → Workers & Pages → Create → Worker (or Workers Static Assets).
- Name it `linearvoid-frontend` (the edge-cache Worker already uses the `linearvoid` name).
2. **API token** with Workers permissions:
- Create at <https://dash.cloudflare.com/profile/api-tokens>
- Permission: `Account → Workers Scripts → Edit` and `Account → Workers Routes → Edit`
- Store in Gitea → Settings → Secrets as `CLOUDFLARE_API_TOKEN`
3. **Account ID** from Cloudflare dashboard overview → store as `CLOUDFLARE_ACCOUNT_ID`
4. **Wrangler config** (`frontend/wrangler.toml`) binding the `frontend/dist` output as static assets.
> **Naming conflict:** Workers share a namespace under "Workers & Pages". The edge-cache Worker (`infrastructure/cloudflare/worker.js`) already uses the `linearvoid` name, so the SPA Worker is named `linearvoid-frontend` (see `frontend/wrangler.toml`).
## Project-specific values
| Setting | Value |
| ------------------ | ---------------------------------------------------------- |
| Framework | Vite + TypeScript |
| Build command | `cd frontend && npm install && npm run build` |
| Output directory | `frontend/dist` |
| Node version | 22 |
| Multi-page entries | `index.html`, `admin/index.html`, `admin/login/index.html` |
## Gitea Actions workflow
Build the SPA and deploy with Wrangler:
```yaml
name: Deploy to Cloudflare Workers
on:
workflow_run:
workflows: [CI]
types: [completed]
workflow_dispatch: {}
jobs:
deploy:
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: cd frontend && npm ci
- name: Build
run: cd frontend && npm run build
- name: Deploy to Cloudflare Workers
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
run: |
cd frontend
npx wrangler deploy
```
## Secrets to add in Gitea
| Secret | Value |
| ----------------------- | ------------------------------------ |
| `CLOUDFLARE_API_TOKEN` | API token with Workers permissions |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID from dashboard |
## Verify deployment
1. Push to `main` or trigger manually from Gitea Actions.
2. Check **Gitea Actions** → job logs for the deploy step.
3. Check **Cloudflare Workers** → `linearvoid-frontend` → Deployments for the new version.
4. Visit `https://linearvoid-frontend.zwitschi82.workers.dev`, then attach `linearvoid.tv` when ready.
## Notes
- The existing `ci.yml` already builds the frontend; the Workers deploy workflow is additive and independent.
- Multi-page SPA (`vite.config.ts`) produces three entry points — Workers static assets serve them correctly.
- For SPA fallback routing, set Workers static assets `not_found_handling` to `single-page-application` (serve `index.html` for unknown paths).
- The deploy workflow lives in `.gitea/workflows/workers-deploy.yml` (triggered after CI succeeds).
## Related resources
- Cloudflare Workers dashboard: `https://dash.cloudflare.com/deb11491c54ec53a24772e98ac6d8128/workers/services/view/linearvoid/production/settings` — for managing the `infrastructure/cloudflare/worker.js` deployment and the SPA Worker
+6
View File
@@ -0,0 +1,6 @@
name = "linearvoid-frontend"
compatibility_date = "2025-08-01"
[assets]
directory = "./dist"
not_found_handling = "single-page-application"