diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 88c3db7..30d6e13 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -11,7 +11,12 @@ jobs: api-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + run: | + git init + git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git + git fetch --depth 1 origin ${GITHUB_SHA} + git checkout FETCH_HEAD - name: Set up Docker run: | docker --version @@ -24,7 +29,12 @@ jobs: package-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + run: | + git init + git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git + git fetch --depth 1 origin ${GITHUB_SHA} + git checkout FETCH_HEAD - name: Set up Python uses: actions/setup-python@v5 with: @@ -53,17 +63,27 @@ jobs: . .venv/bin/activate uv pip install -e ".[dev]" pytest -q + - name: Run matching tests + run: | + cd packages/matching + uv venv + . .venv/bin/activate + uv pip install -e ".[dev]" + pytest -q web-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout repository + run: | + git init + git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git + git fetch --depth 1 origin ${GITHUB_SHA} + git checkout FETCH_HEAD - name: Set up Node uses: actions/setup-node@v4 with: node-version: "22" - cache: npm - cache-dependency-path: apps/web/package-lock.json - name: Install dependencies run: | cd apps/web @@ -75,4 +95,4 @@ jobs: - name: Test run: | cd apps/web - npm test \ No newline at end of file + npm test diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..0ac9373 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,145 @@ +name: Deploy to Production + +on: + workflow_dispatch: + inputs: + version: + description: 'Leave as "auto" to bump from latest git tag, or enter a specific version (e.g. v0.1.2)' + required: false + default: 'auto' + type: string + +jobs: + deploy: + name: Build and deploy + runs-on: ubuntu-latest + steps: + - name: Checkout repository + run: | + git init + git remote add origin https://x-access-token:${FORGEJO_TOKEN}@srvr.nu/git/hermes/jobhunt-platform.git + git fetch --depth 1 origin ${GITHUB_SHA} + git checkout FETCH_HEAD + + - name: Resolve version + run: | + INPUT_VERSION="${{ github.event.inputs.version }}" + if [ -z "$INPUT_VERSION" ] || [ "$INPUT_VERSION" = "auto" ]; then + git fetch --tags origin + LATEST=$(git tag --list 'v*' --sort=-v:refname | head -1) + if [ -z "$LATEST" ]; then LATEST="v0.0.0"; fi + BASE="${LATEST#v}" + MAJOR=$(echo "$BASE" | cut -d. -f1) + MINOR=$(echo "$BASE" | cut -d. -f2) + PATCH=$(echo "$BASE" | cut -d. -f3) + PATCH=$(( ${PATCH:-0} + 1 )) + VERSION="v${MAJOR:-0}.${MINOR:-0}.${PATCH}" + echo "Latest tag: $LATEST → auto-bumped to $VERSION" + else + VERSION="$INPUT_VERSION" + echo "Using manual version: $VERSION" + fi + if ! echo "$VERSION" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "ERROR: resolved version '$VERSION' is not valid semver (expected vX.Y.Z)" + exit 1 + fi + echo "VERSION=$VERSION" >> "$GITHUB_ENV" + + - name: Tag version + run: | + git tag -d ${{ env.VERSION }} 2>/dev/null || true + git push origin --delete ${{ env.VERSION }} 2>/dev/null || true + git tag ${{ env.VERSION }} + git push origin ${{ env.VERSION }} + + - name: Write production .env + env: + LLM_PRIMARY_KEY: ${{ secrets.LLM_PRIMARY_KEY }} + run: | + { + printf 'DATABASE_URL=%s\n' 'postgresql://jobhunt:jobhunt@postgres:5432/jobhunt' + printf 'LLM_PRIMARY_BASE_URL=%s\n' 'https://ollama.com/v1' + printf 'LLM_PRIMARY_KEY=%s\n' "$LLM_PRIMARY_KEY" + printf 'LLM_PRIMARY_MODEL=%s\n' 'glm-5.2' + printf 'LLM_CHEAP_MODEL=%s\n' 'glm-5.2' + printf 'LLM_STRONG_MODEL=%s\n' 'glm-5.2' + printf 'VITE_API_BASE=%s\n' '/api' + } > .env + + - name: Build and start production stack + run: | + docker compose -p jobhunt -f docker-compose.prod.yml down + docker compose -p jobhunt -f docker-compose.prod.yml up --build -d + + - name: Health checks with rollback + run: | + echo "Waiting for services to start..." + sleep 15 + + API_OK=false + for i in 1 2 3 4 5 6 7 8 9 10; do + if docker run --rm --network jobhunt_default curlimages/curl:8.5.0 \ + -sf http://jobhunt-api:8000/api/health > /dev/null; then + echo "API is healthy" + API_OK=true + break + fi + echo "API check attempt $i failed, retrying in 5s..." + sleep 5 + done + + WEB_OK=false + for i in 1 2 3 4 5; do + if docker run --rm --network jobhunt_default curlimages/curl:8.5.0 \ + -sf http://jobhunt-web/ > /dev/null; then + echo "Frontend is serving" + WEB_OK=true + break + fi + echo "Frontend check attempt $i failed, retrying in 5s..." + sleep 5 + done + + if [ "$API_OK" != "true" ] || [ "$WEB_OK" != "true" ]; then + echo "" + echo "═══════════════════════════════════════════════════" + echo " HEALTH CHECK FAILED — DIAGNOSTICS" + echo "═══════════════════════════════════════════════════" + echo "" + docker compose -p jobhunt -f docker-compose.prod.yml ps + echo "" + echo "--- API logs ---" + docker logs jobhunt-api 2>&1 | tail -80 || true + echo "" + echo "--- Postgres logs ---" + docker logs jobhunt-postgres 2>&1 | tail -30 || true + echo "" + echo "═══════════════════════════════════════════════════" + echo " ROLLING BACK DEPLOYMENT" + echo "═══════════════════════════════════════════════════" + echo "" + docker compose -p jobhunt -f docker-compose.prod.yml down + echo "" + echo "Rolled back. Containers stopped. DB volume preserved." + echo "Read API logs above to find the root cause before redeploying." + exit 1 + fi + + - name: Seed demo data (idempotent) + run: | + docker run --rm --network jobhunt_default curlimages/curl:8.5.0 \ + -sf -X POST http://jobhunt-api:8000/api/concierge/seed-demo || \ + echo "WARN: demo seed failed (non-fatal)" + + - name: Print deploy status + run: | + echo "" + echo "═══════════════════════════════════════════════════" + echo " Deployed ${{ env.VERSION }} to production" + echo "═══════════════════════════════════════════════════" + echo "" + docker compose -p jobhunt -f docker-compose.prod.yml ps + echo "" + echo "Web UI: http://tocke:8085" + echo "API: http://tocke:8000/api/health" + echo "" diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..97e5e57 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,57 @@ +# Production stack for jobhunt-platform. +# Built and started by .forgejo/workflows/deploy.yml on the host docker daemon. +# Web UI is published on http://:8085, API on :8000. Postgres is internal only. +services: + postgres: + image: postgres:16 + container_name: jobhunt-postgres + environment: + POSTGRES_USER: jobhunt + POSTGRES_PASSWORD: jobhunt + POSTGRES_DB: jobhunt + volumes: + - jobhunt_pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U jobhunt -d jobhunt"] + interval: 5s + timeout: 3s + retries: 10 + restart: unless-stopped + + api: + build: + context: . + dockerfile: apps/api/Dockerfile.test + image: jobhunt-api + container_name: jobhunt-api + entrypoint: uvicorn app.main:app --host 0.0.0.0 --port 8000 + env_file: .env + environment: + DATABASE_URL: postgresql://jobhunt:jobhunt@postgres:5432/jobhunt + working_dir: /app/apps/api + ports: + - "8000:8000" + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + + web: + build: + context: . + dockerfile: apps/web/Dockerfile + args: + # Baked into the SPA at build time. Relative /api goes through the + # nginx proxy in apps/web/nginx.conf -> http://api:8000/api/ + VITE_API_BASE: /api + image: jobhunt-web + container_name: jobhunt-web + ports: + - "8085:80" + depends_on: + - api + restart: unless-stopped + +volumes: + jobhunt_pgdata: + name: jobhunt_pgdata