From ba907e33c9dc3770f5d19671137cc807c2faa13f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Feb 2026 16:35:18 +0000 Subject: [PATCH] Add Railway PR preview deployment with auto PostgreSQL provisioning - Add railway.json for Railway service configuration - Add railway.template.json defining multi-service setup with PostgreSQL - Update railway.toml with service name and DATABASE_URL variable reference - Add GitHub Actions workflow for PR preview deployments - Auto-provisions PostgreSQL for each PR environment - Comments PR with deployment status - Cleans up environment when PR is closed - Add setup/cleanup scripts for manual Railway environment management https://claude.ai/code/session_015nU4DkwCazDiWo5XDAHBqA --- .github/workflows/pr-deploy.yml | 157 ++++++++++++++++++++++++++++++++ railway.json | 13 +++ railway.template.json | 43 +++++++++ railway.toml | 9 ++ scripts/cleanup-railway-env.sh | 73 +++++++++++++++ scripts/setup-railway-env.sh | 141 ++++++++++++++++++++++++++++ 6 files changed, 436 insertions(+) create mode 100644 .github/workflows/pr-deploy.yml create mode 100644 railway.json create mode 100644 railway.template.json create mode 100755 scripts/cleanup-railway-env.sh create mode 100755 scripts/setup-railway-env.sh diff --git a/.github/workflows/pr-deploy.yml b/.github/workflows/pr-deploy.yml new file mode 100644 index 0000000..df8de2e --- /dev/null +++ b/.github/workflows/pr-deploy.yml @@ -0,0 +1,157 @@ +name: PR Preview Deploy + +on: + pull_request: + types: [opened, synchronize, reopened, closed] + +env: + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + +jobs: + deploy-preview: + if: github.event.action != 'closed' + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Railway CLI + run: | + curl -fsSL https://railway.app/install.sh | sh + echo "$HOME/.railway/bin" >> $GITHUB_PATH + + - name: Setup PR Environment with PostgreSQL + id: setup + run: | + # Set environment name based on PR number + ENV_NAME="pr-${{ github.event.pull_request.number }}" + echo "env_name=$ENV_NAME" >> $GITHUB_OUTPUT + + # Check if environment already exists, create if not + EXISTING_ENV=$(railway environment list 2>/dev/null | grep -w "$ENV_NAME" || true) + + if [ -z "$EXISTING_ENV" ]; then + echo "Creating new environment: $ENV_NAME" + railway environment create "$ENV_NAME" + else + echo "Environment $ENV_NAME already exists" + fi + + # Switch to the PR environment + railway environment "$ENV_NAME" + + # Check if PostgreSQL service exists in this environment + SERVICES=$(railway service list 2>/dev/null || echo "") + + if ! echo "$SERVICES" | grep -q "postgres"; then + echo "Provisioning PostgreSQL for $ENV_NAME..." + + # Create PostgreSQL service from Railway's template + railway service create --name "postgres-$ENV_NAME" </dev/null | jq -r '.deploymentUrl // empty' || echo "") + echo "Deployment URL: $DEPLOY_URL" + + - name: Comment PR with deployment URL + uses: actions/github-script@v7 + with: + script: | + const envName = `pr-${{ github.event.pull_request.number }}`; + const body = `## ๐Ÿš€ Preview Deployment Ready + + Your PR preview environment has been deployed with: + - โœ… Application service + - โœ… PostgreSQL database (auto-provisioned) + + **Environment:** \`${envName}\` + + The DATABASE_URL has been automatically configured for this preview environment. + + > Note: Preview environments are automatically cleaned up when the PR is closed.`; + + // Find existing comment + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const botComment = comments.data.find(c => + c.user.type === 'Bot' && c.body.includes('Preview Deployment Ready') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + cleanup-preview: + if: github.event.action == 'closed' + runs-on: ubuntu-latest + steps: + - name: Install Railway CLI + run: | + curl -fsSL https://railway.app/install.sh | sh + echo "$HOME/.railway/bin" >> $GITHUB_PATH + + - name: Cleanup PR Environment + run: | + ENV_NAME="pr-${{ github.event.pull_request.number }}" + + echo "Cleaning up environment: $ENV_NAME" + + # Delete the PR environment (this removes all services including PostgreSQL) + railway environment delete "$ENV_NAME" --yes 2>/dev/null || echo "Environment may already be deleted" + + echo "Cleanup complete for $ENV_NAME" + + - name: Comment PR about cleanup + uses: actions/github-script@v7 + with: + script: | + const body = `## ๐Ÿงน Preview Environment Cleaned Up + + The preview environment for this PR has been deleted along with its PostgreSQL database.`; + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); diff --git a/railway.json b/railway.json new file mode 100644 index 0000000..9b64eb1 --- /dev/null +++ b/railway.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "NIXPACKS" + }, + "deploy": { + "startCommand": "cd backend && python -m uvicorn main:app --host 0.0.0.0 --port $PORT", + "healthcheckPath": "/health", + "healthcheckTimeout": 100, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 3 + } +} diff --git a/railway.template.json b/railway.template.json new file mode 100644 index 0000000..3d8bd10 --- /dev/null +++ b/railway.template.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "name": "ThoughtsAI", + "description": "AI-powered thinking partner with PostgreSQL database", + "services": [ + { + "name": "app", + "source": { + "repo": "SLatz18/thoughtsAI" + }, + "build": { + "builder": "NIXPACKS" + }, + "deploy": { + "startCommand": "cd backend && python -m uvicorn main:app --host 0.0.0.0 --port $PORT", + "healthcheckPath": "/health", + "healthcheckTimeout": 100, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 3 + }, + "variables": { + "DATABASE_URL": "${{postgres.DATABASE_URL}}", + "ANTHROPIC_API_KEY": { + "description": "Anthropic API key for Claude", + "required": true + }, + "OPENAI_API_KEY": { + "description": "OpenAI API key for Whisper transcription", + "required": false + }, + "DEEPGRAM_API_KEY": { + "description": "Deepgram API key for streaming transcription", + "required": false + } + } + }, + { + "name": "postgres", + "plugin": "postgresql", + "variables": {} + } + ] +} diff --git a/railway.toml b/railway.toml index ae6994c..dc2ca31 100644 --- a/railway.toml +++ b/railway.toml @@ -7,3 +7,12 @@ healthcheckPath = "/health" healthcheckTimeout = 100 restartPolicyType = "on_failure" restartPolicyMaxRetries = 3 + +# Service name for Railway project linking +[service] +name = "app" + +# Database reference - Railway will auto-inject DATABASE_URL +# when a PostgreSQL service named "postgres" exists in the project +[variables] +DATABASE_URL = "${{postgres.DATABASE_URL}}" diff --git a/scripts/cleanup-railway-env.sh b/scripts/cleanup-railway-env.sh new file mode 100755 index 0000000..ccae15a --- /dev/null +++ b/scripts/cleanup-railway-env.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# +# Cleanup Railway PR Preview Environment +# +# This script deletes a Railway environment and all its services +# including the PostgreSQL database. +# +# Usage: +# ./scripts/cleanup-railway-env.sh +# +# Example: +# ./scripts/cleanup-railway-env.sh pr-123 + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Parse arguments +ENV_NAME="${1:-}" + +if [ -z "$ENV_NAME" ]; then + log_error "Environment name is required" + echo "Usage: $0 " + echo "Example: $0 pr-123" + exit 1 +fi + +# Safety check - don't delete production environments +if [ "$ENV_NAME" = "production" ] || [ "$ENV_NAME" = "prod" ] || [ "$ENV_NAME" = "main" ]; then + log_error "Refusing to delete protected environment: $ENV_NAME" + exit 1 +fi + +log_info "Cleaning up Railway environment: $ENV_NAME" + +# Check if environment exists +if ! railway environment list 2>/dev/null | grep -qw "$ENV_NAME"; then + log_warn "Environment $ENV_NAME does not exist or already deleted" + exit 0 +fi + +# Confirm deletion (skip in CI) +if [ -z "$CI" ] && [ -z "$RAILWAY_TOKEN" ]; then + read -p "Are you sure you want to delete environment '$ENV_NAME' and all its data? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + log_info "Cancelled" + exit 0 + fi +fi + +# Delete the environment +log_info "Deleting environment: $ENV_NAME" +railway environment delete "$ENV_NAME" --yes + +log_info "Environment $ENV_NAME has been deleted" +log_info "All services including PostgreSQL database have been removed" diff --git a/scripts/setup-railway-env.sh b/scripts/setup-railway-env.sh new file mode 100755 index 0000000..7f5f5ac --- /dev/null +++ b/scripts/setup-railway-env.sh @@ -0,0 +1,141 @@ +#!/bin/bash +# +# Setup Railway PR Preview Environment with PostgreSQL +# +# This script creates a new Railway environment with a PostgreSQL database +# and configures the app service to use it. +# +# Usage: +# ./scripts/setup-railway-env.sh +# +# Example: +# ./scripts/setup-railway-env.sh pr-123 +# +# Prerequisites: +# - Railway CLI installed (https://railway.app/cli) +# - RAILWAY_TOKEN environment variable set +# - Linked to a Railway project (railway link) + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +# Check prerequisites +check_prerequisites() { + if ! command -v railway &> /dev/null; then + log_error "Railway CLI not found. Install it with: curl -fsSL https://railway.app/install.sh | sh" + exit 1 + fi + + if [ -z "$RAILWAY_TOKEN" ]; then + log_warn "RAILWAY_TOKEN not set. You may need to authenticate interactively." + fi +} + +# Parse arguments +ENV_NAME="${1:-}" + +if [ -z "$ENV_NAME" ]; then + log_error "Environment name is required" + echo "Usage: $0 " + echo "Example: $0 pr-123" + exit 1 +fi + +log_info "Setting up Railway environment: $ENV_NAME" + +check_prerequisites + +# Create or switch to the environment +log_info "Creating/switching to environment: $ENV_NAME" +if railway environment list 2>/dev/null | grep -qw "$ENV_NAME"; then + log_info "Environment $ENV_NAME already exists, switching to it" + railway environment "$ENV_NAME" +else + log_info "Creating new environment: $ENV_NAME" + railway environment create "$ENV_NAME" + railway environment "$ENV_NAME" +fi + +# Check if PostgreSQL service exists +log_info "Checking for PostgreSQL service..." +POSTGRES_EXISTS=$(railway service list 2>/dev/null | grep -i postgres || true) + +if [ -z "$POSTGRES_EXISTS" ]; then + log_info "PostgreSQL not found, provisioning new database..." + + # Add PostgreSQL plugin + railway add --plugin postgresql + + log_info "PostgreSQL provisioned. Waiting for it to be ready..." + + # Wait for PostgreSQL to initialize + MAX_RETRIES=30 + RETRY_COUNT=0 + + while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do + # Try to get the DATABASE_URL + DB_URL=$(railway variables get DATABASE_URL 2>/dev/null || true) + + if [ -n "$DB_URL" ]; then + log_info "PostgreSQL is ready!" + break + fi + + RETRY_COUNT=$((RETRY_COUNT + 1)) + log_info "Waiting for PostgreSQL... ($RETRY_COUNT/$MAX_RETRIES)" + sleep 5 + done + + if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + log_warn "PostgreSQL may still be initializing. Check Railway dashboard." + fi +else + log_info "PostgreSQL already exists in this environment" +fi + +# Deploy the application +log_info "Deploying application..." +railway up --detach + +# Get deployment info +log_info "Getting deployment information..." +DEPLOY_INFO=$(railway status 2>/dev/null || echo "Status unavailable") +echo "$DEPLOY_INFO" + +# Get the DATABASE_URL (masked) +DB_URL=$(railway variables get DATABASE_URL 2>/dev/null || true) +if [ -n "$DB_URL" ]; then + # Mask the password in the URL for display + MASKED_URL=$(echo "$DB_URL" | sed 's/:[^:@]*@/:****@/') + log_info "DATABASE_URL is configured: $MASKED_URL" +else + log_warn "DATABASE_URL not yet available. It will be set once PostgreSQL is fully provisioned." +fi + +log_info "Environment setup complete!" +echo "" +echo "Next steps:" +echo " 1. Set required environment variables in Railway dashboard:" +echo " - ANTHROPIC_API_KEY (required)" +echo " - OPENAI_API_KEY (optional, for Whisper)" +echo " - DEEPGRAM_API_KEY (optional, for Deepgram)" +echo " 2. Check deployment status: railway status" +echo " 3. View logs: railway logs" +echo ""