CICD - Unit | Integration | E2E#125
Open
JoanMVPopov wants to merge 47 commits into
Open
Conversation
Coverage Report for Unit
File CoverageNo changed files found. |
Coverage Report for Integration
File CoverageNo changed files found. |
zakrok
approved these changes
May 8, 2026
…om start to finish
…tions, no redundant redefinitions of containers
… of docker compose for ci, improve generalizability for env variables depending on setup
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
CI/CD Pipeline: Unit, Integration, and E2E Testing
This PR is related to our two-stage CI/CD pipeline that gates merges to main, along with things related to the Docker Compose configuration, and Playwright E2E infrastructure.
Pipeline Overview
The pipeline is defined in
.github/workflows/ci.ymland runs on every pull request targeting main. It consists of two jobs that run in parallel:Unit & Integration Tests - runs on the GitHub Actions runner itself using Vitest and a local Supabase Docker subset
End-to-End Tests - runs Playwright against a real staging deployment on a Hetzner VPS managed by Coolify
Both jobs must pass before a PR can be merged. Merging to main triggers production deployment via Coolify
Job 1: Unit & Integration Tests (
unit-integration)Concurrency: cancel-in-progress per PR. Pushing new commits to the same PR cancels the previous run immediately.
Timeout: 20 minutes.
Steps
Checkout the PR code
Configure system limits - works around a known Supabase/Docker issue where the supavisor container requires a nofile ulimit of 100,000+. GitHub Actions runners default to a lower value, causing the container to fail with "Operation not permitted." This step sets the ulimit to 200,000 and configures the Docker daemon accordingly, then restarts Docker. See CLI on Github Actions supavisor fails on RLIMIT_NOFILE = 100000 supabase/cli#4443
Setup Node.js using the version from
.nvmrcwith npm cachingInstall dependencies via
npm ciLoad app environment variables from
cicd/.env.app.ciinto$GITHUB_ENV. These are the SvelteKit app env vars (DATABASE_URL,PUBLIC_SUPABASE_URL, etc.) and are slightly different from the Docker.env.ciused by the compose stackRun unit tests via
npm run test:unit(Vitest withvitest.config.unit.ts)Restore/load Docker image cache -- uses
actions/cache@v4to cache Docker images between runs, keyed on the CI compose file hash. On cache hit, images are loaded from tar filesRun integration tests via
npm run test:integration, which invokesscripts/run-integration.sh. This script spins up a Supabase subset via Docker Compose, waits for health checks, then runs Vitest withvitest.config.integration.tsSave Docker images to cache on first run (when cache miss)
Upload coverage artifacts (unit and integration) as GitHub Actions artifacts with 7-day retention
Report coverage as PR comments using
davelosert/vitest-coverage-report-action@v2, one comment for unit coverage and one for integrationThe integration test steps run with
if: always()so they execute even if unit tests fail, giving visibility into both results in a single run.Job 2: End-to-End Tests (
e2e)Concurrency: single global group (
staging-test-e2e), no cancel-in-progress. Since staging is a shared single resource, E2E runs queue rather than cancel each other.Timeout: 30 minutes.
Steps
Checkout the code (merge commit)
Setup Node.js and install dependencies
Install Playwright with Chromium only (
npx playwright install --with-deps chromium). Firefox and WebKit are omitted to save CI timePush PR code to the staging branch via
git push --force origin HEAD:refs/heads/staging. This is how the PR's code reaches the staging VPS. Force-push is safe because staging is not a working branch, the concurrency lock prevents parallel pushes, and we are pushing a specific SHA.Stop both applications via
scripts/cicd/stop-and-wait.sh. This calls the Coolify API to stop both the Supabase and SvelteKit applications, then polls until each reaches the exited state. This stop-first approach prevents a race condition where health checks could pass against old (still-running) containers from the previous deploymentDeploy and health-check Supabase via
scripts/cicd/deploy-supabase-to-staging.sh. Triggers a start via the Coolify API, polls until the application status is running, then verifies three endpoints: the dashboard (expects HTTP 307), the auth service (/auth/v1/health, expects 200), and the REST API (/rest/v1/, expects 200).Setup SSH using
webfactory/ssh-agent@v0.9.0with a dedicated cicd user's private keyWipe the staging database by SSHing into the staging server and running a PL/pgSQL block inside the Postgres container. This truncates all tables in the public schema (with RESTART IDENTITY CASCADE) except
_prisma_migrations, which tracks applied migrations and must remain intact. The container is discovered dynamically usingdocker ps --format '{{.Names}}' | grep '^db-'because Coolify generates random container name suffixesDeploy and health-check SvelteKit via
scripts/cicd/deploy-sveltekit-to-staging.sh. Same pattern as Supabase: start via API, poll until running, then check/api/healthreturns 200. On startup, the SvelteKit app runsinit-db.shwhich applies Prisma migrations, sets up SAML, seeds triggers, RLS policies, and storage buckets. This is idempotent and works correctly after a database wipeRun Playwright tests with
npx playwright testagainst the staging URLUpload Playwright HTML report as an artifact with 14-day retention, even on test failure
Required GitHub Secrets for E2E
STAGING_COOLIFY_BASE_URL-- Coolify API base URLSTAGING_COOLIFY_API_TOKEN-- Coolify API bearer tokenSTAGING_COOLIFY_SUPABASE_UUID-- Coolify application UUID for the Supabase stackSTAGING_COOLIFY_SVELTEKIT_UUID-- Coolify application UUID for the SvelteKit appSTAGING_SUPABASE_URL-- public Supabase URL on stagingSTAGING_ANON_KEY-- Supabase anonymous key for stagingSTAGING_DASHBOARD_USERNAME/STAGING_DASHBOARD_PASSWORD-- Supabase dashboard basic auth credentialsSTAGING_URL-- public SvelteKit URL on stagingSTAGING_SSH_PRIVATE_KEY-- ed25519 private key for the cicd user on the VPSSTAGING_SSH_HOST-- staging server IPSTAGING_SSH_USER-- SSH username (cicd)CI/CD Scripts (
scripts/cicd/)All scripts use
set -euo pipefailfor strict error handling.stop-and-wait.shStops both Coolify applications (Supabase and SvelteKit) and polls their status via the Coolify API until each reaches the exited state. Skips the stop request if the application is already stopped. Times out after 150 seconds (30 polls at 5-second intervals). All configuration (
COOLIFY_URL,TOKEN,SUPABASE_UUID,SVELTEKIT_UUID) is read from environment variables injected by GitHub secrets.deploy-supabase-to-staging.shStarts the Supabase application via the Coolify API, polls until the application status is running, then runs health checks against three endpoints (dashboard, auth, REST). Uses
|| trueon all curl commands to preventset -efrom killing the script when the server is unreachable during startup. Uses if blocks instead of&&chains for boolean checks, also to avoid premature exit fromset -e.deploy-sveltekit-to-staging.shSame pattern as the Supabase script but checks a single endpoint (
/api/health).CI override:
cicd/docker-compose.ci.ymlA minimal override file that layers on top of the base compose. It uses the
!overrideYAML tag to replace (not merge) specific properties:./volumes/db/data) and thedb-confignamed volume. Paths use../docker/volumes/because Docker Compose resolves relative paths from the directory of the file they are defined in. Addstmpfs: /var/lib/postgresql/datafor ephemeral storage on the GitHub runnervolumes: !override {}) since the CI setup uses no named volumesUsage in CI:
docker compose -f docker/docker-compose.yml -f cicd/docker-compose.ci.yml --env-file cicd/.env.ciEnvironment files
cicd/.env.ciDocker Compose environment variables for the CI Supabase stack. Contains throwaway credentials safe to commit. Includes variables that were previously hardcoded in the old standalone CI compose file (
MINIO_ROOT_USER,MINIO_ROOT_PASSWORD,SECRET_KEY_BASE,VAULT_ENC_KEY,SECRET_KEY_BASE_REALTIME)cicd/.env.app.ciSvelteKit application environment variables loaded into
$GITHUB_ENVduring CI. ContainsDATABASE_URL,SERVICE_ROLE_KEY,PUBLIC_SUPABASE_URL,PUBLIC_SUPABASE_ANON_KEY,FILESYSTEM, andPUBLIC_ENVIRONMENTPlaywright Configuration
playwright.config.tsSwitches behavior based on
process.env.CI(automatically set to true by GitHub Actions runners):baseURLset fromSTAGING_URLenv var, reporters aregithub(for inline CI annotations) andhtml(downloadable report,open: 'never')baseURLishttp://localhost:4173, reporters arelist(terminal output) andhtml(open: 'never'to prevent the report server from blocking script exit)Workers set to 1 (serial execution). Test directory is
tests/e2e.Testing Scripts
scripts/run-integration.sh(integration tests)Updated to support both CI and local environments using a single script:
NODE_ENV=testCIenvironment variable:docker compose -f docker/docker-compose.yml -f cicd/docker-compose.ci.yml --env-file cicd/.env.ciwith a specific service subset (db,analytics,supavisor,kong,auth,rest,storage,imgproxy,minio,minio-createbucket)docker compose -f docker/docker-compose.yml --env-file docker/.envwith all services-v(ephemeral test data)scripts/run-e2e.sh(local E2E tests)New script for running Playwright locally:
/api/healthfor HTTP 200npx playwright testscripts/init-db.sh(database initialization)Separated from the build step. Previously,
npm run buildincluded database operations (Prisma generate, migrate, seeding). Now:npm run buildonly runsvite build(plusdirname.sh)npm run start:previewandnpm run startruninit-db.shbefore starting the serverinit-db.shruns: prisma generate, prisma migrate deploy, SAML IDP setup, trigger seeding, RLS policy seeding, and bucket seeding (ifFILESYSTEM=SUPABASE)This is idempotent and safe to run after a database wipe, since it only sets up infrastructure (schema, triggers, policies, buckets), not user data
Deployment Configuration
nixpacks.tomlUnchanged in purpose but works with the new script structure:
npm run build(now just vite build)npm start(now runsinit-db.shbefore node build)