Skip to content

feat(den): align app cloud worker flow with landing page #43

feat(den): align app cloud worker flow with landing page

feat(den): align app cloud worker flow with landing page #43

Workflow file for this run

name: Deploy Den
on:
push:
branches:
- dev
paths:
- "services/den/**"
- "services/den-worker-runtime/**"
- ".github/workflows/deploy-den.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: deploy-den-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: blacksmith-4vcpu-ubuntu-2404
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: ${{ secrets.RENDER_DEN_CONTROL_PLANE_SERVICE_ID }}
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
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 }}
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_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX: ${{ vars.DEN_RENDER_WORKER_PUBLIC_DOMAIN_SUFFIX }}
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_DATABASE_URL DEN_BETTER_AUTH_SECRET; do
if [ -z "${!key}" ]; then
echo "::error::Missing required secret: $key"
missing=1
fi
done
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:]')"
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: ${{ secrets.RENDER_DEN_CONTROL_PLANE_SERVICE_ID }}
RENDER_OWNER_ID: ${{ secrets.RENDER_OWNER_ID }}
DEN_DATABASE_URL: ${{ secrets.DEN_DATABASE_URL }}
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 }}
DEN_BETTER_AUTH_URL: ${{ vars.DEN_BETTER_AUTH_URL }}
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_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"
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 ""
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 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": "DATABASE_URL", "value": os.environ["DEN_DATABASE_URL"]},
{"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": "render"},
{"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},
]
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