Merge branch 'main' into fix/gro-354-client-portal-redirect

This commit is contained in:
groombook-qa[bot]
2026-04-01 10:29:17 +00:00
committed by GitHub
4 changed files with 56 additions and 36 deletions
+24 -2
View File
@@ -309,17 +309,39 @@ jobs:
- name: Update dev overlay image tags - name: Update dev overlay image tags
env: env:
TAG: ${{ needs.docker.outputs.tag }} TAG: ${{ needs.docker.outputs.tag }}
SHA: ${{ github.sha }}
run: | run: |
if [ -z "$TAG" ]; then if [ -z "$TAG" ]; then
TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}" TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}"
fi fi
export SHORT_SHA="${SHA::7}"
echo "Updating dev overlay image tags to: $TAG" echo "Updating dev overlay image tags to: $TAG"
echo "Updating migration/seed Job names with SHA: $SHORT_SHA"
cd /tmp/infra cd /tmp/infra
DEV_KUST="apps/groombook/overlays/dev/kustomization.yaml" DEV_KUST="apps/groombook/overlays/dev/kustomization.yaml"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/api")).newTag = env(TAG)' "$DEV_KUST" yq -i '(.images[] | select(.name == "ghcr.io/groombook/api")).newTag = env(TAG)' "$DEV_KUST"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/web")).newTag = env(TAG)' "$DEV_KUST" yq -i '(.images[] | select(.name == "ghcr.io/groombook/web")).newTag = env(TAG)' "$DEV_KUST"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/migrate")).newTag = env(TAG)' "$DEV_KUST" yq -i '(.images[] | select(.name == "ghcr.io/groombook/migrate")).newTag = env(TAG)' "$DEV_KUST"
yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$DEV_KUST" yq -i '(.images[] | select(.name == "ghcr.io/groombook/seed")).newTag = env(TAG)' "$DEV_KUST"
# Update migrate Job name to include short SHA (immutable template fix)
MIGRATE_JOB="apps/groombook/base/migrate-job.yaml"
if [ -f "$MIGRATE_JOB" ]; then
yq -i '.metadata.name = "migrate-schema-" + env(SHORT_SHA)' "$MIGRATE_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$MIGRATE_JOB"
# Ensure ttlSecondsAfterFinished is set for automatic cleanup
yq -i '.spec.ttlSecondsAfterFinished //= 86400' "$MIGRATE_JOB"
fi
# Update seed Job name to include short SHA (immutable template fix)
SEED_JOB="apps/groombook/base/seed-job.yaml"
if [ -f "$SEED_JOB" ]; then
yq -i '.metadata.name = "seed-test-data-" + env(SHORT_SHA)' "$SEED_JOB"
yq -i '.metadata.annotations."groombook.app/deploy-version" = env(TAG)' "$SEED_JOB"
# Ensure ttlSecondsAfterFinished is set for automatic cleanup
yq -i '.spec.ttlSecondsAfterFinished //= 86400' "$SEED_JOB"
fi
git -C /tmp/infra diff --stat git -C /tmp/infra diff --stat
- name: Create PR on groombook/infra - name: Create PR on groombook/infra
@@ -335,8 +357,8 @@ jobs:
git config user.name "groombook-engineer[bot]" git config user.name "groombook-engineer[bot]"
git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com" git config user.email "3141748+groombook-engineer[bot]@users.noreply.github.com"
git checkout -b "chore/update-image-tags-${TAG}" git checkout -b "chore/update-image-tags-${TAG}"
git add apps/groombook/overlays/dev/ git add apps/groombook/overlays/dev/ apps/groombook/base/migrate-job.yaml apps/groombook/base/seed-job.yaml
git commit -m "chore: update image tags to ${TAG}" git commit -m "chore: update image tags and migration/seed Job names to ${TAG}"
git push -u origin "chore/update-image-tags-${TAG}" git push -u origin "chore/update-image-tags-${TAG}"
+13 -15
View File
@@ -37,10 +37,10 @@ const DEMO_PET = {
}; };
const DEMO_SERVICES = [ const DEMO_SERVICES = [
{ name: "Bath & Brush", description: "Full bath, blow-dry, brush out, and ear cleaning", basePriceCents: 4500, durationMinutes: 45 }, { id: "a0000001-0000-0000-0000-000000000001", name: "Bath & Brush", description: "Full bath, blow-dry, brush out, and ear cleaning", basePriceCents: 4500, durationMinutes: 45 },
{ name: "Full Groom — Small", description: "Complete grooming for dogs under 25 lbs", basePriceCents: 6500, durationMinutes: 60 }, { id: "a0000001-0000-0000-0000-000000000002", name: "Full Groom — Small", description: "Complete grooming for dogs under 25 lbs", basePriceCents: 6500, durationMinutes: 60 },
{ name: "Full Groom — Medium", description: "Complete grooming for dogs 25-50 lbs", basePriceCents: 8000, durationMinutes: 75 }, { id: "a0000001-0000-0000-0000-000000000003", name: "Full Groom — Medium", description: "Complete grooming for dogs 25-50 lbs", basePriceCents: 8000, durationMinutes: 75 },
{ name: "Nail Trim", description: "Nail clipping and filing", basePriceCents: 1500, durationMinutes: 15 }, { id: "a0000001-0000-0000-0000-000000000004", name: "Nail Trim", description: "Nail clipping and filing", basePriceCents: 1500, durationMinutes: 15 },
]; ];
adminSeedRouter.post("/seed", async (c) => { adminSeedRouter.post("/seed", async (c) => {
@@ -71,18 +71,16 @@ adminSeedRouter.post("/seed", async (c) => {
results.push(`Created staff '${KNOWN_STAFF.name}' (id: ${created!.id}, oidcSub: ${KNOWN_STAFF.oidcSub})`); results.push(`Created staff '${KNOWN_STAFF.name}' (id: ${created!.id}, oidcSub: ${KNOWN_STAFF.oidcSub})`);
} }
// ── Services: only seed if none exist ───────────────────────────────────── // ── Services: idempotent upsert ─────────────────────────────────────────────
const existingServices = await db.select().from(services).limit(1); for (const svc of DEMO_SERVICES) {
if (existingServices.length > 0) { await db.insert(services)
results.push("Services already exist — skipping"); .values({ ...svc, active: true })
} else { .onConflictDoUpdate({
const created: { id: string; name: string }[] = []; target: services.id,
for (const svc of DEMO_SERVICES) { set: { name: svc.name, description: svc.description, basePriceCents: svc.basePriceCents, durationMinutes: svc.durationMinutes, active: true },
const [row] = await db.insert(services).values({ ...svc, active: true }).returning(); });
created.push(row!);
}
results.push(`Created ${created.length} services: ${created.map((s) => s.name).join(", ")}`);
} }
results.push(`Upserted ${DEMO_SERVICES.length} services`);
// ── Client: Demo Client ─────────────────────────────────────────────────── // ── Client: Demo Client ───────────────────────────────────────────────────
const [existingClient] = await db const [existingClient] = await db
+4 -4
View File
@@ -31,14 +31,14 @@ function parseDate(value: string | undefined, fallback: Date): Date {
function defaultFrom(): Date { function defaultFrom(): Date {
const d = new Date(); const d = new Date();
d.setDate(d.getDate() - 30); d.setUTCDate(d.getUTCDate() - 30);
d.setHours(0, 0, 0, 0); d.setUTCHours(0, 0, 0, 0);
return d; return d;
} }
function defaultTo(): Date { function defaultTo(): Date {
const d = new Date(); const d = new Date();
d.setHours(23, 59, 59, 999); d.setUTCHours(23, 59, 59, 999);
return d; return d;
} }
@@ -283,7 +283,7 @@ reportsRouter.get("/clients", async (c) => {
// Clients with no appointment in last 90 days (churn risk) // Clients with no appointment in last 90 days (churn risk)
const ninetyDaysAgo = new Date(); const ninetyDaysAgo = new Date();
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90); ninetyDaysAgo.setUTCDate(ninetyDaysAgo.getUTCDate() - 90);
const ninetyDaysAgoISO = ninetyDaysAgo.toISOString(); const ninetyDaysAgoISO = ninetyDaysAgo.toISOString();
const churnRisk = await db const churnRisk = await db
+15 -15
View File
@@ -293,22 +293,22 @@ async function seedKnownUsers() {
console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)"); console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)");
} }
// ── Services: only seed if none exist ── // ── Services: idempotent upsert using deterministic IDs ──
const existingServices = await db.select().from(schema.services).limit(1); const demoSvcs = [
if (existingServices.length > 0) { { id: "a0000001-0000-0000-0000-000000000001", name: "Bath & Brush", description: "Full bath, blow-dry, brush out, and ear cleaning", basePriceCents: 4500, durationMinutes: 45 },
console.log("✓ Services already exist — skipping"); { id: "a0000001-0000-0000-0000-000000000002", name: "Full Groom — Small", description: "Complete grooming for dogs under 25 lbs", basePriceCents: 6500, durationMinutes: 60 },
} else { { id: "a0000001-0000-0000-0000-000000000003", name: "Full Groom — Medium", description: "Complete grooming for dogs 25-50 lbs", basePriceCents: 8000, durationMinutes: 75 },
const demoSvcs = [ { id: "a0000001-0000-0000-0000-000000000004", name: "Nail Trim", description: "Nail clipping and filing", basePriceCents: 1500, durationMinutes: 15 },
{ name: "Bath & Brush", description: "Full bath, blow-dry, brush out, and ear cleaning", basePriceCents: 4500, durationMinutes: 45 }, ];
{ name: "Full Groom — Small", description: "Complete grooming for dogs under 25 lbs", basePriceCents: 6500, durationMinutes: 60 }, for (const svc of demoSvcs) {
{ name: "Full Groom — Medium", description: "Complete grooming for dogs 25-50 lbs", basePriceCents: 8000, durationMinutes: 75 }, await db.insert(schema.services)
{ name: "Nail Trim", description: "Nail clipping and filing", basePriceCents: 1500, durationMinutes: 15 }, .values({ ...svc, active: true })
]; .onConflictDoUpdate({
for (const svc of demoSvcs) { target: schema.services.id,
await db.insert(schema.services).values({ ...svc, active: true }); set: { name: svc.name, description: svc.description, basePriceCents: svc.basePriceCents, durationMinutes: svc.durationMinutes, active: true },
} });
console.log(`✓ Created ${demoSvcs.length} services`);
} }
console.log(`✓ Seeded ${demoSvcs.length} services`);
// ── Client: Demo Client ── // ── Client: Demo Client ──
const [existingClient] = await db const [existingClient] = await db