Deploy to Cloudflare Workers #6
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 to Cloudflare Workers | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: Deployment environment name | |
| required: false | |
| default: production | |
| worker_name: | |
| description: Override worker name | |
| required: false | |
| default: "" | |
| d1_name: | |
| description: Override D1 database name | |
| required: false | |
| default: "" | |
| kv_name: | |
| description: Override KV namespace name | |
| required: false | |
| default: "" | |
| concurrency: | |
| group: deploy-cloudflare-workers-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| deploy: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CF_ENVIRONMENT: ${{ github.event.inputs.environment || 'production' }} | |
| CF_WORKER_NAME: ${{ github.event.inputs.worker_name || format('grok2api-{0}', github.repository_owner) }} | |
| CF_D1_NAME: ${{ github.event.inputs.d1_name || format('grok2api-{0}-db', github.repository_owner) }} | |
| CF_KV_NAME: ${{ github.event.inputs.kv_name || format('grok2api-{0}-kv', github.repository_owner) }} | |
| CF_WRANGLER_TEMPLATE: cloudflare/wrangler.cloudflare.jsonc | |
| CF_WRANGLER_OUTPUT: .wrangler.generated.jsonc | |
| CF_RESOURCES_OUTPUT: cloudflare/resources.json | |
| CF_DEPLOY_LOG: cloudflare/deploy-output.log | |
| CF_DEPLOY_META: cloudflare/deploy-meta.json | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.13' | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| - name: Validate required secrets | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| test -n "${CLOUDFLARE_ACCOUNT_ID}" | |
| test -n "${CLOUDFLARE_API_TOKEN}" | |
| test -f "${CF_WRANGLER_TEMPLATE}" | |
| test -f "cloudflare/worker-entry.js" | |
| - name: Validate deployment names | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import os, re | |
| pattern = re.compile(r'^[A-Za-z0-9][A-Za-z0-9._-]{1,62}[A-Za-z0-9]$') | |
| for key in ('CF_WORKER_NAME', 'CF_D1_NAME', 'CF_KV_NAME'): | |
| value = os.environ.get(key, '').strip() | |
| if not value: | |
| raise SystemExit(f'{key} is empty') | |
| if not pattern.fullmatch(value): | |
| raise SystemExit(f'{key} contains invalid characters: {value!r}') | |
| PY | |
| - name: Install Wrangler CLI | |
| run: npm install --global wrangler@4 | |
| - name: Show Wrangler version | |
| run: wrangler --version | |
| - name: Prepare Cloudflare resources | |
| shell: bash | |
| run: | | |
| python scripts/cf_prepare_resources.py --output "${CF_RESOURCES_OUTPUT}" | |
| - name: Render Wrangler config | |
| shell: bash | |
| run: | | |
| python scripts/cf_render_wrangler.py \ | |
| --template "${CF_WRANGLER_TEMPLATE}" \ | |
| --resources "${CF_RESOURCES_OUTPUT}" \ | |
| --output "${CF_WRANGLER_OUTPUT}" | |
| - name: Show rendered Wrangler config path | |
| shell: bash | |
| run: | | |
| echo "Using Wrangler config: ${CF_WRANGLER_OUTPUT}" | |
| test -f "${CF_WRANGLER_OUTPUT}" | |
| - name: Deploy Worker | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p cloudflare | |
| wrangler deploy --config "${CF_WRANGLER_OUTPUT}" 2>&1 | tee "${CF_DEPLOY_LOG}" | |
| - name: Extract deploy metadata | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| python - <<'PY' | |
| import json | |
| import os | |
| import re | |
| from pathlib import Path | |
| log_path = Path(os.environ['CF_DEPLOY_LOG']) | |
| out_path = Path(os.environ['CF_DEPLOY_META']) | |
| text = log_path.read_text(encoding='utf-8', errors='replace') if log_path.exists() else '' | |
| url_match = re.search(r'https://[A-Za-z0-9._/-]+\.workers\.dev(?:/[A-Za-z0-9._~:/?#\[\]@!$&\'\(\)\*\+,;=%-]*)?', text) | |
| data = { | |
| 'worker_url': url_match.group(0).rstrip('/') if url_match else '' | |
| } | |
| out_path.write_text(json.dumps(data, indent=2), encoding='utf-8') | |
| print(json.dumps(data, indent=2)) | |
| PY | |
| - name: Health check | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| worker_url=$(python - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| path = Path(os.environ['CF_DEPLOY_META']) | |
| data = json.loads(path.read_text(encoding='utf-8')) if path.exists() else {} | |
| print(data.get('worker_url', '').rstrip('/')) | |
| PY | |
| ) | |
| if [ -z "$worker_url" ]; then | |
| echo "Unable to determine deployed workers.dev URL from deploy output" >&2 | |
| echo "Deploy log:" >&2 | |
| cat "${CF_DEPLOY_LOG}" >&2 | |
| exit 1 | |
| fi | |
| worker_url="${worker_url}/health" | |
| echo "Checking ${worker_url}" | |
| curl --fail --silent --show-error --retry 5 --retry-all-errors --retry-delay 2 "$worker_url" | |
| - name: Post deploy summary | |
| shell: bash | |
| run: | | |
| worker_url=$(python - <<'PY' | |
| import json | |
| import os | |
| from pathlib import Path | |
| path = Path(os.environ['CF_DEPLOY_META']) | |
| data = json.loads(path.read_text(encoding='utf-8')) if path.exists() else {} | |
| print(data.get('worker_url', '').rstrip('/')) | |
| PY | |
| ) | |
| python scripts/cf_post_deploy.py \ | |
| --resources "${CF_RESOURCES_OUTPUT}" \ | |
| --worker-name "${CF_WORKER_NAME}" \ | |
| --environment "${CF_ENVIRONMENT}" \ | |
| --worker-url "$worker_url" |