From 9e56ff7859faad85edf34eff3aaab1900dc428cd Mon Sep 17 00:00:00 2001 From: zwitschi Date: Sun, 16 Aug 2026 08:17:23 +0200 Subject: [PATCH] feat(ops): replace Cloudflare Pages with Workers for frontend hosting --- .gitea/workflows/pages-deploy.yml | 57 ------- .gitea/workflows/workers-deploy.yml | 34 +++++ .github/MEMORY.md | 2 +- .github/instructions/BACKLOG.md | 10 +- README.md | 2 +- docs/DEPLOYMENT.md | 45 +++--- docs/README.md | 2 +- docs/SETUP.md | 25 ++- docs/architecture/README.md | 20 +-- docs/architecture/adr/ADR-001.md | 4 +- .../cloudflare-account-troubleshooting.md | 14 +- .../cloudflare-uptimerobot-checklist.md | 12 +- ...s-setup.md => cloudflare-workers-setup.md} | 25 +-- docs/operations/gitea-cloudflare-pages.md | 144 ------------------ docs/operations/gitea-cloudflare-workers.md | 95 ++++++++++++ frontend/wrangler.toml | 6 + 16 files changed, 214 insertions(+), 283 deletions(-) delete mode 100644 .gitea/workflows/pages-deploy.yml create mode 100644 .gitea/workflows/workers-deploy.yml rename docs/operations/{cloudflare-pages-setup.md => cloudflare-workers-setup.md} (78%) delete mode 100644 docs/operations/gitea-cloudflare-pages.md create mode 100644 docs/operations/gitea-cloudflare-workers.md create mode 100644 frontend/wrangler.toml diff --git a/.gitea/workflows/pages-deploy.yml b/.gitea/workflows/pages-deploy.yml deleted file mode 100644 index f9a4e68..0000000 --- a/.gitea/workflows/pages-deploy.yml +++ /dev/null @@ -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 diff --git a/.gitea/workflows/workers-deploy.yml b/.gitea/workflows/workers-deploy.yml new file mode 100644 index 0000000..b9c0eae --- /dev/null +++ b/.gitea/workflows/workers-deploy.yml @@ -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 diff --git a/.github/MEMORY.md b/.github/MEMORY.md index 7e2b59c..544bacf 100644 --- a/.github/MEMORY.md +++ b/.github/MEMORY.md @@ -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). diff --git a/.github/instructions/BACKLOG.md b/.github/instructions/BACKLOG.md index 32dfd93..a28b567 100644 --- a/.github/instructions/BACKLOG.md +++ b/.github/instructions/BACKLOG.md @@ -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 diff --git a/README.md b/README.md index a80c84c..c94c49f 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 30e5b74..f95abfd 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -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 diff --git a/docs/README.md b/docs/README.md index fcfd0eb..c870ac8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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. diff --git a/docs/SETUP.md b/docs/SETUP.md index 4160fbb..9a63024 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -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. diff --git a/docs/architecture/README.md b/docs/architecture/README.md index d75ad30..9e0354b 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -164,7 +164,7 @@ flowchart TD subgraph "Cloudflare Global Edge" CDN[Cloudflare CDN
Cache Rules + WAF + TLS] - Pages[Cloudflare Pages
Static SPA Hosting] + Workers[Cloudflare Workers
Static SPA Hosting] R2[Cloudflare R2
HLS Segments + Source MP4s
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 (`