diff --git a/.github/workflows/deploy-railway.yml b/.github/workflows/deploy-railway.yml new file mode 100644 index 0000000..a13bbb9 --- /dev/null +++ b/.github/workflows/deploy-railway.yml @@ -0,0 +1,54 @@ +name: Deploy Railway + +on: + workflow_dispatch: + inputs: + public_api_url: + description: "URL pública para validar o /health (ex: https://seu-app.up.railway.app)" + required: false + type: string + push: + branches: ["main"] + +jobs: + deploy: + runs-on: ubuntu-latest + env: + RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + PUBLIC_API_URL_SECRET: ${{ secrets.PUBLIC_API_URL }} + PUBLIC_API_URL_INPUT: ${{ inputs.public_api_url }} + RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }} + RAILWAY_ENVIRONMENT_ID: ${{ secrets.RAILWAY_ENVIRONMENT_ID }} + RAILWAY_SERVICE_ID: ${{ secrets.RAILWAY_SERVICE_ID }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Railway CLI + run: npm i -g @railway/cli + + - name: Link Railway project (optional) + if: ${{ env.RAILWAY_PROJECT_ID != '' && env.RAILWAY_ENVIRONMENT_ID != '' && env.RAILWAY_SERVICE_ID != '' }} + run: | + railway link \ + --project "$RAILWAY_PROJECT_ID" \ + --environment "$RAILWAY_ENVIRONMENT_ID" \ + --service "$RAILWAY_SERVICE_ID" + + - name: Resolve public API URL + run: | + if [ -n "$PUBLIC_API_URL_INPUT" ]; then + echo "PUBLIC_API_URL=$PUBLIC_API_URL_INPUT" >> "$GITHUB_ENV" + elif [ -n "$PUBLIC_API_URL_SECRET" ]; then + echo "PUBLIC_API_URL=$PUBLIC_API_URL_SECRET" >> "$GITHUB_ENV" + fi + + - name: Deploy to Railway and verify healthcheck + run: ./scripts/deploy_real.sh diff --git a/RAILWAY_DEPLOY.md b/RAILWAY_DEPLOY.md index 87da865..0803c4e 100644 --- a/RAILWAY_DEPLOY.md +++ b/RAILWAY_DEPLOY.md @@ -12,6 +12,38 @@ --- +## ⚡ Deploy real em 1 comando (CLI) + +Se você já tem o projeto vinculado no Railway, use o script automatizado: + +```bash +# Linux/macOS +export RAILWAY_TOKEN=seu_token +./scripts/deploy_real.sh +``` + +```powershell +# Windows (PowerShell) +$env:RAILWAY_TOKEN="seu_token" +.\scripts\deploy_real.ps1 +``` + +Ele executa testes, faz deploy com `railway up --ci` e só finaliza quando `/health` responde 200. + +--- + +## 🤖 Deploy direto pelo GitHub (sem terminal local) + +1. No GitHub: **Settings → Secrets and variables → Actions** +2. Adicione os secrets: + - `RAILWAY_TOKEN` (obrigatório) + - `PUBLIC_API_URL` (recomendado) + - `RAILWAY_PROJECT_ID`, `RAILWAY_ENVIRONMENT_ID`, `RAILWAY_SERVICE_ID` (opcionais para link explícito) +3. Vá em **Actions → Deploy Railway → Run workflow** +4. O workflow usa `.github/workflows/deploy-railway.yml` e executa `./scripts/deploy_real.sh` automaticamente. + +--- + ## 🚀 PASSO A PASSO — Deploy mínimo (sem Stripe/DB ainda) ### 1. Push das correções pro GitHub diff --git a/README.md b/README.md index 1c33eeb..d2afc1a 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,63 @@ curl -X POST http://localhost:8000/api/v1/analyze \ └─────────────┴────────────┘ ``` + +## 🚀 Deploy real (produção) + +Para publicar a API de forma **real** no Railway com validação automática de healthcheck: + +```bash +# Linux/macOS +npm i -g @railway/cli +export RAILWAY_TOKEN=seu_token +./scripts/deploy_real.sh +``` + +```powershell +# Windows (PowerShell) +npm i -g @railway/cli +$env:RAILWAY_TOKEN="seu_token" +.\scripts\deploy_real.ps1 +``` + +Se o PowerShell bloquear execução de script local, rode antes: + +```powershell +Set-ExecutionPolicy -Scope Process Bypass +``` + +O script: +- valida pré-requisitos e estado limpo do Git; +- roda testes (`pytest -q`) antes da publicação; +- executa `railway up --ci`; +- detecta domínio público e valida `/health` até ficar online. + +Se a detecção automática do domínio falhar, informe manualmente: + +```bash +PUBLIC_API_URL=https://seu-app.up.railway.app ./scripts/deploy_real.sh +``` + +```powershell +.\scripts\deploy_real.ps1 -PublicApiUrl "https://seu-app.up.railway.app" +``` + +### Deploy direto pelo GitHub Actions + +Você também pode publicar sem rodar nada localmente: + +1. No GitHub do repositório, vá em **Settings → Secrets and variables → Actions** e crie: + - `RAILWAY_TOKEN` (**obrigatório**) + - `PUBLIC_API_URL` (recomendado, ex: `https://seu-app.up.railway.app`) + - `RAILWAY_PROJECT_ID` (opcional) + - `RAILWAY_ENVIRONMENT_ID` (opcional) + - `RAILWAY_SERVICE_ID` (opcional) +2. Vá em **Actions → Deploy Railway → Run workflow**. +3. (Opcional) Preencha `public_api_url` no dispatch manual. +4. O workflow executa `./scripts/deploy_real.sh`, roda testes, faz deploy e valida `/health`. + +Arquivo do workflow: `.github/workflows/deploy-railway.yml`. + ## 🧪 Testing ```bash diff --git a/scripts/deploy_real.ps1 b/scripts/deploy_real.ps1 new file mode 100644 index 0000000..7b1ad3d --- /dev/null +++ b/scripts/deploy_real.ps1 @@ -0,0 +1,117 @@ +param( + [int]$HealthcheckTimeoutSeconds = 240, + [int]$HealthcheckIntervalSeconds = 8, + [string]$PublicApiUrl = "" +) + +$ErrorActionPreference = 'Stop' + +function Step($message) { + Write-Host "`n==> $message" +} + +function Fail($message) { + Write-Host "`n❌ $message" + exit 1 +} + +function Require-Cmd($name) { + if (-not (Get-Command $name -ErrorAction SilentlyContinue)) { + Fail "Comando obrigatório não encontrado: $name" + } +} + +Step "Validando pré-requisitos" +Require-Cmd python +Require-Cmd pip +Require-Cmd curl +Require-Cmd git +Require-Cmd railway + +if (-not $env:RAILWAY_TOKEN) { + Fail "Defina RAILWAY_TOKEN para deploy não-interativo." +} + +& git diff --quiet +if ($LASTEXITCODE -ne 0) { + Fail "Há alterações não commitadas. Faça commit antes do deploy." +} + +& git diff --cached --quiet +if ($LASTEXITCODE -ne 0) { + Fail "Há alterações em staging não commitadas. Faça commit antes do deploy." +} + +Step "Instalando dependências de runtime/teste" +& pip install -r requirements.txt | Out-Null +if ($LASTEXITCODE -ne 0) { Fail "Falha ao instalar requirements.txt" } + +& pip install pytest | Out-Null +if ($LASTEXITCODE -ne 0) { Fail "Falha ao instalar pytest" } + +Step "Executando testes" +& pytest -q +if ($LASTEXITCODE -ne 0) { Fail "Testes falharam" } + +Step "Publicando nova versão no Railway" +& railway up --ci +if ($LASTEXITCODE -ne 0) { Fail "Falha no railway up --ci" } + +Step "Detectando URL pública" +if ([string]::IsNullOrWhiteSpace($PublicApiUrl)) { + try { + $domainOutput = & railway domain 2>$null + if ($LASTEXITCODE -eq 0 -and $domainOutput) { + foreach ($line in $domainOutput) { + if ($line -match '(https?://[^\s]+|[\w.-]+\.(?:up\.railway\.app|railway\.app))') { + $detected = $Matches[1] + if ($detected -match '^https?://') { + $PublicApiUrl = $detected + } else { + $PublicApiUrl = "https://$detected" + } + break + } + } + } + } catch { + # fallback handled below + } +} + +if ([string]::IsNullOrWhiteSpace($PublicApiUrl)) { + Fail "Não foi possível detectar domínio automaticamente. Informe -PublicApiUrl e rode novamente." +} + +$healthUrl = "{0}/health" -f $PublicApiUrl.TrimEnd('/') +Step "Aguardando healthcheck em $healthUrl" + +$start = Get-Date +while ($true) { + try { + $response = Invoke-WebRequest -Uri $healthUrl -Method GET -TimeoutSec 20 + if ($response.StatusCode -eq 200) { + break + } + } catch { + # retry + } + + $elapsed = (Get-Date) - $start + if ($elapsed.TotalSeconds -ge $HealthcheckTimeoutSeconds) { + Fail "Timeout no healthcheck (${HealthcheckTimeoutSeconds}s). Verifique logs: railway logs" + } + + Start-Sleep -Seconds $HealthcheckIntervalSeconds +} + +Write-Host "`n✅ Deploy concluído com sucesso." +Write-Host "URL: $PublicApiUrl" +Write-Host "Health: $healthUrl`n" + +try { + $healthJson = Invoke-RestMethod -Uri $healthUrl -Method GET -TimeoutSec 20 + $healthJson | ConvertTo-Json -Depth 8 +} catch { + Write-Host "Não foi possível imprimir JSON do healthcheck, mas endpoint respondeu 200." +} diff --git a/scripts/deploy_real.sh b/scripts/deploy_real.sh new file mode 100755 index 0000000..8bea30c --- /dev/null +++ b/scripts/deploy_real.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Real deployment script for Railway with post-deploy verification. +# Usage: +# RAILWAY_TOKEN=xxx ./scripts/deploy_real.sh +# Optional vars: +# HEALTHCHECK_TIMEOUT_SECONDS=240 +# HEALTHCHECK_INTERVAL_SECONDS=8 +# PUBLIC_API_URL=https://your-app.up.railway.app + +HEALTHCHECK_TIMEOUT_SECONDS="${HEALTHCHECK_TIMEOUT_SECONDS:-240}" +HEALTHCHECK_INTERVAL_SECONDS="${HEALTHCHECK_INTERVAL_SECONDS:-8}" + +step() { printf '\n==> %s\n' "$1"; } +fail() { printf '\n❌ %s\n' "$1"; exit 1; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || fail "Comando obrigatório não encontrado: $1" +} + +step "Validando pré-requisitos" +require_cmd python +require_cmd pip +require_cmd curl +require_cmd git +require_cmd railway + +if [[ -z "${RAILWAY_TOKEN:-}" ]]; then + fail "Defina RAILWAY_TOKEN para deploy não-interativo." +fi + +if ! git diff --quiet || ! git diff --cached --quiet; then + fail "Há alterações não commitadas. Faça commit antes do deploy." +fi + +step "Instalando dependências de runtime/teste" +pip install -r requirements.txt >/dev/null +pip install pytest >/dev/null + +step "Executando testes" +pytest -q + +step "Publicando nova versão no Railway" +railway up --ci + +step "Detectando URL pública" +PUBLIC_API_URL="${PUBLIC_API_URL:-}" +if [[ -z "$PUBLIC_API_URL" ]]; then + if railway domain >/tmp/trusthire_railway_domain.txt 2>/dev/null; then + detected_domain="$(awk '/\.(railway|up\.railway)\.app/{print $NF; exit}' /tmp/trusthire_railway_domain.txt | tr -d '\r')" + if [[ -n "$detected_domain" ]]; then + if [[ "$detected_domain" =~ ^https?:// ]]; then + PUBLIC_API_URL="$detected_domain" + else + PUBLIC_API_URL="https://${detected_domain}" + fi + fi + fi +fi + +if [[ -z "$PUBLIC_API_URL" ]]; then + fail "Não foi possível detectar domínio automaticamente. Defina PUBLIC_API_URL e rode novamente." +fi + +HEALTH_URL="${PUBLIC_API_URL%/}/health" +step "Aguardando healthcheck em ${HEALTH_URL}" + +start_ts="$(date +%s)" +while true; do + if curl -fsS "$HEALTH_URL" >/tmp/trusthire_health.json 2>/dev/null; then + break + fi + + now_ts="$(date +%s)" + elapsed=$((now_ts - start_ts)) + if (( elapsed >= HEALTHCHECK_TIMEOUT_SECONDS )); then + fail "Timeout no healthcheck (${HEALTHCHECK_TIMEOUT_SECONDS}s). Verifique logs: railway logs" + fi + sleep "$HEALTHCHECK_INTERVAL_SECONDS" +done + +printf '\n✅ Deploy concluído com sucesso.\n' +printf 'URL: %s\n' "$PUBLIC_API_URL" +printf 'Health: %s\n\n' "$HEALTH_URL" +cat /tmp/trusthire_health.json