Skip to content

Deploy Ollama Proxy to Deno Deploy #27

Deploy Ollama Proxy to Deno Deploy

Deploy Ollama Proxy to Deno Deploy #27

name: Deploy Ollama Proxy to Deno Deploy
on:
workflow_dispatch:
push:
paths:
- 'cloudflare-worker/ollama-proxy/ollama_proxy.ts'
- 'cloudflare-worker/ollama-proxy/deno.json'
- '.github/workflows/deploy-ollama-proxy.yml'
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Deno
uses: denoland/setup-deno@v2
with:
# Pinned: Deno 2.9.x ships a broken built-in `deno deploy` CLI
# (JSR @deno/deploy@0.0.9904) that fails every flag with
# 'Option "--org" can only occur once, but was found several times'.
# 2.4.5 is the latest release whose deploy CLI parses flags correctly.
deno-version: 2.4.5
- name: Deploy to Deno Deploy
env:
DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }}
run: |
set -euo pipefail
ORG="android-poweruser"
APP="screenoperator-ollama-proxy"
SRC_DIR="cloudflare-worker/ollama-proxy"
ENTRYPOINT="ollama_proxy.ts"
API="https://console.deno.com/api/v2"
AUTH="Authorization: Bearer $DENO_DEPLOY_TOKEN"
# Everything runs from inside the proxy directory. The only files the
# dynamic worker needs are here (worker + deno.json). Running from the
# repo root would make the CLI detect the bundled index.html and pick
# the static runtime, which serves files and answers POST/OPTIONS with
# 405 instead of executing the worker.
cd "$SRC_DIR"
# If a previous run left behind an app in the wrong state (static
# runtime, or a mismatched entrypoint) it occupies the plan slot and a
# plain `create` is skipped, so delete it first. A missing app returns
# a non-200 GET which is ignored.
echo "=== Current apps in org (diagnostic) ==="
curl -sS -H "$AUTH" "$API/apps" | head -c 1500; echo
echo "=== GET app $APP ==="
GET_CODE=$(curl -sS -o /tmp/app.json -w '%{http_code}' -H "$AUTH" "$API/apps/$APP" || echo 000)
echo "GET -> $GET_CODE"
echo "--- app body ---"; head -c 1500 /tmp/app.json 2>/dev/null; echo
RT=$(cat /tmp/app.json 2>/dev/null | python3 -c "
import json,sys
try:
a=json.load(sys.stdin)
# app may be wrapped or flat; search for runtime config
cfg=a.get('config') or a.get('app',{}).get('config') or {}
rt=cfg.get('runtime',{}) if isinstance(cfg,dict) else {}
print(rt.get('type','') + '|' + str(rt.get('entrypoint','')))
except Exception:
print('|')
" 2>/dev/null || echo "|")
RT_TYPE="${RT%%|*}"; RT_ENTRY="${RT#*|}"
echo "Parsed: runtime='${RT_TYPE}' entrypoint='${RT_ENTRY}'"
if [ "$GET_CODE" = "200" ] && { [ "$RT_TYPE" = "static" ] || [ "$RT_TYPE" != "dynamic" ] || [ "$RT_ENTRY" != "$ENTRYPOINT" ]; }; then
echo "App exists but is not a healthy dynamic worker with entrypoint $ENTRYPOINT. Deleting it."
DEL_CODE=$(curl -sS -o /tmp/del.out -w '%{http_code}' -X DELETE -H "$AUTH" "$API/apps/$APP" || echo 000)
echo "DELETE -> $DEL_CODE"; head -c 400 /tmp/del.out 2>/dev/null; echo
# Wait for the deletion to take effect
for w in 1 2 3 4 5 6; do
sleep 5
G2=$(curl -sS -o /dev/null -w '%{http_code}' -H "$AUTH" "$API/apps/$APP" || echo 000)
echo "post-delete GET attempt $w: $G2"
[ "$G2" = "404" ] && break
done
fi
# Create the app (idempotent). --do-not-use-detected-build-config makes
# the CLI take the explicit build flags instead of auto-detection
# (which finds no framework here and would drop the runtime/entrypoint,
# causing the revision to fail).
deno deploy create \
--org="$ORG" \
--app="$APP" \
--token="$DENO_DEPLOY_TOKEN" \
--source=local \
--do-not-use-detected-build-config \
--runtime-mode=dynamic \
--entrypoint="$ENTRYPOINT" \
--region=us \
. 2>&1 || echo "create skipped (app already exists)"
# Deploy to production. `deno deploy` takes no positional argument: it
# uploads the current directory and uses the deno.json deploy config.
deno deploy \
--org="$ORG" \
--app="$APP" \
--token="$DENO_DEPLOY_TOKEN" \
--prod
- name: Verify proxy is alive
run: |
set -euo pipefail
URL="https://screenoperator-ollama-proxy.android-poweruser.deno.net/v1/chat/completions"
for i in 1 2 3 4 5 6 7 8 9 10; do
PRE=$(curl -s -o /dev/null -w '%{http_code}' -X OPTIONS "$URL" \
-H "Origin: https://appassets.androidplatform.net" \
-H "Access-Control-Request-Method: POST" --max-time 20 || echo 000)
POST=$(curl -s -w '\n%{http_code}' -X POST "$URL" \
-H "Content-Type: application/json" \
-H "Origin: https://appassets.androidplatform.net" \
-d '{"model":"probe","messages":[],"stream":false}' --max-time 30 | tail -n1)
echo "attempt $i: OPTIONS=$PRE POST=$POST"
# Dynamic worker: OPTIONS preflight returns 200 with CORS headers;
# POST is forwarded to ollama.com (auth/probe error => NOT 405/404).
if [ "$PRE" = "200" ] && [ "$POST" != "405" ] && [ "$POST" != "404" ] && [ "$POST" != "000" ]; then
echo "Proxy is live and forwarding to ollama.com."
exit 0
fi
sleep 10
done
echo "::error::Proxy did not answer correctly (still static / unreachable)."
exit 1