Skip to content

feat(mentorship-sync): implement mentorship-sync CronJob with fixture source for DEV#141

Merged
lewisojile merged 41 commits into
mainfrom
feat/mentorship-sync
Jun 16, 2026
Merged

feat(mentorship-sync): implement mentorship-sync CronJob with fixture source for DEV#141
lewisojile merged 41 commits into
mainfrom
feat/mentorship-sync

Conversation

@mlehotskylf

@mlehotskylf mlehotskylf commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the mentorship-sync K8s CronJob that pulls Mentorship program data from ANALYTICS.GOLD_FACT.MENTORSHIP_PROGRAMS in Snowflake and upserts initiative_type = mentorship rows into CF Postgres.

What's included:

  • snowflake.Client — connects via RSA key-pair JWT auth, selects PROGRAM_ID, PROGRAM_NAME, PROGRAM_STATUS, OWNER_LF_USERNAME from the gold model
  • snowflake.FixtureSource — reads a JSON file for DEV/local testing (no Snowflake credentials needed)
  • mentorshipSource interface — both implementations satisfy it; selected by MENTORSHIP_SYNC_FIXTURE_FILE env var
  • MentorshipRepository — upserts initiative rows, resolves owner_id by upserting a stub user row keyed on OWNER_LF_USERNAME (satisfies NOT NULL FK), atomic beneficiary replacement (delete-then-insert in a transaction)
  • Migration 002 — adds UNIQUE (jobspring_project_id) constraint required for ON CONFLICT upsert
  • Dockerfile.mentorship-sync with testdata embedded for DEV fixture use
  • Helm CronJob template + values (mentorshipSyncCronJob) in the backend chart
  • CI: ci-backend.yml and ci-release.yml updated to build and push ghcr.io/linuxfoundation/lfx-crowdfunding-mentorship-sync

Companion PRs:

  • lfx-v2-argocd#959 — enables the CronJob in dev/staging/prod
  • lfx-secrets-management#273 — syncs SNOWFLAKE_PRIVATE_KEY to AWS Secrets Manager

Additional changes (post-review)

Default status=published on initiative list endpoints (behaviour change — intentional)

GET /v1/initiatives and GET /v1/me/initiatives now default to status=published when the ?status query param is omitted. Previously an absent param meant no status filter, so all statuses were returned. Callers can still request other statuses explicitly (e.g. ?status=submitted). This aligns the API with the expected public-facing behaviour where unapproved initiatives should not be visible by default.

Helm chart: mentorshipSyncCronJob.image.tag is now required when enabled

Previously, omitting mentorshipSyncCronJob.image.tag silently fell back to the main API image.tag, which would pull from the wrong image repository. A validate.yaml guard now aborts helm install/upgrade with a clear error when the CronJob is enabled but its dedicated image tag is unset.

Syncer: OwnerLFUsername is trimmed before validation and upsert

strings.TrimSpace was previously only used in the guard condition; the untrimmed value was passed to UpsertProgram. The trimmed value is now assigned back to p.OwnerLFUsername so whitespace-padded usernames from Snowflake are cleaned before being persisted.

Context

OWNER_LF_USERNAME is sourced from data:lfid in the Jobspring bronze model (added in lf-dbt#2504). On each sync, a stub users row is created via INSERT ... ON CONFLICT (username) DO NOTHING to satisfy the initiatives.owner_id NOT NULL FK without touching existing user profiles.

Status values from Snowflake are Title-case (Published, Hidden, Pending, Rejected). The syncer lowercases all values and additionally maps the legacy Jobspring hidehidden.

Beneficiaries (SELECTED_MENTEES) are not yet fetched — the query will be extended in a follow-up once the gold model exposes them as structured columns. UpsertBeneficiaries is skipped when Beneficiaries is nil (Snowflake path) to avoid deleting existing rows.

Test Plan

  • cd backend && make test — all packages pass
  • go build ./cmd/mentorship-sync/... — binary compiles
  • Run locally with MENTORSHIP_SYNC_FIXTURE_FILE — syncer completes with total=3 upserted=3 errors=0
  • Run locally against real Snowflake (ANALYTICS.GOLD_FACT.MENTORSHIP_PROGRAMS) — total=1485 upserted=1485 errors=0 in ~18s
  • Migration 002 applies cleanly — UNIQUE constraint created
  • status = hidden normalised from Snowflake Hidden and legacy hide
  • GET /v1/initiatives without ?status returns only published initiatives
  • helm template with mentorshipSyncCronJob.enabled=true and no image.tag aborts with a clear error

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 12, 2026 21:19
Comment thread backend/go.mod
Comment thread backend/go.mod
Comment thread backend/go.mod
Comment thread backend/go.mod
Comment thread backend/go.mod
Comment thread backend/go.mod
Comment thread backend/go.mod

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Implements the backend pieces needed to run a new mentorship-sync CronJob that sources Mentorship program data from Snowflake (or a JSON fixture in DEV) and upserts mentorship initiatives plus beneficiary rows into the Crowdfunding Postgres database.

Changes:

  • Added a Snowflake client and a DEV-only JSON fixture source behind a shared FetchPrograms shape.
  • Added mentorship domain models and a MentorshipRepository contract + Postgres implementation.
  • Added the mentorship-sync command (syncer, entrypoint), fixture data, and a dedicated Dockerfile.

Reviewed changes

Copilot reviewed 16 out of 17 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
docs/superpowers/plans/2026-06-12-mentorship-sync.md Implementation plan and task checklist for the mentorship-sync workstream.
backend/internal/infrastructure/snowflake/fixture_source.go JSON fixture-backed Snowflake source for DEV/local runs.
backend/internal/infrastructure/snowflake/fixture_source_test.go Unit tests for fixture source parsing and missing-file behavior.
backend/internal/infrastructure/snowflake/client.go Snowflake client for fetching mentorship programs via SQL.
backend/internal/infrastructure/snowflake/client_test.go Mock-driver unit test asserting query/table reference and scan mapping.
backend/internal/infrastructure/db/mentorship_repository.go Postgres repository for program upsert + beneficiary replacement.
backend/internal/infrastructure/db/mentorship_repository_test.go Compile-time interface conformance test for mentorship repository.
backend/internal/domain/repository.go Adds MentorshipRepository interface to domain contracts.
backend/internal/domain/models/mentorship_sync.go Adds mentorship sync domain models (MentorshipProgram, MentorshipBeneficiary).
backend/go.mod Adds Snowflake driver dependency graph to module metadata.
backend/go.sum Adds checksums for new dependencies (Snowflake driver and transitive deps).
backend/Dockerfile.mentorship-sync Builds a standalone mentorship-sync container image and embeds fixture data.
backend/cmd/mentorship-sync/testdata/programs.json DEV fixture dataset (active/pending/hidden) including beneficiaries.
backend/cmd/mentorship-sync/syncer.go Sync orchestration logic (fetch → upsert program → upsert beneficiaries).
backend/cmd/mentorship-sync/syncer_test.go Unit tests for syncer behavior (counts, status normalization, error handling).
backend/cmd/mentorship-sync/main.go CronJob entrypoint wiring DB pool + source selection via env vars.
backend/cmd/mentorship-sync/helpers_test.go Test helper logger for syncer tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread backend/internal/infrastructure/db/mentorship_repository.go Outdated
Comment thread backend/internal/infrastructure/snowflake/client.go
Comment thread backend/internal/infrastructure/snowflake/client.go Outdated
Comment thread backend/internal/infrastructure/snowflake/client.go
Comment thread backend/cmd/mentorship-sync/syncer.go Outdated
mlehotskylf added a commit that referenced this pull request Jun 13, 2026
- db/migrations: add migration 002 — UNIQUE constraint on
  initiatives.jobspring_project_id, required for ON CONFLICT upsert
  (per copilot-pull-request-reviewer)

- snowflake/client.go: remove duplicate blank import of gosnowflake
  (per copilot-pull-request-reviewer)

- snowflake/client.go: set Authenticator: gosnowflake.AuthTypeJwt —
  SDK defaults to password auth; JWT must be explicit for key-pair auth
  (per copilot-pull-request-reviewer)

- mentorship_sync.go + syncer.go: skip UpsertBeneficiaries when
  Beneficiaries is nil (source did not provide data) to prevent
  unconditional delete of existing rows; non-nil empty slice still
  triggers delete as intended. Added two tests covering both cases
  (per copilot-pull-request-reviewer)

- go.mod: bump github.com/apache/thrift 0.22.0 → 0.23.0 to fix
  CVE-2026-41602 (HIGH, integer overflow in TFramedTransport);
  mark gosnowflake as direct dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 13, 2026 02:58
@mlehotskylf

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed

Commit: 8b23ee4

Changes Made

  • db/migrations/002_jobspring_project_id_unique.up.sql: Added UNIQUE (jobspring_project_id) constraint — required for ON CONFLICT upsert to work (per copilot-pull-request-reviewer)
  • snowflake/client.go: Removed duplicate blank import of gosnowflake (per copilot-pull-request-reviewer)
  • snowflake/client.go: Added Authenticator: gosnowflake.AuthTypeJwt — SDK defaults to password auth; JWT must be set explicitly for key-pair auth (per copilot-pull-request-reviewer)
  • syncer.go + mentorship_sync.go: Skip UpsertBeneficiaries when Beneficiaries is nil (source did not provide data), preventing unconditional delete of existing rows. Non-nil empty slice still triggers delete as intended. Added two tests covering both cases (per copilot-pull-request-reviewer)
  • go.mod: Bumped github.com/apache/thrift 0.22.0 → 0.23.0 to resolve CVE-2026-41602 (HIGH — integer overflow in TFramedTransport, introduced as transitive dep of gosnowflake); marked gosnowflake as direct dependency

Declined

  • snowflake/client.go — PEM len(rest) > 0 check: pem.Decode consumes all trailing whitespace/newlines; remaining bytes only occur with a second PEM block or corrupt input. Strictness is intentional (flagged by copilot-pull-request-reviewer)

Threads Resolved

  • 5 copilot-pull-request-reviewer threads (4 fixed, 1 declined)
  • 7 github-license-compliance threads — known false positives for LicenseRef-scancode-google-patent-license-golang on Google-authored Go packages; compatible with project MIT license

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 20 changed files in this pull request and generated 5 comments.

Comment thread backend/cmd/mentorship-sync/syncer.go Outdated
Comment thread backend/internal/infrastructure/snowflake/fixture_source.go
Comment thread backend/cmd/mentorship-sync/testdata/programs.json
Comment thread backend/cmd/mentorship-sync/testdata/programs.json
Comment thread backend/cmd/mentorship-sync/syncer_test.go
mlehotskylf and others added 13 commits June 12, 2026 20:48
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
…ory interface

Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Implement FixtureSource to read mentorship programs from a JSON fixture file.
Enables testing mentorship-sync logic without Snowflake credentials. All tests pass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Implements the Snowflake Client that connects using key-pair authentication
and queries the ANALYTICS.GOLD_FACT.MENTORSHIP_PROGRAMS table. Includes unit
test with mock driver to verify SQL and field mapping without requiring live
Snowflake credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
…iciary replacement

Implements domain.MentorshipRepository against PostgreSQL with three methods:
- UpsertProgram: Creates/updates mentorship initiatives by jobspring_project_id
- UpsertBeneficiaries: Replaces beneficiary lists in a transaction
- ListJobspringIDs: Returns all mentorship program identifiers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Adds syncer that orchestrates the mentorship-sync run cycle (fetch programs,
normalize status, upsert to DB, and log per-run counters) plus main entry
point for K8s CronJob deployment. Includes comprehensive test coverage for
normal flow, error handling, and status normalization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
…o prevent silent data loss

Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
…OGRAMS schema

The original query used column names that do not exist in
ANALYTICS.GOLD_FACT.MENTORSHIP_PROGRAMS, which would have caused a SQL
compilation error on the first real run.

Changes:
- Fix column names: jobspring_project_id→PROGRAM_ID, name→PROGRAM_NAME,
  status→PROGRAM_STATUS
- Remove mentee_goal_cents — no such column exists in the Snowflake view
- Remove MenteeGoalCents from MentorshipProgram model
- Normalise status with strings.ToLower instead of a hard-coded "hide"
  check — real values are Title-case (Published, Pending, Hidden, Rejected)
- Update all tests and fixture data to use real column names, real
  PROGRAM_ID UUIDs, and real Title-case status values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Adds a dedicated mentorship-sync CronJob to the Helm chart:
- templates/cronjob-mentorship-sync.yaml — CronJob resource that runs
  /app/mentorship-sync daily at 02:00 UTC from the dedicated image
  ghcr.io/linuxfoundation/lfx-crowdfunding-mentorship-sync
- values.yaml — mentorshipSyncCronJob block with schedule, image,
  resources, and fixtureFile (injected as MENTORSHIP_SYNC_FIXTURE_FILE
  when set, enabling fixture-based DEV runs without Snowflake creds)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
- db/migrations: add migration 002 — UNIQUE constraint on
  initiatives.jobspring_project_id, required for ON CONFLICT upsert
  (per copilot-pull-request-reviewer)

- snowflake/client.go: remove duplicate blank import of gosnowflake
  (per copilot-pull-request-reviewer)

- snowflake/client.go: set Authenticator: gosnowflake.AuthTypeJwt —
  SDK defaults to password auth; JWT must be explicit for key-pair auth
  (per copilot-pull-request-reviewer)

- mentorship_sync.go + syncer.go: skip UpsertBeneficiaries when
  Beneficiaries is nil (source did not provide data) to prevent
  unconditional delete of existing rows; non-nil empty slice still
  triggers delete as intended. Added two tests covering both cases
  (per copilot-pull-request-reviewer)

- go.mod: bump github.com/apache/thrift 0.22.0 → 0.23.0 to fix
  CVE-2026-41602 (HIGH, integer overflow in TFramedTransport);
  mark gosnowflake as direct dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
- fixture_source.go: preserve nil Beneficiaries when JSON field is absent
  — make([]T, 0) always returns non-nil, breaking nil=skip semantics;
  now only allocates when r.Beneficiaries != nil (per copilot-pull-request-reviewer)

- syncer.go: add explicit "hide" → "hidden" normalisation after ToLower
  — strings.ToLower("hide") == "hide", not "hidden"; legacy Jobspring
  value requires a dedicated remap step (per copilot-pull-request-reviewer)

- syncer_test.go: add "hide" → "hidden" case to normalisation table test
  (per copilot-pull-request-reviewer)

- testdata/programs.json: replace real mentee names/emails with
  anonymised example.com addresses; add a "hide" status entry to
  exercise the legacy normalisation path (per copilot-pull-request-reviewer)

- fixture_source_test.go: add test asserting nil Beneficiaries when
  "beneficiaries" JSON key is absent (per copilot-pull-request-reviewer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Michal Lehotsky <mlehotsky@linuxfoundation.org>
Copilot AI review requested due to automatic review settings June 13, 2026 03:49
@mlehotskylf mlehotskylf force-pushed the feat/mentorship-sync branch from d423a4b to 09cd611 Compare June 13, 2026 03:49
@mlehotskylf

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (round 2)

Commit: 09cd611

Changes Made

  • fixture_source.go: Preserve nil Beneficiaries when JSON beneficiaries field is absent — make([]T, 0) always returns non-nil, which would cause the syncer to delete existing beneficiary rows. Now only allocates when r.Beneficiaries != nil (per copilot-pull-request-reviewer)
  • syncer.go: Added explicit "hide" → "hidden" remap after strings.ToLowerToLower("hide") == "hide", not "hidden" (per copilot-pull-request-reviewer)
  • syncer_test.go: Added {"hide", "hidden"} case to normalisation table test (per copilot-pull-request-reviewer)
  • testdata/programs.json: Replaced real mentee names/emails with anonymised example.com addresses; replaced one entry with "status": "hide" to exercise legacy normalisation (per copilot-pull-request-reviewer)
  • fixture_source_test.go: Added test asserting nil Beneficiaries when JSON field is absent (per copilot-pull-request-reviewer)

Threads Resolved

5 of 5 unresolved threads addressed.

@mlehotskylf

Copy link
Copy Markdown
Contributor Author

⚠️ Pre-existing CI failures — the following MegaLinter checks are failing but are unrelated to this PR's changes and were already failing on main before this diff:

  • trivy: esbuild HIGH vulnerability (GHSA-gv7w-rqvm-qjhr) in frontend/pnpm-lock.yaml — pre-existing frontend dependency
  • checkov: Helm chart render fails without required values (STRIPE_RETURN_URL, image.tag) — checkov renders charts without env-specific values, pre-existing behaviour

No action needed from this PR.

Without this, a row that already exists with a NULL or stale
jobspring_project_id (e.g. from a manual insert or earlier schema
version) would never be corrected. Adding EXCLUDED.jobspring_project_id
keeps the column consistent and makes the UNIQUE constraint meaningful.

Signed-off-by: lewisojile <lewisojile@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 21 out of 22 changed files in this pull request and generated 4 comments.

Comment thread backend/internal/infrastructure/snowflake/client.go
Comment thread backend/internal/infrastructure/db/mentorship_repository.go
Comment thread backend/cmd/mentorship-sync/syncer.go Outdated
Comment thread backend/internal/infrastructure/db/mentorship_repository.go
When no ?status query param is supplied, GET /v1/initiatives and
GET /v1/me/initiatives now filter by status=published by default.
Callers can still override by passing ?status=<value> explicitly.

Signed-off-by: lewisojile <lewisojile@gmail.com>
Copilot AI review requested due to automatic review settings June 15, 2026 15:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated 4 comments.

Comment thread backend/internal/infrastructure/snowflake/client.go
Comment thread backend/charts/lfx-crowdfunding-backend/templates/cronjob-mentorship-sync.yaml Outdated
Comment thread backend/cmd/mentorship-sync/syncer.go Outdated
Comment thread backend/internal/handler/initiative_handler.go
…nabled

Previously, omitting mentorshipSyncCronJob.image.tag silently fell back to
the main API image.tag, which would pull from the wrong image repository
(lfx-crowdfunding-mentorship-sync vs lfx-crowdfunding-backend).

Add a validate.yaml guard that aborts helm install/upgrade when the CronJob
is enabled but its image tag is unset. Remove the fallback default from the
CronJob template so the tag is used directly.

Signed-off-by: lewisojile <lewisojile@gmail.com>
TrimSpace was only used in the guard condition, so a whitespace-padded
username would pass the check but be persisted with surrounding whitespace,
potentially creating stub users.username entries or causing FK lookup issues.

Assign the trimmed value back to p.OwnerLFUsername before the empty check
so the cleaned value flows through to UpsertProgram.

Signed-off-by: lewisojile <lewisojile@gmail.com>
Copilot AI review requested due to automatic review settings June 15, 2026 16:04

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 2 comments.

Comment thread backend/internal/handler/initiative_handler.go
Comment thread backend/internal/infrastructure/snowflake/client.go
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: lewisojile <lewisojile@gmail.com>
Copilot AI review requested due to automatic review settings June 15, 2026 16:13
@lewisojile lewisojile dismissed their stale review June 15, 2026 16:15

All implemented.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 3 comments.

Comment thread backend/internal/handler/initiative_handler.go
Comment thread backend/internal/infrastructure/db/mentorship_repository.go Outdated
Comment thread backend/internal/infrastructure/snowflake/client.go
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: lewisojile <lewisojile@gmail.com>
Copilot AI review requested due to automatic review settings June 15, 2026 16:26
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: lewisojile <lewisojile@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 23 out of 24 changed files in this pull request and generated 2 comments.

Comment thread backend/internal/infrastructure/snowflake/client.go
Comment thread backend/internal/infrastructure/snowflake/client.go
Signed-off-by: lewisojile <lewisojile@gmail.com>
Copilot AI review requested due to automatic review settings June 16, 2026 06:38

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Not ready to approve

The syncer currently normalizes Snowflake “Rejected” to “rejected”, which is not a supported initiative status in this codebase and should be mapped to an existing status (e.g. “declined”) to avoid persisting unexpected values.

Copilot's findings
  • Files reviewed: 23/24 changed files
  • Comments generated: 2

Note

Your feedback helps us improve the quality of this feature.
Please use 👍 or 👎 to tell us whether this assessment is correct.

Comment thread backend/cmd/mentorship-sync/syncer.go
Comment thread backend/cmd/mentorship-sync/syncer_test.go
Pull OWNER_EMAIL, OWNER_FIRST_NAME, OWNER_LAST_NAME, OWNER_AVATAR_URL
from ANALYTICS.GOLD_FACT.MENTORSHIP_PROGRAMS and persist them into the
owner's users row on each sync run.

- Add OwnerEmail/FirstName/LastName/AvatarURL to MentorshipProgram model
- Extend fetchProgramsQuery with 4 COALESCE'd owner columns
- Update FetchPrograms scan + struct build in snowflake client
- Update UpsertProgram to upsert owner profile fields using
  COALESCE(NULLIF(EXCLUDED.col,''), existing) so empty values never
  overwrite richer data already present (e.g. from Auth0)
- Update fixtureProgram + FetchPrograms conversion in FixtureSource
- Add owner profile fields to testdata/programs.json fixture
- Update mock Columns()/rows in client_test.go; add field assertions

Signed-off-by: lewisojile <lewisojile@gmail.com>
@lewisojile lewisojile merged commit 5f37a10 into main Jun 16, 2026
11 checks passed
@lewisojile lewisojile deleted the feat/mentorship-sync branch June 16, 2026 07:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants