feat: handle GitHub invitation provisioning lifecycle (IGA-1212)#139
feat: handle GitHub invitation provisioning lifecycle (IGA-1212)#139
Conversation
|
Claude finished @highb's task in 1m 4s —— View job PR Review: Return ActionRequiredResult for org invites
Overall: This is a clean, well-scoped fix. The change correctly addresses the semantic mismatch where What changed
FeedbackLooks good — no blocking issues. A few observations:
LGTM — this is a well-targeted fix for the root cause described in the PR summary. The C1 engine will no longer treat an invitation as a completed account creation, which should resolve the downstream team membership grant failures. |
pkg/connector/invitation.go
Outdated
| }) | ||
| if err != nil { | ||
| if isAlreadyOrgMemberError(err, resp) { | ||
| return &v2.CreateAccountResponse_AlreadyExistsResult{}, nil, nil, nil |
There was a problem hiding this comment.
I think you need to return the resource as part of the response so c1 caches this value?
|
Claude finished @highb's task in 2m 1s —— View job PR Review: Return ActionRequiredResult for org invites
Overall: Well-structured PR that correctly fixes a semantic mismatch. The core change — returning What looks good
Issues to consider1. Failed invitation check blocks re-invitation (potential blocker)
If the intent is to surface the expired invite so an operator can take manual action, this makes sense — but the message should be clearer (e.g., "Manual intervention required: previous invitation expired"). If the intent is that C1 should retry, this blocks retries permanently since the failed invitation stays in the list. Question: Is the expected C1 engine behavior to surface this to an operator, or should this fall through to actually create a new invitation? 2.
|
add921d to
b1a6608
Compare
|
Claude finished @highb's task in 2m 9s —— View job PR Review: Return ActionRequiredResult for org invites
Overall: The core fix is correct and well-targeted. Returning What looks good
Issues1. Failed invitation pre-check creates a dead end (blocking concern)
This creates a permanent blocker: C1 sees Question: Is this intentional (surface to operator for manual action)? If so, the message should say something like "Manual intervention required" instead of "A new invitation must be sent" — currently it reads like the system should retry. If the intent is to allow re-invitation, this check should be moved after the 2.
|
|
|
||
| // Before attempting to create an invitation, check if a previous invite | ||
| // expired. If so, return a distinct error so C1 can surface it in the UI. | ||
| failedInv, failedErr := i.lookupFailedInvitation(ctx, params.org, params.login, *params.email) |
There was a problem hiding this comment.
hm, github allows us to override the previous invitation. I don't think we should return an error if the invitation exists.
There was a problem hiding this comment.
Yep, just caught that. I'll warn if it detects a failed invite and then resend.
Invitations are transient pending actions, not identities. Having TRAIT_USER caused the C1 uplift pipeline to create fake AppUsers with invitation-typed SourceConnectorIds that broke downstream grant operations. Invitations should only exist as pending state until the user accepts and appears as a real user resource via sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrite CreateAccount to properly handle the async invitation lifecycle across GitHub product tiers (Free, Team, Enterprise Cloud with personal accounts): - Return ActionRequiredResult (not SuccessResult) for new invites, since the user must accept before team membership can be granted - Detect "already a member" (422) and return AlreadyExistsResult with the user resource when possible, or nil resource to let C1 resolve from its DB - Detect "already invited" (422) and return ActionRequiredResult with the pending invitation resource - Add github_username as an optional schema field for reliable user lookup when email is private - Add lookupUser (tries Users.Get by username, falls back to email search) and lookupPendingInvitation (matches by login or email) - Match GitHub's actual error message "already a part of" in addition to "already a member" Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Enterprise Managed Users (EMU) orgs don't support the invitation API — accounts are provisioned directly by the IdP via SCIM. Detect the EMU-specific 422 error and return a clear message instead of a confusing generic failure. The EMU check runs after already-a-member and already-invited checks since those are success cases regardless of org type. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Check ListFailedOrgInvitations before calling CreateOrgInvitation. If a matching expired/failed invite is found, log a warning with the failure reason and timestamp, then proceed to send a new invitation. This provides an audit trail of expired invites without blocking re-invitations. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
b1a6608 to
20d23fc
Compare
|
Claude finished @highb's task in 2m 45s —— View job PR Review: Return ActionRequiredResult for org invites
Overall: The core fix is correct and important. Returning What changed (4 commits)
What looks good
Issues1. @Bencheng21's concern about already-invited handling
The current behavior returns Worth clarifying: Can the invite be re-sent via the API to reset the expiry timer? If so, that might be more useful than just reporting the existing pending state. 2.
|
Summary
Fixes the GitHub org invitation provisioning flow to properly handle the async invitation lifecycle across all GitHub product tiers (Free, Team, Enterprise Cloud with personal accounts, EMU).
The core problem (IGA-1212):
CreateAccountreturnedSuccessResultafter creating an org invitation, causing C1 to immediately attempt team provisioning. But GitHub invitations are async — the user must accept before they're an org member, so team membership operations fail with "only users can be granted to team membership."Changes
Remove
TRAIT_USERfrom invitation resource type — Invitations are transient pending actions, not identities.TRAIT_USERcaused the C1 uplift pipeline to create fake AppUsers with invitation-typed SourceConnectorIds that broke downstream grant operations.Return
ActionRequiredResultinstead ofSuccessResultfor new invitations, signaling to C1 that the user must accept before further provisioning can proceed.Handle "already a member" (422) — Return
AlreadyExistsResultwith the user resource when possible (looked up by GitHub username or email), or with nil resource to let C1 resolve from its DB where sync already created the AppUser.Handle "already invited" (422) — Return
ActionRequiredResultwith the pending invitation resource, matching by login or email.Add
github_usernameschema field — Optional field for reliable user lookup when email is private. GitHub usernames are the only reliable key since many users don't have public emails. C1 can map this fromsubject.github_usernamevia CEL.Detect EMU orgs — Enterprise Managed Users orgs don't support the invitation API (accounts are provisioned by the IdP via SCIM). Returns a clear error instead of a confusing 422. Checked after already-a-member/already-invited since those are success cases regardless of org type.
Detect expired/failed invitations — Checks
ListFailedOrgInvitationsbefore creating a new invite. If a previous invite expired (7-day TTL), logs a warning with the failure reason and timestamp, then proceeds to send a new invitation.Match GitHub's actual error message —
isAlreadyOrgMemberErrornow matches "already a part of" in addition to "already a member" (GitHub's actual 422 message is "A user with this email address is already a part of this organization").Lookup strategy
lookupUser(login, email)resolves a GitHub user to aresourceTypeUserresource:login(GitHub username) is provided, callsGET /users/{username}— works regardless of email privacyGET /search/users?q={email}+in:email— only finds public emailslookupPendingInvitationandlookupFailedInvitationboth useinvitationMatcheswhich checks login first, then email.GitHub product tier coverage
Test plan
ActionRequiredResultwith invitation resourceAlreadyExistsResultActionRequiredResultwith pending inviteTRAIT_USER)github_usernamefield enables lookup when email is private🤖 Generated with Claude Code