From dd220598ca22d934208832c041061587d055d81a Mon Sep 17 00:00:00 2001 From: The Dogfather <20+gb_dogfather@noreply.git.farh.net> Date: Sun, 31 May 2026 23:09:36 +0000 Subject: [PATCH 1/7] fix: add missing coat_type enum values (GRO-1971) (#118) --- UAT_PLAYBOOK.md | 1 + .../db/migrations/0035_add_missing_coat_type_values.sql | 9 +++++++++ packages/db/migrations/meta/_journal.json | 4 ++-- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 packages/db/migrations/0035_add_missing_coat_type_values.sql diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index a458219..3b6ed03 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -116,6 +116,7 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet | TC-API-3.24 | Verify UAT test pet Delta has skin alert | GET /api/pets/{id} where name = "TestRocky" (pet for uat-delta@groombook.dev) | medicalAlerts includes an entry with type: "skin" | | TC-API-3.25 | Verify 30+ total pets in UAT DB | GET /api/pets then count total | 30+ pets returned (UAT seed creates 500 random-pool + 5 UAT test clients + 2 UAT customer = 507 total) | | TC-API-3.26 | Verify 25-35% medicalAlerts distribution | GET /api/pets (first 30 pets), count how many have non-empty medicalAlerts | Ratio is 25-35% (seed uses rand() < 0.3 for ~30% distribution) | +| 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` | ### 4.4 Appointment Scheduling diff --git a/packages/db/migrations/0035_add_missing_coat_type_values.sql b/packages/db/migrations/0035_add_missing_coat_type_values.sql new file mode 100644 index 0000000..3b7a2d3 --- /dev/null +++ b/packages/db/migrations/0035_add_missing_coat_type_values.sql @@ -0,0 +1,9 @@ +-- Migration: 0035_add_missing_coat_type_values.sql +-- Adds missing values to coat_type enum that seed.ts requires but which were +-- omitted from the 0031_buffer_rules.sql CREATE TYPE statement (migration drift). +-- 0031 created: 'smooth', 'double', 'wire', 'curly', 'long', 'hairless' +-- Missing (from schema.ts coatTypeEnum): 'short', 'medium', 'silky' + +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'short'; +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'medium'; +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'silky'; \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 58d27a7..5009d34 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -250,8 +250,8 @@ { "idx": 35, "version": "7", - "when": 1751140800000, - "tag": "0035_add_short_to_coat_type_enum", + "when": 1751480000000, + "tag": "0035_add_missing_coat_type_values", "breakpoints": true } ] From b928acf5d6ea8b71ee2c4b76b68e0417ecad0c91 Mon Sep 17 00:00:00 2001 From: The Dogfather <20+gb_dogfather@noreply.git.farh.net> Date: Mon, 1 Jun 2026 00:08:19 +0000 Subject: [PATCH 2/7] =?UTF-8?q?fix(seed):=20update=20credential=20password?= =?UTF-8?q?=20on=20existing=20accounts=20=E2=80=94=20not=20skip=20(GRO-197?= =?UTF-8?q?7)=20(#120)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/seed-uat-credentials.test.ts | 48 ++++++++++++++++++- apps/api/src/db/seed.ts | 10 +++- .../0036_add_missing_coat_type_values.sql | 9 ++++ packages/db/migrations/meta/_journal.json | 4 +- 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 packages/db/migrations/0036_add_missing_coat_type_values.sql diff --git a/apps/api/src/__tests__/seed-uat-credentials.test.ts b/apps/api/src/__tests__/seed-uat-credentials.test.ts index 7f954ae..75eaffc 100644 --- a/apps/api/src/__tests__/seed-uat-credentials.test.ts +++ b/apps/api/src/__tests__/seed-uat-credentials.test.ts @@ -173,7 +173,10 @@ async function seedUatCredentials( ); if (existingAccount) { - // skip — already has credential account + // Re-hash and update the password (mirrors seed.ts behavior) + const { hashPassword } = await import("better-auth/crypto"); + const passwordHash = await hashPassword(password); + existingAccount.password = passwordHash; } else { // Use Better-Auth's hashPassword so test helper matches production seed.ts const { hashPassword } = await import("better-auth/crypto"); @@ -351,6 +354,49 @@ describe("seedUatCredentials — credential provisioning logic", () => { expect(insertedAccounts).toHaveLength(0); }); + // ── AC-8: existing account password IS updated (not frozen at first-seed) ── + + it("AC-8: re-seeding with a changed password env var updates the stored hash", async () => { + const ORIGINAL_PASSWORD = "original-password"; + const ROTATED_PASSWORD = "rotated-password-456"; + + process.env.SEED_UAT_CUSTOMER_PASSWORD = ROTATED_PASSWORD; + + const preExistingUsers: UserRow[] = [ + { id: "pre-existing-user", email: "uat-customer@groombook.dev", name: "UAT Customer", emailVerified: true }, + ]; + // Account was created with the original password on first seed + const originalHash = await hashPassword(ORIGINAL_PASSWORD); + const preExistingAccounts: AccountRow[] = [ + { + id: "pre-existing-acct", + accountId: "pre-existing-user", + providerId: "credential", + userId: "pre-existing-user", + password: originalHash, + }, + ]; + + // Re-seed with the rotated password env var + await seedUatCredentials([UAT_ACCOUNTS[2]!], { + users: preExistingUsers, + accounts: preExistingAccounts, + staff: [], + }); + + // No new user or account created + expect(insertedUsers).toHaveLength(0); + expect(insertedAccounts).toHaveLength(0); + + // The pre-existing account's password WAS updated (not frozen at first-seed). + // hashPassword uses a random salt so we verify by format + that it is a new, + // different valid hash from the original. + const updatedAcct = preExistingAccounts[0]!; + expect(updatedAcct.password).toBeDefined(); + expect(updatedAcct.password).toMatch(/^[a-f0-9]{32}:[a-f0-9]{128}$/); + expect(updatedAcct.password).not.toBe(originalHash); // it actually changed + }); + // ── AC-6: missing env var skips with warning ──────────────────────────────── it("AC-6: missing SEED_UAT_*_PASSWORD env var skips that account (no error)", async () => { diff --git a/apps/api/src/db/seed.ts b/apps/api/src/db/seed.ts index fc65098..e5601d1 100644 --- a/apps/api/src/db/seed.ts +++ b/apps/api/src/db/seed.ts @@ -594,7 +594,15 @@ async function seedKnownUsers() { .limit(1); if (existingAccount) { - console.log(`✓ Credential account for '${acct.email}' already exists — skipping`); + // Re-hash and update the password so that re-seeding rotates credentials + // when the env var changes (e.g. after a password rotation). Previously + // this branch skipped entirely, freezing the hash at first-seed. + const { hashPassword } = await import("better-auth/crypto"); + const passwordHash = await hashPassword(password); + await db.update(schema.account) + .set({ password: passwordHash }) + .where(eq(schema.account.id, existingAccount.id)); + console.log(`✓ Credential account for '${acct.email}' already exists — password updated`); } else { // Use Better-Auth's own hashPassword to guarantee parameter/encoding match. // better-auth/crypto uses: N=16384, r=16, p=1, dkLen=64, salt as 16-byte random diff --git a/packages/db/migrations/0036_add_missing_coat_type_values.sql b/packages/db/migrations/0036_add_missing_coat_type_values.sql new file mode 100644 index 0000000..026c5ef --- /dev/null +++ b/packages/db/migrations/0036_add_missing_coat_type_values.sql @@ -0,0 +1,9 @@ +-- Migration: 0036_add_missing_coat_type_values.sql +-- Adds missing values to coat_type enum that seed.ts requires but which were +-- omitted from the 0031_buffer_rules.sql CREATE TYPE statement (migration drift). +-- 0031 created: 'smooth', 'double', 'wire', 'curly', 'long', 'hairless' +-- Missing (from schema.ts coatTypeEnum): 'short', 'medium', 'silky' + +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'short'; +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'medium'; +ALTER TYPE "coat_type" ADD VALUE IF NOT EXISTS 'silky'; \ No newline at end of file diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 5009d34..1c7c56a 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -248,10 +248,10 @@ "breakpoints": true }, { - "idx": 35, + "idx": 36, "version": "7", "when": 1751480000000, - "tag": "0035_add_missing_coat_type_values", + "tag": "0036_add_missing_coat_type_values", "breakpoints": true } ] From 1faa7945c6e3026261427b7b65aaa9fcd2fbe5f7 Mon Sep 17 00:00:00 2001 From: Flea Flicker <22+gb_flea@noreply.git.farh.net> Date: Mon, 1 Jun 2026 00:23:53 +0000 Subject: [PATCH 3/7] fix(seed): update credential password on re-run instead of skipping (GRO-1977) (#121) fix(seed): update credential password on re-run instead of skipping (GRO-1977) --- UAT_PLAYBOOK.md | 2 + .../__tests__/seed-uat-credentials.test.ts | 41 ++++++++++++++++--- apps/api/src/db/seed.ts | 2 +- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/UAT_PLAYBOOK.md b/UAT_PLAYBOOK.md index 3b6ed03..1c4243f 100644 --- a/UAT_PLAYBOOK.md +++ b/UAT_PLAYBOOK.md @@ -41,6 +41,8 @@ GroomBook API is a Hono-based REST service (TypeScript/Node.js) powering the pet | TC-API-1.8 | Email+password — invalid password | POST /api/auth/sign-in/email with wrong password | 400 Bad Request, error returned | | TC-API-1.9 | Email+password — unknown user | POST /api/auth/sign-in/email with non-existent email | 400 Bad Request, error returned | | TC-API-1.10 | Auto-provision on first OIDC login | First login as a Better-Auth user with no existing staff record | 200 OK, access granted; groomer staff record auto-created with name/email from user table | + +> **Note (GRO-1977):** Seed credential provisioning is idempotent — re-running the seed with updated `SEED_UAT_*_PASSWORD` env vars rotates stored credential hashes. TC-API-1.4 through TC-API-1.7 now return 200 for all 4 UAT personas (previously returned 401 due to frozen-hash bug). | TC-API-1.11 | Existing staff unaffected by OIDC login | Login as uat-groomer@groombook.dev (email+password), then GET /api/staff to find that record | 200 OK, staff record unchanged — no duplicate created, original role and isSuperUser preserved | | TC-API-1.12 | Auto-provisioned role and superUser flags | After TC-API-1.10, GET /api/staff and inspect the auto-created record | role = "groomer", isSuperUser = false, active = true | | TC-API-1.13 | Name fallback — user.name present | Auto-provision where Better-Auth user has name set | Staff name = user.name value from user table | diff --git a/apps/api/src/__tests__/seed-uat-credentials.test.ts b/apps/api/src/__tests__/seed-uat-credentials.test.ts index 75eaffc..9bfccbf 100644 --- a/apps/api/src/__tests__/seed-uat-credentials.test.ts +++ b/apps/api/src/__tests__/seed-uat-credentials.test.ts @@ -67,6 +67,7 @@ let dbAccounts: AccountRow[] = []; let dbStaff: StaffRow[] = []; let insertedUsers: UserRow[] = []; let insertedAccounts: AccountRow[] = []; +let updatedAccounts: Array<{ id: string; password: string }> = []; let updatedStaff: Array<{ id: string; userId: string }> = []; const originalEnv = { ...process.env }; @@ -77,6 +78,7 @@ function resetMock() { dbStaff = []; insertedUsers = []; insertedAccounts = []; + updatedAccounts = []; updatedStaff = []; process.env = { ...originalEnv }; } @@ -173,10 +175,11 @@ async function seedUatCredentials( ); if (existingAccount) { - // Re-hash and update the password (mirrors seed.ts behavior) + // Idempotent update: re-hash the current env password and update the stored hash. const { hashPassword } = await import("better-auth/crypto"); const passwordHash = await hashPassword(password); existingAccount.password = passwordHash; + updatedAccounts.push({ id: existingAccount.id, password: passwordHash }); } else { // Use Better-Auth's hashPassword so test helper matches production seed.ts const { hashPassword } = await import("better-auth/crypto"); @@ -315,9 +318,9 @@ describe("seedUatCredentials — credential provisioning logic", () => { expect(updatedStaff).toHaveLength(0); }); - // ── AC-5: idempotent — skips when user already exists ─────────────────────── + // ── AC-5: idempotent — does not insert duplicate records ─────────────────── - it("AC-5: re-running does not duplicate user or account records (idempotent)", async () => { + it("AC-5: re-running does not insert duplicate user or account records", async () => { process.env.SEED_UAT_CUSTOMER_PASSWORD = TEST_PASSWORD; const preExistingUsers: UserRow[] = [ @@ -333,25 +336,53 @@ describe("seedUatCredentials — credential provisioning logic", () => { }, ]; - // First call — nothing inserted (user + account pre-exist) await seedUatCredentials([UAT_ACCOUNTS[2]!], { users: preExistingUsers, accounts: preExistingAccounts, staff: [], }); + // No inserts — user and account already exist expect(insertedUsers).toHaveLength(0); expect(insertedAccounts).toHaveLength(0); + }); + + // ── AC-5b: password rotation on re-seed ───────────────────────────────────── + + it("AC-5b: re-running with a new password updates the stored credential hash", async () => { + const OLD_PASSWORD = "old-password-abc"; + const NEW_PASSWORD = "new-password-xyz"; + process.env.SEED_UAT_CUSTOMER_PASSWORD = NEW_PASSWORD; + + const preExistingUsers: UserRow[] = [ + { id: "pre-existing-user", email: "uat-customer@groombook.dev", name: "UAT Customer", emailVerified: true }, + ]; + const preExistingAccounts: AccountRow[] = [ + { + id: "pre-existing-acct", + accountId: "pre-existing-user", + providerId: "credential", + userId: "pre-existing-user", + password: await hashPassword(OLD_PASSWORD), + }, + ]; - // Second call — still nothing inserted await seedUatCredentials([UAT_ACCOUNTS[2]!], { users: preExistingUsers, accounts: preExistingAccounts, staff: [], }); + // No new records inserted expect(insertedUsers).toHaveLength(0); expect(insertedAccounts).toHaveLength(0); + // Password WAS updated to the new env value + expect(updatedAccounts).toHaveLength(1); + expect(updatedAccounts[0]!.id).toBe("pre-existing-acct"); + // New hash is valid Better-Auth format (salt:key, each hex) + const newHashParts = updatedAccounts[0]!.password.split(":"); + expect(Buffer.from(newHashParts[0]!, "hex")).toHaveLength(16); + expect(Buffer.from(newHashParts[1]!, "hex")).toHaveLength(64); }); // ── AC-8: existing account password IS updated (not frozen at first-seed) ── diff --git a/apps/api/src/db/seed.ts b/apps/api/src/db/seed.ts index e5601d1..5b48dd6 100644 --- a/apps/api/src/db/seed.ts +++ b/apps/api/src/db/seed.ts @@ -602,7 +602,7 @@ async function seedKnownUsers() { await db.update(schema.account) .set({ password: passwordHash }) .where(eq(schema.account.id, existingAccount.id)); - console.log(`✓ Credential account for '${acct.email}' already exists — password updated`); + console.log(`✓ Updated credential account password for '${acct.email}'`); } else { // Use Better-Auth's own hashPassword to guarantee parameter/encoding match. // better-auth/crypto uses: N=16384, r=16, p=1, dkLen=64, salt as 16-byte random From 97da5f332e9925a18838d322c4981f57fef5b00f Mon Sep 17 00:00:00 2001 From: Paperclip Date: Mon, 1 Jun 2026 00:34:50 +0000 Subject: [PATCH 4/7] fix(seed): restore deterministic alerts for TestCooper/TestRocky (GRO-1962) 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 --- packages/db/src/seed.ts | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 27500c4..cf65909 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1106,14 +1106,17 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { - // ~30% of pets get alerts; TestCooper/TestRocky get deterministic types + // TestCooper always has a behavioral alert; TestRocky always has a skin alert. + // All other UAT test pets follow the 30% random distribution. + // Deterministic alerts on 2 of 507 pets (~0.4%) do not meaningfully shift + // the overall distribution from the 25-35% target band. + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } if (rand() < 0.3) { - if (uc.petName === "TestCooper") { - return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); - } - if (uc.petName === "TestRocky") { - return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); - } const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); } @@ -1136,14 +1139,17 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { - // ~30% of pets get alerts; TestCooper/TestRocky get deterministic types + // TestCooper always has a behavioral alert; TestRocky always has a skin alert. + // All other UAT test pets follow the 30% random distribution. + // Deterministic alerts on 2 of 507 pets (~0.4%) do not meaningfully shift + // the overall distribution from the 25-35% target band. + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } if (rand() < 0.3) { - if (uc.petName === "TestCooper") { - return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); - } - if (uc.petName === "TestRocky") { - return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); - } const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); } From b15a53a19b398f370edd4fd4b612b254f0f81049 Mon Sep 17 00:00:00 2001 From: The Dogfather <20+gb_dogfather@noreply.git.farh.net> Date: Mon, 1 Jun 2026 00:35:35 +0000 Subject: [PATCH 5/7] fix(seed): restore deterministic alerts for TestCooper/TestRocky (GRO-1962) (#122) Co-authored-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net> Co-committed-by: The Dogfather <20+gb_dogfather@noreply.git.farh.net> --- packages/db/src/seed.ts | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 27500c4..cf65909 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -1106,14 +1106,17 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { - // ~30% of pets get alerts; TestCooper/TestRocky get deterministic types + // TestCooper always has a behavioral alert; TestRocky always has a skin alert. + // All other UAT test pets follow the 30% random distribution. + // Deterministic alerts on 2 of 507 pets (~0.4%) do not meaningfully shift + // the overall distribution from the 25-35% target band. + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } if (rand() < 0.3) { - if (uc.petName === "TestCooper") { - return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); - } - if (uc.petName === "TestRocky") { - return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); - } const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); } @@ -1136,14 +1139,17 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { - // ~30% of pets get alerts; TestCooper/TestRocky get deterministic types + // TestCooper always has a behavioral alert; TestRocky always has a skin alert. + // All other UAT test pets follow the 30% random distribution. + // Deterministic alerts on 2 of 507 pets (~0.4%) do not meaningfully shift + // the overall distribution from the 25-35% target band. + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } if (rand() < 0.3) { - if (uc.petName === "TestCooper") { - return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); - } - if (uc.petName === "TestRocky") { - return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); - } const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); } From 17d261fa9439c3b4ceabbabd4924b003021e472d Mon Sep 17 00:00:00 2001 From: Paperclip Date: Mon, 1 Jun 2026 11:58:33 +0000 Subject: [PATCH 6/7] fix(docker): install pnpm via npm instead of corepack shim (GRO-1983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Dockerfile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Dockerfile b/Dockerfile index b9d73bf..5fea669 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,10 @@ FROM node:22-alpine AS base -RUN corepack enable && corepack install -g pnpm@9.15.4 -ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 -ENV COREPACK_ENABLE_STRICT=0 +# Install pnpm as a real binary via npm (not corepack shim) so runtime +# invocations of `pnpm` work without DNS access to registry.npmjs.org. +# The corepack shim delegates to corepack, which re-validates against +# npmjs.org on first use — that fails in air-gapped UAT seed/migrate/reset +# Jobs. GRO-1983 / GRO-1889 / GRO-1909. +RUN npm install -g pnpm@9.15.4 WORKDIR /app # Install deps @@ -22,9 +25,7 @@ RUN pnpm --filter @groombook/types build && \ # Runtime FROM node:22-alpine AS runner -RUN corepack enable && corepack install -g pnpm@9.15.4 -ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 -ENV COREPACK_ENABLE_STRICT=0 +RUN npm install -g pnpm@9.15.4 WORKDIR /app ENV NODE_ENV=production @@ -53,7 +54,4 @@ CMD ["pnpm", "--filter", "@groombook/db", "seed"] # Reset stage — drops all tables, re-runs migrations, and re-seeds FROM builder AS reset -RUN corepack enable && corepack install -g pnpm@9.15.4 -ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 -ENV COREPACK_ENABLE_STRICT=0 CMD ["pnpm", "--filter", "@groombook/db", "reset"] From f262c19561e7314cef79760d736eb8df7ebb20f8 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Mon, 1 Jun 2026 00:42:05 +0000 Subject: [PATCH 7/7] =?UTF-8?q?feat(db):=20add=200037=5Fadd=5Fextra=5Flarg?= =?UTF-8?q?e=5Fto=5Fpet=5Fsize=5Fcategory=20=E2=80=94=20register=20extra?= =?UTF-8?q?=5Flarge=20in=20journal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ...7_add_extra_large_to_pet_size_category.sql | 19 +++++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 +++++++ 2 files changed, 26 insertions(+) create mode 100644 packages/db/migrations/0037_add_extra_large_to_pet_size_category.sql diff --git a/packages/db/migrations/0037_add_extra_large_to_pet_size_category.sql b/packages/db/migrations/0037_add_extra_large_to_pet_size_category.sql new file mode 100644 index 0000000..e7eac1a --- /dev/null +++ b/packages/db/migrations/0037_add_extra_large_to_pet_size_category.sql @@ -0,0 +1,19 @@ +-- Migration: 0037_add_extra_large_to_pet_size_category.sql +-- GRO-1979: Adds the 'extra_large' value to the pet_size_category enum. +-- +-- 0031_buffer_rules.sql created pet_size_category with values +-- ('small', 'medium', 'large', 'xlarge'), but seed.ts and the drizzle +-- schema (PetSizeCategory type) both use 'extra_large' — a mismatch that +-- caused the UAT seed job to fail with: +-- invalid input value for enum pet_size_category: "extra_large" +-- +-- 0035/0036 (GRO-1971) registered 'short'/'medium'/'silky' in coat_type. +-- This migration is the pet_size_category counterpart: register +-- 'extra_large' so seed.ts can write the value the schema declares. +-- +-- Postgres restriction: ALTER TYPE ADD VALUE cannot run inside a +-- transaction block. The drizzle migrate runner does not wrap +-- individual statements in an explicit transaction, so this applies +-- as a single auto-commit DDL. + +ALTER TYPE "pet_size_category" ADD VALUE IF NOT EXISTS 'extra_large'; diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 1c7c56a..5ae5432 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -253,6 +253,13 @@ "when": 1751480000000, "tag": "0036_add_missing_coat_type_values", "breakpoints": true + }, + { + "idx": 37, + "version": "7", + "when": 1751500000000, + "tag": "0037_add_extra_large_to_pet_size_category", + "breakpoints": true } ] } \ No newline at end of file