Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d3269ba
docs(plans): fold Projects + task deep links into phase 5, shared /@ …
angeloreale Aug 17, 2026
1e31a1f
feat(db): add Project model, list public-profile fields, task job pos…
angeloreale Aug 17, 2026
39cf453
feat(services): project service, task applications, public list seria…
angeloreale Aug 17, 2026
4f8780c
feat(api): public tasklists, job apply flow, projects routes
angeloreale Aug 17, 2026
2a57677
feat(ui): public list/job/project pages, job board, forms, task deep …
angeloreale Aug 17, 2026
d967b10
feat(i18n): Phase 5 key groups in en.json, openapi sync, lint cleanups
angeloreale Aug 17, 2026
4c77f1b
feat(ledger): DPIP off-chain ledger, dual-mode transfers, wallet life…
angeloreale Aug 17, 2026
be74323
feat(orgs): organizations as first-class owners with shared /@ handles
angeloreale Aug 17, 2026
8d5dd81
feat(events): events core — Event model, pages, RSVP, list/project links
angeloreale Aug 17, 2026
ea1627a
feat(ar/ds): adding e2e tests
angeloreale Aug 17, 2026
aa8fe54
test(e2e): Playwright suite + CI merge gate; fixes found by the tests
angeloreale Aug 17, 2026
4a37f62
fix(ar/ci): e2e checks
angeloreale Aug 17, 2026
e8861c3
fix(ar/ci): e2e checks
angeloreale Aug 17, 2026
028f3c2
feat(ar/ds): adding e2e tests
angeloreale Aug 17, 2026
0dea84d
fix(ci): run E2E Mongo as single-node replica set (fixes P2031)
angeloreale Aug 18, 2026
4d34a12
fix(ci): remove ::error:: workflow command annotation from secrets check
Copilot Aug 18, 2026
c46480c
fix(ci): remove secrets check step that logged secret names to stdout
Copilot Aug 18, 2026
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
178 changes: 178 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
name: E2E

# Merge gate for dev: the whole-stack smoke + user journey must pass.
# Required repo secrets (GitHub → Settings → Secrets and variables → Actions):
# CLERK_SECRET_KEY — Clerk backend key (DEVELOPMENT instance)
# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY — the app's publishable key
#
# DB: a fresh MongoDB container per run, started as a SINGLE-NODE REPLICA SET.
# A standalone mongod is not enough: Prisma wraps nested writes in a
# transaction, so even `prisma.user.create({ include: { profiles: true } })` in
# `ensureUserAndProfile` fails with P2031 ("requires your MongoDB server to be
# run as a replica set") — no User row is ever created and every downstream
# request 404s with "User not found". `services:` cannot pass `--replSet` to
# the container's command, so Mongo is started with `docker run` instead.
#
# Running a replica set also means CI exercises the SAME ledger path as
# production (single interactive `prisma.$transaction`) rather than the
# standalone sequential fallback, so the dual-mode deviation is covered where
# it actually ships.
#
# `prisma db push` alone reproduces the current schema; the 0021–0028 data
# migrations are all no-ops on an empty DB (their backfills/repairs only touch
# legacy rows, which never exist here), so they are not run. The harness
# self-seeds the SYSTEM:treasury wallet.

on:
# Every PR gets E2E — the stacked phase PRs target each other, not dev;
# only the bottom PR's check can be made required for the dev merge.
pull_request:
push:
branches: [dev]

concurrency:
group: e2e-${{ github.ref }}
cancel-in-progress: true

env:
MONGO_IMAGE: mongo:7
MONGO_REPLICA_SET: rs0

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 60
# Clerk secrets are not exposed to pull requests from forks, so the suite
# cannot run there. Skip instead of failing red on every fork PR.
if: >-
github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository
env:
# `replicaSet=rs0` makes the driver do topology discovery and mark the
# deployment transaction-capable; without it Prisma still sees a
# standalone topology and refuses to open a transaction.
DATABASE_URL: mongodb://localhost:27017/e2e?replicaSet=rs0
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
INTERNAL_FETCH_SECRET: e2e-internal-secret
NEXT_PUBLIC_BASE_URL: http://localhost:3000
# The replica set is real here, so the ledger must take the transactional
# path — never silently degrade to the dev fallback in CI.
LEDGER_REQUIRE_TRANSACTIONS: 'true'
steps:
- uses: actions/checkout@v4

- name: Start MongoDB (single-node replica set)
run: |
set -euo pipefail
docker run -d --name mongo -p 27017:27017 "$MONGO_IMAGE" \
--replSet "$MONGO_REPLICA_SET" --bind_ip_all

echo "Waiting for mongod to accept connections..."
for i in $(seq 1 60); do
if docker exec mongo mongosh --quiet --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; then
break
fi
if [ "$i" -eq 60 ]; then
echo "::error::mongod did not become reachable"; docker logs mongo; exit 1
fi
sleep 2
done

# `host: localhost:27017` is what the driver on the runner is told to
# connect to during topology discovery, and the container publishes
# that port on the host — so the advertised member address is
# reachable from both inside and outside the container.
docker exec mongo mongosh --quiet --eval "
try {
rs.status()
} catch (e) {
rs.initiate({ _id: '$MONGO_REPLICA_SET', members: [{ _id: 0, host: 'localhost:27017' }] })
}"

echo "Waiting for PRIMARY..."
for i in $(seq 1 60); do
if docker exec mongo mongosh --quiet --eval 'db.hello().isWritablePrimary' 2>/dev/null | grep -q true; then
echo "Replica set $MONGO_REPLICA_SET is PRIMARY"
break
fi
if [ "$i" -eq 60 ]; then
echo "::error::replica set never reached PRIMARY"; docker logs mongo; exit 1
fi
sleep 2
done
Comment on lines +68 to +104

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot why would you even do this in Opus 5 with MAX context when having fixed the replica set core issue already?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ensureUserAndProfile split only removes one implicit transaction, but other Prisma operations in the E2E suite (e.g. any create with a nested include, connectOrCreate, or upsert) still open implicit transactions and will hit P2031 against a standalone mongod. Running CI against a replica set is the correct long-term fix — it matches production topology and ensures any future Prisma write won't silently fail. The ensureUserAndProfile change is a bonus that also helps local dev without a replica set.


- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm

- name: Prepare env
run: cp .env.public .env

- name: Install dependencies
run: npm ci --legacy-peer-deps

- name: Generate Prisma client
run: npx prisma generate

# Fails in 2 seconds with a clear message instead of burying the run in
# hundreds of P2031 stack traces. Mirrors `supportsTransactions()`.
- name: Verify the database supports transactions
run: |
node -e "
const { PrismaClient } = require('./generated/prisma/client')
const prisma = new PrismaClient()
prisma.\$runCommandRaw({ hello: 1 })
.then(async (hello) => {
await prisma.\$disconnect()
if (!hello.setName) {
console.error('DATABASE_URL does not point at a replica set (hello.setName is empty).')
console.error('Prisma needs transactions for nested writes (P2031); the suite cannot pass.')
process.exit(1)
}
console.log('Replica set OK:', hello.setName, '| primary:', hello.isWritablePrimary)
})
.catch((error) => { console.error(error); process.exit(1) })
"

- name: Push schema (indexes/collections)
run: npx prisma db push --skip-generate

- name: Cache Next build
uses: actions/cache@v4
with:
path: .next/cache
key: ${{ runner.os }}-next-${{ hashFiles('package-lock.json') }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-next-${{ hashFiles('package-lock.json') }}-

- name: Build
run: npm run build

- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}

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

- name: Run E2E tests
run: npx playwright test

- name: Dump MongoDB logs on failure
if: failure()
run: docker logs mongo || true

- name: Upload report on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
test-results/
retention-days: 7
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,8 @@ dist

# Sentry Config File
.env.sentry-build-plugin

# Playwright
test-results/
playwright-report/
blob-report/
31 changes: 23 additions & 8 deletions docs/plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ One file per phase. Each phase is one PR unless stated otherwise.
| 2 | Do — frontend rebuild | doPage/doView/taskGrid/forms rebuilt on plain SWR | ✅ done | `../do-rebuild-plan.md` §Phase 2 |
| 3 | Do dry-out + shared primitives | Kill remaining duplication, extract ownership/social/public-page/date kits | ✅ done (PR stacked on #483) | `phase-03-do-dry-out.md` |
| 4 | Media, storage & geolocation foundation | iDrive e2 uploads, compression, EXIF, Google Places, map, Write composer | ✅ done (PR stacked on #483; + CV attachments, bell notifications) | `phase-04-media-geo-foundation.md` |
| 5 | Public list profiles + job board | Public list pages, public tasks as job posts, applications | ⬜ planned | `phase-05-public-lists-job-board.md` |
| 5 | Public list profiles + job board + Projects + task deep links | Public list pages, public tasks as job posts, applications, Project public profiles (`/p/[url]`) above lists, `/app/do/list/{id}/{taskId}` deep link | ⬜ planned | `phase-05-public-lists-job-board.md` |
| 6 | DPIP ledger & wallets | Off-chain authoritative balances, atomic transfers, wallet at signup | ⬜ planned | `phase-06-dpip-ledger.md` |
| 7 | Organizations | Clerk Orgs mirror, org profiles/lists/wallets, polymorphic ownership | ⬜ planned | `phase-07-organizations.md` |
| 7 | Organizations | Clerk Orgs mirror, org profiles (`/@handle` → `/o/{handle}`), org lists/projects/wallets, polymorphic ownership | ⬜ planned | `phase-07-organizations.md` |
| 8 | Events core | Event model, event pages, `/app/be/events` listing, RSVP/social | ⬜ planned | `phase-08-events-core.md` |
| 9 | Ticketing & checkout | Tiers, promo windows, bundles, buy/reserve with DPIP, escrow | ⬜ planned | `phase-09-ticketing.md` |
| 10 | QR attendance & door control | Rotating signed QR, scanner API + UI, attendance records, pay-at-door | ⬜ planned | `phase-10-qr-attendance.md` |
Expand All @@ -25,20 +25,22 @@ One file per phase. Each phase is one PR unless stated otherwise.

```mermaid
graph TD
P3[3 Do dry-out<br/>shared primitives] --> P5[5 Public lists + job board]
P3[3 Do dry-out<br/>shared primitives] --> P5[5 Public lists + job board + Projects]
P4[4 Media + geo foundation] --> P5
P4 --> P8[8 Events core]
P3 --> P8
P5 --> P7[7 Organizations]
P6[6 DPIP ledger] --> P9[9 Ticketing]
P6 --> P11[11 Subscription allowances]
P7[7 Organizations] --> P8
P7 --> P8
P8 --> P9
P9 --> P10[10 QR attendance]
P5 --> P8
```

Phases 3, 4, 6 are independent of each other and can run in parallel. 7 can start any time after 3.
9 is the only hard blocker for 10. 11 only needs 6.
Phases 3, 4, 6 are independent of each other and can run in parallel. 7 can start any time after 3,
but builds on 5's `Project` for org-owned projects (hence the P5 → P7 edge). 9 is the only hard
blocker for 10. 11 only needs 6. Project donations via DPIP ledger transfers are a follow-up after 6.

## Confirmed decisions

Expand All @@ -48,14 +50,26 @@ Phases 3, 4, 6 are independent of each other and can run in parallel. 7 can star
`Wallet.onChainSyncedAt`). No Kaleido call is on the critical path of a ticket purchase.
2. **Organizations** — Clerk Organizations remain the identity/membership source of truth
(already used by chat via `ChatOrgMembership`). We add a Prisma `Organization` mirror and a
polymorphic owner (`ownerType: USER | ORG` + `orgId`) on `Profile`, `List`, `Event`, `Wallet`.
polymorphic owner (`ownerType: USER | ORG` + `orgId`) on `List`, `Wallet`, `Project`, `Event`.
**Shared `/@` handle namespace across users, orgs and projects**: `/@handle` resolves via the
middleware against `Profile.username` → `/{locale}/profile/`, `Organization.username` →
`/{locale}/o/`, `Project.username` → `/{locale}/p/`; handles are globally unique across all
three collections (DB `@unique` per collection + creation-time cross-check). Phase 6's
wallet-resolve endpoint honours the same namespace.
3. **Data preservation** — every destructive schema change ships with an idempotent migration in
`src/migrations/` and snapshots dropped fields into a `legacy Json?` column (established in
Phases 1–2). Next free migration number: **0020**.
Phases 1–2). Next free migration number: **0021** (0020 shipped with Phase 1; Phase 5 adds no
new migration).
4. **Delivery** — one PR per phase, each self-verifiable with `npx prisma generate && npm run build
&& npm run lint` plus the manual checklist at the end of each phase file.
5. **Money is never trusted from the client** — prices, discounts, deposits and balances are
recomputed server-side on every write (existing rule for job earnings; extended to tickets).
6. **Projects** — a `Project` (Phase 5, user-owned; ORG ownership in Phase 7) is the public
container between users/orgs and lists: `/p/[username]` with a `username @unique` handle in the
shared `/@` namespace, photo + cover + bio + links, `spotlight` flag, likes
(`entityType: 'project'`), stats computed in the serializer, and `supportUrl` as a plain link
until a post-Phase-6 follow-up routes donations through the DPIP ledger. Lists optionally
belong to one project (`List.projectId`) and act as its job boards.

## Cross-cutting conventions (apply to every phase)

Expand Down Expand Up @@ -102,6 +116,7 @@ Then the phase's manual checklist. Commits via the `the-committer` skill; migrat
| Overselling tickets | Conditional claim on **both** `TicketTier.sold` and `Event.soldCount` inside the checkout transaction; reservations expire via cron | 9 |
| QR screenshot sharing | Rotating HMAC token bound to a per-ticket secret, single-use check-in, fail-closed offline | 10 |
| Nullable `@unique` slugs are not sparse on Mongo | Slugs are required and generated at creation, never null | 5, 7, 8 |
| Handle collisions in the shared `/@` namespace (users, orgs, projects) | `username @unique` per collection + creation-time cross-check against the other two collections; the `/@` middleware resolves all three and 404s on nothing | 5, 7 |
| Clerk billing event names differ per Clerk version | Webhook handler is event-name tolerant + daily cron reconciles **against Clerk's own subscription list**, not just local mirrors | 11 |
| Disguised uploads served from our origin | Magic-byte inspection server-side + isolated media origin with forced download/`Content-Security-Policy` headers | 4 |
| Vercel 4.5 MB body limit | Presigned direct-to-S3 uploads | 4 |
Expand Down
Loading