Skip to content

feat(W3): ingest endpoint + applyCatalogRows only-if-newer engine + I… #222

feat(W3): ingest endpoint + applyCatalogRows only-if-newer engine + I…

feat(W3): ingest endpoint + applyCatalogRows only-if-newer engine + I… #222

Workflow file for this run

name: CI Checks – App
on:
push:
branches: [main, dev]
pull_request:
branches: [main, dev]
schedule:
# Nightly re-audit of prod deps so a new CVE on an unchanged pin surfaces
# even with no push. Only the `audit` job runs on this trigger.
- cron: '17 3 * * *'
permissions:
contents: read
concurrency:
group: ci-checks-${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ─────────────────────────────────────────────────────────────
# Code Quality Checks (lint, prettier, typecheck, knip)
# ─────────────────────────────────────────────────────────────
test-matrix:
name: ${{ matrix.task }}
# Schedule runs the nightly dependency audit only — skip the quality matrix.
if: github.event_name != 'schedule'
strategy:
matrix:
task:
[
'lint',
'prettier',
'typecheck',
'knip',
'unit',
'oxlint',
'prettier-root',
]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate dependencies
run: pnpm doctor
# Prisma client only for typecheck
- if: matrix.task == 'typecheck'
name: Generate Prisma Client
run: pnpm --filter caramel-app exec prisma generate
# oxlint runs from the repo root (both apps), not per-app like the rest
# of this matrix — kept in the same matrix so it reports as its own
# "oxlint" status check, sibling to lint/prettier/typecheck/knip.
- if: matrix.task == 'oxlint'
name: Run oxlint
run: pnpm lint:oxlint
# prettier-root checks repo-root + .github files (workflows YAML, root
# package.json, README, issue templates) that the per-app prettier task
# doesn't cover — runs from the repo root, like oxlint.
- if: matrix.task == 'prettier-root'
name: Run root prettier-check
run: pnpm prettier-check:root
- if: matrix.task != 'oxlint' && matrix.task != 'prettier-root'
name: Run ${{ matrix.task }}
working-directory: ./apps/caramel-app
run: |
case ${{ matrix.task }} in
lint) pnpm lint ;;
prettier) pnpm prettier-check ;;
typecheck) pnpm tsc --noEmit ;;
knip) pnpm knip ;;
unit) pnpm test ;;
esac
# ─────────────────────────────────────────────────────────────
# Integration (W1) — real prisma against a live Postgres
# ─────────────────────────────────────────────────────────────
# The unit job (test-matrix) mocks prisma; this is its DB-backed sibling:
# tests/integration/**/*.itest.ts run the real prisma client against a live
# Postgres so the coupon_signals read/write path is exercised for real, not
# mocked. Same trigger as test-matrix (PR + push, not the nightly audit) so a
# regression is caught on every change, not just on PRs.
integration:
name: Integration (DB)
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
timeout-minutes: 10
services:
postgres:
# Prod-matching: docker-compose.yml pins postgres:18.4. (The e2e-pr
# job's postgres:15 is a separate, known pin-drift punch-list item —
# deliberately NOT copied here.)
image: postgres:18.4
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: caramel
ports: ['58005:5432']
options: >-
--health-cmd "pg_isready -U postgres -d caramel"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Writes apps/caramel-app/.env with a DATABASE_URL pointing at the
# postgres service (postgres:postgres@localhost:58005/caramel) — the same
# value vitest.integration.config.ts's loadEnvFile('.env') reads.
- name: Write minimal CI env
run: pnpm --filter caramel-app run setup:ci-env
- name: Generate Prisma Client
run: pnpm --filter caramel-app exec prisma generate
- name: Apply Prisma migrations
working-directory: ./apps/caramel-app
run: pnpm exec prisma migrate deploy
- name: Run integration tests
run: pnpm --filter caramel-app test:integration
# ─────────────────────────────────────────────────────────────
# Dependency Vulnerability Audit (prod deps, high + critical)
# ─────────────────────────────────────────────────────────────
audit:
name: Dependency audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit production dependencies (high + critical)
run: pnpm audit --prod --audit-level=high
# ─────────────────────────────────────────────────────────────
# Schema Drift Check (only when prisma files change)
# ─────────────────────────────────────────────────────────────
schema-drift:
name: Schema Drift
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: caramel_ci
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/caramel_ci'
SHADOW_DATABASE_URL: 'postgresql://postgres:postgres@localhost:5432/caramel_ci?schema=__shadow'
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Cache Prisma engines
uses: actions/cache@v4
with:
path: ~/.cache/prisma
key: ${{ runner.os }}-prisma-engines-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-prisma-engines-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Prisma validate
working-directory: ./apps/caramel-app
run: npx prisma validate
- name: Prepare shadow schema
run: |
sudo apt-get -yqq install postgresql-client
psql "postgresql://postgres:postgres@localhost:5432/caramel_ci" \
-c "CREATE SCHEMA IF NOT EXISTS __shadow;"
- name: Drift check (schema ⇄ migrations)
working-directory: ./apps/caramel-app
run: |
npx prisma migrate diff \
--from-migrations prisma/migrations \
--to-schema-datamodel prisma/schema.prisma \
--shadow-database-url "$SHADOW_DATABASE_URL" \
--exit-code || echo "No drift detected or no migrations yet"
- name: Apply migrations
working-directory: ./apps/caramel-app
run: npx prisma migrate deploy --preview-feature || echo "No migrations to apply"
- name: Drift check (migrations ⇄ Postgres)
working-directory: ./apps/caramel-app
run: |
npx prisma migrate diff \
--from-migrations prisma/migrations \
--to-url "$DATABASE_URL" \
--shadow-database-url "$SHADOW_DATABASE_URL" \
--exit-code || echo "No drift detected"
# ─────────────────────────────────────────────────────────────
# E2E & Visual Regression (PR – local dev server)
# ─────────────────────────────────────────────────────────────
e2e-pr:
name: E2E & Visual Regression (PR)
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
services:
postgres:
image: postgres:15
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: caramel
ports: ['58005:5432']
options: >-
--health-cmd "pg_isready -U postgres -d caramel"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_API_BASE_URL: ${{ vars.ARGOS_API_BASE_URL }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright
run: pnpm --filter caramel-app exec playwright install --with-deps chromium
- name: Write minimal CI env
run: pnpm --filter caramel-app run setup:ci-env
- name: Generate Prisma Client
run: pnpm --filter caramel-app exec prisma generate
- name: Apply Prisma migrations
working-directory: ./apps/caramel-app
run: pnpm exec prisma migrate deploy
- name: Run E2E tests
working-directory: ./apps/caramel-app
env:
PLAYWRIGHT_BASE_URL: http://localhost:58000
NEXT_PUBLIC_BASE_URL: http://localhost:58000
PLAYWRIGHT_WEB_COMMAND: pnpm dev
PLAYWRIGHT_START_SERVER: 'true'
PLAYWRIGHT_WEB_TIMEOUT: '300000'
run: pnpm test:e2e
- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-pr
path: apps/caramel-app/playwright-report/
retention-days: 14
# ─────────────────────────────────────────────────────────────
# E2E & Visual Regression (push – deployed site baseline)
# ─────────────────────────────────────────────────────────────
e2e-push:
name: E2E & Visual Regression (push)
if: github.event_name == 'push'
runs-on: ubuntu-latest
timeout-minutes: 10
env:
ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }}
ARGOS_API_BASE_URL: ${{ vars.ARGOS_API_BASE_URL }}
NEXT_PUBLIC_BASE_URL: ${{ github.ref == 'refs/heads/dev' && 'https://dev.grabcaramel.com' || 'https://grabcaramel.com' }}
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright
run: pnpm --filter caramel-app exec playwright install --with-deps chromium
- name: Run E2E tests
working-directory: ./apps/caramel-app
env:
PLAYWRIGHT_BASE_URL: ${{ github.ref == 'refs/heads/dev' && 'https://dev.grabcaramel.com' || 'https://grabcaramel.com' }}
PLAYWRIGHT_START_SERVER: 'false'
run: pnpm test:e2e
- name: Upload test report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report-push
path: apps/caramel-app/playwright-report/
retention-days: 14
# ─────────────────────────────────────────────────────────────
# One-root-compose build (F-016) — clean-volume build + boot + migrate proof
# ─────────────────────────────────────────────────────────────
compose-build:
name: One-root-compose build
# PR-only + heavy, like e2e-pr. checks-app.yml is a shared workflow, so it
# can't use on.push.paths without gating every other job — a first `git
# diff` step path-filters this job instead (mirrors the intent of
# ai-evals.yml's path filter). A clean runner gives clean volumes.
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect compose-relevant changes
id: filter
run: |
git fetch --no-tags origin "${{ github.base_ref }}" || true
changed="$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" || git diff --name-only "origin/${{ github.base_ref }}" HEAD)"
echo "changed files:"
echo "$changed"
if echo "$changed" | grep -Eq '^(Dockerfile|\.dockerignore|docker-compose\.yml|docker-entrypoint\.sh|package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml|apps/caramel-app/package\.json|apps/caramel-app/next\.config\.mjs|apps/caramel-app/prisma/|apps/caramel-app/\.env\.example)'; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
echo "No compose-relevant paths changed — skipping the heavy build."
fi
- name: Provide a throwaway app env (auth secret only; compose env overrides the DB URLs)
if: steps.filter.outputs.run == 'true'
run: printf 'BETTER_AUTH_SECRET=ci_compose_build_throwaway_secret\n' > apps/caramel-app/.env
- name: Build and boot the stack
if: steps.filter.outputs.run == 'true'
run: docker compose up -d --build
- name: Wait for web to become healthy
if: steps.filter.outputs.run == 'true'
run: |
cid="$(docker compose ps -q web)"
for i in $(seq 1 60); do
status="$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo starting)"
echo "attempt $i: web=$status"
case "$status" in
healthy) exit 0 ;;
unhealthy)
echo '::error::web became unhealthy'
docker compose logs web
exit 1
;;
esac
sleep 5
done
echo '::error::web did not become healthy in time'
docker compose logs web
exit 1
- name: Assert the entrypoint ran prisma migrate deploy
if: steps.filter.outputs.run == 'true'
run: |
docker compose logs web | grep -F '[entrypoint] applying prisma migrate deploy'
docker compose logs web | grep -F '[entrypoint] migrations applied'
- name: Assert the homepage serves 200
if: steps.filter.outputs.run == 'true'
run: curl -fsS -o /dev/null -w 'homepage HTTP %{http_code}\n' http://127.0.0.1:58000/
- name: Tear down (clean volumes)
if: ${{ always() && steps.filter.outputs.run == 'true' }}
run: docker compose down -v