Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Dedicated test environment. Loaded by the Vitest and Playwright harnesses
# INSTEAD of .env / .env.local so tests never inherit developer or
# production-like configuration. Contains NO secrets.
#
# - Integration tests (Vitest) derive a UNIQUE, test-named database from
# DATABASE_URI's host (vitest.globalSetup.ts) and drop it around the suite.
# - E2E tests (Playwright) run the app against DATABASE_URI directly.
# Both require a LOCAL MongoDB (CI provides a service container). The isolation
# guard rejects any shared/hosted host (Atlas, DocumentDB, etc.).

NODE_ENV=test
PAYLOAD_SECRET=test-payload-secret-not-for-production

# Local, test-named database host. Overridden by CI to point at the MongoDB
# service container. Never a shared/hosted cluster.
DATABASE_URI=mongodb://127.0.0.1:27017/promisetracker_e2e_test

# Enforce the isolated-database guard at app startup during E2E runs.
PT_ASSERT_TEST_DB=true

# Canonical locale set — must match what the committed payload-types.ts was
# generated with (see src/utils/locales.ts). Keeps `pnpm generate:all`
# deterministic across local and CI runs.
NEXT_PUBLIC_LOCALES=en,fr
NEXT_PUBLIC_DEFAULT_LOCALE=en
22 changes: 22 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version: 2
updates:
# Continuous software-composition security updates for app dependencies.
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
groups:
payload:
patterns:
- "@payloadcms/*"
- "payload"
ai-sdk:
patterns:
- "@ai-sdk/*"
- "ai"

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
18 changes: 18 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: CI

on:
pull_request:
push:
branches-ignore: [main]

concurrency:
group: "${{ github.workflow }} @ ${{ github.ref }}"
cancel-in-progress: true

# Least-privilege default for GITHUB_TOKEN across all jobs.
permissions:
contents: read

jobs:
verify:
uses: ./.github/workflows/verify.yml
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
10 changes: 10 additions & 0 deletions .github/workflows/deploy-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ concurrency:
group: "${{ github.workflow }} @ ${{ github.ref }}"
cancel-in-progress: true

# Least-privilege default for GITHUB_TOKEN. Deployment authenticates to
# DockerHub/Dokku via dedicated secrets, not the workflow token.
permissions:
contents: read

env:
APP_NAME: promisetracker-v2
DOKKU_REMOTE_BRANCH: "master"
Expand All @@ -18,7 +23,12 @@ env:
SENTRY_ENVIRONMENT: "development"

jobs:
# Mandatory verification gate. Deployment cannot run when this fails.
verify:
uses: ./.github/workflows/verify.yml

deploy:
needs: verify
runs-on: ${{ matrix.os }}
strategy:
matrix:
Expand Down
125 changes: 125 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
name: Verify

# Reusable verification pipeline. Called by CI (on pull requests) and by the
# deploy workflow (which must not run when this fails).
on:
workflow_call:

# Least-privilege default for GITHUB_TOKEN; jobs only read repository contents.
permissions:
contents: read

env:
NODE_VERSION: "22.12.0"
# Isolated, local, test-named database — never a shared/hosted cluster.
DATABASE_URI: mongodb://127.0.0.1:27017/promisetracker_ci_test
PAYLOAD_SECRET: test-payload-secret-not-for-production
PT_ASSERT_TEST_DB: "true"
NODE_OPTIONS: "--no-deprecation"
# Canonical locale set. Payload's generated types are locale-dependent
# (see src/utils/locales.ts → NEXT_PUBLIC_LOCALES); this must match the set
# the committed src/payload-types.ts was generated with, or the drift check
# regenerates an en-only file and fails.
NEXT_PUBLIC_LOCALES: "en,fr"
NEXT_PUBLIC_DEFAULT_LOCALE: "en"

jobs:
verify:
name: Lint, types, tests
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:7
ports:
- 27017:27017
options: >-
--health-cmd "mongosh --quiet --eval 'db.runCommand({ ping: 1 })'"
--health-interval 10s
--health-timeout 5s
--health-retries 10
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

# Frozen installation: fail if the lockfile is out of date.
- name: Install dependencies
run: pnpm install --frozen-lockfile

# Generated-artifact drift: regenerate committed types/import map and
# fail if anything changed.
- name: Check generated artifacts are up to date
run: |
pnpm run generate:all
if ! git diff --exit-code; then
echo "::error::Generated artifacts are out of date. Run 'pnpm run generate:all' and commit the result."
exit 1
fi

- name: Lint
run: pnpm run lint

- name: Type-check
run: pnpm run typecheck

- name: Integration tests
run: pnpm run test:int

- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium

- name: Critical E2E smoke tests
run: pnpm run test:e2e

- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
if-no-files-found: ignore

security:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
name: Dependency security scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up pnpm
uses: pnpm/action-setup@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

# Software-composition analysis. Reported on every run and surfaced in the
# job summary. It is intentionally NON-BLOCKING: the production dependency
# tree carries a large number of *transitive* advisories with no available
# non-breaking fix (e.g. axios via airtable-ts, fast-xml-parser via the
# AWS SDK under mongoose, minimatch/rollup under the Sentry plugin). Hard
# failing the pipeline on those would block all deploys without improving
# security. Direct-dependency advisories that need action (notably the
# Payload account-takeover/SSRF/SQLi fixes in >=3.79.1) are tracked as a
# dedicated upgrade PR — see docs and issue tracker.
- name: Audit dependencies
continue-on-error: true
run: |
echo "## Dependency security scan" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
pnpm audit --audit-level high --prod 2>&1 | tee -a "$GITHUB_STEP_SUMMARY" || true
echo '```' >> "$GITHUB_STEP_SUMMARY"
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"generate:ai-model-ids": "node scripts/generate-ai-provider-model-ids.mjs",
"generate:all": "pnpm run generate:importmap && pnpm run generate:types",
"lint": "cross-env NODE_OPTIONS=--no-deprecation next lint",
"typecheck": "tsc --noEmit",
"payload": "cross-env NODE_OPTIONS=--no-deprecation payload",
"migrate": "NODE_OPTIONS=--no-deprecation payload migrate",
"start": "cross-env NODE_OPTIONS=--no-deprecation next start",
Expand Down Expand Up @@ -97,6 +98,7 @@
"eslint": "^9.34.0",
"eslint-config-next": "15.4.6",
"jsdom": "26.1.0",
"mongodb": "^7.5.0",
"playwright": "1.55.0",
"playwright-core": "1.55.0",
"prettier": "^3.6.2",
Expand Down
48 changes: 37 additions & 11 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,67 @@
import { defineConfig, devices } from '@playwright/test'
import { config as loadEnv } from 'dotenv'

/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
import 'dotenv/config'
// Load the DEDICATED test environment (never .env / .env.local) so E2E runs
// use isolated configuration. CI overrides DATABASE_URI to point at its
// MongoDB service container.
loadEnv({ path: ".env.test" })

/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests/e2e',
// Enforce the isolated-DB guard and reset the database before the suite.
globalSetup: './tests/e2e/globalSetup.ts',
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',

baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
// `next dev` compiles each route lazily on first request; on CI a cold
// compile of the block-heavy pages can take a while. Give navigations and
// actions a wide margin so the first hit to a route is not a flake.
navigationTimeout: 90 * 1000,
actionTimeout: 30 * 1000,
},
// Per-test timeout, also generous to cover first-request compilation.
timeout: 120 * 1000,
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
// Run the app in dev mode: the E2E suite exercises /dev/update-dialog,
// which is deliberately disabled (notFound) in production builds, so a
// production `next start` cannot serve these tests. `next dev` keeps
// NODE_ENV development and that route available.
command: 'pnpm dev',
reuseExistingServer: true,
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
// Readiness probe: wait on the dev-only, DB-free /dev/update-dialog route,
// which returns 200. The homepage is NOT a valid readiness signal — on an
// empty database it resolves to notFound() (HTTP 404), which Playwright's
// webServer never treats as "ready", so the run would time out even though
// the server is up.
url: 'http://localhost:3000/dev/update-dialog',
// Pass the isolated test database, secret, and locale set to the app.
env: {
DATABASE_URI: process.env.DATABASE_URI ?? '',
PAYLOAD_SECRET: process.env.PAYLOAD_SECRET ?? '',
PT_ASSERT_TEST_DB: 'true',
NEXT_PUBLIC_LOCALES: process.env.NEXT_PUBLIC_LOCALES ?? 'en,fr',
NEXT_PUBLIC_DEFAULT_LOCALE: process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en',
},
// Wide window to cover a cold `next dev` boot + first compile on CI.
timeout: 240 * 1000,
},
})
Loading
Loading