Skip to content

CS-656 Create script to Migrate all users from old domain to new domain - remove duplicate people records#3304

Draft
github-actions[bot] wants to merge 31 commits into
mainfrom
chas/email-migration-script
Draft

CS-656 Create script to Migrate all users from old domain to new domain - remove duplicate people records#3304
github-actions[bot] wants to merge 31 commits into
mainfrom
chas/email-migration-script

Conversation

@github-actions

@github-actions github-actions Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

This is an automated pull request to merge chas/email-migration-script into dev.
It was created by the [Auto Pull Request] action.


Summary by cubic

Adds trigger tasks merge-duplicate-user and migrate-org-email-domain to merge duplicate users during org email-domain migrations. Member relations are now auto-discovered from Postgres and re-pointed safely with a final dangling-reference check; completes CS-656.

  • New Features

    • 30s transactional merge with org:{organizationId} tags; recomputes the old user’s other-org membership inside the transaction.
    • Catalog-driven discovery of Member.id foreign keys from Postgres, generic repointing, and a safety check that fails if any relation still points at the old member; explicit handling for Policy.signedBy and IsmsObjective.ownerMemberId.
    • Dedupes unique-constrained rows (background checks, offboarding checklist/revocations, training completions); updates invitations.
    • User-level: keep the old user; when single-org, move accounts/tokens/consents, logs/errors, evidence submissions, integration results, audit logs, findings authors, and offboarding actors, then clear sessions; otherwise skip user-level changes.
    • Email-domain migration: lowercases emails/domains, no-ops when normalized domains match, pairs old→new by local part among active members, triggers merges, and returns merged/failed counts with pair details.
    • Adds an idempotent seed script and Jest tests.
  • Bug Fixes

    • Only re-points OffboardingAccessRevocation.revokedById when the old user has no other orgs.
    • Validates that newEmail differs from oldEmail case-insensitively.

Written for commit 4465ddb. Summary will update on new commits.

Review in cubic

@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jul 22, 2026 3:52pm
comp-framework-editor Ready Ready Preview, Comment Jul 22, 2026 3:52pm
portal Ready Ready Preview, Comment Jul 22, 2026 3:52pm

Request Review

@cubic-dev-ai cubic-dev-ai Bot 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.

4 issues found across 1 file

Confidence score: 2/5

  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, there is no guard for oldEmail === newEmail, so a self-merge can delete the very user/member being kept; merging as-is risks destructive data loss on valid-looking input — add an explicit equality short-circuit before any delete/update steps.
  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, the training completion migration drops id and then later updates by id, which can make that migration fail or silently affect zero rows; this creates a concrete partial-migration/regression risk — keep id in the projection (or change downstream updates to available keys) and verify with a targeted migration test before merging.
  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, resolving members outside the transaction introduces a TOCTOU window where merge targets can change mid-operation, leading to inconsistent merges under concurrency — move member resolution inside the same transaction (or lock rows) before merge.
  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, the final merge log reports deleted IDs as survivors, which can mislead incident response and audits after a bad merge — correct survivor/deleted ID mapping in the log payload before shipping.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – portal June 30, 2026 17:00 Inactive
@vercel
vercel Bot temporarily deployed to Preview – app June 30, 2026 17:00 Inactive
@chasprowebdev chasprowebdev changed the title [dev] [chasprowebdev] chas/email-migration-script CS-656 Create script to Migrate all users from old domain to new domain - remove duplicate people records Jun 30, 2026
@linear

linear Bot commented Jun 30, 2026

Copy link
Copy Markdown

CS-656

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/tasks/people/migrate-org-email-domain.ts Outdated
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 2 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/tasks/people/migrate-org-email-domain.ts
@chasprowebdev

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review it

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review it

@chasprowebdev I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

All reported issues were addressed across 2 files

Confidence score: 5/5

  • Safe to merge after the addressed issues were fixed.

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – app June 30, 2026 18:51 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portal June 30, 2026 18:51 Inactive
@chasprowebdev

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review this.

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review this.

@chasprowebdev I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

1 issue found across 7 files

Confidence score: 3/5

  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, ISMS records can retain ownerMemberId references to the merged-away member, which risks orphaned ownership links or broken ownership lookups after deduplication; migrate ownerMemberId from old member ID to new member ID in the same transaction before deleting the old member.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/trigger/tasks/people/merge-duplicate-user.ts">

<violation number="1" location="apps/api/src/trigger/tasks/people/merge-duplicate-user.ts:273">
P1: This migration can rewrite revocation actor attribution outside the target org, because `revokedById` is updated globally by userId even when the old user still has memberships elsewhere. Scoping this update by `organizationId` (or moving it into the user-level branch) avoids cross-org data changes.</violation>
</file>

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – app July 3, 2026 12:06 Inactive
@vercel
vercel Bot temporarily deployed to Preview – portal July 3, 2026 12:06 Inactive
@chasprowebdev

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review it.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review it.

@chasprowebdev I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

2 issues found across 7 files

Confidence score: 2/5

  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, the merge path can resolve oldEmail and newEmail to the same membership id and still execute tx.member.delete({ id: o }), which risks deleting the surviving target membership and breaking account/org access after merge — add an explicit same-member guard (or schema-level inequality constraint) before allowing delete and verify with a same-account test before merging.
  • In apps/api/src/trigger/tasks/people/merge-duplicate-user.ts, oldUserHasOtherOrgs is computed before the transaction, so concurrent membership changes can make that safety check stale and lead to incorrect merge decisions under load — recompute membership state inside the same transaction (or lock/read consistently) to de-risk race conditions before merge.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/trigger/tasks/people/merge-duplicate-user.ts">

<violation number="1" location="apps/api/src/trigger/tasks/people/merge-duplicate-user.ts:7">
P1: This task can delete the target membership when `oldEmail` and `newEmail` are the same account, because the merge still reaches `tx.member.delete({ id: o })` after resolving both members to one id. A schema-level inequality guard for the two emails would prevent this destructive path.</violation>

<violation number="2" location="apps/api/src/trigger/tasks/people/merge-duplicate-user.ts:62">
P2: User-level merge safety is decided from a pre-transaction membership count, so concurrent org-membership writes can make `oldUserHasOtherOrgs` stale during execution. Recomputing that condition inside the same transaction (or locking rows) would keep relation migration aligned with final membership state.</violation>

<violation number="3" location="apps/api/src/trigger/tasks/people/merge-duplicate-user.ts:273">
P1: This migration can rewrite revocation actor attribution outside the target org, because `revokedById` is updated globally by userId even when the old user still has memberships elsewhere. Scoping this update by `organizationId` (or moving it into the user-level branch) avoids cross-org data changes.</violation>
</file>

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Fix all with cubic | Re-trigger cubic

Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
Comment thread apps/api/src/trigger/tasks/people/merge-duplicate-user.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – portal July 3, 2026 13:20 Inactive
@vercel
vercel Bot temporarily deployed to Preview – app July 3, 2026 13:20 Inactive
@chasprowebdev

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review it.

@cubic-dev-ai

cubic-dev-ai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai please review it.

@chasprowebdev I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot 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.

No issues found across 7 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot 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.

3 issues found across 8 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts">

<violation number="1" location="apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts:84">
P2: Historical checklist completions whose template was deleted can be lost during a merge: all nullable `templateItemId` values collapse to the same `"null"` dedupe key. Treat NULL template IDs as non-conflicting and migrate them rather than dropping them.</violation>

<violation number="2" location="apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts:150">
P1: Merges with the same training video completed by both members always roll back: `toDrop` remains attached to the old member and the final FK safety check rejects it before the cascade delete. Delete those duplicate rows before the assertion, as done for the other unique exceptions.</violation>

<violation number="3" location="apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts:226">
P1: ISMS role assignments and audit-finding owners still point to the deleted member because these two plain member-ID fields are not catalog-discoverable and are omitted from both repointing and the safety check. Repoint and assert `IsmsRoleAssignment.memberId` and `IsmsAuditFinding.ownerMemberId` alongside `IsmsObjective.ownerMemberId`.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Fix all with cubic | Re-trigger cubic

);

await assertNoDanglingMemberReferences(tx, foreignKeys, oldMemberId, {
'IsmsObjective.ownerMemberId': () =>

@cubic-dev-ai cubic-dev-ai Bot Jul 22, 2026

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.

P1: ISMS role assignments and audit-finding owners still point to the deleted member because these two plain member-ID fields are not catalog-discoverable and are omitted from both repointing and the safety check. Repoint and assert IsmsRoleAssignment.memberId and IsmsAuditFinding.ownerMemberId alongside IsmsObjective.ownerMemberId.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts, line 226:

<comment>ISMS role assignments and audit-finding owners still point to the deleted member because these two plain member-ID fields are not catalog-discoverable and are omitted from both repointing and the safety check. Repoint and assert `IsmsRoleAssignment.memberId` and `IsmsAuditFinding.ownerMemberId` alongside `IsmsObjective.ownerMemberId`.</comment>

<file context>
@@ -0,0 +1,239 @@
+  );
+
+  await assertNoDanglingMemberReferences(tx, foreignKeys, oldMemberId, {
+    'IsmsObjective.ownerMemberId': () =>
+      tx.ismsObjective.count({ where: { ownerMemberId: oldMemberId } }),
+    'Policy.signedBy': () =>
</file context>
Fix with cubic

newCompletionKeys,
(c) => c.videoId,
);
if (completionSplit.toMigrate.length > 0) {

@cubic-dev-ai cubic-dev-ai Bot Jul 22, 2026

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.

P1: Merges with the same training video completed by both members always roll back: toDrop remains attached to the old member and the final FK safety check rejects it before the cascade delete. Delete those duplicate rows before the assertion, as done for the other unique exceptions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts, line 150:

<comment>Merges with the same training video completed by both members always roll back: `toDrop` remains attached to the old member and the final FK safety check rejects it before the cascade delete. Delete those duplicate rows before the assertion, as done for the other unique exceptions.</comment>

<file context>
@@ -0,0 +1,239 @@
+    newCompletionKeys,
+    (c) => c.videoId,
+  );
+  if (completionSplit.toMigrate.length > 0) {
+    await tx.employeeTrainingVideoCompletion.updateMany({
+      where: { id: { in: completionSplit.toMigrate.map((c) => c.id) } },
</file context>
Fix with cubic

const checklistSplit = splitDuplicates(
existingChecklist,
newChecklistKeys,
(c) => String(c.templateItemId),

@cubic-dev-ai cubic-dev-ai Bot Jul 22, 2026

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.

P2: Historical checklist completions whose template was deleted can be lost during a merge: all nullable templateItemId values collapse to the same "null" dedupe key. Treat NULL template IDs as non-conflicting and migrate them rather than dropping them.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/trigger/tasks/people/merge-duplicate-user-member-relations.ts, line 84:

<comment>Historical checklist completions whose template was deleted can be lost during a merge: all nullable `templateItemId` values collapse to the same `"null"` dedupe key. Treat NULL template IDs as non-conflicting and migrate them rather than dropping them.</comment>

<file context>
@@ -0,0 +1,239 @@
+  const checklistSplit = splitDuplicates(
+    existingChecklist,
+    newChecklistKeys,
+    (c) => String(c.templateItemId),
+  );
+  if (checklistSplit.toMigrate.length > 0) {
</file context>
Fix with cubic

@chasprowebdev
chasprowebdev marked this pull request as draft July 24, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants