Compare commits

..

102 Commits

Author SHA1 Message Date
Flea Flicker 784418937d fix(test): de-flake authProvider.test.ts OOBE test-connection (GRO-2745) [uat → main]
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 27s
2026-08-23 08:23:47 +00:00
Flea Flicker 40899d135a fix(test): de-flake authProvider.test.ts OOBE test-connection (GRO-2745)
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 23s
CI / Lint & Typecheck (pull_request) Successful in 18s
CI / Test (pull_request) Successful in 25s
CI / Build & Push Docker Images (pull_request) Successful in 25s
Merge dev→uat after QA approval (GRO-2746)
2026-08-22 09:25:37 +00:00
Flea Flicker 62c80ca8d0 fix(test): de-flake authProvider.test.ts OOBE test-connection (GRO-2745)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 26s
CI / Build & Push Docker Images (push) Successful in 23s
CI / Build & Push Docker Images (pull_request) Successful in 26s
2026-08-22 08:49:43 +00:00
Flea Flicker bb55c658a5 fix(test): de-flake authProvider.test.ts OOBE test-connection timeout (GRO-2745)
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (pull_request) Successful in 46s
Mock global.fetch in the test-connection test so it no longer hits the
real network. The route has a 10s fetch timeout, but the test used the
default 5s vitest timeout, causing intermittent failures when DNS/network
was slow. Add afterEach(vi.restoreAllMocks) to clean up between tests,
and reconcile the misleading "returns 400" test title to match the 200
assertion the route actually produces.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-22 08:39:25 +00:00
Scrubs McBarkley 365d4a5023 fix(db): GRO-2722 schema-safe reset (TRUNCATE, no DROP) [uat→main]
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 20s
CI / Build & Push Docker Images (push) Successful in 29s
Merges GRO-2722 root-cause fix for GRO-2678 prod outage. DROP-based reset replaced with TRUNCATE RESTART IDENTITY CASCADE. All SDLC gates passed: UAT  (GRO-2725), Security  (GRO-2726), QA  (GRO-2724), CTO review .
2026-08-22 08:36:55 +00:00
Flea Flicker 8b812dfd22 dev → uat: GRO-2722 schema-safe reset (TRUNCATE, no DROP)
CI / Test (push) Failing after 3s
CI / Lint & Typecheck (push) Successful in 20s
CI / Build & Push Docker Images (push) Has been skipped
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Test (pull_request) Successful in 51s
CI / Build & Push Docker Images (pull_request) Successful in 34s
Merged after QA (Lint Roller) approval on Gitea. All 6 CI contexts green on 38380d0.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 13:26:51 +00:00
Flea Flicker 38380d0398 fix(db): GRO-2722 schema-safe reset — TRUNCATE, no DROP
CI / Lint & Typecheck (pull_request) Successful in 23s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (pull_request) Successful in 27s
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 24s
2026-08-16 13:16:59 +00:00
Flea Flicker 7c6346c799 docs(uat): add TC-API-3.30 schema-safe reset verification (GRO-2722)
CI / Lint & Typecheck (pull_request) Successful in 21s
CI / Test (pull_request) Successful in 23s
CI / Build & Push Docker Images (pull_request) Successful in 1m12s
UAT_PLAYBOOK.md §3 — new test case TC-API-3.30:
trigger reset-demo-data CronJob manually, confirm job succeeds,
public tables/enums and drizzle schema survive, demo data reseeded,
/api/readyz stays 200.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 13:14:40 +00:00
Flea Flicker 7af16ee4e6 fix(db): replace DROP-based reset with TRUNCATE RESTART IDENTITY CASCADE
GRO-2722: packages/db/src/reset.ts and apps/api/src/db/reset.ts no longer
emit any DROP TABLE / DROP TYPE / DROP SCHEMA / DROP DATABASE DDL. The four
destructive DO-blocks are replaced with a single:

  TRUNCATE <all public tables> RESTART IDENTITY CASCADE

Table names are enumerated dynamically via pg_tables WHERE schemaname='public'
so new tables are picked up automatically. The drizzle schema and
__drizzle_migrations table are untouched (different schema), keeping
drizzle-kit migrate a no-op on an already-migrated DB.

Preserves: production guard (NODE_ENV=production && ALLOW_RESET!='true'),
advisory lock, migrations (drizzle-kit migrate), and seed (runSeedBody).

Fixes the GRO-2678 prod outage root cause.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 13:13:29 +00:00
Flea Flicker e0a8cd30ab feat(api): promote uat → main — /api/readyz DB health check (GRO-2687)
CI / Lint & Typecheck (push) Successful in 21s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 35s
All SDLC gates satisfied: QA approved (Lint Roller, review #5055), UAT passed (Shedward, GRO-2692), Security passed (Barkley, GRO-2698), CTO approved (review #5067 on head 9e948d6). cc @cpfarhood
2026-08-09 11:04:15 +00:00
Flea Flicker 9e948d6a8d promote(api): dev → uat — /api/readyz auth bypass fix + /health/ready revert (GRO-2687)
CI / Lint & Typecheck (pull_request) Successful in 24s
CI / Test (pull_request) Successful in 31s
CI / Build & Push Docker Images (pull_request) Successful in 53s
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Successful in 22s
CI / Build & Push Docker Images (push) Successful in 36s
QA approved (Lint Roller, review #5061). Phase 2 self-merge per SDLC.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-09 10:26:48 +00:00
Flea Flicker 413849f066 fix(auth): add /api/readyz to auth bypass list (GRO-2692)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (pull_request) Successful in 26s
CI / Build & Push Docker Images (push) Successful in 49s
CI passed (run #4322). Self-merging Phase 1 per SDLC gate.
2026-08-09 10:15:45 +00:00
Flea Flicker 9227cf4883 fix(api): add /api/readyz to authMiddleware bypass (GRO-2687 UAT fix)
CI / Test (push) Successful in 19s
CI / Lint & Typecheck (push) Successful in 24s
CI / Lint & Typecheck (pull_request) Successful in 20s
CI / Test (pull_request) Successful in 25s
CI / Build & Push Docker Images (push) Successful in 58s
CI / Build & Push Docker Images (pull_request) Successful in 29s
2026-08-09 09:54:35 +00:00
Flea Flicker 7dfa1ad830 fix(auth): add /api/readyz to auth middleware bypass list (GRO-2692)
CI / Lint & Typecheck (pull_request) Successful in 24s
CI / Test (pull_request) Successful in 35s
CI / Build & Push Docker Images (pull_request) Successful in 1m19s
/api/readyz is a public monitoring endpoint like /api/health.
app.basePath("/api") + api.use("*", authMiddleware) applies to all
/api/* paths regardless of route registration order (GRO-2692 UAT
failure: Hono basePath middleware bypass).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-09 09:53:19 +00:00
Flea Flicker 8f5a069e77 fix(api): add /api/readyz to authMiddleware bypass list (GRO-2687)
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 21s
CI / Build & Push Docker Images (pull_request) Successful in 55s
/api/readyz was returning 401 Unauthorized in UAT (GRO-2692 Shedward
regression). The authMiddleware for /api/* did not whitelist /api/readyz,
causing every unauthenticated probe request to be rejected.

Add /api/readyz to the bypass condition alongside /api/auth/* and
/api/health. Add readyz-auth-bypass.test.ts confirming the route passes
through the middleware without auth and that non-whitelisted paths still
block (503 when auth not configured).

Closes GRO-2692 regression (UAT fail).
2026-08-09 09:52:37 +00:00
Flea Flicker 07717afd01 revert(api): remove /health/ready — superseded by /api/readyz (GRO-2689)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 43s
2026-08-09 09:50:43 +00:00
Flea Flicker d8f6981be1 revert(api): remove /health/ready — superseded by /api/readyz (GRO-2689)
CI / Lint & Typecheck (pull_request) Successful in 18s
CI / Test (pull_request) Successful in 25s
CI / Build & Push Docker Images (pull_request) Successful in 53s
CTO architectural ruling (PR #235 review): K8s readiness/liveness probes
must remain DB-less to prevent transient DB blips from cycling pods. The
/api/readyz endpoint (GRO-2687) is the canonical DB-health signal for the
monitoring layer and satisfies the GRO-2678 detection-gap requirement.

Removes:
- GET /health/ready route from src/index.ts
- src/__tests__/health-ready.test.ts

Refs GRO-2689, GRO-2687, GRO-2678
2026-08-09 09:48:10 +00:00
Flea Flicker 0c8e943b72 promote: dev → uat (GRO-2687 /api/readyz DB health check)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 26s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (pull_request) Successful in 22s
Merging dev→uat after QA approval (Lint Roller). All 6 CI checks green.
2026-08-09 09:29:37 +00:00
Flea Flicker d7bb314087 feat(api): add DB-touching /health/ready readiness probe (GRO-2678)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 34s
CI / Build & Push Docker Images (pull_request) Successful in 23s
CI / Lint & Typecheck (pull_request) Successful in 20s
CI / Test (pull_request) Successful in 21s
2026-08-09 09:26:17 +00:00
Flea Flicker 7679fada0a feat(api): add DB-touching /health/ready readiness probe (GRO-2678)
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 21s
CI / Build & Push Docker Images (pull_request) Successful in 3m25s
Register GET /health/ready before the /api/* auth middleware so it is
public and reachable by K8s readinessProbe on port 3000 without auth.
On success → 200 {"status":"ready"}; on any DB/schema failure → 503
{"status":"degraded"}. Logs the pg error code; never leaks SQL in body.
A dropped schema (42P01) surfaces as non-200, closing the /health mask
that allowed the GRO-2678 incident to go undetected for ~43h.

Add health-ready.test.ts covering the 200 success path, 503 on schema
drop (42P01), and 503 on connection error; all assert no SQL leakage.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-09 09:21:56 +00:00
Flea Flicker a164c24e8e feat(api): add DB-touching /api/readyz so schema loss cannot hide behind /health (GRO-2678)
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 21s
CI / Build & Push Docker Images (pull_request) Successful in 31s
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Successful in 22s
CI / Build & Push Docker Images (push) Successful in 34s
2026-08-09 09:11:35 +00:00
Flea Flicker 6148ae6439 ci: retrigger after seed-image registry push flake
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Test (pull_request) Successful in 19s
CI / Build & Push Docker Images (pull_request) Successful in 46s
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-09 09:10:10 +00:00
Flea Flicker f54a13fc8a feat(api): add DB-touching /api/readyz so schema loss cannot hide behind /health (GRO-2678)
CI / Test (pull_request) Failing after 30s
CI / Lint & Typecheck (pull_request) Successful in 2m31s
CI / Build & Push Docker Images (pull_request) Has been skipped
- Register GET /api/readyz after /api/health in src/index.ts (before authMiddleware)
- Runs `getDb().select({id: staff.id}).from(staff).limit(1)` inside try/catch
- Success → 200 {"status":"ready"}; failure → 503 {"status":"degraded","check":"db"}
- Raw driver error is console.error'd but never leaked to the response body
- /health and /api/health are unchanged (K8s probes remain DB-less per CTO decision)
- New unit tests: mock getDb to resolve → assert 200/ready; throw → assert 503/degraded
- Updated UAT_PLAYBOOK.md §4.0 with TC-API-0.2 and TC-API-0.3

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-09 09:02:39 +00:00
Flea Flicker 3aaa440561 fix(db): GRO-2672 swap drizzle-orm migrate() for drizzle-kit migrate in reset.ts (uat→main)
CI / Lint & Typecheck (push) Successful in 18s
CI / Test (push) Successful in 20s
CI / Build & Push Docker Images (push) Successful in 29s
CTO-approved (gb_dogfather, review #5050). All pipeline phases passed: CI , QA  (PR #229), UAT  (GRO-2673), Security  (GRO-2674).
2026-08-06 09:53:11 +00:00
Flea Flicker 89013f29ce fix(db): swap drizzle-orm migrate() for drizzle-kit migrate in reset.ts (GRO-2672)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 20s
CI / Build & Push Docker Images (push) Successful in 33s
CI / Lint & Typecheck (pull_request) Successful in 20s
CI / Test (pull_request) Successful in 21s
CI / Build & Push Docker Images (pull_request) Successful in 32s
Root cause: drizzle-orm's HWM-based migrate() marked migrations as applied in the ledger but rolled back schema changes when it detected a dirty state. Replacing with execSync("pnpm exec drizzle-kit migrate") matches the K8s migrate-schema Job, ensuring reset leaves a fully-migrated schema before seeding. Fixes reset-demo-data CronJob failures and resolves seed-test-data relation-does-not-exist reconcile loop.

QA approved: gb_lint (PR #229, head f7f90a7)
2026-08-06 09:33:45 +00:00
Flea Flicker f7f90a71fc fix(GRO-2672): use drizzle-kit migrate in reset.ts to bypass HWM bug
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 37s
CI / Test (pull_request) Successful in 20s
CI / Lint & Typecheck (pull_request) Successful in 37s
CI / Build & Push Docker Images (pull_request) Successful in 42s
drizzle-orm's migrate() has a high-water-mark (HWM) bug: on a fresh DB,
migration 0000 sets the watermark to 2026-03-17. Migrations 0001, 0003,
0010, 0011 have stale 2025-era `when` timestamps and are silently
skipped. Migration 0003 (recurring_series) is the blocker — its skip
leaves `recurring_series`, `appointments.series_id`, and
`appointments.series_index` missing. A downstream migration inside
migrate()'s single Postgres transaction then fails, rolling back
everything including 0000's `staff` and `services` tables.

Replace the drizzle-orm migrate() call with `pnpm exec drizzle-kit
migrate` (hash-based). drizzle-kit applies every unhashed migration
regardless of `when` ordering, matching the K8s migrate Job exactly.
2026-08-06 09:14:55 +00:00
Flea Flicker 552a4d9f28 Merge pull request 'feat: GRO-2652 boot ECONNRESET resilience + CORS enforcement + OOBE endpoint (uat→main)' (#226) from uat into main
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 33s
CTO Approved (review 5044). UAT (Shedward) PASS. Security (Barkley) PASS.
2026-08-05 11:57:24 +00:00
gb_flea 03bdd7ec4a merge(uat): integrate main divergence; resolve UAT_PLAYBOOK conflict
CI / Lint & Typecheck (push) Successful in 19s
CI / Lint & Typecheck (pull_request) Successful in 25s
CI / Test (push) Successful in 25s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (push) Successful in 28s
CI / Build & Push Docker Images (pull_request) Successful in 35s
Merge origin/main into uat to resolve divergence (main was 27 commits
ahead). Only real conflict: UAT_PLAYBOOK.md — kept uat's §4.19 Boot
Resilience and §4.20 OOBE clients-from-auth test cases; excluded
.mcp.json and trigger-uat-1779751324.txt from merge result (CTO review
5042 — these must not land on main).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-05 11:55:52 +00:00
Flea Flicker 81a833e392 chore: remove CI-trigger litter and agent tooling artifact
CI / Lint & Typecheck (pull_request) Successful in 21s
CI / Test (pull_request) Successful in 22s
CI / Build & Push Docker Images (pull_request) Successful in 25s
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Successful in 21s
CI / Build & Push Docker Images (push) Successful in 24s
Remove trigger-uat-1779751324.txt (empty CI-trigger file) and .mcp.json
(agent tooling config with bearer token) — neither belongs in main.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-05 10:57:27 +00:00
Flea Flicker 1df834c5bc fix(GRO-2652): promote boot ECONNRESET resilience to UAT (dev→uat)
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 20s
CI / Build & Push Docker Images (push) Successful in 28s
CI / Lint & Typecheck (pull_request) Successful in 23s
CI / Test (pull_request) Successful in 23s
CI / Build & Push Docker Images (pull_request) Successful in 22s
Merges boot ECONNRESET resilience fix + CORS enforcement + OOBE endpoint to uat. QA approved (Lint Roller) at #223 (comment)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-05 10:22:02 +00:00
Flea Flicker f1b0a53520 test(GRO-2652): stub fetch in auth tests to fix OIDC discovery timeout flake
CI / Lint & Typecheck (push) Successful in 19s
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 20s
CI / Build & Push Docker Images (pull_request) Successful in 52s
CI / Build & Push Docker Images (push) Successful in 1m45s
AbortSignal.timeout(5000) in initAuth's discovery fetch races with
vitest's 5000ms default test timeout, causing intermittent failures on CI push
runs. Stub fetch to return ok:false instantly so tests complete in <1s.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-05 10:04:31 +00:00
Flea Flicker f32e9a6889 fix(GRO-2652): reset authInitPromise on failure so retry loop actually retries
CI / Lint & Typecheck (push) Successful in 20s
CI / Test (push) Failing after 21s
CI / Build & Push Docker Images (push) Has been skipped
CI / Lint & Typecheck (pull_request) Successful in 18s
CI / Test (pull_request) Failing after 25s
CI / Build & Push Docker Images (pull_request) Has been skipped
Merging Phase 1 fix — CI all green (lint, test, build all success). Resolves QA's retry-memoization concern from PR #223 review.
2026-08-05 10:00:12 +00:00
Flea Flicker a1b27b5501 fix(GRO-2652): reset authInitPromise on failure so retry loop actually retries
CI / Lint & Typecheck (pull_request) Successful in 19s
CI / Test (pull_request) Successful in 21s
CI / Build & Push Docker Images (pull_request) Successful in 53s
Previously the initAuth() function set authInitPromise to the async IIFE's
Promise but never cleared it on rejection. The index.ts retry loop called
initAuth() up to 10 times, but on attempt 2+ the check
`if (authInitPromise) { await authInitPromise; return; }` would immediately
re-throw the original rejection without performing a real retry.

Fix: wrap the final `await authInitPromise` in try/catch and reset
authInitPromise = null on error. This allows the index.ts retry loop to
create a fresh attempt on each call after a failure.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-05 09:52:31 +00:00
Flea Flicker ae0ce3824f fix(GRO-2652): reset authInitPromise on failure so retry loop actually retries
CI / Lint & Typecheck (pull_request) Failing after 10s
CI / Test (pull_request) Failing after 24s
CI / Build & Push Docker Images (pull_request) Has been skipped
Previously the initAuth() function set authInitPromise to the async IIFE's
Promise but never cleared it on rejection. The index.ts retry loop called
initAuth() up to 10 times, but on attempt 2+ the check
`if (authInitPromise) { await authInitPromise; return; }` would immediately
re-throw the original rejection without performing a real retry.

Fix: wrap the final `await authInitPromise` in try/catch and reset
authInitPromise = null on error. This allows the index.ts retry loop to
create a fresh attempt on each call after a failure.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-05 09:44:12 +00:00
Flea Flicker f98c5ccaf7 fix(GRO-2652): restore ci.yml from double-base64 corruption + UAT_PLAYBOOK §4.20 clients-from-auth
CI / Lint & Typecheck (push) Successful in 21s
CI / Test (pull_request) Successful in 21s
CI / Lint & Typecheck (pull_request) Successful in 34s
CI / Test (push) Failing after 40s
CI / Build & Push Docker Images (push) Has been skipped
CI / Build & Push Docker Images (pull_request) Successful in 27s
2026-08-05 09:29:16 +00:00
Flea Flicker 98b1171f98 uat→main (PROD): GRO-2586 CORS origin allowlist enforcement (frozen @2d4edb6) (#221)
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 32s
CI / Build & Push Docker Images (push) Successful in 39s
uat→main (PROD): GRO-2586 CORS origin allowlist enforcement (frozen @2d4edb6)
2026-06-26 14:23:34 +00:00
Flea Flicker 2d4edb6452 promote(GRO-2586): dev → uat — CORS origin allowlist enforcement (#220)
CI / Lint & Typecheck (push) Successful in 39s
CI / Test (push) Successful in 40s
CI / Build & Push Docker Images (push) Successful in 50s
promote(GRO-2586): dev → uat — CORS origin allowlist enforcement
2026-06-26 13:46:44 +00:00
Flea Flicker 1b6cd5825a uat→main (PROD): GRO-2425 comma-split CORS_ORIGIN (frozen @63d7aaa) (#218)
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 28s
CI / Build & Push Docker Images (push) Successful in 48s
feat: support comma-split CORS_ORIGIN for multiple trusted auth origins (GRO-2425)

Co-authored-by: Flea Flicker <flea@groombook.dev>
Co-committed-by: Flea Flicker <flea@groombook.dev>
2026-06-18 02:14:23 +00:00
Flea Flicker 63d7aaa8c2 chore: promote dev → uat (GRO-2425 comma-split CORS_ORIGIN) (#217)
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Images (push) Successful in 47s
chore: promote dev → uat (GRO-2425 comma-split CORS_ORIGIN)

Co-authored-by: Flea Flicker <flea@groombook.dev>
Co-committed-by: Flea Flicker <flea@groombook.dev>
2026-06-18 01:30:43 +00:00
Flea Flicker ed51a59c80 docs: add AGENTS.md and CONTRIBUTING.md (GRO-2381) (#215)
CI / Lint & Typecheck (push) Successful in 30s
CI / Test (push) Successful in 31s
CI / Build & Push Docker Images (push) Successful in 1m20s
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-12 17:00:40 +00:00
Flea Flicker bedeb05a67 Promote uat → main (PROD): GRO-2359 OOBE portal-creation routing (api) (#214)
CI / Lint & Typecheck (push) Successful in 30s
CI / Test (push) Failing after 30s
CI / Build & Push Docker Images (push) Has been skipped
GRO-2359: add POST /api/portal/clients-from-auth for OOBE (#214)
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-12 16:47:30 +00:00
Flea Flicker a629331a04 Merge pull request 'Promote dev → uat: GRO-2359 clients-from-auth endpoint' (#213) from promote/GRO-2359-dev-to-uat into uat
CI / Test (push) Successful in 27s
CI / Lint & Typecheck (push) Successful in 34s
CI / Build & Push Docker Images (push) Successful in 38s
Promote dev → uat: GRO-2359 clients-from-auth endpoint (#213)
2026-06-11 16:44:52 +00:00
Flea Flicker 5363e1d5dc feat(GRO-2359): add POST /api/portal/clients-from-auth for OOBE (web)
CI / Test (pull_request) Successful in 28s
CI / Lint & Typecheck (pull_request) Successful in 34s
CI / Build & Push Docker Images (pull_request) Successful in 42s
The OOBE flow on the web portal calls this endpoint to create a fresh
`clients` row bound to the Better Auth user's email when the SSO
bridge returns 404. Returns 201 on success, 409 if a client with that
email already exists (portal-selection case), 401/503 on auth issues,
400 on invalid body.

The OOBE success path navigates the user back to `/` and lets the
existing `session-from-auth` re-bridge; the new client is now
resolvable by email, so the bridge mints a real portal session.

Tests cover: 401 (no session), 400 (zod), 201 + persisted values
(name trimmed, optional fields normalized to null), 409 (existing
client or unique-constraint race), 503 (auth not configured).

Paired with the web PR on `feature/2357-p2-sso-to-oobe-routing`.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
(cherry picked from commit cdeebec021)
2026-06-11 16:35:26 +00:00
Flea Flicker 58305d7a89 uat→main (PROD): GRO-2342 portal waitlist service {id, name} (frozen @47e2021 + cherry-pick c737bfe) (#211)
CI / Test (push) Successful in 29s
CI / Lint & Typecheck (push) Successful in 32s
CI / Build & Push Docker Images (push) Failing after 57s
Merge pull request 'GRO-2342: portal/appointments — symmetric service {id, name} on both card paths' (#211) from release/main-GRO-2342-api into main

GRO-2342: GET /portal/appointments populates service: {id, name} on the synthetic waitlist card (was {id} only) and on the appointment card (consistent shape). TC-API-8.20 in UAT_PLAYBOOK.md.

Approved CTO. Squashed from release/main-GRO-2342-api @ c737bfe.

Refs: GRO-2342, GRO-2344, GRO-2345, GRO-2346, PR #211.
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-11 08:33:52 +00:00
Flea Flicker 4cc51b32d3 Merge pull request 'Promote dev → uat: GRO-2342 portal waitlist service {id, name}' (#209) from promote/dev-to-uat-gro-2342 into uat
CI / Test (push) Successful in 27s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Images (push) Successful in 26s
CI / Test (pull_request) Successful in 24s
CI / Lint & Typecheck (pull_request) Failing after 10m19s
CI / Build & Push Docker Images (pull_request) Has been skipped
2026-06-10 09:24:53 +00:00
Flea Flicker e932050b45 Promote dev → uat: GRO-2342 portal waitlist service {id, name}
CI / Test (pull_request) Successful in 25s
CI / Lint & Typecheck (pull_request) Successful in 25s
CI / Build & Push Docker Images (pull_request) Successful in 33s
Resolves conflicts in UAT_PLAYBOOK.md, src/routes/portal.ts, and
src/__tests__/portal.test.ts (dev side wins — GRO-2342 changes are
the only diff in scope). Carries forward GRO-2139 reset.ts advisory
lock + GRO-2294 infra mcp trigger that were merged to dev but not
yet promoted to uat.

- src/routes/portal.ts: GET /portal/appointments now populates
  service: {id, name} on both the synthetic waitlist card and the
  appointment card (was {id} only). Same shape, no portal change
  required.
- src/__tests__/portal.test.ts: services mock + TC-API-8.20 GRO-2342
  assertions on the synthetic waitlist card service name.
- UAT_PLAYBOOK.md: TC-API-8.20 (GRO-2342) appended; TC-API-8.19
  (GRO-2319) retained verbatim.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-10 09:17:15 +00:00
Flea Flicker 47e2021cf4 Promote uat → main (PROD): GRO-2319 portal waitlist surfacing + seed (#207)
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 26s
CI / Build & Push Docker Images (push) Successful in 41s
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-10 08:58:26 +00:00
Flea Flicker 31404befee uat→main (PROD): GRO-2311 seed portal StatusBadge appointments (frozen @df5e768) (#206)
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 29s
CI / Build & Push Docker Images (push) Successful in 31s
uat→main (PROD): GRO-2311 seed portal StatusBadge appointments (squash)

Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-09 11:18:03 +00:00
Flea Flicker 18640908ed feat(GRO-2319): dev→uat — portal waitlist surfacing + seed (api) (#205)
CI / Test (push) Successful in 30s
CI / Lint & Typecheck (push) Successful in 36s
CI / Build & Push Docker Images (push) Successful in 1m15s
2026-06-09 11:04:16 +00:00
Scrubs McBarkley a9db0ca9ac uat→main (PROD): GRO-2172 pet extended-field schema fix (frozen @c4385617) (#203)
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 29s
CI / Build & Push Docker Images (push) Successful in 1m7s
Phase-4 promotion. CTO-approved, CI 3/3 green. Single-file diff (src/routes/pets.ts +26/-2), frozen @c4385617. GRO-2311 excluded.
2026-06-09 10:52:37 +00:00
Flea Flicker 4bbb0c9fc5 uat→main (PROD): GRO-2172 pet extended-field schema fix (frozen @c4385617)
CI / Test (pull_request) Successful in 30s
CI / Lint & Typecheck (pull_request) Successful in 34s
CI / Build & Push Docker Images (pull_request) Successful in 1m21s
Promote GRO-2172 from uat to main. Pins src/routes/pets.ts to its exact
content at uat merge commit c4385617 (PR #200), adding the extended pet
profile fields to createPetSchema/updatePetSchema and wiring medicalAlerts
into POST/PATCH /pets:

- temperamentScore: int 1–5
- temperamentFlags: string[] (≤20, each ≤100 chars)
- medicalAlerts: {type,description,severity}[] (≤50)
- preferredCuts: string[] (≤20, each ≤200 chars)
- coatType already present on main; schema now references all 5 fields

Based on main HEAD (03f79a37) so the PR diff is limited to src/routes/pets.ts.
GRO-2311 (uat HEAD 807ccb45) is intentionally excluded.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-09 10:19:25 +00:00
Flea Flicker 807ccb455f dev → uat: GRO-2311 seed portal StatusBadge appointments (#201) (#202)
CI / Lint & Typecheck (push) Successful in 28s
CI / Test (push) Successful in 24s
CI / Build & Push Docker Images (push) Successful in 1m23s
2026-06-09 09:56:34 +00:00
Flea Flicker c4385617c6 dev → uat: GRO-2172 extended pet fields (#200)
CI / Test (push) Successful in 23s
CI / Lint & Typecheck (push) Successful in 24s
CI / Build & Push Docker Images (push) Successful in 39s
2026-06-09 09:22:12 +00:00
Flea Flicker 03f79a3701 uat → main: GRO-2299 redact googleMapsApiKey from PATCH /api/admin/settings (#198)
CI / Test (push) Successful in 27s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Images (push) Successful in 30s
GRO-2299: redact googleMapsApiKey from PATCH /api/admin/settings response
Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-09 07:49:49 +00:00
Flea Flicker 2b92c2ab6c uat→main (PROD): GRO-2294 Route Optimization security hardening (frozen @2566fb8) (#197)
CI / Lint & Typecheck (push) Successful in 30s
CI / Test (push) Failing after 11m41s
CI / Build & Push Docker Images (push) Has been skipped
feat(security): GRO-2294 Route Optimization security hardening [squash]

Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-09 07:38:02 +00:00
Flea Flicker 8cd5a2ef4d dev → uat: GRO-2299 redact googleMapsApiKey from PATCH /api/admin/settings (#196)
CI / Test (push) Failing after 10m55s
CI / Lint & Typecheck (push) Failing after 10m55s
CI / Build & Push Docker Images (push) Has been skipped
2026-06-09 06:58:39 +00:00
Flea Flicker 2566fb8f20 Promote GRO-2294 to UAT: Route Optimization security hardening (#194)
CI / Lint & Typecheck (push) Successful in 28s
CI / Test (push) Successful in 29s
CI / Build & Push Docker Images (push) Successful in 39s
CI / Test (pull_request) Successful in 25s
CI / Lint & Typecheck (pull_request) Successful in 37s
CI / Build & Push Docker Images (pull_request) Successful in 1m8s
2026-06-09 06:27:17 +00:00
Flea Flicker e9ad92de01 uat→main (PROD): GRO-2157 nav export + GRO-2225/2235 (frozen @4868f18) (#192)
CI / Test (push) Successful in 28s
CI / Lint & Typecheck (push) Successful in 31s
CI / Build & Push Docker Images (push) Successful in 28s
feat: nav export + conflict guard + UAT seed (GRO-2157, GRO-2225, GRO-2235)

Squash-merges PR #192: uat→main PROD promotion.
Freezes at validated SHA 4868f18 (UAT regression GRO-2261 11/11 PASS).
Bundles: GRO-2157 (nav export), GRO-2225 (UAT seed), GRO-2235 (conflict guard).
CTO-reviewed and approved (review #4542).

Co-authored-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
Co-committed-by: Flea Flicker <22+gb_flea@noreply.git.farh.net>
2026-06-09 01:23:06 +00:00
Flea Flicker bfe1a29c08 Merge pull request 'uat→main (PROD): GRO-2234 portal session fix + validated batch' (#191) from flea/uat-to-main-gro-2234-api into main
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 29s
CI / Build & Push Docker Images (push) Successful in 28s
2026-06-09 00:37:35 +00:00
Lint Roller 4868f18dfd Merge pull request 'Promote dev→uat: GRO-2225 + GRO-2235 + GRO-2157 (atomic)' (#188) from promote/dev-to-uat-gro-2225 into uat
CI / Test (push) Successful in 29s
CI / Lint & Typecheck (push) Successful in 36s
CI / Build & Push Docker Images (push) Successful in 41s
CI / Test (pull_request) Successful in 28s
CI / Lint & Typecheck (pull_request) Successful in 31s
CI / Build & Push Docker Images (pull_request) Successful in 1m17s
Promote dev→uat: GRO-2225 + GRO-2235 + GRO-2157 (atomic)

QA-approved on 37e42b3. CI green (Test, Lint & Typecheck, Build & Push).
2026-06-09 00:26:18 +00:00
Flea Flicker 37e42b3104 ci: re-trigger checks (transient pnpm/action-setup runner flake)
CI / Test (pull_request) Successful in 26s
CI / Lint & Typecheck (pull_request) Successful in 30s
CI / Build & Push Docker Images (pull_request) Successful in 27s
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-09 00:21:03 +00:00
Flea Flicker d617c69571 Merge remote-tracking branch 'origin/dev' into promote/dev-to-uat-gro-2225
CI / Test (pull_request) Failing after 5s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Images (pull_request) Has been skipped
2026-06-09 00:18:24 +00:00
Scrubs McBarkley 1ad43ce701 Merge pull request 'promote(uat→main FROZEN @6120b96): + GRO-2156 route buffer/reorder (supersedes #185)' (#186) from release/main-6120b96 into main
CI / Test (push) Successful in 25s
CI / Lint & Typecheck (push) Successful in 29s
CI / Build & Push Docker Images (push) Successful in 1m19s
promote(uat→main FROZEN @6120b96): GRO-2214+GRO-2211+GRO-2203+GRO-2155+GRO-2163+GRO-2156

CTO-reviewed, CEO-merged per SDLC Phase 4 governance.
Carries: GRO-2214 waitlist validation, GRO-2211, GRO-2203 pet PATCH, GRO-2155+GRO-2163 route optimization, GRO-2156 route buffer/reorder.
All gates passed: QA, Security, UAT 6/6 PASS.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-08 23:29:58 +00:00
Flea Flicker 76d9850464 Promote dev→uat: GRO-2225 UAT seed route cohort + receptionist credential
CI / Test (pull_request) Successful in 30s
CI / Lint & Typecheck (pull_request) Successful in 31s
CI / Build & Push Docker Images (pull_request) Failing after 15s
2026-06-08 23:16:51 +00:00
Flea Flicker 96dbb8c41d Merge pull request 'Promote dev → uat: GRO-2155/2156/2203/2211/2163 + GRO-2234 (cumulative batch)' (#182) from flea/dev-to-uat-gro-2156 into uat
CI / Test (push) Successful in 25s
CI / Lint & Typecheck (push) Successful in 30s
CI / Build & Push Docker Images (push) Successful in 1m24s
CI / Test (pull_request) Successful in 27s
CI / Lint & Typecheck (pull_request) Successful in 30s
CI / Build & Push Docker Images (pull_request) Successful in 1m11s
2026-06-08 19:42:25 +00:00
Flea Flicker 636fa713e1 Merge dev into uat: add GRO-2234 portal session sliding TTL + re-mint to dev→uat batch
CI / Test (pull_request) Successful in 28s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Images (pull_request) Successful in 27s
2026-06-08 19:17:15 +00:00
Flea Flicker 6120b96c7c Merge dev into uat: promote GRO-2156 route travel buffer + reorder (Phase 2.2)
CI / Test (pull_request) Successful in 26s
CI / Lint & Typecheck (pull_request) Successful in 28s
CI / Build & Push Docker Images (pull_request) Successful in 1m2s
Resolves UAT_PLAYBOOK.md conflict by unioning uat-only TC-UAT-2/3 (GRO-2100)
with dev's §4.16 update + new §4.17. Code files taken from dev (superset).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-08 18:11:05 +00:00
Flea Flicker eb92f99c4a dev → uat: GRO-2203 portal pet PATCH malformed-petId 500→404 (#178)
CI / Test (push) Successful in 27s
CI / Lint & Typecheck (push) Successful in 32s
CI / Build & Push Docker Images (push) Successful in 1m1s
CI / Test (pull_request) Successful in 27s
CI / Lint & Typecheck (pull_request) Successful in 31s
CI / Build & Push Docker Images (pull_request) Successful in 1m4s
2026-06-08 17:53:01 +00:00
Flea Flicker 587fd4ec95 dev → uat: GRO-2155 route optimization endpoints (carries GRO-2163) (#176)
CI / Test (push) Successful in 26s
CI / Lint & Typecheck (push) Successful in 27s
CI / Build & Push Docker Images (push) Successful in 25s
2026-06-08 16:45:44 +00:00
Flea Flicker 6e2e46daf8 Merge uat → main: portal pet PATCH + photoKey S3 key-hijack fix (GRO-2187) (#174)
CI / Test (push) Successful in 25s
CI / Lint & Typecheck (push) Successful in 28s
CI / Build & Push Docker Images (push) Successful in 40s
2026-06-08 13:25:46 +00:00
Flea Flicker 8cf72d926d dev → uat: portal photoKey S3 key-hijack fix (GRO-2187/GRO-2198) (#173)
CI / Test (push) Successful in 22s
CI / Lint & Typecheck (push) Successful in 27s
CI / Build & Push Docker Images (push) Successful in 43s
CI / Test (pull_request) Successful in 27s
CI / Lint & Typecheck (pull_request) Successful in 32s
CI / Build & Push Docker Images (pull_request) Successful in 39s
2026-06-08 12:39:52 +00:00
Flea Flicker 8721f0b63c dev → uat: GRO-2154 geocoding endpoints (Phase 1.3) (#171)
CI / Test (push) Successful in 24s
CI / Lint & Typecheck (push) Successful in 27s
CI / Build & Push Docker Images (push) Successful in 35s
2026-06-08 12:06:43 +00:00
Flea Flicker 027e012a58 Merge pull request 'dev → uat: GRO-2153 abstracted geocoding service' (#168) from dev-to-uat-gro-2153 into uat
CI / Test (push) Successful in 1m5s
CI / Lint & Typecheck (push) Successful in 43m29s
CI / Build & Push Docker Images (push) Successful in 1m7s
2026-06-08 10:51:17 +00:00
Flea Flicker b3db206588 Merge pull request 'dev → uat: GRO-2187 portal pet PATCH + GET enrichment (carries GRO-2152)' (#166) from dev-to-uat-gro-2187 into uat
CI / Test (push) Successful in 1m19s
CI / Lint & Typecheck (push) Successful in 1m25s
CI / Build & Push Docker Images (push) Successful in 3m58s
2026-06-08 10:02:17 +00:00
Flea Flicker fc072d51f4 Merge pull request 'promote(uat→main): GRO-2123 seed advisory lock + GRO-2100 uat-groomer linkage ordering' (#157) from uat into main
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 17s
CI / Build & Push Docker Images (push) Successful in 41s
2026-06-04 12:53:06 +00:00
Flea Flicker 6538406db2 Merge pull request 'chore: delete stale apps/api/src/db/seed.ts duplicate (GRO-2129)' (#158) from dev into uat
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 18s
CI / Build & Push Docker Images (push) Successful in 38s
CI / Test (pull_request) Successful in 22s
CI / Lint & Typecheck (pull_request) Successful in 25s
CI / Build & Push Docker Images (pull_request) Successful in 38s
2026-06-04 12:45:24 +00:00
Flea Flicker e2eacbc9fe Merge pull request 'dev → uat: GRO-2123 seed advisory lock' (#156) from dev-to-uat-gro-2123 into uat
CI / Test (push) Successful in 16s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Images (push) Successful in 40s
CI / Test (pull_request) Successful in 12s
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Build & Push Docker Images (pull_request) Successful in 39s
2026-06-04 11:32:06 +00:00
Flea Flicker e639cc82d1 chore(uat): GRO-2100 promote uat-groomer seed-linkage ordering fix to uat (#154)
CI / Test (push) Successful in 16s
CI / Lint & Typecheck (push) Successful in 19s
CI / Build & Push Docker Images (push) Successful in 27s
Co-authored-by: Flea Flicker <flea@groombook.dev>
Co-committed-by: Flea Flicker <flea@groombook.dev>
2026-06-02 20:23:54 +00:00
Flea Flicker f2931d7be2 Merge pull request 'Promote dev→uat: GRO-2100 uat-groomer ↔ UAT Pup Alpha linkage' (#152) from promote/dev-to-uat-gro-2100 into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 18s
CI / Build & Push Docker Images (push) Successful in 26s
Merge pull request #152 from groombook/promote/dev-to-uat-gro-2100

Promote dev→uat: GRO-2100 uat-groomer ↔ UAT Pup Alpha linkage
2026-06-02 19:11:46 +00:00
Paperclip d4a4ddce37 ci: retrigger GRO-2100 PR #152 Build & Push Docker Images (Reset image build failed — docker registry flake)
CI / Test (pull_request) Successful in 13s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Images (pull_request) Successful in 40s
2026-06-02 18:28:17 +00:00
Paperclip bd384bdf5c docs(UAT_PLAYBOOK): add TC-UAT-2/3 for uat-groomer linked/unlinked pet profile-summary (GRO-2100)
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Test (pull_request) Successful in 2m20s
CI / Build & Push Docker Images (pull_request) Failing after 36s
Lint Roller review on PR #152 flagged that the GRO-2100 seed change produces
new observable UAT API behavior that the playbook must reflect. Add two
deterministic rows pinning the contract GRO-1987 TC-UAT-2/3 will exercise:

- TC-UAT-2: uat-groomer + linked pet c0000001-...-002 (UAT Pup Alpha) → 200
- TC-UAT-3: uat-groomer + unlinked pet c0000001-...-003 (UAT Pup Beta) → 403

The 403-vs-404 note in TC-UAT-3 mirrors the verification note in the
GRO-2100 issue body so the QA runner knows where to file if the API
returns 404 (a separate RBAC defect, not against the seed).
2026-06-02 18:24:40 +00:00
Scrubs McBarkley c92fb2539d promote(uat→main): owner-bypass audit fix (GRO-2062) + services seed-idempotency fix (GRO-2064)
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Images (push) Successful in 25s
2026-06-02 06:00:02 +00:00
The Dogfather 411c42b2c4 Merge pull request 'Promote dev→uat: GRO-2033 services_pkey seed fix (fc6c6ef7)' (#149) from dev into uat
CI / Test (push) Successful in 14s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Images (push) Successful in 39s
CI / Test (pull_request) Successful in 12s
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Build & Push Docker Images (pull_request) Successful in 38s
2026-06-02 05:06:34 +00:00
The Dogfather bf97849324 promote(dev→uat): owner-bypass read audit row (GRO-2063) (#147)
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 17s
CI / Build & Push Docker Images (push) Successful in 41s
Promote GRO-2063 defense-in-depth audit row to uat. CI green. QA + CTO approved on dev PR #146.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-02 04:21:43 +00:00
Scrubs McBarkley 2a6242d3de Merge pull request 'promote(main): GRO-2033 prod migration fix + GRO-2013/2014 + rbac auto-provision (uat→main)' (#145) from uat into main
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 18s
CI / Build & Push Docker Images (push) Successful in 30s
promote(main): GRO-2033 prod migration fix + GRO-2013/2014 + rbac auto-provision (uat→main)

CI green. UAT regression GRO-2035 PASS. Migrations 0039/0040 idempotent — signed off by CEO.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-02 03:22:21 +00:00
The Dogfather 7181d41b24 Merge pull request 'Promote dev→uat: rbac Better-Auth auto-provision (GRO-2052)' (#144) from dev into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 15s
CI / Build & Push Docker Images (push) Failing after 13s
CI / Test (pull_request) Successful in 12s
CI / Lint & Typecheck (pull_request) Successful in 15s
CI / Build & Push Docker Images (pull_request) Successful in 41s
Promote dev→uat: rbac Better-Auth auto-provision (GRO-2052)

Makes the pets.ts owner-bypass reachable for Better-Auth email/password customers by auto-provisioning a groomer staff row keyed on user.id. Unblocks GRO-2050 and GRO-2035.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 02:42:19 +00:00
The Dogfather 4e9c4c5e08 Merge pull request 'promote(uat): GRO-2013 owner-bypass + GRO-2033 idempotent migrations (dev→uat)' (#142) from dogfather/gro-2013-promote-uat into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 18s
CI / Build & Push Docker Images (push) Successful in 39s
2026-06-01 20:14:14 +00:00
The Dogfather 16c959434b promote(uat): GRO-2013 owner-bypass + GRO-2033 idempotent migrations (dev→uat)
CI / Test (pull_request) Successful in 11s
CI / Lint & Typecheck (pull_request) Successful in 16s
CI / Build & Push Docker Images (pull_request) Successful in 41s
Merge dev into uat. Resolves test-file/playbook conflicts created by PR #138's
squash merge by taking dev's superset versions (verified: all GRO-2014 tests +
TC ids preserved, plus GRO-2013 additions). No-ff merge so dev becomes an
ancestor of uat, preventing future squash-divergence conflicts.

Carries:
- GRO-2013 deployed-tree owner-bypass (src/routes/pets.ts, reconciled 20-test file)
- GRO-2033 idempotent migrations 0039/0040

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-01 20:10:51 +00:00
The Dogfather 23484dc90a promote(uat): GRO-2014 profile-summary error-handling fix (dev→uat) (#138)
CI / Test (push) Successful in 10s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Images (push) Successful in 39s
2026-06-01 18:27:42 +00:00
Scrubs McBarkley 766728865e Merge pull request 'promote: uat → main — pnpm-offline Docker hardening + accumulated UAT fixes (GRO-1985)' (#136) from uat into main
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 14s
CI / Build & Push Docker Images (push) Successful in 1m19s
promote: uat → main — pnpm-offline Docker hardening + accumulated UAT fixes (GRO-1985)

UAT PASS: GRO-2015
Security PASS: GRO-2024 (Barkley Trimsworth)
UAT CI: run #2313 — 3/3 jobs green (incl. offline pnpm smoke tests)
2026-06-01 18:07:30 +00:00
The Dogfather 6a81a52a50 Merge pull request 'Promote dev → uat: UAT seed-password source-of-truth playbook (GRO-2000)' (#134) from dev into uat
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 15s
CI / Build & Push Docker Images (push) Successful in 27s
CI / Test (pull_request) Successful in 11s
CI / Lint & Typecheck (pull_request) Successful in 13s
CI / Build & Push Docker Images (pull_request) Successful in 1m10s
2026-06-01 17:41:47 +00:00
The Dogfather 5a4b9a98bd Merge pull request 'promote(docker): bake pnpm via npm to remove Corepack runtime downloads (GRO-1981)' (#133) from dev into uat
CI / Test (push) Successful in 12s
CI / Lint & Typecheck (push) Successful in 14s
CI / Build & Push Docker Images (push) Successful in 40s
Promote GRO-1985 (parent GRO-1981) dev->uat. cc @cpfarhood
2026-06-01 16:30:54 +00:00
The Dogfather f7f88156e1 Merge pull request 'promote(db): register extra_large via migration 0038 to UAT (GRO-2004)' (#131) from dev into uat
CI / Test (push) Successful in 11s
CI / Lint & Typecheck (push) Successful in 15s
CI / Build & Push Docker Images (push) Successful in 35s
2026-06-01 14:52:13 +00:00
The Dogfather 8af5a49d14 Merge pull request 'Promote dev→uat: GRO-1982 pet_size_category extra_large enum migration' (#126) from dev into uat
CI / Test (push) Successful in 13s
CI / Lint & Typecheck (push) Successful in 16s
CI / Build & Push Docker Images (push) Successful in 37s
Promote dev→uat: GRO-1983 seed-job pnpm fix + GRO-1982 extra_large enum migration

Carries the accumulated dev state into uat (PR #125 docker pnpm fix + 0037 migration).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-06-01 12:44:20 +00:00
Scrubs McBarkley 403634eb96 Merge pull request 'promote: uat → main (GRO-1757 SSO auto-provision fix)' (#89) from uat into main
CI / Lint & Typecheck (push) Successful in 9s
CI / Test (push) Successful in 9s
CI / Build & Push Docker Images (push) Successful in 50s
2026-05-26 02:15:57 +00:00
The Dogfather 152abfc4d5 fix(ci): remove duplicate provenance keys causing YAML parse error
CI / Test (push) Successful in 9s
CI / Lint & Typecheck (push) Successful in 12s
CI / Build & Push Docker Images (push) Successful in 1m10s
Duplicate 'provenance: false' in each docker/build-push-action step caused
Gitea to reject the workflow file, breaking push CI and workflow_dispatch.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-26 01:26:05 +00:00
Flea Flicker c8bbb12edb Merge pull request 'promote(dev→main): GRO-1752 schema fix for UAT (CI trigger)' (#82) from dev into main 2026-05-25 23:28:27 +00:00
Flea Flicker ba95088653 Merge pull request 'chore: trigger CI from uat for GRO-1754' (#81) from fix/gro-1754-uat-ci into main 2026-05-25 23:23:15 +00:00
Flea Flicker dd83f29736 chore: trigger CI from uat for GRO-1754 2026-05-25 23:22:04 +00:00
Chris Farhood 185fce8e17 Add .mcp.json
CI / Lint & Typecheck (push) Successful in 15s
CI / Test (push) Successful in 13s
CI / Build & Push Docker Images (push) Successful in 2m44s
2026-05-24 18:14:57 +00:00
Scrubs McBarkley 081379c189 Merge pull request 'promote: uat → main (GRO-1509 OIDC accountLinking fix)' (#46) from uat into main
CI / Test (push) Successful in 9s
CI / Lint & Typecheck (push) Successful in 12s
CI / Build & Push Docker Images (push) Successful in 50s
Merge uat → main: GRO-1509 OIDC accountLinking fix

Sign-offs cleared:
- QA: GRO-1510 ✓
- UAT: GRO-1515 ✓
- Security: GRO-1516 ✓
- Infra: groombook/infra PR #413
2026-05-22 14:03:43 +00:00
The Dogfather e01c12a316 Merge pull request 'chore: migrate .github/workflows to .gitea/workflows' (#22) from gitea/migrate-workflows into main
CI / Lint & Typecheck (push) Successful in 15s
CI / Test (push) Successful in 20s
CI / Build & Push Docker Image (push) Failing after 1m47s
chore: migrate .github/workflows to .gitea/workflows

Migrate CI workflow from GitHub Actions to Gitea Actions.
- Registry: ghcr.io → git.farh.net
- Auth: secrets.GITHUB_TOKEN → gitea.token
- Cache: type=gha → type=registry

Part of GRO-1315.
2026-05-20 01:34:04 +00:00
16 changed files with 425 additions and 101 deletions
-11
View File
@@ -1,11 +0,0 @@
{
"mcpServers": {
"gitea": {
"type": "http",
"url": "https://git-mcp.farh.net/mcp",
"headers": {
"Authorization": "Bearer ${GITEA_TOKEN}"
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
# AGENTS.md
This repository (`groombook/api`) is part of the GroomBook application stack. The
authoritative process, quality bar, and safety rules live in the shared
[`groombook/org`](https://git.farh.net/groombook/org) skills repository. Read
those first; this file is only a pointer.
## Authoritative skills
- **SDLC (branching, PRs, phases, handoffs):**
[`groombook/org/skills/sdlc/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/sdlc/SKILL.md)
- **Coding standards (priority ordering, PR discipline, tests, no-hardcoded-values, CalVer):**
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md)
- **Safety (no plaintext secrets, no direct `kubectl apply` to `groombook`, no self-merge, board approval for destructive actions):**
[`groombook/org/skills/safety/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/safety/SKILL.md)
For human contributors and humans reviewing agent work, see
[`CONTRIBUTING.md`](./CONTRIBUTING.md) in this repo for the phase-by-phase PR
flow and the `uat→main` merge-gate policy summary.
## Non-negotiable operational rules
These mirror the org skills; they are restated here so any agent landing in
this repo sees them without a cross-repo fetch.
- **All changes go through a PR.** Never push directly to `dev`, `uat`, or `main`.
- **Branch strategy:** `feature/<name>``dev``uat``main`. Engineers
always target `dev` first.
- **No self-merge contract.** The engineer who opened a PR clicks merge only
after the named reviewer (CI / QA / UAT / Security / CTO per phase)
approves. Issue-thread QA / UAT / security approvals do **not** clear the
Gitea `required_approvals` gate on `uat→main` — only a Gitea **Approve**
click from a member of the `approvals_whitelist_username` does. On this
repo that whitelist is `["gb_flea", "gb_dogfather"]` (engineer team).
Board-level accounts cannot give the Approve click by policy.
- **Always include `cc @cpfarhood`** at the bottom of every PR body for
board visibility (not as a reviewer).
- **Secrets in code are forbidden.** Use Bitnami Sealed Secrets; never commit
plaintext. See the `safety` skill.
- **Production (`groombook` namespace) is Flux-managed.** Never
`kubectl apply` directly. Infrastructure changes go through PRs in
`groombook/infra`.
## Local development
See the repo's own README, package scripts, and CI workflow. The
authoritative pipeline (Gitea Actions, image build, deploy hooks) is the
shared `groombook/infra` overlay; do not reimplement it here.
## When uncertain
If a task conflicts with the org skills, **the org skills win**. Open an
issue in `groombook/org` to propose a change rather than encoding a local
exception.
+117
View File
@@ -0,0 +1,117 @@
# Contributing to `groombook/api`
Thanks for contributing. This document is the human-facing companion to
[`AGENTS.md`](./AGENTS.md) and the authoritative
[`groombook/org`](https://git.farh.net/groombook/org) skills. The org skills
govern; this file is a quick-reference for the human/agent PR flow in this
repo.
## Branch strategy
Three long-lived branches; one PR per promotion step.
| Branch | Environment | Who merges | Prerequisites for merge |
|---------|-------------|-----------|-------------------------|
| `dev` | Dev | Engineer | CI passes |
| `uat` | UAT | Engineer | QA code review approval |
| `main` | Production | Engineer | UAT validation + CTO Gitea Approve when the `uat→main` merge-gate policy applies (see below) |
Engineers always target `dev` first. Feature branches: `<agent-name>/<short-description>`.
## Phase-by-phase PR flow
### Phase 1 — Dev
1. Branch from `dev`: `git checkout -b <name>/<short-description> origin/dev`.
2. Write code + tests. Run unit tests, type check, and lint locally (or rely on CI).
3. Open a PR against `dev`:
```bash
tea pr create --base dev --title "..." --body "..."
```
Include `cc @cpfarhood` at the bottom of the body for board visibility.
4. CI must pass. CI green → engineer self-merges.
5. CI builds and deploys to Dev automatically.
### Phase 2 — UAT promotion
1. Open a PR from `dev` to `uat`.
2. CI must pass.
3. **QA (Lint Roller)** reviews and approves on the Gitea PR.
4. QA approved → engineer self-merges.
5. CI builds and deploys to UAT automatically.
### Phase 3 — UAT regression + Security review
1. **UAT (Shedward Scissorhands)** runs full regression against UAT — every
feature, old and new, no exceptions.
2. **Security (Barkley Trimsworth)** reviews the changes.
3. Failures in either gate bounce back to Phase 1.
### Phase 4 — Production promotion (`uat → main`)
This is the gate the org PR
[`groombook/org#13`](https://git.farh.net/groombook/org/pulls/13) defines.
The full rule is in
[`groombook/org/skills/sdlc/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/sdlc/SKILL.md)
and
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md);
the summary is below.
**The CTO Gitea Approve click is NOT the default gate.** Once the four
pre-gates (QA, UAT deploy, UAT regression, security) are green, the engineer
self-merges.
**A CTO Gitea Approve click IS required** only for PRs in one of three
categories:
1. **Novel auth / session paths** — login, OIDC, OOBE, session middleware,
token issuance, password reset, MFA, new auth provider integrations.
Routine auth-gated UI (button styling, error messages, form layout) is
**not** in this category.
2. **Infra / prod-affecting merges** — deploys, infra manifests, secrets,
GitOps overlays, CI/CD, `main` branch protection, production
routing/ingress, prod state mutations. All Phase 5 infra overlay PRs in
`groombook/infra` require CTO Gitea Approve without exception.
3. **Risk-flagged merges** — `risk:cto-approve` label, or explicit CTO/CEO
sign-off request in the PR or issue thread.
The engineer opens the `uat→main` PR, classifies it against the three
categories above, and adds `cc @cpfarhood`. If the PR is in scope, the CTO
clicks Approve; once approved (and the four pre-gates are green), the
engineer merges.
### Phase 5 — Production deployment
A separate PR in `groombook/infra` bumps the overlay image tag for prod.
Handed to QA (Lint Roller) for review, then self-merged by the engineer.
## The four pre-gates (uat→main)
A `uat→main` PR is mergeable when **all four** are green:
1. **QA code review** — done on the dev→uat promotion PR.
2. **UAT deploy** — the UAT image built from the uat tip is live in UAT.
3. **UAT regression** — Shedward's full-feature UAT pass is green (no
pre-existing defects, no new defects).
4. **Security review** — Barkley's security code review is green.
Issue-thread QA / UAT / security approvals do **not** clear the Gitea
`required_approvals` gate. Only a Gitea **Approve** click from a member of
the `approvals_whitelist_username` for `main` clears it. In this repo that
whitelist is the engineer team (`gb_flea`, `gb_dogfather`).
## Style, tests, and quality bar
See
[`groombook/org/skills/coding-standards/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/coding-standards/SKILL.md)
for the engineering priority ordering, test requirements, no-hardcoded-values
rules, CalVer versioning policy, and the `git.farh.net` container registry
policy.
## Safety
See
[`groombook/org/skills/safety/SKILL.md`](https://git.farh.net/groombook/org/src/branch/main/skills/safety/SKILL.md)
for the non-negotiable rules: no plaintext secrets, no `kubectl apply` to
`groombook`, no self-merge, no direct `tofu` runs, board approval for
destructive actions, escalation protocol.
+4
View File
@@ -65,8 +65,11 @@ Expected: one row, `role = 'groomer'`. If zero rows return, the request hit the
| # | Scenario | Steps | Expected |
|---|----------|-------|----------|
| TC-API-0.1 | Unauthenticated health check | GET /api/health | 200 OK, `{"status":"ok"}` |
| TC-API-0.2 | DB-touching readiness check — healthy (GRO-2678) | GET /api/readyz | 200 OK, `{"status":"ready"}` |
| TC-API-0.3 | DB-touching readiness check — response body safe | GET /api/readyz and inspect body | Body contains only `status` and (on error) `check` fields — no raw SQL, driver messages, or stack traces |
> **Note (GRO-1544):** Health endpoint registered on `api` basePath before auth middleware at `/api/health`. The old path `/health` was incorrect (routed to web pod via HTTPRoute `/*` rule).
> **Note (GRO-2678):** `/api/readyz` is a separate DB-touching endpoint for monitoring. It is intentionally NOT used for K8s liveness/readiness probes — those remain DB-less to avoid pod cycling on transient DB blips.
### 4.1 Authentication
@@ -193,6 +196,7 @@ Geocoding turns a client's street address into `latitude`/`longitude` + `geocode
| TC-API-3.27 | Verify coat_type enum has all seed values | After UAT seed completes, inspect the coat_type enum on the UAT DB — it must contain: short, medium, long, double, wire, silky, curly, hairless | UAT seed jobs (`reset-demo-data`, `seed-test-data`) complete 1/1 with no `enum_in` error; coat_type includes all 8 values used by seed.ts `coatTypePool` |
| TC-API-3.28 | Verify pet_size_category enum has all seed values | After UAT seed completes, inspect the pet_size_category enum on the UAT DB — it must contain: small, medium, large, extra_large | UAT seed jobs (`reset-demo-data`, `seed-test-data`) complete 1/1 with no `enum_in` error; pet_size_category includes all 4 values used by seed.ts `petSizeCategoryPool` (regression for GRO-1999, mirrors TC-API-3.27) |
| TC-API-3.29 | Verify `reset-demo-data` CronJob does not fail with FK 23503 on `invoice_tip_splits` (GRO-2123) | Trigger the CronJob manually: `kubectl create job --from=cronjob/reset-demo-data verify-gro2123 -n groombook-uat`. Wait for pod to terminate. Inspect logs: `kubectl logs -n groombook-uat -l job-name=verify-gro2123` | Pod reaches `Completed` state; logs show `✓ Acquired seed advisory lock` and `✓ Released seed advisory lock` from `seed.ts`; no `PostgresError: … violates foreign key constraint "invoice_tip_splits_invoice_id_invoices_id_fk"` (code 23503); final counts unchanged (500 clients, ~4000 invoices) |
| TC-API-3.30 | Verify `reset-demo-data` CronJob is schema-safe — TRUNCATE only, no DDL drops (GRO-2722) | 1. Trigger: `kubectl -n groombook-uat create job --from=cronjob/reset-demo-data reset-verify-$(date +%s) --dry-run=client -o name \| xargs kubectl -n groombook-uat apply -f -` or simply `kubectl -n groombook-uat create job --from=cronjob/reset-demo-data reset-verify-$(date +%s)`. 2. Wait for job completion: `kubectl -n groombook-uat wait job/reset-verify-<ts> --for=condition=complete --timeout=300s`. 3. Check logs: `kubectl -n groombook-uat logs job/reset-verify-<ts>`. 4. Verify schema survival: `kubectl -n groombook-uat exec deploy/api -- psql $DATABASE_URL -c "\dt public.*"` and `kubectl -n groombook-uat exec deploy/api -- psql $DATABASE_URL -c "\dt drizzle.*"`. 5. Verify readyz: `curl -s https://uat.groombook.dev/api/readyz` | Job reaches `Completed` (exit 0); logs show `✓ All public tables truncated, sequences reset` — no `DROP TABLE`/`DROP TYPE`/`DROP SCHEMA` in output; all `public.*` tables still present after reset; `drizzle.__drizzle_migrations` still populated (row count unchanged); `https://uat.groombook.dev/api/readyz` returns HTTP 200; demo data reseeded (500 clients visible via GET /api/clients) |
### 4.4 Appointment Scheduling
+5
View File
@@ -69,9 +69,14 @@ describe("auth init", () => {
beforeEach(() => {
dbSelectResult = [];
vi.clearAllMocks();
// Stub fetch so OIDC discovery requests resolve instantly during tests.
// Without this, AbortSignal.timeout(5000) in auth.ts races with vitest's
// 5000ms default test timeout and causes flaky failures.
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 }));
});
afterEach(() => {
vi.unstubAllGlobals();
process.env = { ...originalEnv };
});
+9 -3
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { Hono } from "hono";
import { authProviderRouter } from "../routes/authProvider.js";
@@ -227,6 +227,7 @@ describe("PUT /admin/auth-provider", () => {
describe("POST /admin/auth-provider/test", () => {
beforeEach(resetMock);
afterEach(() => vi.restoreAllMocks());
it("returns ok=false for unreachable issuer", async () => {
const app = makeApp(mockSuperUser);
@@ -242,7 +243,12 @@ describe("POST /admin/auth-provider/test", () => {
expect(body.error).toBeTruthy();
}, 15000); // timeout must exceed the 10s fetch timeout in the route handler
it("returns 400 for missing clientSecret (not required for test)", async () => {
it("returns 200 when clientSecret is omitted (not required by test-connection schema)", async () => {
// Mock fetch so the route does not make a real network request.
vi.spyOn(global, "fetch").mockResolvedValueOnce({
ok: true,
json: async () => ({ issuer: "https://auth.example.com" }),
} as Response);
const app = makeApp(mockSuperUser);
const { status } = await post(app, "/admin/auth-provider/test", {
providerId: "authentik",
@@ -250,7 +256,7 @@ describe("POST /admin/auth-provider/test", () => {
issuerUrl: "https://auth.example.com",
clientId: "client",
}, mockSuperUser);
expect(status).toBe(200); // clientSecret omitted intentionally for test
expect(status).toBe(200);
});
});
+30 -36
View File
@@ -1,10 +1,17 @@
/**
* reset.ts — Drop all application tables and re-run migrations + seed.
* reset.ts — Truncate all public tables and restart identity sequences.
*
* Intended for local development only. Never run against production.
* Schema-safe: never issues destructive DDL against any schema.
* The drizzle schema and __drizzle_migrations table are preserved so
* drizzle-kit migrate remains a no-op on an already-migrated DB.
*
* NOTE: this file is NOT the deployed reset entrypoint — the reset image
* builds from packages/db and runs `pnpm --filter @groombook/db reset`.
* apps/api db:reset delegates there too (see apps/api/package.json).
* Keep in sync with packages/db/src/reset.ts (GRO-2722).
*
* Usage:
* DATABASE_URL=postgres://... npx tsx packages/db/src/reset.ts
* DATABASE_URL=postgres://... npx tsx apps/api/src/db/reset.ts
*/
import postgres from "postgres";
@@ -23,43 +30,30 @@ async function reset() {
const client = postgres(url, { max: 1 });
console.log("Dropping all application tables...\n");
console.log("Truncating all public tables...\n");
// Drop in dependency order (children before parents)
await client`
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
) LOOP
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END $$;
// Enumerate all base tables in the public schema dynamically, then
// issue a single TRUNCATE with RESTART IDENTITY CASCADE so FK cycles
// are not a problem. The drizzle schema and __drizzle_migrations are
// intentionally excluded (different schema) so drizzle-kit migrate
// stays a no-op on an already-migrated DB.
const tables = await client<{ tablename: string }[]>`
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
`;
// Drop custom enums
await client`
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (
SELECT typname FROM pg_type
WHERE typtype = 'e' AND typnamespace = (
SELECT oid FROM pg_namespace WHERE nspname = 'public'
)
) LOOP
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
END LOOP;
END $$;
`;
if (tables.length > 0) {
// Double-quote each identifier (escaping embedded quotes) to handle
// any table name safely without a pg-specific quote_ident helper.
const tableList = tables
.map((t) => `"${t.tablename.replace(/"/g, '""')}"`)
.join(", ");
await client.unsafe(
`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`,
);
}
// Drop the drizzle migrations tracking table
await client`DROP TABLE IF EXISTS drizzle.__drizzle_migrations CASCADE`;
await client`DROP SCHEMA IF EXISTS drizzle CASCADE`;
console.log("✓ All tables and enums dropped\n");
console.log("✓ All public tables truncated, sequences reset\n");
await client.end();
}
+43 -46
View File
@@ -1,15 +1,14 @@
/**
* reset.ts — Drop all application tables, re-run migrations, and re-seed.
* reset.ts — Truncate all public tables and restart identity sequences.
*
* Intended for local development only. Never run against production.
* Schema-safe: never issues destructive DDL against any schema.
* The drizzle schema and __drizzle_migrations table are preserved so
* drizzle-kit migrate remains a no-op on an already-migrated DB.
*
* Usage:
* DATABASE_URL=postgres://... npx tsx packages/db/src/reset.ts
*
* GRO-2139: the entire drop→migrate→seed chain runs inside a single
* GRO-2139: the entire truncate→migrate→seed chain runs inside a single
* Postgres advisory lock (SEED_ADVISORY_LOCK_KEY) so a concurrent
* `seed.ts` (e.g. the dev `seed-test-data-*` Job being recreated at
* the top of the hour) cannot interleave between `reset.ts` (DROP)
* the top of the hour) cannot interleave between `reset.ts` (TRUNCATE)
* and `seed.ts` (TRUNCATE+insert) and collide on `invoices_pkey`.
*
* Why this matters: `seed.ts` derives every primary key from a single
@@ -22,17 +21,21 @@
* GRO-2123 added the advisory lock around `runSeedBody` but left
* `reset.ts` and `drizzle-kit migrate` outside the lock. This script
* now wraps the *whole* chain in the same lock: `withSeedAdvisoryLock`
* pins the lock to one reserved session and the DROP → migrate → seed
* pins the lock to one reserved session and the TRUNCATE → migrate → seed
* work runs on the rest of the pool, so the lock guarantees mutual
* exclusion against any concurrent seeder for the entire chain.
*
* For a full local schema teardown (nuke tables, enums, drizzle schema)
* use `pnpm --filter @groombook/db db:nuke` instead — never change this
* script to be destructive (GRO-2722 / GRO-2678 prod incident).
*
* See: groombook/infra `apps/base/reset-cronjob.yaml` (CronJob) and
* `apps/base/seed-job.yaml` (one-shot Job) — both invoke the same
* `seed.ts` code path on the same database in `groombook-dev`.
*/
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import { execSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import * as schema from "./schema.js";
@@ -46,7 +49,6 @@ import {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const MIGRATIONS_FOLDER = resolve(__dirname, "../migrations");
async function reset() {
const url = process.env.DATABASE_URL;
@@ -68,12 +70,12 @@ async function reset() {
// Pool sizing is load-bearing here. `withSeedAdvisoryLock` does
// `pool.reserve()` to pin the advisory lock to one dedicated session
// (a session-level lock released on a *different* pooled connection is
// a no-op), and the DROP / migrate / seed work then runs on the
// a no-op), and the TRUNCATE / migrate / seed work then runs on the
// *remaining* pooled connections. The lock provides mutual exclusion
// across processes regardless of how many connections the work uses —
// it does NOT require the work to share the lock's session.
//
// Therefore `max` must be 2: 1 reserved for the lock + 1 free for
// Therefore `max` must be >= 2: 1 reserved for the lock + >=1 free for
// the work. `max: 1` would let `reserve()` consume the only connection
// and every query inside the callback would block forever waiting for
// a connection that never frees (connection-starvation deadlock). We
@@ -83,46 +85,41 @@ async function reset() {
try {
await withSeedAdvisoryLock(client, async () => {
console.log("Dropping all application tables...\n");
console.log("Truncating all public tables...\n");
// Drop dependencies (tables) first
await client`
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
) LOOP
EXECUTE 'DROP TABLE IF EXISTS public.' || quote_ident(r.tablename) || ' CASCADE';
END LOOP;
END $$;
// Enumerate all base tables in the public schema dynamically, then
// issue a single TRUNCATE with RESTART IDENTITY CASCADE so FK cycles
// are not a problem. The drizzle schema and __drizzle_migrations are
// intentionally excluded (different schema) so drizzle-kit migrate
// stays a no-op on an already-migrated DB.
const tables = await client<{ tablename: string }[]>`
SELECT tablename FROM pg_tables
WHERE schemaname = 'public'
`;
// Drop custom enums
await client`
DO $$ DECLARE
r RECORD;
BEGIN
FOR r IN (
SELECT typname FROM pg_type
WHERE typtype = 'e' AND typnamespace = (
SELECT oid FROM pg_namespace WHERE nspname = 'public'
)
) LOOP
EXECUTE 'DROP TYPE IF EXISTS ' || quote_ident(r.typname) || ' CASCADE';
END LOOP;
END $$;
`;
if (tables.length > 0) {
// Double-quote each identifier (escaping embedded quotes) to handle
// any table name safely without a pg-specific quote_ident helper.
const tableList = tables
.map((t) => `"${t.tablename.replace(/"/g, '""')}"`)
.join(", ");
await client.unsafe(
`TRUNCATE ${tableList} RESTART IDENTITY CASCADE`,
);
}
// Drop the drizzle migrations tracking table
await client`DROP TABLE IF EXISTS drizzle.__drizzle_migrations CASCADE`;
await client`DROP SCHEMA IF EXISTS drizzle CASCADE`;
console.log("✓ All tables and enums dropped\n");
console.log("✓ All public tables truncated, sequences reset\n");
console.log("Running migrations...");
await migrate(db, { migrationsFolder: MIGRATIONS_FOLDER });
// GRO-2672: drizzle-orm's migrate() has a high-water-mark bug that skips
// migrations with stale `when` timestamps (0001, 0003, 0010, 0011). Use
// drizzle-kit instead -- it applies migrations by hash, matching the K8s
// migrate Job behaviour exactly.
execSync("pnpm exec drizzle-kit migrate", {
stdio: "inherit",
env: { ...process.env },
cwd: resolve(__dirname, ".."),
});
console.log("✓ Migrations applied\n");
console.log("Seeding database...");
+5
View File
@@ -69,9 +69,14 @@ describe("auth init", () => {
beforeEach(() => {
dbSelectResult = [];
vi.clearAllMocks();
// Stub fetch so OIDC discovery requests resolve instantly during tests.
// Without this, AbortSignal.timeout(5000) in auth.ts races with vitest's
// 5000ms default test timeout and causes flaky failures.
vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: false, status: 503 }));
});
afterEach(() => {
vi.unstubAllGlobals();
process.env = { ...originalEnv };
});
+9 -3
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { Hono } from "hono";
import { authProviderRouter } from "../routes/authProvider.js";
@@ -227,6 +227,7 @@ describe("PUT /admin/auth-provider", () => {
describe("POST /admin/auth-provider/test", () => {
beforeEach(resetMock);
afterEach(() => vi.restoreAllMocks());
it("returns ok=false for unreachable issuer", async () => {
const app = makeApp(mockSuperUser);
@@ -242,7 +243,12 @@ describe("POST /admin/auth-provider/test", () => {
expect(body.error).toBeTruthy();
}, 15000); // timeout must exceed the 10s fetch timeout in the route handler
it("returns 400 for missing clientSecret (not required for test)", async () => {
it("returns 200 when clientSecret is omitted (not required by test-connection schema)", async () => {
// Mock fetch so the route does not make a real network request.
vi.spyOn(global, "fetch").mockResolvedValueOnce({
ok: true,
json: async () => ({ issuer: "https://auth.example.com" }),
} as Response);
const app = makeApp(mockSuperUser);
const { status } = await post(app, "/admin/auth-provider/test", {
providerId: "authentik",
@@ -250,7 +256,7 @@ describe("POST /admin/auth-provider/test", () => {
issuerUrl: "https://auth.example.com",
clientId: "client",
}, mockSuperUser);
expect(status).toBe(200); // clientSecret omitted intentionally for test
expect(status).toBe(200);
});
});
+44
View File
@@ -0,0 +1,44 @@
import { describe, it, expect, vi } from "vitest";
import { Hono } from "hono";
// Mock auth lib so getAuth() throws — non-bypass paths hit the 503 "not configured" guard.
vi.mock("../lib/auth.js", () => ({
getAuth: () => {
throw new Error("auth not configured");
},
initAuth: vi.fn(),
getActiveProviders: vi.fn(() => []),
}));
describe("authMiddleware bypass: /api/readyz", () => {
it("serves /api/readyz without auth (bypass before auth check)", async () => {
// Ensure AUTH_DISABLED is not set so the bypass is exercised, not AUTH_DISABLED shortcut.
const prev = process.env.AUTH_DISABLED;
delete process.env.AUTH_DISABLED;
const { authMiddleware } = await import("../middleware/auth.js");
const app = new Hono();
app.use("/api/*", authMiddleware);
app.get("/api/readyz", (c) => c.json({ status: "ok" }, 200));
const res = await app.request("/api/readyz", { method: "GET" });
expect(res.status).toBe(200);
if (prev !== undefined) process.env.AUTH_DISABLED = prev;
});
it("blocks non-whitelisted /api/* paths when auth is not configured", async () => {
const prev = process.env.AUTH_DISABLED;
delete process.env.AUTH_DISABLED;
const { authMiddleware } = await import("../middleware/auth.js");
const app = new Hono();
app.use("/api/*", authMiddleware);
app.get("/api/staff", (c) => c.json({ ok: true }, 200));
const res = await app.request("/api/staff", { method: "GET" });
expect(res.status).toBe(503);
if (prev !== undefined) process.env.AUTH_DISABLED = prev;
});
});
+87
View File
@@ -0,0 +1,87 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { Hono } from "hono";
// ─── Mock db module ───────────────────────────────────────────────────────────
let selectImpl: () => Promise<unknown>;
vi.mock("@groombook/db", () => {
const staff = new Proxy(
{ _name: "staff" },
{
get(_target, prop) {
if (prop === "_name") return "staff";
return { table: "staff", column: prop };
},
}
);
return {
getDb: () => ({
select: (_fields: unknown) => ({
from: (_table: unknown) => ({
limit: (_n: number) => selectImpl(),
}),
}),
}),
staff,
};
});
// ─── Build test app ───────────────────────────────────────────────────────────
async function makeApp() {
// Import after mocks are in place
const { getDb, staff } = await import("@groombook/db");
const app = new Hono();
app.get("/api/readyz", async (c) => {
try {
await getDb().select({ id: staff.id }).from(staff).limit(1);
return c.json({ status: "ready" }, 200);
} catch (err) {
console.error("[readyz] DB check failed:", err);
return c.json({ status: "degraded", check: "db" }, 503);
}
});
return app;
}
// ─── Tests ────────────────────────────────────────────────────────────────────
describe("GET /api/readyz", () => {
beforeEach(() => {
vi.restoreAllMocks();
});
it("returns 200 {status:'ready'} when DB query succeeds", async () => {
selectImpl = () => Promise.resolve([{ id: "staff-1" }]);
const app = await makeApp();
const res = await app.request("/api/readyz", { method: "GET" });
const body = (await res.json()) as Record<string, unknown>;
expect(res.status).toBe(200);
expect(body.status).toBe("ready");
});
it("returns 503 {status:'degraded',check:'db'} when DB query throws", async () => {
selectImpl = () => Promise.reject(new Error("42P01: relation staff does not exist"));
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
const app = await makeApp();
const res = await app.request("/api/readyz", { method: "GET" });
const body = (await res.json()) as Record<string, unknown>;
expect(res.status).toBe(503);
expect(body.status).toBe("degraded");
expect(body.check).toBe("db");
// Raw SQL / driver error must NOT appear in the response body
expect(JSON.stringify(body)).not.toContain("42P01");
expect(JSON.stringify(body)).not.toContain("relation");
consoleSpy.mockRestore();
});
});
+11
View File
@@ -65,6 +65,17 @@ app.use(
app.get("/health", (c) => c.json({ status: "ok" }));
// /api/health: used by Gateway HTTPRoute (/api/* → API pod)
app.get("/api/health", (c) => c.json({ status: "ok" }));
// /api/readyz: DB-touching deep health check consumed by monitoring (not K8s probes)
// Distinct from /health so a dropped schema triggers an alert without cycling pods (GRO-2678)
app.get("/api/readyz", async (c) => {
try {
await getDb().select({ id: staff.id }).from(staff).limit(1);
return c.json({ status: "ready" }, 200);
} catch (err) {
console.error("[readyz] DB check failed:", err);
return c.json({ status: "degraded", check: "db" }, 503);
}
});
// Public booking routes — no auth required, must be registered before auth middleware
app.route("/api/book", bookRouter);
+6 -1
View File
@@ -329,5 +329,10 @@ export async function initAuth(): Promise<void> {
});
})();
await authInitPromise;
try {
await authInitPromise;
} catch (err) {
authInitPromise = null; // allow retry on next call
throw err;
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ if (process.env.AUTH_DISABLED === "true") {
}
export const authMiddleware: MiddlewareHandler = async (c, next) => {
if (c.req.path.startsWith("/api/auth/") || c.req.path === "/api/health") {
if (c.req.path.startsWith("/api/auth/") || c.req.path === "/api/health" || c.req.path === "/api/readyz") {
await next();
return;
}
View File