mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
ci: PR gate with minimal DB + build + migration-lock; stage gate without build
This commit is contained in:
parent
6ac26404d2
commit
777fde2eed
4 changed files with 178 additions and 2 deletions
102
.github/workflows/ci-pr.yml
vendored
Normal file
102
.github/workflows/ci-pr.yml
vendored
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# PR gate: feature → stage (and stage → main promote PRs).
|
||||
# Full gate against the MINIMAL synthetic database: service Postgres → migrate →
|
||||
# db:seed:minimal → test:int → build. Build runs here (local service DB only) and
|
||||
# NEVER in ci-stage.yml — `pnpm build` applies migrations to whatever DATABASE_URL
|
||||
# it sees, so it is forbidden anywhere near stage/prod URLs.
|
||||
name: CI (PR)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [stage, main]
|
||||
|
||||
concurrency:
|
||||
group: ci-pr-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
checks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
postgres:
|
||||
# Matches production Neon major (17) and docker-compose.yml.
|
||||
image: postgres:17-alpine
|
||||
env:
|
||||
POSTGRES_USER: teqo
|
||||
POSTGRES_PASSWORD: teqo
|
||||
POSTGRES_DB: teqo_test
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U teqo"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 12
|
||||
env:
|
||||
# Tests load .env.test themselves (database teqo_test on localhost:5432).
|
||||
# These are only for the migrate/seed/build steps, which read the process env.
|
||||
DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test
|
||||
PAYLOAD_SECRET: test-only-secret-not-used-in-production
|
||||
NEXT_PUBLIC_SITE_URL: http://localhost:3000
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint (zero warnings)
|
||||
run: pnpm lint
|
||||
|
||||
- name: Format check (prettier)
|
||||
run: pnpm format:check
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Dead code (knip)
|
||||
run: pnpm exec knip
|
||||
|
||||
- name: Circular dependencies (madge)
|
||||
run: pnpm check:cycles
|
||||
|
||||
- name: Unit tests
|
||||
run: pnpm test:unit
|
||||
|
||||
- name: Migrate test database
|
||||
run: pnpm migrate
|
||||
|
||||
- name: Seed minimal test database
|
||||
run: pnpm db:seed:minimal
|
||||
|
||||
- name: Integration tests
|
||||
run: pnpm test:int
|
||||
|
||||
- name: Build (against the local service database)
|
||||
run: pnpm build
|
||||
|
||||
migration-lock:
|
||||
# At most ONE open PR may touch schema (migrations or generated types).
|
||||
# Two in flight guarantees a broken rebase and a poisoned drizzle snapshot
|
||||
# chain; the second PR must wait or rebase.
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Count open PRs touching schema files
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
count=$(gh pr list --state open --limit 100 --json number,files \
|
||||
--jq '[.[] | select(.files != null and ([.files[].path | test("^(src/migrations/|src/payload-types.ts$|payload-types.ts$)")] | any))] | length')
|
||||
echo "Open schema-touching PRs: $count"
|
||||
if [ "$count" -gt 1 ]; then
|
||||
echo "::error::More than one open PR touches src/migrations/ or payload-types.ts. Merge or close the other first."
|
||||
exit 1
|
||||
fi
|
||||
60
.github/workflows/ci-stage.yml
vendored
Normal file
60
.github/workflows/ci-stage.yml
vendored
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Stage gate: runs AFTER a merge lands on `stage`, against the stage database
|
||||
# (Neon branch cloned from prod, secret STAGE_DATABASE_URL in the `stage`
|
||||
# GitHub Environment).
|
||||
#
|
||||
# HARD RULE: never `pnpm build` here — the build step runs `payload migrate`
|
||||
# against whatever DATABASE_URL it sees, and this workflow holds a stage URL.
|
||||
# No db:seed:minimal either: stage data is the prod snapshot, not the synthetic
|
||||
# minimal set.
|
||||
name: CI (stage)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [stage]
|
||||
|
||||
concurrency:
|
||||
group: ci-stage
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
stage-checks:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
environment: stage
|
||||
env:
|
||||
DATABASE_URL: ${{ secrets.STAGE_DATABASE_URL }}
|
||||
PAYLOAD_SECRET: ${{ secrets.STAGE_PAYLOAD_SECRET }}
|
||||
NEXT_PUBLIC_SITE_URL: https://teqo-stage.vercel.app
|
||||
# The seed/migrate guards refuse non-local hosts without this flag. Stage
|
||||
# is the ONE documented remote target, and only this workflow may use it.
|
||||
ALLOW_REMOTE_DB: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
cache: pnpm
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Apply pending migrations to the stage database
|
||||
run: pnpm migrate
|
||||
|
||||
- name: Point the test env at the stage snapshot
|
||||
run: |
|
||||
{
|
||||
echo "DATABASE_URL=${DATABASE_URL}"
|
||||
echo "PAYLOAD_SECRET=${PAYLOAD_SECRET}"
|
||||
} > .env.test
|
||||
|
||||
- name: Integration tests against the stage snapshot
|
||||
run: pnpm test:int
|
||||
env:
|
||||
# Documented escape hatch in tests/helpers/assertTestDatabase.ts —
|
||||
# only this workflow may set it (vitest.setup.ts loads .env.test
|
||||
# with override: true, written by the step above).
|
||||
ALLOW_STAGE_TEST_DB: "true"
|
||||
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
|
|
@ -1,11 +1,12 @@
|
|||
# Continuous integration on GitHub Actions (GitHub is the only home).
|
||||
# Runs the same gate as .husky/pre-push plus tests and dead-code analysis.
|
||||
# Post-merge gate on `main` (promote from stage already passed ci-pr on the PR).
|
||||
# PRs run in ci-pr.yml (with build + migration-lock); merges to `stage` run in
|
||||
# ci-stage.yml (stage Neon snapshot, never a build).
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,19 @@ export function assertTestDatabase(databaseUrl: string | undefined): void {
|
|||
|
||||
const databaseName = decodeURIComponent(parsedUrl.pathname.replace(/^\//, ''))
|
||||
const localHosts = new Set(['localhost', '127.0.0.1', '::1', '[::1]'])
|
||||
|
||||
// Documented stage escape hatch: ONLY the ci-stage workflow (GitHub
|
||||
// Environment `stage`, STAGE_DATABASE_URL secret) may point the int suite at
|
||||
// the stage Neon snapshot, and it sets this flag explicitly. Agents and local
|
||||
// runs never set it — the default stays the exact local teqo_test.
|
||||
if (process.env.ALLOW_STAGE_TEST_DB === 'true') {
|
||||
if (parsedUrl.protocol === 'postgresql:' && databaseName.length > 0) return
|
||||
throw new Error(
|
||||
'ALLOW_STAGE_TEST_DB is set but DATABASE_URL is not a postgresql: connection string ' +
|
||||
`with a database name (got "${parsedUrl.protocol || '(unknown)'}").`,
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
parsedUrl.protocol !== 'postgresql:' ||
|
||||
databaseName !== 'teqo_test' ||
|
||||
|
|
|
|||
Loading…
Reference in a new issue