mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
* feat(den): add daytona-backed docker dev flow * fix(den): allow multiple cloud workers in dev * fix(den): use Daytona snapshots for sandbox runtime Use a prebuilt Daytona snapshot for the dev worker runtime so sandboxes start with openwork and opencode already installed. Pass the snapshot through the local Docker flow and add a helper to build the snapshot image for repeatable setup. * chore(den): lower Daytona snapshot defaults Reduce the default snapshot footprint to 1 CPU and 2GB RAM so local Daytona worker testing fits smaller org limits more easily. * Omar is comfortable Make Daytona-backed cloud workers stable enough to reconnect through a dedicated proxy instead of persisting expiring signed preview URLs. Split the proxy into its own deployable service, share Den schema access through a common package, and fix the web badge so healthy workers show ready. * chore(den-db): add Drizzle package scripts Move the shared schema package toward owning its own migration workflow by adding generate and migrate commands plus a local Drizzle config. * chore: update lockfile Refresh the workspace lockfile so the new den-db Drizzle tooling is captured in pnpm-lock.yaml. * feat(den-worker-proxy): make Vercel deployment-ready Align the proxy service with Vercel's Hono runtime entry pattern and keep a separate Node server entry for Docker/local runs. Also scaffold the Vercel project/env setup and wire Render deploy sync to pass Daytona variables needed for daytona mode. * feat(den-db): add db mode switch for PlanetScale Support DB_MODE=planetscale with Drizzle's PlanetScale serverless driver while keeping mysql2 as the local default. This lets Vercel-hosted services use HTTP database access without changing local development workflows. * refactor(den-db): adopt shared TypeID ids Move the Den TypeID system into a shared utils package and use it across auth, org, worker, and sandbox records so fresh databases get one consistent internal ID format. Wire Better Auth into the same generator and update Den request boundaries to normalize typed ids cleanly. * fix(den): restore docker dev stack after refactor Include the shared utils package in the Den Docker images, expose MySQL to the host for local inspection, and fix the remaining Den build/runtime issues surfaced by the Docker path after the shared package and TypeID changes. * docs(den): document Daytona snapshot setup Add README guidance for building and publishing the prebuilt Daytona runtime snapshot, including the helper script, required env, and how to point Den at the snapshot for local Daytona mode. * refactor(den-db): reset migrations and load env files Replace the old Den SQL migration history with a fresh baseline for the current schema, and let Drizzle commands load database credentials from env files. Default to mysql when DATABASE_URL is present and otherwise use PlanetScale credentials so local Docker and hosted environments can share the same DB package cleanly. * fix(den): prepare manual PlanetScale deploys Update the Render workflow and Docker build path for the shared workspace packages, support PlanetScale credentials in the manual SQL migration runner, and stop auto-running DB migrations on Den startup so schema changes stay manual. * feat(den-v2): add Daytona-first control plane Create a new den-v2 service from the current Daytona-enabled control plane, default it to Daytona provisioning, and add a dedicated Render deployment workflow targeting the new v2 Render service. * feat(den-worker-proxy): redirect root to landing Send root proxy traffic to openworklabs.com so direct visits to the worker proxy domain do not hit worker-resolution errors. --------- Co-authored-by: OmarMcAdam <gh@mcadam.io>
395 lines
20 KiB
YAML
395 lines
20 KiB
YAML
name: Deploy Den v2
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- dev
|
|
paths:
|
|
- "services/den-v2/**"
|
|
- "packages/den-db/**"
|
|
- "packages/utils/**"
|
|
- ".github/workflows/deploy-den-v2.yml"
|
|
workflow_dispatch:
|
|
inputs:
|
|
render_service_id:
|
|
description: "Optional Render service id override for test/staging deploys"
|
|
required: false
|
|
type: string
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: deploy-den-v2-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
deploy:
|
|
runs-on: ubuntu-latest
|
|
if: github.repository == 'different-ai/openwork'
|
|
steps:
|
|
- name: Validate required secrets
|
|
env:
|
|
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
|
RENDER_DEN_CONTROL_PLANE_SERVICE_ID: ${{ inputs.render_service_id || 'srv-d6sajsua2pns7383mis0' }}
|
|
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
|
|
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL || secrets.DATABASE_URL }}
|
|
DEN_DATABASE_HOST: ${{ secrets.DEN_DATABASE_HOST || secrets.DATABASE_HOST }}
|
|
DEN_DATABASE_USERNAME: ${{ secrets.DEN_DATABASE_USERNAME || secrets.DATABASE_USERNAME }}
|
|
DEN_DATABASE_PASSWORD: ${{ secrets.DEN_DATABASE_PASSWORD || secrets.DATABASE_PASSWORD }}
|
|
DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
|
|
DEN_GITHUB_CLIENT_ID: ${{ secrets.DEN_GITHUB_CLIENT_ID }}
|
|
DEN_GITHUB_CLIENT_SECRET: ${{ secrets.DEN_GITHUB_CLIENT_SECRET }}
|
|
DEN_GOOGLE_CLIENT_ID: ${{ secrets.DEN_GOOGLE_CLIENT_ID }}
|
|
DEN_GOOGLE_CLIENT_SECRET: ${{ secrets.DEN_GOOGLE_CLIENT_SECRET }}
|
|
DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
|
|
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
|
|
POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
|
|
POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
|
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
|
DEN_PROVISIONER_MODE: ${{ vars.DEN_PROVISIONER_MODE }}
|
|
DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX: ${{ vars.DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX }}
|
|
DEN_DAYTONA_WORKER_PROXY_BASE_URL: ${{ vars.DEN_DAYTONA_WORKER_PROXY_BASE_URL }}
|
|
DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
|
|
run: |
|
|
missing=0
|
|
for key in RENDER_API_KEY RENDER_DEN_CONTROL_PLANE_SERVICE_ID RENDER_OWNER_ID DEN_BETTER_AUTH_SECRET; do
|
|
if [ -z "${!key}" ]; then
|
|
echo "::error::Missing required secret: $key"
|
|
missing=1
|
|
fi
|
|
done
|
|
|
|
if [ -z "$DEN_DATABASE_URL" ]; then
|
|
for key in DEN_DATABASE_HOST DEN_DATABASE_USERNAME DEN_DATABASE_PASSWORD; do
|
|
if [ -z "${!key}" ]; then
|
|
echo "::error::Missing required database secret: $key (required when DEN_DATABASE_URL is not set)"
|
|
missing=1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
vanity_suffix="${DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX:-openwork.studio}"
|
|
if [ -n "$vanity_suffix" ] && [ -z "$VERCEL_TOKEN" ]; then
|
|
echo "::error::Missing required secret: VERCEL_TOKEN (required when vanity domains are enabled)"
|
|
missing=1
|
|
fi
|
|
|
|
feature_enabled="${DEN_POLAR_FEATURE_GATE_ENABLED:-false}"
|
|
feature_enabled="$(echo "$feature_enabled" | tr '[:upper:]' '[:lower:]')"
|
|
provisioner_mode="${DEN_PROVISIONER_MODE:-daytona}"
|
|
provisioner_mode="$(echo "$provisioner_mode" | tr '[:upper:]' '[:lower:]')"
|
|
|
|
if [ "$provisioner_mode" = "daytona" ]; then
|
|
if [ -z "$DAYTONA_API_KEY" ]; then
|
|
echo "::error::Missing required secret: DAYTONA_API_KEY (required when DEN_PROVISIONER_MODE=daytona)"
|
|
missing=1
|
|
fi
|
|
|
|
if [ -z "$DEN_DAYTONA_WORKER_PROXY_BASE_URL" ]; then
|
|
echo "::error::Missing required variable: DEN_DAYTONA_WORKER_PROXY_BASE_URL (required when DEN_PROVISIONER_MODE=daytona)"
|
|
missing=1
|
|
fi
|
|
fi
|
|
|
|
if [ "$feature_enabled" = "true" ]; then
|
|
for key in POLAR_ACCESS_TOKEN POLAR_PRODUCT_ID POLAR_BENEFIT_ID; do
|
|
if [ -z "${!key}" ]; then
|
|
echo "::error::Missing required paywall secret: $key"
|
|
missing=1
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ -n "$DEN_GITHUB_CLIENT_ID" ] && [ -z "$DEN_GITHUB_CLIENT_SECRET" ]; then
|
|
echo "::error::Missing required secret: DEN_GITHUB_CLIENT_SECRET (required when DEN_GITHUB_CLIENT_ID is set)"
|
|
missing=1
|
|
fi
|
|
|
|
if [ -n "$DEN_GITHUB_CLIENT_SECRET" ] && [ -z "$DEN_GITHUB_CLIENT_ID" ]; then
|
|
echo "::error::Missing required secret: DEN_GITHUB_CLIENT_ID (required when DEN_GITHUB_CLIENT_SECRET is set)"
|
|
missing=1
|
|
fi
|
|
|
|
if [ -n "$DEN_GOOGLE_CLIENT_ID" ] && [ -z "$DEN_GOOGLE_CLIENT_SECRET" ]; then
|
|
echo "::error::Missing required secret: DEN_GOOGLE_CLIENT_SECRET (required when DEN_GOOGLE_CLIENT_ID is set)"
|
|
missing=1
|
|
fi
|
|
|
|
if [ -n "$DEN_GOOGLE_CLIENT_SECRET" ] && [ -z "$DEN_GOOGLE_CLIENT_ID" ]; then
|
|
echo "::error::Missing required secret: DEN_GOOGLE_CLIENT_ID (required when DEN_GOOGLE_CLIENT_SECRET is set)"
|
|
missing=1
|
|
fi
|
|
|
|
if [ "$missing" -ne 0 ]; then
|
|
exit 1
|
|
fi
|
|
|
|
- name: Sync Render env vars and deploy latest commit
|
|
env:
|
|
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
|
|
RENDER_DEN_CONTROL_PLANE_SERVICE_ID: ${{ inputs.render_service_id || 'srv-d6sajsua2pns7383mis0' }}
|
|
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
|
|
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL || secrets.DATABASE_URL }}
|
|
DEN_DATABASE_HOST: ${{ secrets.DEN_DATABASE_HOST || secrets.DATABASE_HOST }}
|
|
DEN_DATABASE_USERNAME: ${{ secrets.DEN_DATABASE_USERNAME || secrets.DATABASE_USERNAME }}
|
|
DEN_DATABASE_PASSWORD: ${{ secrets.DEN_DATABASE_PASSWORD || secrets.DATABASE_PASSWORD }}
|
|
DEN_BETTER_AUTH_SECRET: ${{ secrets.DEN_BETTER_AUTH_SECRET }}
|
|
DEN_GITHUB_CLIENT_ID: ${{ secrets.DEN_GITHUB_CLIENT_ID }}
|
|
DEN_GITHUB_CLIENT_SECRET: ${{ secrets.DEN_GITHUB_CLIENT_SECRET }}
|
|
DEN_GOOGLE_CLIENT_ID: ${{ secrets.DEN_GOOGLE_CLIENT_ID }}
|
|
DEN_GOOGLE_CLIENT_SECRET: ${{ secrets.DEN_GOOGLE_CLIENT_SECRET }}
|
|
DAYTONA_API_KEY: ${{ secrets.DAYTONA_API_KEY }}
|
|
DEN_BETTER_AUTH_URL: ${{ vars.DEN_BETTER_AUTH_URL }}
|
|
DEN_PROVISIONER_MODE: ${{ vars.DEN_PROVISIONER_MODE }}
|
|
DEN_RENDER_WORKER_PLAN: ${{ vars.DEN_RENDER_WORKER_PLAN }}
|
|
DEN_RENDER_WORKER_OPENWORK_VERSION: ${{ vars.DEN_RENDER_WORKER_OPENWORK_VERSION }}
|
|
DEN_CORS_ORIGINS: ${{ vars.DEN_CORS_ORIGINS }}
|
|
DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX: ${{ vars.DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX }}
|
|
DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS: ${{ vars.DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS }}
|
|
DEN_VERCEL_API_BASE: ${{ vars.DEN_VERCEL_API_BASE }}
|
|
DEN_VERCEL_TEAM_ID: ${{ vars.DEN_VERCEL_TEAM_ID }}
|
|
DEN_VERCEL_TEAM_SLUG: ${{ vars.DEN_VERCEL_TEAM_SLUG }}
|
|
DEN_VERCEL_DNS_DOMAIN: ${{ vars.DEN_VERCEL_DNS_DOMAIN }}
|
|
DEN_DAYTONA_API_URL: ${{ vars.DEN_DAYTONA_API_URL }}
|
|
DEN_DAYTONA_TARGET: ${{ vars.DEN_DAYTONA_TARGET }}
|
|
DEN_DAYTONA_SNAPSHOT: ${{ vars.DEN_DAYTONA_SNAPSHOT }}
|
|
DEN_DAYTONA_WORKER_PROXY_BASE_URL: ${{ vars.DEN_DAYTONA_WORKER_PROXY_BASE_URL }}
|
|
DEN_DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS: ${{ vars.DEN_DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS }}
|
|
DEN_DAYTONA_OPENWORK_VERSION: ${{ vars.DEN_DAYTONA_OPENWORK_VERSION }}
|
|
DEN_POLAR_FEATURE_GATE_ENABLED: ${{ vars.DEN_POLAR_FEATURE_GATE_ENABLED }}
|
|
DEN_POLAR_API_BASE: ${{ vars.DEN_POLAR_API_BASE }}
|
|
DEN_POLAR_SUCCESS_URL: ${{ vars.DEN_POLAR_SUCCESS_URL }}
|
|
DEN_POLAR_RETURN_URL: ${{ vars.DEN_POLAR_RETURN_URL }}
|
|
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
|
|
POLAR_ACCESS_TOKEN: ${{ secrets.POLAR_ACCESS_TOKEN }}
|
|
POLAR_PRODUCT_ID: ${{ secrets.POLAR_PRODUCT_ID }}
|
|
POLAR_BENEFIT_ID: ${{ secrets.POLAR_BENEFIT_ID }}
|
|
run: |
|
|
python3 <<'PY'
|
|
import json
|
|
import os
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
api_key = os.environ["RENDER_API_KEY"]
|
|
service_id = os.environ["RENDER_DEN_CONTROL_PLANE_SERVICE_ID"]
|
|
owner_id = os.environ["RENDER_OWNER_ID"]
|
|
openwork_version = os.environ.get("DEN_RENDER_WORKER_OPENWORK_VERSION")
|
|
worker_plan = os.environ.get("DEN_RENDER_WORKER_PLAN") or "standard"
|
|
provisioner_mode = (os.environ.get("DEN_PROVISIONER_MODE") or "daytona").strip().lower() or "daytona"
|
|
configured_cors_origins = os.environ.get("DEN_CORS_ORIGINS") or ""
|
|
worker_public_domain_suffix = os.environ.get("DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX") or "openwork.studio"
|
|
custom_domain_ready_timeout_ms = os.environ.get("DEN_RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS") or "240000"
|
|
vercel_api_base = os.environ.get("DEN_VERCEL_API_BASE") or "https://api.vercel.com"
|
|
vercel_team_id = os.environ.get("DEN_VERCEL_TEAM_ID") or ""
|
|
vercel_team_slug = os.environ.get("DEN_VERCEL_TEAM_SLUG") or "prologe"
|
|
vercel_dns_domain = os.environ.get("DEN_VERCEL_DNS_DOMAIN") or worker_public_domain_suffix
|
|
vercel_token = os.environ.get("VERCEL_TOKEN") or ""
|
|
daytona_api_url = os.environ.get("DEN_DAYTONA_API_URL") or "https://app.daytona.io/api"
|
|
daytona_api_key = os.environ.get("DAYTONA_API_KEY") or ""
|
|
daytona_target = os.environ.get("DEN_DAYTONA_TARGET") or ""
|
|
daytona_snapshot = os.environ.get("DEN_DAYTONA_SNAPSHOT") or ""
|
|
daytona_worker_proxy_base_url = os.environ.get("DEN_DAYTONA_WORKER_PROXY_BASE_URL") or ""
|
|
daytona_signed_preview_expires_seconds = os.environ.get("DEN_DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS") or "86400"
|
|
daytona_openwork_version = os.environ.get("DEN_DAYTONA_OPENWORK_VERSION") or ""
|
|
paywall_enabled = (os.environ.get("DEN_POLAR_FEATURE_GATE_ENABLED") or "false").lower() == "true"
|
|
polar_api_base = os.environ.get("DEN_POLAR_API_BASE") or "https://api.polar.sh"
|
|
polar_success_url = os.environ.get("DEN_POLAR_SUCCESS_URL") or "https://app.openwork.software"
|
|
polar_return_url = os.environ.get("DEN_POLAR_RETURN_URL") or polar_success_url
|
|
polar_access_token = os.environ.get("POLAR_ACCESS_TOKEN") or ""
|
|
polar_product_id = os.environ.get("POLAR_PRODUCT_ID") or ""
|
|
polar_benefit_id = os.environ.get("POLAR_BENEFIT_ID") or ""
|
|
github_client_id = os.environ.get("DEN_GITHUB_CLIENT_ID") or ""
|
|
github_client_secret = os.environ.get("DEN_GITHUB_CLIENT_SECRET") or ""
|
|
google_client_id = os.environ.get("DEN_GOOGLE_CLIENT_ID") or ""
|
|
google_client_secret = os.environ.get("DEN_GOOGLE_CLIENT_SECRET") or ""
|
|
better_auth_url = os.environ.get("DEN_BETTER_AUTH_URL") or "https://app.openwork.software"
|
|
|
|
if bool(github_client_id) != bool(github_client_secret):
|
|
raise RuntimeError(
|
|
"DEN_GITHUB_CLIENT_ID and DEN_GITHUB_CLIENT_SECRET must either both be set or both be empty"
|
|
)
|
|
|
|
if bool(google_client_id) != bool(google_client_secret):
|
|
raise RuntimeError(
|
|
"DEN_GOOGLE_CLIENT_ID and DEN_GOOGLE_CLIENT_SECRET must either both be set or both be empty"
|
|
)
|
|
|
|
def validate_redirect_url(name: str, value: str):
|
|
parsed = urllib.parse.urlparse(value)
|
|
if parsed.scheme not in {"http", "https"} or not parsed.netloc:
|
|
raise RuntimeError(f"{name} must be an absolute http(s) URL, got: {value}")
|
|
|
|
validate_redirect_url("DEN_POLAR_SUCCESS_URL", polar_success_url)
|
|
validate_redirect_url("DEN_POLAR_RETURN_URL", polar_return_url)
|
|
validate_redirect_url("DEN_BETTER_AUTH_URL", better_auth_url)
|
|
|
|
if provisioner_mode == "daytona":
|
|
if not daytona_api_key:
|
|
raise RuntimeError("DEN_PROVISIONER_MODE=daytona requires DAYTONA_API_KEY")
|
|
if not daytona_worker_proxy_base_url:
|
|
raise RuntimeError("DEN_PROVISIONER_MODE=daytona requires DEN_DAYTONA_WORKER_PROXY_BASE_URL")
|
|
validate_redirect_url("DEN_DAYTONA_WORKER_PROXY_BASE_URL", daytona_worker_proxy_base_url)
|
|
|
|
if paywall_enabled and (not polar_access_token or not polar_product_id or not polar_benefit_id):
|
|
raise RuntimeError(
|
|
"DEN_POLAR_FEATURE_GATE_ENABLED=true requires POLAR_ACCESS_TOKEN, POLAR_PRODUCT_ID, and POLAR_BENEFIT_ID"
|
|
)
|
|
|
|
def normalize_origin(value: str) -> str:
|
|
trimmed = value.strip()
|
|
if trimmed == "*":
|
|
return trimmed
|
|
return trimmed.rstrip("/")
|
|
|
|
def build_cors_origins(raw: str, defaults: list[str]) -> str:
|
|
candidates: list[str] = []
|
|
if raw.strip():
|
|
candidates.extend(raw.split(","))
|
|
candidates.extend(defaults)
|
|
|
|
seen = set()
|
|
normalized = []
|
|
for value in candidates:
|
|
origin = normalize_origin(value)
|
|
if not origin or origin in seen:
|
|
continue
|
|
seen.add(origin)
|
|
normalized.append(origin)
|
|
|
|
if not normalized:
|
|
raise RuntimeError("Unable to derive CORS_ORIGINS for Den deployment")
|
|
|
|
return ",".join(normalized)
|
|
|
|
headers = {
|
|
"Authorization": f"Bearer {api_key}",
|
|
"Accept": "application/json",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
def request(method: str, path: str, body=None):
|
|
url = f"https://api.render.com/v1{path}"
|
|
data = None
|
|
if body is not None:
|
|
data = json.dumps(body).encode("utf-8")
|
|
req = urllib.request.Request(url, data=data, method=method, headers=headers)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
|
text = resp.read().decode("utf-8")
|
|
return resp.status, json.loads(text) if text else None
|
|
except urllib.error.HTTPError as err:
|
|
text = err.read().decode("utf-8", "replace")
|
|
raise RuntimeError(f"{method} {path} failed ({err.code}): {text[:600]}")
|
|
|
|
_, service = request("GET", f"/services/{service_id}")
|
|
service_url = (service.get("serviceDetails") or {}).get("url")
|
|
if not service_url:
|
|
raise RuntimeError(f"Render service {service_id} has no public URL")
|
|
|
|
cors_origins = build_cors_origins(
|
|
configured_cors_origins,
|
|
[
|
|
"https://app.openwork.software",
|
|
"https://api.openwork.software",
|
|
service_url,
|
|
],
|
|
)
|
|
|
|
env_vars = [
|
|
{"key": "BETTER_AUTH_SECRET", "value": os.environ["DEN_BETTER_AUTH_SECRET"]},
|
|
{"key": "BETTER_AUTH_URL", "value": better_auth_url},
|
|
{"key": "GITHUB_CLIENT_ID", "value": github_client_id},
|
|
{"key": "GITHUB_CLIENT_SECRET", "value": github_client_secret},
|
|
{"key": "GOOGLE_CLIENT_ID", "value": google_client_id},
|
|
{"key": "GOOGLE_CLIENT_SECRET", "value": google_client_secret},
|
|
{"key": "CORS_ORIGINS", "value": cors_origins},
|
|
{"key": "PROVISIONER_MODE", "value": provisioner_mode},
|
|
{"key": "RENDER_API_BASE", "value": "https://api.render.com/v1"},
|
|
{"key": "RENDER_API_KEY", "value": api_key},
|
|
{"key": "RENDER_OWNER_ID", "value": owner_id},
|
|
{"key": "RENDER_WORKER_REPO", "value": "https://github.com/different-ai/openwork"},
|
|
{"key": "RENDER_WORKER_BRANCH", "value": "dev"},
|
|
{"key": "RENDER_WORKER_ROOT_DIR", "value": "services/den-worker-runtime"},
|
|
{"key": "RENDER_WORKER_PLAN", "value": worker_plan},
|
|
{"key": "RENDER_WORKER_REGION", "value": "oregon"},
|
|
{"key": "RENDER_WORKER_NAME_PREFIX", "value": "den-worker-openwork"},
|
|
{"key": "RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX", "value": worker_public_domain_suffix},
|
|
{"key": "RENDER_CUSTOM_DOMAIN_READY_TIMEOUT_MS", "value": custom_domain_ready_timeout_ms},
|
|
{"key": "RENDER_PROVISION_TIMEOUT_MS", "value": "900000"},
|
|
{"key": "RENDER_HEALTHCHECK_TIMEOUT_MS", "value": "180000"},
|
|
{"key": "RENDER_POLL_INTERVAL_MS", "value": "5000"},
|
|
{"key": "VERCEL_API_BASE", "value": vercel_api_base},
|
|
{"key": "VERCEL_TOKEN", "value": vercel_token},
|
|
{"key": "VERCEL_TEAM_ID", "value": vercel_team_id},
|
|
{"key": "VERCEL_TEAM_SLUG", "value": vercel_team_slug},
|
|
{"key": "VERCEL_DNS_DOMAIN", "value": vercel_dns_domain},
|
|
{"key": "POLAR_FEATURE_GATE_ENABLED", "value": "true" if paywall_enabled else "false"},
|
|
{"key": "POLAR_API_BASE", "value": polar_api_base},
|
|
{"key": "POLAR_ACCESS_TOKEN", "value": polar_access_token},
|
|
{"key": "POLAR_PRODUCT_ID", "value": polar_product_id},
|
|
{"key": "POLAR_BENEFIT_ID", "value": polar_benefit_id},
|
|
{"key": "POLAR_SUCCESS_URL", "value": polar_success_url},
|
|
{"key": "POLAR_RETURN_URL", "value": polar_return_url},
|
|
]
|
|
|
|
database_url = os.environ.get("DEN_DATABASE_URL") or ""
|
|
database_host = os.environ.get("DEN_DATABASE_HOST") or ""
|
|
database_username = os.environ.get("DEN_DATABASE_USERNAME") or ""
|
|
database_password = os.environ.get("DEN_DATABASE_PASSWORD") or ""
|
|
|
|
if database_url:
|
|
env_vars.append({"key": "DATABASE_URL", "value": database_url})
|
|
else:
|
|
env_vars.extend(
|
|
[
|
|
{"key": "DATABASE_HOST", "value": database_host},
|
|
{"key": "DATABASE_USERNAME", "value": database_username},
|
|
{"key": "DATABASE_PASSWORD", "value": database_password},
|
|
]
|
|
)
|
|
|
|
if provisioner_mode == "daytona":
|
|
env_vars.extend(
|
|
[
|
|
{"key": "DAYTONA_API_URL", "value": daytona_api_url},
|
|
{"key": "DAYTONA_API_KEY", "value": daytona_api_key},
|
|
{"key": "DAYTONA_TARGET", "value": daytona_target},
|
|
{"key": "DAYTONA_SNAPSHOT", "value": daytona_snapshot},
|
|
{"key": "DAYTONA_WORKER_PROXY_BASE_URL", "value": daytona_worker_proxy_base_url},
|
|
{"key": "DAYTONA_SIGNED_PREVIEW_EXPIRES_SECONDS", "value": daytona_signed_preview_expires_seconds},
|
|
]
|
|
)
|
|
|
|
if daytona_openwork_version:
|
|
env_vars.append({"key": "DAYTONA_OPENWORK_VERSION", "value": daytona_openwork_version})
|
|
|
|
if openwork_version:
|
|
env_vars.append({"key": "RENDER_WORKER_OPENWORK_VERSION", "value": openwork_version})
|
|
|
|
request("PUT", f"/services/{service_id}/env-vars", env_vars)
|
|
_, deploy = request("POST", f"/services/{service_id}/deploys", {})
|
|
deploy_id = deploy.get("id") or (deploy.get("deploy") or {}).get("id")
|
|
if not deploy_id:
|
|
raise RuntimeError(f"Unexpected deploy response: {deploy}")
|
|
|
|
terminal = {"live", "update_failed", "build_failed", "canceled"}
|
|
started = time.time()
|
|
|
|
while time.time() - started < 1800:
|
|
_, deploys = request("GET", f"/services/{service_id}/deploys?limit=1")
|
|
latest = deploys[0]["deploy"] if deploys else None
|
|
if latest and latest.get("id") == deploy_id and latest.get("status") in terminal:
|
|
status = latest.get("status")
|
|
if status != "live":
|
|
raise RuntimeError(f"Render deploy {deploy_id} ended with {status}")
|
|
print(f"Render deploy {deploy_id} is live at {service_url}")
|
|
break
|
|
time.sleep(10)
|
|
else:
|
|
raise RuntimeError(f"Timed out waiting for deploy {deploy_id}")
|
|
PY
|