Repository navigation
feat: support inviting organization members by email in github_membership - #3661
kazuki-hanai wants to merge 6 commits into
Conversation
…ship GitHub's membership API (EditOrgMembership) can only address people by username, so github_membership could not invite someone who does not have a GitHub account yet. Add an 'email' argument that uses the organization invitation API (CreateOrgInvitation) instead. Because there is no endpoint to look up an invitation (or the resulting member) by email, the email flavour of the resource manages the invitation lifecycle: pending invitations are adopted, cancelled on destroy (including failed invitations), and re-sent when the role changes. Once an invitation is accepted the resource keeps it in state rather than re-inviting an existing member on every apply. Closes integrations#2197 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
👋 Hi, and thank you for this contribution! This repo is maintained by GitHub and community members on a best-effort basis. We'll get to this as soon as we can. You can help us prioritize by joining the discussion on open issues and PRs, sharing details on the changes you need, and reviewing other contributions. 🤖 This is an automated message. |
|
Duplicate of #3229 |
|
Thanks for the triage @deiga. I understand this addresses the same issue, but I would like to ask you to reconsider which PR to keep:
Given that, closing the stalled PR and keeping this one open seems like the better outcome for #2197. I put real effort into this implementation and I am motivated to address review feedback quickly, so I would appreciate the chance to get it reviewed. If the team has firmly settled on the separate-resource approach instead, let me know and I will help review #3229. |
|
@kazuki-hanai I think you raise an excellent point. And while I can't promise how quickly we can get your change merged, I'll re-open this and mark the others as stale |
There was a problem hiding this comment.
🟡 Changes recommended
Core invitation update, adoption, and failed-invitation reconciliation paths lack test coverage.
1 open finding
What changed in this PR
These provider review instructions are being used. This PR extends github_membership to manage organization invitations by email.
Changes:
- Adds email-based invitation lifecycle handling.
- Adds schema validation, documentation, and examples.
- Adds acceptance coverage for creation, import, and deletion.
| File | Description |
|---|---|
github/resource_github_membership.go |
Implements email invitation lifecycle. |
github/resource_github_membership_test.go |
Adds invitation tests. |
github/acc_test.go |
Adds invitation email configuration. |
examples/resources/membership/example_2.tf |
Demonstrates email invitations. |
templates/resources/membership.md.tmpl |
Documents the new behavior. |
docs/resources/membership.md |
Regenerates user-facing documentation. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Steps: []resource.TestStep{ | ||
| { | ||
| Config: testAccGithubMembershipConfigEmail(testAccConf.testInvitationEmail), | ||
| Check: resource.ComposeTestCheckFunc( | ||
| resource.TestCheckResourceAttr(rn, "email", testAccConf.testInvitationEmail), |
| // Inviting an email that already has a pending invitation fails, so adopt | ||
| // an existing pending invitation when it matches the configured role. | ||
| invitation, err := findPendingOrgInvitationByEmail(ctx, owner, email) | ||
| if err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
|
|
||
| if invitation != nil && invitationRoleToMembershipRole(invitation.GetRole()) != roleName { | ||
| if _, err := owner.v3client.Organizations.CancelInvite(ctx, orgName, invitation.GetID()); err != nil { | ||
| return diag.FromErr(err) | ||
| } | ||
| invitation = nil | ||
| } |
There was a problem hiding this comment.
issue: This sounds like something that should be left to the user with an import statement and not part of Create 🤔
There was a problem hiding this comment.
Done in 3741f90. Create now just sends the invitation; a pre-existing invitation surfaces the API error, which points the user at terraform import. Cancel-and-reinvite only runs on role updates, since invitations cannot be edited.
| orgName := meta.(*Owner).name | ||
| _, username, err := parseID2(d.Id()) | ||
| orgName := owner.name | ||
| _, usernameOrEmail, err := parseID2(d.Id()) |
There was a problem hiding this comment.
issue: lets not parse the ID, but instead d.Get from the attributes
There was a problem hiding this comment.
Done in 7d52b47. Read now dispatches on d.Get("email"). To keep imports working, a custom importer maps an email-shaped import ID onto the email attribute, so ID parsing stays confined to the importer.
| }) | ||
| } | ||
|
|
||
| func TestAccGithubMembershipEmailInvitation(t *testing.T) { |
There was a problem hiding this comment.
issue: Add tests as subtests of the one existing test
There was a problem hiding this comment.
Done in 5429008. The email test is now a subtest of TestAccGithubMembership, and it gained a role-change step covering the update path.
| ## Example Usage | ||
|
|
||
| {{ tffile "examples/resources/membership/example_1.tf" }} | ||
|
|
||
| Inviting by email address: | ||
|
|
||
| {{ tffile "examples/resources/membership/example_2.tf" }} | ||
|
|
There was a problem hiding this comment.
suggestion: Could you update this to use the auto-discovery of example files?
There was a problem hiding this comment.
Done in d69b9e9. The template now uses .ExampleFiles, and the example files moved to examples/resources/github_membership/resource_*.tf to match the discovery convention.
|
Thank you for reopening this and for the quick review, @deiga! I will address the feedback shortly. |
Create now just sends the invitation; a pre-existing invitation surfaces the API error and points the user at terraform import. Cancelling and re-inviting only happens on role updates, since invitations cannot be edited. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A custom importer maps an email-shaped import ID onto the email attribute, so ID parsing stays confined to the importer. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also add a role-change step covering the cancel-and-reinvite update path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Example files move to examples/resources/github_membership/resource_*.tf to match the tfplugindocs discovery convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
aaae5d9 to
d69b9e9
Compare

Resolves #2197
Before the change?
github_membershipcan only address people by username (it usesEditOrgMembership), so there is no way to invite someone to the organization who does not have a GitHub account yet.After the change?
github_membershipacceptsemailas an alternative tousernameand sends an organization invitation (POST /orgs/{org}/invitations):The design is shaped by an API limitation: there is no endpoint to look up an invitation — or the member who later accepted it — by email. Concretely:
username.email+downgrade_on_destroy = trueis rejected at plan time (ConflictsWithcannot express this because an explicitdowngrade_on_destroy = falsewould also trigger it).Relation to #3464: that PR proposes a separate
github_organization_invitationresource. This PR instead extends the existing resource, which is what #2197 asks for and follows the review guidance there that additions to existing resources are preferred over new resources.Testing
go build,go vet, and both golangci-lint configs (default and strict-on-new-code) pass; docs regenerated withmake generatedocs.GH_TEST_INVITATION_EMAILenv var (the address must not belong to an existing org member). I have not yet run the acceptance tests against a real organization.Per the contributing guidelines: this PR was largely generated with an AI assistant (Claude Code), driven and reviewed iteratively by me.
Pull request checklist
Does this introduce a breaking change?
usernamechanges fromRequiredtoOptionalwithExactlyOneOf, which keeps all existing configurations valid.🤖 Generated with Claude Code