Prod cumulative promotion 2026.06.01-7667288 (PR #596) revealed that
0034_extend_pet_profile_columns (temperament_score + 3 jsonb cols) and
0036_add_missing_coat_type_values (short/medium/silky) were silently
skipped on the prod database, leaving the seed/reset path with:
Seed failed: PostgresError: column "temperament_score" does not exist
## Root cause: drizzle high-water-mark, same shape as GRO-1999
drizzle-orm@0.38.4 `pg-core/dialect.js#migrate` only applies a journal
entry when its `folderMillis` is strictly greater than the most recent
`__drizzle_migrations.created_at`:
if (!lastDbMigration || Number(lastDbMigration.created_at) < migration.folderMillis) {
// apply SQL + record hash
}
`packages/db/migrations/meta/_journal.json` has 0033's when at
1779500000000 (2026-05-23) — but 0034 was registered with when
1751140800000 (2025-06-28) and 0036 with 1751480000000 (2025-07-02).
Both are below the 0033 watermark, so on the prod DB (whose newest
applied migration was 0033) drizzle silently skipped 0034 and 0036.
0038 (when 1780000000000) was above the watermark, so it applied — and
the migrate Job exits 0 with 'migrations applied successfully!'. The
schema didn't change. GRO-1999 documented the same bug for 0037 → 0038.
UAT/dev are unaffected because their watermarks were already below the
0034/0036 entries when those originally ran.
## Fix
Add two new idempotent migrations with monotonic 'when':
- 0039_extend_pet_profile_columns_idempotent.sql, when 1780000000001:
ALTER TABLE pets ADD COLUMN IF NOT EXISTS temperament_score integer;
-- + temperament_flags jsonb, medical_alerts jsonb, preferred_cuts jsonb
- 0040_register_missing_coat_type_values.sql, when 1780000000002:
ALTER TYPE coat_type ADD VALUE IF NOT EXISTS 'short';
-- + 'medium', 'silky'
Both are 'IF NOT EXISTS' — safe no-ops on UAT/dev where 0034/0036
applied normally, and effective forward-fix on prod where they were
skipped. Do NOT modify 0034/0036 in place (per the GRO-1999 pattern):
UAT/dev have already applied them and re-running would fail.
## Verification
- packages/db/migrations/meta/_journal.json now has 41 entries with idx
39 and 40 strictly monotonic in 'when'.
- python3 -c 'import json; json.load(open(...))' parses cleanly.
- ALTER TYPE ADD VALUE IF NOT EXISTS is permitted inside a tx on
PostgreSQL 18.3 (prod cluster image confirmed via CNPG status).
## UAT Playbook
No user-visible behaviour change — schema only. Existing TC-API-3.8 / 3.9 /
3.11 / 3.13 (extended pet profile) and 3.19a (profile summary) continue to
pass and now ALSO act as smoke tests after the prod image roll-forward.
## Refs
- Issue: GRO-2033
- Same-shape prior bug: GRO-1999 (0037 → 0038), commit 423d4bf
- Mitigation: groombook/infra PR #597 (suspend prod reset-demo-data
CronJob while this lands)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds an owner-bypass in the profile-summary handler for customers signed in via Better Auth, using the existing X-Impersonation-Session-Id portal session header. When a groomer-role staff row carries a valid impersonation session whose clientId matches the pet's clientId, skip groomerLinkageCheck and serve the summary. Otherwise fall through to the existing linkage check.
Resolves a 403 Forbidden where the customer (auto-provisioned by resolveStaffMiddleware as a 'groomer' staff row with no appointment linkage) could not read their own pet's profile.
Scope: GRO-2013 profile-summary endpoint only — no rbac.ts/schema/Dockerfile changes.
Tests: 6 new cases (owner-bypass, no-header, cross-tenant, expired, manager regression, linked-groomer regression); 294/294 pass.
UAT_PLAYBOOK.md: TC-API-3.19a/b/c.
Closes GRO-2013.
Co-authored-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net>
Co-committed-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net>
GRO-1979 added 0037_add_extra_large_to_pet_size_category with a journal
'when' of 1751500000000 — below the 0033 high-water mark (1779500000000)
on existing UAT/persistent DBs. Drizzle only applies a migration when its
journal.when is strictly greater than max(applied created_at), so 0037
was silently skipped, leaving pet_size_category without 'extra_large'
and crashing the UAT seed-test-data job (22P02 enum error).
This adds 0038 with a monotonic 'when' (1780000000000) so it applies on
both existing UAT/persistent DBs and fresh DBs. Statement is idempotent
(ADD VALUE IF NOT EXISTS) and a single auto-commit DDL (ADD VALUE cannot
run inside a transaction block).
Do not modify 0033/0034/0036/0037 — re-registering extra_large is correct
since the drizzle PetSizeCategory type and seed.ts both use that value.
GRO-2004
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The GRO-1983 fast restoration swapped Corepack's pnpm shim for a real
`npm install -g pnpm@9.15.4` binary, which is the right move. But the
GRO-1997 evidence gate still showed the first `reset-demo-data` pod
(...-nh7vg) hitting `getaddrinfo EAI_AGAIN registry.npmjs.org` before a
retry succeeded — the cache was writable, the cold-cache registry
download wasn't eliminated. This is the durable fix:
1. `ENV COREPACK_ENABLE_DOWNLOAD_FALLBACK=0` in `base` and `runner`:
defence in depth so a Corepack shim can never silently re-download
pnpm, even if it is somehow re-introduced.
2. `ENV HOME=/tmp` in the `migrate`, `seed`, and `reset` stages:
under `readOnlyRootFilesystem: true` + `runAsUser: 1000`, the
default HOME path is read-only, and pnpm fails the first time it
tries to write a config or state file. The job pods already mount a
writable emptyDir at `/tmp`; point HOME there.
3. CI smoke tests for `seed` and `reset` images (matching the existing
`migrate` smoke): point `registry.npmjs.org` at 127.0.0.1 in a
throwaway container, assert `which pnpm` resolves to
`/usr/local/bin/pnpm` (real binary, not shim), and that `pnpm
--version` succeeds without network egress. If Corepack ever sneaks
back in, CI catches it on every PR.
The vestigial `RUN mkdir -p /home/node/.cache/node/corepack` in the
`builder` stage (mentioned in the spec) was already removed in GRO-1909
(commit 0a3eb8a), so nothing to do there.
Follow-on cleanup of the per-job `COREPACK_HOME` env vars and
`node-cache` emptyDir mounts in `groombook/infra` is intentionally
deferred to a coordinated infra PR once the new image is deployed —
keeping the existing infra in place during the transition avoids a
flag-day.
GRO-1985, hardening follow-up to GRO-1984 / GRO-1983.
Closes parent: GRO-1981.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This merge resolves a journal conflict between dev's idx 37 entry (0037_add_extra_large_to_pet_size_category) and the diverged uat branch. Both branches want the idx 37 entry; keeping the dev version which adds the migration.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
GRO-1979: The pet_size_category enum created in 0031_buffer_rules.sql
contained ('small', 'medium', 'large', 'xlarge'), but the drizzle schema
and seed.ts both use 'extra_large'. The mismatch caused the UAT seed job
to fail with:
invalid input value for enum pet_size_category: "extra_large"
This migration adds the 'extra_large' value to pet_size_category and
registers it at idx 37 in the drizzle journal (sequel to 0035/0036
which registered short/medium/silky in coat_type under GRO-1971).
Non-transactional per Postgres restriction on ALTER TYPE ADD VALUE.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The seed/migrate/reset Jobs all invoke `pnpm` at runtime via the
`pnpm --filter @groombook/db ...` CMD. In the current image, `/usr/local/bin/pnpm`
is a symlink to corepack's pnpm.js shim, which delegates to corepack and
re-validates the package against https://registry.npmjs.org on first use.
The UAT pod network is air-gapped, so corepack fails with:
Error: getaddrinfo EAI_AGAIN registry.npmjs.org
This causes every seed Job to fail, leaving the Better Auth credential
hashes frozen at their last successful seed run — even when the SealedSecret
`seed-uat-passwords` is rotated.
Replace `corepack install -g pnpm@9.15.4` with `npm install -g pnpm@9.15.4`
in the base and runner stages. `npm install -g` writes the real pnpm binary
to /usr/local/bin/pnpm, bypassing the corepack shim entirely. The seed,
migrate, and reset stages inherit from builder (which inherits from base)
so they all get the real pnpm without needing their own install line.
The reset stage had a redundant corepack install that can be removed.
GRO-1983, supersedes GRO-1909 (incomplete — corepack shim still tried to
download pnpm at runtime).
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Restore deterministic alerts so TC-API-3.23/3.24 no longer flaky:
- TestCooper always gets a behavioral alert
- TestRocky always gets a skin alert
- Their deterministic alerts (~0.4% of total pets) do not shift
the overall 25-35% medicalAlerts distribution
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Promotes 6 dev commits to uat. PR #111 (latest dev tip) QA-approved by Lint Roller. CI all-green.
Follow-up: Shedward UAT regression task to be created.
Adds smoke-test step after the migrate image build that runs the image with registry.npmjs.org pointed at 127.0.0.1; pnpm --version must succeed without npm access. Guards against corepack-offline regression from GRO-1916.
QA: Lint Roller APPROVED (commit 5ec9e9a8) — CI all-green.
CTO: signed off (self-approval blocked by Gitea — I authored).
Closes #GRO-1954
Closes #GRO-1957
Closes #GRO-1958
Relates #GRO-1939
- Fix API image tag typo: groombok -> groombook (line 103)
- Fix Reset image cache-from/cache-to indentation: moved from under tags: (12 spaces) to under with: (10 spaces)
- This corrects the Reset image build failure in CI runs.
Use sql\`count(*)::int\` instead of selecting appointments.id, which was
causing TS2339 under noUncheckedIndexedAccess (arr[0] is T | undefined).
Import sql from @groombook/db. Use countRow?.count ?? 0 to stay
noUncheckedIndexedAccess-safe.
Matches the working implementation in apps/api/src/routes/pets.ts:365.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Adds profile-summary endpoint for groombook web to display:
- Basic pet fields (name, species, breed, coatType, etc.)
- Recent grooming history (last 10 completed appointments with staff names)
- Visit count (completed appointments)
- Upcoming appointment (next scheduled/confirmed)
Groomer RBAC: groomers can only see pets they've had appointments with.
Non-groomer staff (admin/super) can see all pets.
Fixes GRO-1802 (UAT regression: profile-summary route never deployed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Use `corepack install -g` instead of `corepack prepare --activate` to write
pnpm to a stable global path (/usr/local/bin/pnpm) rather than relying on
corepack shims that re-validate against npmjs.org at runtime.
Set COREPACK_ENABLE_DOWNLOAD_PROMPT=0 and COREPACK_ENABLE_STRICT=0 to suppress
the interactive download prompt and strict version checks that also trigger
network access.
Remove the dead `RUN mkdir -p /home/node/.cache/node/corepack` line from the
builder stage (vestigial cache-location configuration).
Fixes: GRO-1916 (prod migrate-schema EAI_AGAIN on registry.npmjs.org)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>