Skip to content

Commit de57afc

Browse files
Fix: skip unchanged GitHub team memberships in provisioner (#86)
## Summary Root cause fix for the CODEOWNER self-merge regression. The provisioner was calling `PUT /teams/{slug}/memberships/{user}` on every run for every member, even when the role was already correct. This re-added members via the API, changing their membership state from "org-owner implicit" to "explicit API-added", which caused GitHub to block self-merge on CODEOWNER-required PRs. Now fetches current member roles first and only calls PUT when a role change is actually needed. ## What happened 1. PR #80 changed provisioner to default `role: member` 2. Registry #16 merged → provisioner ran for first time with new code 3. Provisioner called `PUT memberships/alexanderamiri {"role": "maintainer"}` — role was already maintainer, but the API call changed the membership source 4. GitHub stopped allowing self-merge on CODEOWNER PRs ## Also reverted Removed the org-admin bypass actor from the ruleset (was a band-aid, not the fix). ## Test plan - [ ] Merge this PR (should be self-mergeable — provisioner hasn't run again) - [ ] Trigger provisioner via registry merge - [ ] Verify team membership unchanged (check GitHub API) - [ ] Create a test PR → verify self-merge still works
1 parent 9c6afb5 commit de57afc

1 file changed

Lines changed: 25 additions & 18 deletions

File tree

terraform/lambda-src/team_provisioner/handler.py

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -568,23 +568,26 @@ def sync_github_team(team):
568568
else:
569569
logger.info("Created GitHub team %s", team_slug)
570570

571-
# Current team members (paginated — GitHub returns max 100 per page)
572-
current_members = set()
573-
page = 1
574-
while True:
575-
members_resp = _github_api(
576-
"GET",
577-
f"/orgs/{GITHUB_ORG}/teams/{team_slug}/members?per_page=100&page={page}",
578-
token,
579-
)
580-
if not isinstance(members_resp, list) or not members_resp:
581-
break
582-
current_members.update(m["login"].lower() for m in members_resp)
583-
if len(members_resp) < 100:
584-
break
585-
page += 1
586-
587-
# Desired members
571+
# Current team members with roles (paginated — GitHub returns max 100 per page)
572+
# We need roles to avoid unnecessary PUT calls that can change membership state.
573+
current_members = {} # login -> role
574+
for role_filter in ("maintainer", "member"):
575+
page = 1
576+
while True:
577+
members_resp = _github_api(
578+
"GET",
579+
f"/orgs/{GITHUB_ORG}/teams/{team_slug}/members?role={role_filter}&per_page=100&page={page}",
580+
token,
581+
)
582+
if not isinstance(members_resp, list) or not members_resp:
583+
break
584+
for m in members_resp:
585+
current_members[m["login"].lower()] = role_filter
586+
if len(members_resp) < 100:
587+
break
588+
page += 1
589+
590+
# Desired members — only call PUT if role needs changing
588591
desired_users = set()
589592
for member in team.get("members", []):
590593
m = _normalize_member(member)
@@ -601,6 +604,10 @@ def sync_github_team(team):
601604
if github_role not in ("maintainer", "member"):
602605
logger.warning("Invalid role '%s' for %s — defaulting to member", github_role, github_user)
603606
github_role = "member"
607+
current_role = current_members.get(github_user)
608+
if current_role == github_role:
609+
logger.info("Skipping %s in team %s — already %s", github_user, team_slug, github_role)
610+
continue
604611
_github_api(
605612
"PUT",
606613
f"/orgs/{GITHUB_ORG}/teams/{team_slug}/memberships/{github_user}",
@@ -610,7 +617,7 @@ def sync_github_team(team):
610617
logger.info("Set %s as %s in team %s", github_user, github_role, team_slug)
611618

612619
# Remove members no longer in the team definition
613-
for login in current_members - desired_users:
620+
for login in set(current_members.keys()) - desired_users:
614621
_github_api(
615622
"DELETE",
616623
f"/orgs/{GITHUB_ORG}/teams/{team_slug}/memberships/{login}",

0 commit comments

Comments
 (0)