Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 294 additions & 0 deletions .github/workflows/deploy-workers.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
name: Deploy Cloudflare Workers

on:
push:
branches:
- main
- staging
workflow_dispatch:
inputs:
environment:
description: Deployment target
required: true
default: staging
type: choice
options:
- staging
- production

concurrency:
group: worker-deploy-${{ github.ref }}
cancel-in-progress: true

jobs:
deploy:
name: Deploy Worker
runs-on: ubuntu-latest
permissions:
contents: read
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CF_ACCOUNT_ID: ${{ vars.CF_ACCOUNT_ID }}
CF_ZONE_NAME: ${{ vars.CF_ZONE_NAME }}
CF_STAGING_WORKER_NAME: ${{ vars.CF_STAGING_WORKER_NAME }}
CF_STAGING_WORKER_ROUTE: ${{ vars.CF_STAGING_WORKER_ROUTE }}
CF_PROD_WORKER_NAME: ${{ vars.CF_PROD_WORKER_NAME }}
CF_PROD_WORKER_ROUTE: ${{ vars.CF_PROD_WORKER_ROUTE }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Resolve target environment
id: target
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
target="${{ github.event.inputs.environment }}"
elif [[ "${GITHUB_REF_NAME}" == "main" ]]; then
target="production"
elif [[ "${GITHUB_REF_NAME}" == "staging" ]]; then
target="staging"
else
echo "Unsupported branch: ${GITHUB_REF_NAME}" >&2
exit 1
fi

echo "target=${target}" >> "${GITHUB_OUTPUT}"
echo "Deploy target: ${target}"

- name: Validate required configuration
shell: bash
run: |
missing=0
required=(
CLOUDFLARE_API_TOKEN
CF_ACCOUNT_ID
CF_ZONE_NAME
CF_STAGING_WORKER_NAME
CF_STAGING_WORKER_ROUTE
CF_PROD_WORKER_NAME
CF_PROD_WORKER_ROUTE
)
for key in "${required[@]}"; do
if [[ -z "${!key:-}" ]]; then
echo "Missing required GitHub configuration: ${key}" >&2
missing=1
fi
done

if [[ "${missing}" -ne 0 ]]; then
exit 1
fi

- name: Create worker environment file
shell: bash
run: |
cat > .worker.local.env <<EOF
CF_ACCOUNT_ID=${CF_ACCOUNT_ID}
CF_ZONE_NAME=${CF_ZONE_NAME}
CF_STAGING_WORKER_NAME=${CF_STAGING_WORKER_NAME}
CF_STAGING_WORKER_ROUTE=${CF_STAGING_WORKER_ROUTE}
CF_PROD_WORKER_NAME=${CF_PROD_WORKER_NAME}
CF_PROD_WORKER_ROUTE=${CF_PROD_WORKER_ROUTE}
EOF

- name: Deploy Cloudflare Worker
id: deploy
shell: bash
run: |
bash scripts/worker.sh deploy "${{ steps.target.outputs.target }}"

- name: Slack notify success
if: success()
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
shell: bash
run: |
set -euo pipefail

if [[ -z "${SLACK_WEBHOOK_URL:-}" ]]; then
echo "SLACK_WEBHOOK_URL not set; skipping success notification"
exit 0
fi

target="${{ steps.target.outputs.target }}"
short_sha="${GITHUB_SHA:0:7}"
commit_url="${{ github.server_url }}/${{ github.repository }}/commit/${GITHUB_SHA}"
run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

if [[ "$target" == "production" ]]; then
worker_name="${CF_PROD_WORKER_NAME}"
route_pattern="${CF_PROD_WORKER_ROUTE}"
else
worker_name="${CF_STAGING_WORKER_NAME}"
route_pattern="${CF_STAGING_WORKER_ROUTE}"
fi

route_url=""
if [[ -n "${route_pattern:-}" ]]; then
route_host="${route_pattern%%/*}"
if [[ -n "$route_host" ]]; then
route_url="https://${route_host}"
fi
fi

payload=$(jq -n \
--arg env "$target" \
--arg branch "$GITHUB_REF_NAME" \
--arg actor "$GITHUB_ACTOR" \
--arg short_sha "$short_sha" \
--arg commit_url "$commit_url" \
--arg run_url "$run_url" \
--arg worker_name "$worker_name" \
--arg route_url "$route_url" \
'{
text: "[aiproxy] Deploy Succeeded",
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "aiproxy | \($env) | Deploy Succeeded"
}
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: "*Environment*\n\($env)" },
{ type: "mrkdwn", text: "*Worker*\n\($worker_name)" },
{ type: "mrkdwn", text: "*Branch*\n\($branch)" },
{ type: "mrkdwn", text: "*Actor*\n\($actor)" },
{ type: "mrkdwn", text: "*Commit*\n<\($commit_url)|\($short_sha)>" }
]
},
{
type: "actions",
elements: (
[
{
type: "button",
text: { type: "plain_text", text: "Open Workflow Run" },
url: $run_url
}
]
+
(
if ($route_url | test("^https?://")) then
[
{
type: "button",
text: { type: "plain_text", text: "Open Route" },
url: $route_url,
style: "primary"
}
]
else
[]
end
)
)
}
]
}')

curl -sS -X POST -H "Content-type: application/json" --data "$payload" "$SLACK_WEBHOOK_URL" >/dev/null

- name: Slack notify failure
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
shell: bash
run: |
set -euo pipefail

if [[ -z "${SLACK_WEBHOOK_URL:-}" ]]; then
echo "SLACK_WEBHOOK_URL not set; skipping failure notification"
exit 0
fi

target="${{ steps.target.outputs.target }}"
short_sha="${GITHUB_SHA:0:7}"
commit_url="${{ github.server_url }}/${{ github.repository }}/commit/${GITHUB_SHA}"
run_url="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

if [[ "$target" == "production" ]]; then
worker_name="${CF_PROD_WORKER_NAME}"
route_pattern="${CF_PROD_WORKER_ROUTE}"
else
worker_name="${CF_STAGING_WORKER_NAME}"
route_pattern="${CF_STAGING_WORKER_ROUTE}"
fi

route_url=""
if [[ -n "${route_pattern:-}" ]]; then
route_host="${route_pattern%%/*}"
if [[ -n "$route_host" ]]; then
route_url="https://${route_host}"
fi
fi

payload=$(jq -n \
--arg env "$target" \
--arg branch "$GITHUB_REF_NAME" \
--arg actor "$GITHUB_ACTOR" \
--arg short_sha "$short_sha" \
--arg commit_url "$commit_url" \
--arg run_url "$run_url" \
--arg worker_name "$worker_name" \
--arg route_url "$route_url" \
'{
text: "[aiproxy] Deploy Failed",
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: "aiproxy | \($env) | Deploy Failed"
}
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: "*Environment*\n\($env)" },
{ type: "mrkdwn", text: "*Worker*\n\($worker_name)" },
{ type: "mrkdwn", text: "*Branch*\n\($branch)" },
{ type: "mrkdwn", text: "*Actor*\n\($actor)" },
{ type: "mrkdwn", text: "*Commit*\n<\($commit_url)|\($short_sha)>" }
]
},
{
type: "actions",
elements: (
[
{
type: "button",
text: { type: "plain_text", text: "Open Workflow Run" },
url: $run_url,
style: "danger"
}
]
+
(
if ($route_url | test("^https?://")) then
[
{
type: "button",
text: { type: "plain_text", text: "Open Route" },
url: $route_url
}
]
else
[]
end
)
)
}
]
}')

curl -sS -X POST -H "Content-type: application/json" --data "$payload" "$SLACK_WEBHOOK_URL" >/dev/null
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
.env
.env.modelkeys
.env.modelspecs
.node.local.env
.node.local.env.*
!.node.local.env.example
.worker.local.env
.worker.local.env.*
!.worker.local.env.example
node_modules/
npm-debug.log*
yarn-debug.log*
Expand Down
9 changes: 9 additions & 0 deletions .node.local.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Optional local-only Node/DO overrides.
#
# `start.sh` remains the primary startup path; this file is for machine-specific
# Node/DO preferences that should not be committed.

PORT=3000
NODE_START_COMMAND="bash start.sh"
NODE_SERVER_COMMAND="node server.cjs"
DO_APP_URL="https://example.com"
19 changes: 19 additions & 0 deletions .worker.local.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CF_ACCOUNT_ID=your_cloudflare_account_id
CF_ZONE_NAME=example.com

# Use names and routes appropriate for your account and DNS zone.
CF_STAGING_WORKER_NAME=aiproxy-staging
CF_STAGING_WORKER_ROUTE=staging-worker.example.com/*
CF_STAGING_CUSTOM_DOMAIN=staging-worker.example.com

CF_PROD_WORKER_NAME=aiproxy
CF_PROD_WORKER_ROUTE=worker.example.com/*
CF_PROD_CUSTOM_DOMAIN=worker.example.com

# Optional overrides for the generic Worker template.
CF_GEMINI_MODEL=gemini-2.5-flash
CF_GEMINI_API_BASE=https://generativelanguage.googleapis.com
CF_GH_URL=https://models.inference.ai.azure.com/chat/completions
CF_OPENROUTER_URL=https://openrouter.ai/api/v1/chat/completions
CF_DEEPSEEK_URL=https://api.deepseek.com/chat/completions
CF_UPSTREAM_TIMEOUT_MS=30000
Loading