Deploy Ollama Proxy to Deno Deploy #27
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 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 |