Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ jobs:
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ${{ secrets.POSTGRES_USER }} -d ${{ secrets.POSTGRES_DB }}"
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
NODE_ENV: test
# Points to the service container above — never touches the VPS.
DATABASE_URL: ${{ secrets.DATABASE_URL }}
# JWT_SECRET must be ≥32 chars; using a fixed CI-only value is safe here.
# Built from individual secrets so the port is always 5432 (service container).
DATABASE_URL: postgresql://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
LOG_LEVEL: error
PORT: 3000
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

steps:
- name: 🚚 Checkout Repository
Expand All @@ -49,7 +49,7 @@ jobs:
- name: ⚡️ Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
bun-version: 1.3.11

- name: 📦 Install Dependencies
run: bun install --frozen-lockfile
Expand Down
306 changes: 153 additions & 153 deletions .github/workflows/stage.yml
Original file line number Diff line number Diff line change
@@ -1,153 +1,153 @@
name: Staging Deploy

# Triggers on pushes to the `develop` branch only.
# This is the full staging pipeline: test with an isolated service container DB,
# then deploy the whole stack to the staging VPS if tests pass.
# The staging VPS uses its own .env.staging file — completely isolated from
# the CI DB (ephemeral) and production DB.
on:
push:
branches: [develop]

concurrency:
# Cancel any in-progress staging deploy if a new commit is pushed.
group: staging-deploy-${{ github.ref }}
cancel-in-progress: true

jobs:
# ─────────────────────────────────────────────────────────────────────────────
# JOB 1: Test
# GitHub Actions service container provides an isolated, ephemeral Postgres.
# No staging or production DB credentials are used here.
# ─────────────────────────────────────────────────────────────────────────────
test:
name: 🧪 Test (Service Container DB)
runs-on: ubuntu-latest

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: ci_user
POSTGRES_PASSWORD: ci_password
POSTGRES_DB: ci_test_db
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U ci_user -d ci_test_db"
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
NODE_ENV: test
CI_TEST_DB_URL: postgresql://ci_user:ci_password@localhost:5432/ci_test_db
JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
LOG_LEVEL: error
PORT: 3000

steps:
- name: 🚚 Checkout
uses: actions/checkout@v4

- name: ⚡️ Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: 📦 Install Dependencies
run: bun install --frozen-lockfile

- name: 🧹 Lint (Biome)
run: bun run lint

- name: 🛡️ Type Check
run: bun run typecheck

- name: 🗄️ Push Schema → Service Container DB
run: bunx drizzle-kit push --config=src/config/drizzle.config.ts --force

- name: 🧪 Unit Tests
run: bun run test:unit

- name: 🧪 Integration Tests
run: bun run test:integration

- name: 🏗️ Build
run: bun run build

# ─────────────────────────────────────────────────────────────────────────────
# JOB 2: Deploy Full Stack to Staging VPS
# Runs ONLY when the test job above passes.
# SSHes into the staging VPS and brings the entire docker compose stack up,
# including the backend, database, Loki, and Grafana services.
# The staging VPS reads .env.staging which holds STAGING_DATABASE_URL,
# Loki/Grafana credentials, etc. — none of these ever appear in this file.
# ─────────────────────────────────────────────────────────────────────────────
deploy-staging:
name: 🚀 Deploy to Staging VPS
needs: test
runs-on: ubuntu-latest

environment:
name: staging
url: ${{ secrets.STAGING_APP_URL }}

steps:
- name: 🔑 Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.STAGING_VPS_HOST }}
username: ${{ secrets.STAGING_VPS_USER }}
key: ${{ secrets.STAGING_VPS_SSH_PRIVATE_KEY }}
port: ${{ secrets.STAGING_VPS_SSH_PORT }}
script: |
set -e

echo "──────────────────────────────────────────"
echo " Deploying commit ${{ github.sha }} to staging"
echo "──────────────────────────────────────────"

cd ${{ secrets.STAGING_APP_DIR }}

# Pull latest code from develop branch
git fetch origin develop
git checkout develop
git pull origin develop

# Install / update dependencies
bun install --frozen-lockfile

# Apply any schema changes to the staging DB.
# NODE_ENV=production so drizzle.config.ts picks up PROD_DATABASE_URL,
# which in .env.staging points to the staging Postgres — not production.
NODE_ENV=production bunx drizzle-kit push \
--config=src/config/drizzle.config.ts \
--force

# Bring up the full stack (backend + db + loki + grafana)
# --remove-orphans cleans up containers from old compose definitions
docker compose \
-f docker-compose.staging.yml \
--env-file .env.staging \
up -d \
--build \
--remove-orphans

echo "✅ Staging deploy complete"

- name: 🩺 Health Check
# Wait up to 60 s for the app to respond before marking the deploy successful.
run: |
echo "Waiting for staging app to become healthy..."
for i in $(seq 1 12); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ secrets.STAGING_APP_URL }}/health" || true)
if [ "$STATUS" = "200" ]; then
echo "✅ Health check passed (HTTP 200)"
exit 0
fi
echo " Attempt $i/12 — got HTTP $STATUS, retrying in 5s..."
sleep 5
done
echo "❌ Health check failed after 60s"
exit 1
# name: Staging Deploy

# # Triggers on pushes to the `develop` branch only.
# # This is the full staging pipeline: test with an isolated service container DB,
# # then deploy the whole stack to the staging VPS if tests pass.
# # The staging VPS uses its own .env.staging file — completely isolated from
# # the CI DB (ephemeral) and production DB.
# on:
# push:
# branches: [develop]

# concurrency:
# # Cancel any in-progress staging deploy if a new commit is pushed.
# group: staging-deploy-${{ github.ref }}
# cancel-in-progress: true

# jobs:
# # ─────────────────────────────────────────────────────────────────────────────
# # JOB 1: Test
# # GitHub Actions service container provides an isolated, ephemeral Postgres.
# # No staging or production DB credentials are used here.
# # ─────────────────────────────────────────────────────────────────────────────
# test:
# name: 🧪 Test (Service Container DB)
# runs-on: ubuntu-latest

# services:
# postgres:
# image: postgres:16-alpine
# env:
# POSTGRES_USER: ci_user
# POSTGRES_PASSWORD: ci_password
# POSTGRES_DB: ci_test_db
# ports:
# - 5432:5432
# options: >-
# --health-cmd "pg_isready -U ci_user -d ci_test_db"
# --health-interval 5s
# --health-timeout 5s
# --health-retries 10

# env:
# NODE_ENV: test
# CI_TEST_DB_URL: postgresql://ci_user:ci_password@localhost:5432/ci_test_db
# JWT_SECRET: ${{ secrets.CI_JWT_SECRET }}
# LOG_LEVEL: error
# PORT: 3000

# steps:
# - name: 🚚 Checkout
# uses: actions/checkout@v4

# - name: ⚡️ Setup Bun
# uses: oven-sh/setup-bun@v2
# with:
# bun-version: latest

# - name: 📦 Install Dependencies
# run: bun install --frozen-lockfile

# - name: 🧹 Lint (Biome)
# run: bun run lint

# - name: 🛡️ Type Check
# run: bun run typecheck

# - name: 🗄️ Push Schema → Service Container DB
# run: bunx drizzle-kit push --config=src/config/drizzle.config.ts --force

# - name: 🧪 Unit Tests
# run: bun run test:unit

# - name: 🧪 Integration Tests
# run: bun run test:integration

# - name: 🏗️ Build
# run: bun run build

# # ─────────────────────────────────────────────────────────────────────────────
# # JOB 2: Deploy Full Stack to Staging VPS
# # Runs ONLY when the test job above passes.
# # SSHes into the staging VPS and brings the entire docker compose stack up,
# # including the backend, database, Loki, and Grafana services.
# # The staging VPS reads .env.staging which holds STAGING_DATABASE_URL,
# # Loki/Grafana credentials, etc. — none of these ever appear in this file.
# # ─────────────────────────────────────────────────────────────────────────────
# deploy-staging:
# name: 🚀 Deploy to Staging VPS
# needs: test
# runs-on: ubuntu-latest

# environment:
# name: staging
# url: ${{ secrets.STAGING_APP_URL }}

# steps:
# - name: 🔑 Deploy via SSH
# uses: appleboy/ssh-action@v1.0.3
# with:
# host: ${{ secrets.STAGING_VPS_HOST }}
# username: ${{ secrets.STAGING_VPS_USER }}
# key: ${{ secrets.STAGING_VPS_SSH_PRIVATE_KEY }}
# port: ${{ secrets.STAGING_VPS_SSH_PORT }}
# script: |
# set -e

# echo "──────────────────────────────────────────"
# echo " Deploying commit ${{ github.sha }} to staging"
# echo "──────────────────────────────────────────"

# cd ${{ secrets.STAGING_APP_DIR }}

# # Pull latest code from develop branch
# git fetch origin develop
# git checkout develop
# git pull origin develop

# # Install / update dependencies
# bun install --frozen-lockfile

# # Apply any schema changes to the staging DB.
# # NODE_ENV=production so drizzle.config.ts picks up PROD_DATABASE_URL,
# # which in .env.staging points to the staging Postgres — not production.
# NODE_ENV=production bunx drizzle-kit push \
# --config=src/config/drizzle.config.ts \
# --force

# # Bring up the full stack (backend + db + loki + grafana)
# # --remove-orphans cleans up containers from old compose definitions
# docker compose \
# -f docker-compose.staging.yml \
# --env-file .env.staging \
# up -d \
# --build \
# --remove-orphans

# echo "✅ Staging deploy complete"

# - name: 🩺 Health Check
# # Wait up to 60 s for the app to respond before marking the deploy successful.
# run: |
# echo "Waiting for staging app to become healthy..."
# for i in $(seq 1 12); do
# STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${{ secrets.STAGING_APP_URL }}/health" || true)
# if [ "$STATUS" = "200" ]; then
# echo "✅ Health check passed (HTTP 200)"
# exit 0
# fi
# echo " Attempt $i/12 — got HTTP $STATUS, retrying in 5s..."
# sleep 5
# done
# echo "❌ Health check failed after 60s"
# exit 1
9 changes: 4 additions & 5 deletions src/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,8 @@ export function getDb(): PostgresJsDatabase<typeof schema> {
return database;
}

export const db = new Proxy({} as PostgresJsDatabase<typeof schema>, {
get(_target, prop, receiver) {
return Reflect.get(getDb(), prop, receiver);
},
});
export let db: PostgresJsDatabase<typeof schema> =
null as unknown as PostgresJsDatabase<typeof schema>;

export const startDatabase = async (): Promise<void> => {
queryClient = isProd
Expand All @@ -40,6 +37,7 @@ export const startDatabase = async (): Promise<void> => {
});

database = drizzle(queryClient, { schema });
db = database;

await queryClient`SELECT 1`;
logger.info(`Database connected [${env.NODE_ENV.toUpperCase()}]`);
Expand All @@ -54,6 +52,7 @@ export const stopDatabase = async (): Promise<void> => {
await queryClient.end();
queryClient = null;
database = null;
db = null as unknown as PostgresJsDatabase<typeof schema>;
logger.info("Database connection closed");
} catch (error) {
logger.error({ err: error }, "Error closing database connection");
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { db } from "@/database";
import { getDb } from "@/database";
import { UserHelpers, todos, users } from "@/database/schema";
import { signJwt } from "@/utils/auth";

export async function setupTestContext(username: string) {
await db.delete(users);
await getDb().delete(users);

const user = await UserHelpers.create(db, {
const user = await UserHelpers.create(getDb(), {
username,
passwordRaw: "test-password",
});
Expand Down Expand Up @@ -57,7 +57,7 @@ export async function createTestTodo(
userId: number,
overrides: Partial<{ title: string; completed: boolean }> = {},
) {
const [todo] = await db
const [todo] = await getDb()
.insert(todos)
.values({
title: "Test todo",
Expand Down
Loading