diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cb52d8bb..37f7092c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ on: branches: - main - develop - - "*" + - '*' pull_request: jobs: @@ -36,6 +36,29 @@ jobs: validate: runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: teachlink + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 10 + + env: + DATABASE_HOST: localhost + DATABASE_PORT: 5432 + DATABASE_USER: postgres + DATABASE_PASSWORD: postgres + DATABASE_NAME: teachlink + NODE_ENV: ci + steps: - name: Checkout code uses: actions/checkout@v4 @@ -62,6 +85,15 @@ jobs: - name: Build application run: pnpm run build + - name: Run migrations + run: pnpm run migration:run + + - name: Check for schema drift + run: pnpm run migration:generate:check + + - name: Revert migrations + run: pnpm run migration:revert + # Vulnerability and license compliance scanning. Runs in addition to the # validation job so deploy-blocking checks do not couple with lint/typecheck. security-scan: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 29739534..b0d31f1e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -353,11 +353,14 @@ Defined in `.github/workflows/ci.yml`. Full documentation in [README.md](README. | Job | Description | Blocks merge | | ------------ | ---------------------------- | ------------ | -| `install` | `npm ci` with cache | Yes | +| `install` | `pnpm install` with cache | Yes | | `lint` | ESLint zero-warning check | Yes | | `format` | Prettier check (no rewrite) | Yes | | `typecheck` | `tsc --noEmit` | Yes | -| `build` | `nest build` | Yes | +| `build` | `pnpm build` | Yes | +| `migrations` | `pnpm run migration:run` | Yes | +| `drift-check` | `pnpm run migration:generate:check` | Yes | +| `revert` | `pnpm run migration:revert` | Yes | | `unit-tests` | Jest + coverage ≥ 70% | Yes | | `e2e-tests` | Supertest + Postgres + Redis | Yes | | `ci-success` | Aggregate gate | Yes | @@ -374,13 +377,66 @@ npm run format:check # Type error npm run typecheck +# Build failure +npm run build + # Unit test / coverage failure npm run test:ci +# Migration failure (requires Postgres + Redis running locally) +npm run migration:run +npm run migration:revert + +# Schema drift check (fails if model changes exist without a migration) +npm run migration:generate:check + # E2E failure (requires local Postgres + Redis) npm run test:e2e ``` +### Migration testing + +When model definitions change, a corresponding migration file must be created and committed alongside the model changes. Failing to do so will cause the `drift-check` CI job to fail. + +#### Creating a new migration + +```bash +# Generate a migration from model changes +npm run migration:generate -- +``` + +#### Verifying migrations + +```bash +# Apply all pending migrations +npm run migration:run + +# Revert the last migration (verifies reversibility) +npm run migration:revert + +# Re-apply to restore the database state +npm run migration:run +``` + +#### Checking for schema drift + +Schema drift occurs when entity definitions have changed but no migration file has been generated to reflect those changes. The CI pipeline runs a drift check on every PR: + +```bash +# Fails if model changes exist without a corresponding migration +npm run migration:generate:check +``` + +If this check fails locally, run the migration generator and commit the new migration file: + +```bash +npm run migration:generate -- add-example-field +git add src/migrations/ +git commit -m "feat(module): add example field migration (#)" +``` + +--- + If CI fails on your PR, fix the issue locally, confirm it passes, then push. Do not ask reviewers to approve while CI is red. --- diff --git a/README.md b/README.md index c3705b14..4bc96c84 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ Every pull request and every push to `main` / `develop` runs an automated pipeli | **Format** | Prettier | Any file that would be reformatted | | **Type Check** | `tsc --noEmit` | Any TypeScript error | | **Build** | NestJS CLI | Compilation failure | +| **Migrations** | TypeORM CLI | Migration error or schema drift | | **Unit Tests** | Jest + ts-jest | Test failure or coverage below 70 % | | **E2E Tests** | Jest + Supertest | Test failure (uses real Postgres + Redis) | @@ -138,6 +139,15 @@ pnpm test:ci # E2E tests (requires Postgres + Redis running locally) pnpm test:e2e + +# Run database migrations +pnpm run migration:run + +# Revert last migration (verifies reversibility) +pnpm run migration:revert + +# Check for schema drift (fails if model changes exist without a migration) +pnpm run migration:generate:check ``` ### Coverage thresholds @@ -168,6 +178,7 @@ Every pull request and every push to `main` / `develop` runs an automated pipeli | **Format** | Prettier | Any file that would be reformatted | | **Type Check** | `tsc --noEmit` | Any TypeScript error | | **Build** | NestJS CLI | Compilation failure | +| **Migrations** | TypeORM CLI | Migration error or schema drift | | **Unit Tests** | Jest + ts-jest | Test failure or coverage below 70 % | | **E2E Tests** | Jest + Supertest | Test failure (uses real Postgres + Redis) | @@ -191,6 +202,15 @@ npm run test:ci # E2E tests (requires Postgres + Redis running locally) npm run test:e2e + +# Run database migrations +npm run migration:run + +# Revert last migration (verifies reversibility) +npm run migration:revert + +# Check for schema drift (fails if model changes exist without a migration) +npm run migration:generate:check ``` ### Coverage thresholds diff --git a/docs/migrations.md b/docs/migrations.md index 588ca48a..a775bf32 100644 --- a/docs/migrations.md +++ b/docs/migrations.md @@ -102,7 +102,7 @@ pnpm migrate:status # Check status pnpm build # Run migrations using TypeORM CLI -npx typeorm-ts-node-commonjs migration:run -d src/config/datasource.ts +npx typeorm migration:run -d src/config/datasource.ts ``` --- diff --git a/package.json b/package.json index 4c2a9d6a..536d5bf1 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,10 @@ "migrate:rollback:count": "curl -s -X POST http://localhost:3000/migrations/rollback/${COUNT:-1} | npx json", "migrate:rollback:to": "curl -s -X POST http://localhost:3000/migrations/rollback/to/${MIGRATION_NAME} | npx json", "migrate:reset": "curl -s -X DELETE http://localhost:3000/migrations/reset | npx json", + "migration:run": "npx typeorm migration:run -d src/config/datasource.ts", + "migration:revert": "npx typeorm migration:revert -d src/config/datasource.ts", + "migration:generate": "npx typeorm migration:generate -d src/config/datasource.ts", + "migration:generate:check": "BEFORE=$(find src/migrations -maxdepth 1 -name '[0-9]*.ts' | wc -l) && npx typeorm migration:generate -d src/config/datasource.ts src/migrations/drift-check-$(date +%s) && AFTER=$(find src/migrations -maxdepth 1 -name '[0-9]*.ts' | wc -l) && find src/migrations -maxdepth 1 -name '*drift-check*' -delete && if [ \"$AFTER\" -gt \"$BEFORE\" ]; then echo \"ERROR: Schema drift detected - model changes exist without a corresponding migration. Run npm run migration:generate to create the missing migration file.\" && exit 1; fi", "docs:generate": "node scripts/generate-api-docs.js", "docs:validate": "node scripts/validate-openapi.js", "docs:check": "npm run docs:generate && npm run docs:generate:examples && git diff --exit-code -- openapi-spec.json docs/api/openapi-spec.json docs/api/examples.md docs/site docs/examples", diff --git a/src/config/datasource.ts b/src/config/datasource.ts new file mode 100644 index 00000000..4b385fbf --- /dev/null +++ b/src/config/datasource.ts @@ -0,0 +1,9 @@ +import { DataSource, DataSourceOptions } from 'typeorm'; +import { getDatabaseConfig } from './database.config'; + +export const AppDataSource = new DataSource({ + ...(getDatabaseConfig() as DataSourceOptions), + synchronize: false, + migrations: ['src/migrations/**/*.{ts,js}'], + migrationsTableName: 'migrations', +});