Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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
190 changes: 190 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# =============================================================================
# CI/CD Pipeline
# =============================================================================
#
# Two-stage pipeline that gates merges to main:
# Stage 1 (test): Unit + integration tests on GH Actions runner
# [TODO] Stage 2 (e2e): Playwright E2E against a real staging deployment
#
# Both stages must pass before a PR can be merged to main.
# Merging to main triggers production deploy via Coolify (existing setup).
#
# REQUIRED BRANCH PROTECTION RULES ON main:
# - Require status checks for unit-integration and e2e
# - Require branches to be up to date before merging
# =============================================================================

name: CI Pipeline

on:
pull_request:
branches: [main]

# =============================================================================
# WHY NO WORKFLOW-LEVEL CONCURRENCY:
#
# We need different concurrency strategies per job:
# - unit-integration: cancel-in-progress per PR (fast feedback on new pushes)
# - e2e: queue globally (staging is a shared single resource)
#
# Workflow-level concurrency would force one strategy for both.
# =============================================================================

jobs:
# ===========================================================================
# STAGE 1: Unit & Integration Tests
# ===========================================================================
#
# Runs Vitest against a local Supabase subset.
#
# Concurrency: cancel-in-progress PER PR. If you push again to the same PR
# while tests are running, the old run is killed immediately. No point
# finishing tests on stale code.
# ===========================================================================
unit-integration:
name: Unit & Integration Tests
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write

concurrency:
group: test-pr-${{ github.event.pull_request.number }}
cancel-in-progress: true

# -------------------------------------------------------------------------
# maximum amount of time a job or step can run before GitHub automatically cancels it
# -------------------------------------------------------------------------
timeout-minutes: 20

steps:
- uses: actions/checkout@v4

# RLIMIT ISSUE temp fix
# this step is only necessary because of an issue with Docker and Supabase where it cannot set the `ulimit`.
# this step sets the `ulimit` higher than the Supabase supavisor container's 100000, to prevent an "Operation not permitted"
# error stopping the supavisor from starting.
# https://github.com/supabase/cli/issues/4443
- name: Configure system limits
run: |
echo "This is due to a bug with Supabase and GitHub runner: https://github.com/supabase/cli/issues/4443"
# Set ulimit for system (higher than Supabase's 100000)
# Run ulimit within a root shell since it's a builtin and cannot be run like `sudo ulimit`
sudo bash -c 'ulimit -n 200000 && ulimit -u 16384'

# Configure Docker daemon with merged config
DOCKER_CONFIG="/etc/docker/daemon.json"

# Read existing config or start with empty object
if [ -f "$DOCKER_CONFIG" ]; then
EXISTING_CONFIG=$(cat "$DOCKER_CONFIG")
else
EXISTING_CONFIG="{}"
fi

# Merge new limits with existing config using jq (higher than Supabase's 100000)
echo "$EXISTING_CONFIG" | jq '. + {
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 200000,
"Soft": 200000
},
"nproc": {
"Name": "nproc",
"Hard": 16384,
"Soft": 16384
}
}
}' | sudo tee "$DOCKER_CONFIG" > /dev/null

# Restart Docker to apply changes
sudo systemctl restart docker

# Wait for Docker to be ready
timeout 30 sh -c 'until docker info > /dev/null 2>&1; do sleep 1; done'


- uses: actions/setup-node@v4
# use Node version enforced for the project
# see https://github.com/actions/setup-node
with:
node-version-file: '.nvmrc'
cache: "npm"

- name: Install dependencies
run: npm ci

# load the env variables for the sveltekit app
# (slightly) different from .env.ci
- name: Load app env
run: cat cicd/.env.app.ci >> "$GITHUB_ENV"

# -----------------------------------------------------------------------
- name: Run Unit Tests
run: npm run test:unit

- name: Restore Docker Image cache
if: always()
id: docker-cache
uses: actions/cache@v4
with:
path: ~/.docker-cache
key: docker-${{ runner.os }}-${{ hashFiles('cicd/docker-compose.ci.yml') }}
save-always: true

- name: Load cached images
if: always() && steps.docker-cache.outputs.cache-hit == 'true'
run: |
for f in ~/.docker-cache/*.tar; do docker load -i "$f"; done

- name: Run Integration Tests
if: always()
run: npm run test:integration

- name: Save images to cache
if: steps.docker-cache.outputs.cache-hit != 'true' && always()
run: |
mkdir -p ~/.docker-cache
docker compose -f cicd/docker-compose.ci.yml --env-file cicd/.env.ci config --images | while read img; do
fname=$(echo "$img" | tr '/:' '__')
docker save "$img" -o ~/.docker-cache/"$fname".tar
done


- name: Upload unit coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-unit
path: reports/coverage/unit
retention-days: 7

- name: Upload integration coverage
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-integration
path: reports/coverage/integration
retention-days: 7

# https://github.com/davelosert/vitest-coverage-report-action
- name: Report Unit Coverage
# Set if: always() to also generate the report if tests are failing
# Only works if you set `reportOnFailure: true` in your vite config as specified above
if: always()
uses: davelosert/vitest-coverage-report-action@v2
with:
name: Unit
json-summary-path: reports/coverage/unit/coverage-summary.json
json-final-path: reports/coverage/unit/coverage-final.json
vite-config-path: vitest.config.unit.ts

- name: Report Integration Coverage
if: always()
uses: davelosert/vitest-coverage-report-action@v2
with:
name: Integration
json-summary-path: reports/coverage/integration/coverage-summary.json
json-final-path: reports/coverage/integration/coverage-final.json
vite-config-path: vitest.config.integration.ts
7 changes: 7 additions & 0 deletions cicd/.env.app.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DATABASE_URL=postgres://postgres.1:123@localhost:6543/postgres?pgbouncer=true
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIn0.PqdH6E8yzZhWwB_c9o9e4LjdYXDTbEf5tdAqbBIrzKQ
DIRECT_URL=postgres://postgres.1:123@localhost:5432/postgres
PUBLIC_SUPABASE_URL=http://localhost:8000
PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
FILESYSTEM=SUPABASE
PUBLIC_ENVIRONMENT=dev
66 changes: 66 additions & 0 deletions cicd/.env.ci
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# =============================================================================
# .env.ci — Environment variables for docker-compose.ci.yml
# =============================================================================
# Throwaway dev credentials for ephemeral CI runners. NOT prod secrets.
# Safe to commit — these only live for the ~5 min lifespan of a test run.
# =============================================================================

# ---- Keys / JWT ----
POSTGRES_PASSWORD=123
JWT_SECRET=1cbdb23a925debd7c7fa16c4586df98ef676329bebba99177d3d6928ae405255
JWT_EXPIRY=3600
ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyAgCiAgICAicm9sZSI6ICJhbm9uIiwKICAgICJpc3MiOiAic3VwYWJhc2UtZGVtbyIsCiAgICAiaWF0IjogMTY0MTc2OTIwMCwKICAgICJleHAiOiAxNzk5NTM1NjAwCn0.dc_X5iR_VP_qT0zsiyj_I_OZ2T9FtRU2BBNWN8Bu4GE
SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIn0.PqdH6E8yzZhWwB_c9o9e4LjdYXDTbEf5tdAqbBIrzKQ

# ---- Database ----
POSTGRES_HOST=db
POSTGRES_DB=postgres
POSTGRES_PORT=5432

# ---- Kong ----
KONG_HTTP_PORT=8000
KONG_HTTPS_PORT=8443
DASHBOARD_USERNAME=supabase
DASHBOARD_PASSWORD=this_password_is_insecure_and_should_be_updated

# ---- Auth (GoTrue) ----
SITE_URL=http://localhost:3000
ADDITIONAL_REDIRECT_URLS=
API_EXTERNAL_URL=http://localhost:8000
DISABLE_SIGNUP=false
ENABLE_EMAIL_SIGNUP=true
ENABLE_EMAIL_AUTOCONFIRM=true
ENABLE_ANONYMOUS_USERS=false
ENABLE_PHONE_SIGNUP=true
ENABLE_PHONE_AUTOCONFIRM=true

# ---- SAML (disabled in CI) ----
GOTRUE_SAML_ENABLED=false
GOTRUE_SAML_PRIVATE_KEY=

# ---- SMTP (unused in CI, but GoTrue expects the vars) ----
SMTP_ADMIN_EMAIL=admin@example.com
SMTP_HOST=localhost
SMTP_PORT=25
SMTP_USER=fake_mail_user
SMTP_PASS=fake_mail_password
SMTP_SENDER_NAME=fake_sender
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
MAILER_URLPATHS_INVITE=/auth/v1/verify
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify

# ---- Supavisor (connection pooler) ----
POOLER_PROXY_PORT_TRANSACTION=6543
POOLER_DEFAULT_POOL_SIZE=20
POOLER_MAX_CLIENT_CONN=100
POOLER_TENANT_ID=1

# ---- PostgREST ----
PGRST_DB_SCHEMAS=public,storage,graphql_public

# ---- Analytics (Logflare) ----
LOGFLARE_API_KEY=your-super-secret-and-long-logflare-key

# ---- Imgproxy ----
IMGPROXY_ENABLE_WEBP_DETECTION=true
Loading
Loading