mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
Delete .github/workflows/deploy-den.yml
This commit is contained in:
310
.github/workflows/deploy-den.yml
vendored
310
.github/workflows/deploy-den.yml
vendored
@@ -1,310 +0,0 @@
|
||||
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_BETTER_AUTH_TRUSTED_ORIGINS: ${{ vars.DEN_BETTER_AUTH_TRUSTED_ORIGINS }}
|
||||
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"
|
||||
configured_better_auth_trusted_origins = os.environ.get("DEN_BETTER_AUTH_TRUSTED_ORIGINS") or ""
|
||||
|
||||
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,
|
||||
],
|
||||
)
|
||||
|
||||
better_auth_trusted_origins = build_cors_origins(
|
||||
configured_better_auth_trusted_origins,
|
||||
cors_origins.split(","),
|
||||
)
|
||||
|
||||
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": "DEN_BETTER_AUTH_TRUSTED_ORIGINS", "value": better_auth_trusted_origins},
|
||||
{"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
|
||||
Reference in New Issue
Block a user