From 6db004a7c89cdac79c5d1dc1f7bef8865c162277 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=B3=E5=93=88=E5=93=88?= Date: Thu, 5 Mar 2026 14:37:58 +0800 Subject: [PATCH] feat: add Railway deployment config and GitHub Actions CI Closes #4 - Dockerfile (oven/bun:1, health check on /health) - railway.json (DOCKERFILE builder, ON_FAILURE restart) - .env.example with all required vars documented - DEPLOY.md step-by-step deployment guide (CLI + dashboard + custom domain) - .github/workflows/ci.yml (bun test + tsc --noEmit on push/PR to main) - scripts/smoke-test.sh (health + 402 payment checks) --- .env.example | 11 ++++ .github/workflows/ci.yml | 27 ++++++++ DEPLOY.md | 137 +++++++++++++++++++++++++++++++++++++++ Dockerfile | 14 ++++ railway.json | 12 ++++ scripts/smoke-test.sh | 57 ++++++++++++++++ 6 files changed, 258 insertions(+) create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 DEPLOY.md create mode 100644 Dockerfile create mode 100644 railway.json create mode 100755 scripts/smoke-test.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..424b4c9 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# Brave Search API key (required) — https://api.search.brave.com/ +BRAVE_API_KEY= + +# OpenAI API key for GPT-4o-mini synthesis (required) — https://platform.openai.com/ +OPENAI_API_KEY= + +# Cache TTL in seconds (optional, default: 300) +CACHE_TTL_SECONDS=300 + +# Server port (optional, default: 3000) +PORT=3000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..7e2ad8f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Run tests + run: bun test + + - name: Type check + run: bun run tsc -- --noEmit diff --git a/DEPLOY.md b/DEPLOY.md new file mode 100644 index 0000000..152b902 --- /dev/null +++ b/DEPLOY.md @@ -0,0 +1,137 @@ +![CI](https://github.com/langoustine69/queryx/actions/workflows/ci.yml/badge.svg) + +# Deploying queryx to Railway + +## Prerequisites + +- [Railway account](https://railway.app/) (free tier works) +- [Railway CLI](https://docs.railway.app/develop/cli) installed: `npm install -g @railway/cli` +- [Docker](https://www.docker.com/) installed (for local testing) +- `BRAVE_API_KEY` from [Brave Search API](https://api.search.brave.com/) +- `OPENAI_API_KEY` from [OpenAI Platform](https://platform.openai.com/) + +--- + +## Option A: Railway CLI Deployment + +```bash +# 1. Log in to Railway +railway login + +# 2. Create a new Railway project +railway new + +# 3. Link to the project (if already created) +railway link + +# 4. Set required environment variables +railway variables set BRAVE_API_KEY=your_brave_api_key_here +railway variables set OPENAI_API_KEY=your_openai_api_key_here + +# Optional variables +railway variables set CACHE_TTL_SECONDS=300 +railway variables set PORT=3000 + +# 5. Deploy +railway up +``` + +--- + +## Option B: Dashboard Deployment + +1. Go to [railway.app](https://railway.app/) and create a new project. +2. Click **New Service** and choose **GitHub Repo**. +3. Connect your GitHub account and select the `queryx` repository. +4. Railway will auto-detect the `Dockerfile` and use it for builds. +5. Navigate to the **Variables** tab in your service settings. +6. Add the following environment variables: + + | Variable | Required | Description | + |---|---|---| + | `BRAVE_API_KEY` | Yes | Brave Search API key | + | `OPENAI_API_KEY` | Yes | OpenAI API key | + | `CACHE_TTL_SECONDS` | No (default: 300) | Cache TTL in seconds | + | `PORT` | No (default: 3000) | Server port | + +7. Click **Deploy** — Railway will build the Docker image and deploy it. + +--- + +## Custom Domain Setup + +1. In the Railway dashboard, go to your service settings. +2. Click **Settings** > **Networking** > **Custom Domain**. +3. Enter your domain name and follow the DNS configuration instructions. +4. Railway provides a CNAME record to add to your DNS provider. +5. Once DNS propagates, your service will be accessible at your custom domain. + +--- + +## Health Check Verification + +After deployment, verify the service is healthy: + +```bash +# Replace with your Railway app URL +curl https://your-app.railway.app/health +``` + +Expected response: + +```json +{"status":"ok"} +``` + +A successful response confirms the service is running correctly. + +--- + +## Rollback + +To roll back to a previous deployment: + +```bash +# List recent deployments +railway deployments + +# Roll back to the previous deployment +railway rollback +``` + +Or roll back from the Railway dashboard by clicking **Deployments** and selecting a previous deploy. + +--- + +## Local Docker Test + +Test your Docker build locally before deploying: + +```bash +# Copy the example env file and fill in your values +cp .env.example .env +# Edit .env with your actual API keys + +# Build the Docker image +docker build -t queryx . + +# Run the container +docker run -p 3000:3000 --env-file .env queryx + +# Verify locally +curl http://localhost:3000/health +``` + +--- + +## Smoke Test + +Run the included smoke test script against any deployment: + +```bash +# Test local instance +bash scripts/smoke-test.sh http://localhost:3000 + +# Test production +bash scripts/smoke-test.sh https://your-app.railway.app +``` diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..304a400 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM oven/bun:1 AS base +WORKDIR /app + +COPY package.json bun.lock ./ +RUN bun install --frozen-lockfile + +COPY . . + +EXPOSE 3000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -f http://localhost:3000/health || exit 1 + +CMD ["bun", "run", "src/index.ts"] diff --git a/railway.json b/railway.json new file mode 100644 index 0000000..7ac2a6d --- /dev/null +++ b/railway.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://railway.app/railway.schema.json", + "build": { + "builder": "DOCKERFILE" + }, + "deploy": { + "healthcheckPath": "/health", + "healthcheckTimeout": 10, + "restartPolicyType": "ON_FAILURE", + "restartPolicyMaxRetries": 3 + } +} diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..91a8f25 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +BASE_URL="${1:-http://localhost:3000}" +PASS=0 +FAIL=0 + +check() { + local name="$1" + local result="$2" + if [ "$result" = "ok" ]; then + echo " PASS: $name" + PASS=$((PASS + 1)) + else + echo " FAIL: $name — $result" + FAIL=$((FAIL + 1)) + fi +} + +echo "Smoke test: $BASE_URL" +echo "---" + +# Test /health returns 200 +status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/health") +if [ "$status" = "200" ]; then + check "/health returns 200" "ok" +else + check "/health returns 200" "got $status" +fi + +# Test /health returns {"status":"ok"} +body=$(curl -s "$BASE_URL/health") +if echo "$body" | grep -q '"status"'; then + check "/health body has status field" "ok" +else + check "/health body has status field" "got: $body" +fi + +# Test /v1/search returns 402 without payment +status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL/v1/search?q=test") +if [ "$status" = "402" ]; then + check "/v1/search returns 402 without payment" "ok" +else + check "/v1/search returns 402 without payment" "got $status" +fi + +# Test 402 response has x402 headers +headers=$(curl -sI "$BASE_URL/v1/search?q=test") +if echo "$headers" | grep -qi "x-payment\|402\|payment-required"; then + check "402 response has payment headers" "ok" +else + check "402 response has payment headers" "missing payment headers" +fi + +echo "---" +echo "Results: $PASS passed, $FAIL failed" +[ "$FAIL" -eq 0 ] && exit 0 || exit 1