feat(W3): ingest endpoint + applyCatalogRows only-if-newer engine + I… #222
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
| 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 |