Skip to content

Commit 6dd90ee

Browse files
committed
implement the removal of members
Assisted-by: claude-haiku-4-5, claude-sonnet-4-6 Signed-off-by: Felix Kronlage-Dammers <fkr@hazardous.org>
1 parent 81c3fb8 commit 6dd90ee

4 files changed

Lines changed: 141 additions & 3 deletions

File tree

.github/workflows/dryrun-manage-github-repositories.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,9 @@ jobs:
5151
pipenv run ansible-playbook playbook.yaml -e api_token=$API_TOKEN --check --diff
5252
env:
5353
API_TOKEN: ${{ secrets[format('GHP_{0}', github.actor)] }}
54+
55+
- name: Dry-run removal of members no longer defined in data.yaml
56+
run: |
57+
pipenv run python3 remove_members.py --dry-run
58+
env:
59+
API_TOKEN: ${{ secrets[format('GHP_{0}', github.actor)] }}

.github/workflows/manage-github-repositories.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,9 @@ jobs:
5252
pipenv run python3 manage.py --keep_labels TRUE
5353
env:
5454
API_TOKEN: ${{ secrets[format('GHP_{0}', github.actor)] }}
55+
56+
- name: Remove members no longer defined in data.yaml
57+
run: |
58+
pipenv run python3 remove_members.py
59+
env:
60+
API_TOKEN: ${{ secrets[format('GHP_{0}', github.actor)] }}

README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,23 @@ You can use the following procedure to test, debug or improve github manager on
3535
```
3636
pipenv run ./check_consistency.py
3737
```
38+
* Remove members no longer in data.yaml (dry run first)
39+
```sh
40+
pipenv run python3 remove_members.py --dry-run
41+
pipenv run python3 remove_members.py
42+
```
3843

3944
## Limitiations
4045

4146
* It is not possible to add already created, but still empty, repositories here. Before this is possible,
4247
at least one commit must have been made on the main branch.
4348

44-
* It is not possible to remove members from the organization or any team. Please first delete the corresponding
45-
lines in `data.yaml` here in this repository and delete the user afterwards via the GitHub UI.
49+
* To remove a member from the organization or a team, delete the corresponding lines in `data.yaml`
50+
and push to main. The `remove_members.py` script will automatically remove them from GitHub on the
51+
next workflow run. `exclusive: true` must be set in `data.yaml` (it is set by default) for removals
52+
to take effect.
4653

47-
We're working on these issues upstream: <https://github.com/opentelekomcloud/ansible-collection-gitcontrol> and
54+
We're working on upstream improvements: <https://github.com/opentelekomcloud/ansible-collection-gitcontrol> and
4855
<https://github.com/opentelekomcloud-infra/gitstyring>
4956

5057
## Github Actions

remove_members.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Remove org members and team members that are no longer defined in data.yaml.
4+
5+
Uses data.yaml as the source of truth. When exclusive: true is set,
6+
any member present on GitHub but absent from the YAML will be removed.
7+
"""
8+
9+
import logging
10+
import os
11+
import sys
12+
from argparse import ArgumentParser
13+
14+
import github
15+
import yaml
16+
from github import Github
17+
18+
logging.basicConfig(
19+
format="%(asctime)s - %(message)s", level=logging.INFO, datefmt="%Y-%m-%d %H:%M:%S"
20+
)
21+
22+
API_TOKEN = os.environ.get("API_TOKEN")
23+
ORGANIZATION = os.environ.get("ORGANIZATION", "SovereignCloudStack")
24+
25+
26+
def load_data(data_file: str) -> dict:
27+
with open(data_file) as f:
28+
return yaml.safe_load(f)
29+
30+
31+
def remove_org_members(gh: Github, org_name: str, defined_logins: set, dry_run: bool) -> int:
32+
"""Remove org members not defined in data.yaml. Returns number of removals."""
33+
org = gh.get_organization(org_name)
34+
removed = 0
35+
36+
for member in org.get_members():
37+
login = member.login.lower()
38+
if login not in defined_logins:
39+
logging.info(f"Removing org member: {member.login}")
40+
if not dry_run:
41+
org.remove_from_members(member)
42+
removed += 1
43+
44+
return removed
45+
46+
47+
def remove_team_members(gh: Github, org_name: str, yaml_teams: list, dry_run: bool) -> int:
48+
"""Remove team members/maintainers not defined in data.yaml for each team. Returns number of removals."""
49+
org = gh.get_organization(org_name)
50+
removed = 0
51+
52+
defined_teams = {t["slug"]: t for t in yaml_teams}
53+
54+
for gh_team in org.get_teams():
55+
slug = gh_team.slug
56+
if slug not in defined_teams:
57+
# Team not in YAML — skip (team creation/deletion is handled by Ansible)
58+
continue
59+
60+
yaml_team = defined_teams[slug]
61+
yaml_members = {m.lower() for m in yaml_team.get("member", [])}
62+
yaml_maintainers = {m.lower() for m in yaml_team.get("maintainer", [])}
63+
yaml_all = yaml_members | yaml_maintainers
64+
65+
for gh_member in gh_team.get_members():
66+
login = gh_member.login.lower()
67+
if login not in yaml_all:
68+
logging.info(f"Removing {gh_member.login} from team {slug}")
69+
if not dry_run:
70+
gh_team.remove_membership(gh_member)
71+
removed += 1
72+
73+
return removed
74+
75+
76+
def main():
77+
parser = ArgumentParser(description="Remove GitHub org/team members not defined in data.yaml")
78+
parser.add_argument(
79+
"--dry-run",
80+
action="store_true",
81+
default=False,
82+
help="Report what would be removed without making any changes",
83+
)
84+
parser.add_argument(
85+
"--data-file",
86+
default=f"orgs/{ORGANIZATION}/data.yaml",
87+
help="Path to the data.yaml file",
88+
)
89+
args = parser.parse_args()
90+
91+
if args.dry_run:
92+
logging.info("DRY RUN — no changes will be made")
93+
94+
data = load_data(args.data_file)
95+
96+
if not data.get("exclusive", False):
97+
logging.info("exclusive: false — skipping removal")
98+
sys.exit(0)
99+
100+
defined_logins = {m["login"].lower() for m in data.get("members", [])}
101+
yaml_teams = data.get("teams", [])
102+
103+
gh = Github(login_or_token=API_TOKEN)
104+
105+
org_removals = remove_org_members(gh, ORGANIZATION, defined_logins, args.dry_run)
106+
team_removals = remove_team_members(gh, ORGANIZATION, yaml_teams, args.dry_run)
107+
108+
total = org_removals + team_removals
109+
if total == 0:
110+
logging.info("No members to remove — everything is in sync")
111+
else:
112+
action = "Would remove" if args.dry_run else "Removed"
113+
logging.info(f"{action} {org_removals} org member(s) and {team_removals} team member(s)")
114+
115+
sys.exit(0)
116+
117+
118+
if __name__ == "__main__":
119+
main()

0 commit comments

Comments
 (0)