Deploy Natali Proxy Worker #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy Natali Proxy Worker | |
| on: | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| deploy-proxy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Deploy Natali Proxy Worker | |
| env: | |
| CF_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: | | |
| set -x | |
| ZONE_ID="5b3f8237bfa2518bff375eb8163623a5" | |
| WORKER_NAME="natalie-proxy" | |
| cat > /tmp/worker.js << 'WORKEREOF' | |
| const RAW_BASE = 'https://raw.githubusercontent.com/spivanatalie64/spivanatalie64.github.io/gh-pages'; | |
| const MIME_TYPES = { | |
| '.html': 'text/html; charset=utf-8', | |
| '.css': 'text/css; charset=utf-8', | |
| '.js': 'application/javascript; charset=utf-8', | |
| '.json': 'application/json; charset=utf-8', | |
| '.png': 'image/png', | |
| '.jpg': 'image/jpeg', | |
| '.jpeg': 'image/jpeg', | |
| '.gif': 'image/gif', | |
| '.svg': 'image/svg+xml', | |
| '.ico': 'image/x-icon', | |
| '.webp': 'image/webp', | |
| '.pdf': 'application/pdf', | |
| '.woff': 'font/woff', | |
| '.woff2': 'font/woff2', | |
| '.ttf': 'font/ttf', | |
| '.otf': 'font/otf', | |
| }; | |
| addEventListener('fetch', event => { | |
| event.respondWith(handleRequest(event.request)); | |
| }); | |
| async function fetchWithRetry(url, retries = 2) { | |
| for (let i = 0; i <= retries; i++) { | |
| try { | |
| const controller = new AbortController(); | |
| const timeout = setTimeout(() => controller.abort(), 5000); | |
| const response = await fetch(url, { signal: controller.signal }); | |
| clearTimeout(timeout); | |
| if (response.ok) return response; | |
| } catch (e) { | |
| if (i === retries) throw e; | |
| } | |
| } | |
| return null; | |
| } | |
| async function handleRequest(request) { | |
| try { | |
| const url = new URL(request.url); | |
| let path = url.pathname; | |
| // Normalize path | |
| if (path.endsWith('/') && path.length > 1) { | |
| path = path.slice(0, -1); | |
| } | |
| // Determine if this is a static asset | |
| const extMatch = path.match(/\.(\w+)$/); | |
| const isAsset = extMatch && extMatch[0] !== '.html' && extMatch[0] !== '.htm'; | |
| if (!isAsset) { | |
| // SPA: serve index.html for all non-asset routes | |
| const indexUrl = RAW_BASE + '/index.html'; | |
| const response = await fetch(indexUrl); | |
| if (response.ok) { | |
| const newHeaders = new Headers(response.headers); | |
| newHeaders.set('Content-Type', 'text/html; charset=utf-8'); | |
| newHeaders.set('X-Proxied-By', 'natalie-worker'); | |
| newHeaders.set('Cache-Control', 'public, max-age=0, s-maxage=300'); | |
| return new Response(response.body, { | |
| status: 200, | |
| statusText: 'OK', | |
| headers: newHeaders, | |
| }); | |
| } | |
| } | |
| // Try to fetch the asset from raw.githubusercontent.com | |
| if (isAsset) { | |
| const assetUrl = RAW_BASE + path; | |
| const response = await fetchWithRetry(assetUrl); | |
| if (response && response.ok) { | |
| const ext = extMatch[0]; | |
| const contentType = MIME_TYPES[ext] || 'application/octet-stream'; | |
| const newHeaders = new Headers(response.headers); | |
| newHeaders.set('Content-Type', contentType); | |
| newHeaders.set('X-Proxied-By', 'natalie-worker'); | |
| newHeaders.set('Cache-Control', 'public, max-age=86400, s-maxage=604800'); | |
| return new Response(response.body, { | |
| status: 200, | |
| statusText: 'OK', | |
| headers: newHeaders, | |
| }); | |
| } | |
| } | |
| // Fallback: serve index.html | |
| const indexUrl = RAW_BASE + '/index.html'; | |
| const response = await fetch(indexUrl); | |
| if (response.ok) { | |
| const newHeaders = new Headers(response.headers); | |
| newHeaders.set('Content-Type', 'text/html; charset=utf-8'); | |
| newHeaders.set('X-Proxied-By', 'natalie-worker'); | |
| return new Response(response.body, { | |
| status: 200, | |
| statusText: 'OK', | |
| headers: newHeaders, | |
| }); | |
| } | |
| return new Response('Not Found', { status: 404 }); | |
| } catch (err) { | |
| return new Response('Worker Error: ' + err.message, { | |
| status: 500, | |
| headers: { 'Content-Type': 'text/plain' }, | |
| }); | |
| } | |
| } | |
| WORKEREOF | |
| cat > /tmp/metadata.json << 'METADATAEOF' | |
| { | |
| "body_part": "script", | |
| "bindings": [], | |
| "compatibility_date": "2026-06-20", | |
| "compatibility_flags": [] | |
| } | |
| METADATAEOF | |
| curl -s -X PUT \ | |
| "https://api.cloudflare.com/client/v4/accounts/6a46b418508c26278ae2d3373f41da81/workers/scripts/$WORKER_NAME" \ | |
| -H "Authorization: Bearer $CF_API_TOKEN" \ | |
| -F "metadata=@/tmp/metadata.json;type=application/json" \ | |
| -F "script=@/tmp/worker.js;type=application/javascript" | jq . | |
| EXISTING_ROUTES=$(curl -s -H "Authorization: Bearer $CF_API_TOKEN" \ | |
| "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/workers/routes") | |
| NATALIE_ROUTE_ID=$(echo "$EXISTING_ROUTES" | jq -r '.result[] | select(.pattern == "natalie.acreetionos.org/*") | .id') | |
| curl -s -X PUT \ | |
| "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/workers/routes/$NATALIE_ROUTE_ID" \ | |
| -H "Authorization: Bearer $CF_API_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{ | |
| \"pattern\": \"natalie.acreetionos.org/*\", | |
| \"script\": \"$WORKER_NAME\" | |
| }" | jq . | |
| echo "=== Done ===" |