Manage repository collaborator invites
Fixes GH-57 Invitations can also be extended to outside collaborators at the repository level. These changes extend the existing code to poll for all such invitations (each repository in the organization must be interrogated).
This commit is contained in:
Родитель
150e67214d
Коммит
61eb739587
17
README.md
17
README.md
|
@ -1,8 +1,7 @@
|
|||
# github org helper scripts
|
||||
|
||||
**NOTE: branch 'invitations' requires a version of github3.py which has
|
||||
not yet been released.** See
|
||||
[PR](https://github.com/sigmavirus24/github3.py/pull/675) for details.
|
||||
**NOTE: the main helper library, GitHub3.py, has been updated to version 1.3.0.
|
||||
Not all scripts have been verified against this version.**
|
||||
|
||||
These are some API helper scripts for sanely managing a github org. For now this is somewhat hardcoded for the mozilla org; no need for it to remain that way though.
|
||||
|
||||
|
@ -30,15 +29,19 @@ Find all hooks configured for an organization -- see --help for details
|
|||
### get_org_info.py
|
||||
Output basic info about an org, more if you have permissions. See --help for details
|
||||
|
||||
### team_update.py
|
||||
Update administrative teams so they can be used for the new GitHub discussion
|
||||
feature. Use the ``--help`` option for more information.
|
||||
|
||||
### lfs.py
|
||||
Get current LFS Billing values using a headless firefox via selenium
|
||||
(``geckodriver`` must be installed). Credentials as environment
|
||||
variables, and 2FA token passed as input.
|
||||
|
||||
### manage_invitations.py
|
||||
Cancel all org & repository invitations older than a specified age (default 2
|
||||
weeks). See --help for details.
|
||||
|
||||
### team_update.py
|
||||
Update administrative teams so they can be used for the new GitHub discussion
|
||||
feature. Use the ``--help`` option for more information.
|
||||
|
||||
### Audit logs
|
||||
Sadly, the org audit log does not have an API, so we'll screen scrape a little.
|
||||
|
||||
|
|
|
@ -14,6 +14,12 @@ those with:
|
|||
manage_invitations | cut -d ' ' -f1
|
||||
Or invitee & inviter:
|
||||
manage_invitations | awk '!/^Proc/ {print $1 " by " $NF;}'
|
||||
|
||||
ToDo:
|
||||
- Better handling of Security Advisory private repos. These return a 404
|
||||
on invitation calls, which are simply reported now. These repos
|
||||
usually have a name ending in the string '-ghsa-' followed by hex
|
||||
digits.
|
||||
"""
|
||||
# hwine believes keeping the doc above together is more important than PEP-8
|
||||
import argparse # NOQA
|
||||
|
@ -28,6 +34,9 @@ if not hasattr(github3.orgs.Organization, 'invitations'):
|
|||
raise NotImplementedError("Your version of github3.py does not support "
|
||||
"invitations. Try "
|
||||
"https://github.com/hwine/github3.py/tree/invitations") # NOQA
|
||||
if (1,3,0) > github3.__version_info__:
|
||||
raise NotImplementedError("Your version of github3.py does not support "
|
||||
"collaborator invitations. Version '1.3.0' or later is known to work.")
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -49,28 +58,70 @@ def check_invites(gh, org_name, cancel=False, cutoff_delta="weeks=-2"):
|
|||
cutoff_time = get_cutoff_time(cutoff_delta)
|
||||
try:
|
||||
for invite in org.invitations():
|
||||
extended_at = arrow.get(invite['created_at'])
|
||||
extended_at = arrow.get(invite.created_at)
|
||||
line_end = ": " if cancel else "\n"
|
||||
if extended_at < cutoff_time:
|
||||
invite['ago'] = extended_at.humanize()
|
||||
context = invite.as_dict()
|
||||
context['ago'] = extended_at.humanize()
|
||||
print('{login} ({email}) was invited {ago} by '
|
||||
'{inviter[login]}'.format(**invite),
|
||||
'{inviter[login]}'.format(**context),
|
||||
end=line_end)
|
||||
if cancel:
|
||||
success = org.remove_membership(username=invite['login'])
|
||||
success = org.remove_membership(invite.id)
|
||||
if success:
|
||||
print("Cancelled")
|
||||
else:
|
||||
print("FAILED to cancel")
|
||||
logger.warning("Couldn't cancel invite for {login} "
|
||||
"from {created_at}".format(**invite))
|
||||
"from {created_at}".format(**context))
|
||||
except ForbiddenError:
|
||||
logger.error("You don't have 'admin:org' permissions for org '%s'",
|
||||
org_name)
|
||||
else:
|
||||
# now handle collaborator invitations (GH-57)
|
||||
for repo in org.repositories():
|
||||
# occasionally get a 404 when looking for invitations.
|
||||
# Assume this is a race condition and ignore. That may leave
|
||||
# some invites uncanceled, but a 2nd run should catch.
|
||||
try:
|
||||
for invite in repo.invitations():
|
||||
extended_at = arrow.get(invite.created_at)
|
||||
line_end = ": " if cancel else "\n"
|
||||
if extended_at < cutoff_time:
|
||||
context = invite.as_dict()
|
||||
context['ago'] = extended_at.humanize()
|
||||
context['repo'] = repo.name
|
||||
context['inviter'] = invite.inviter.login
|
||||
context['invitee'] = invite.invitee.login
|
||||
print('{invitee} was invited to {repo} {ago} by '
|
||||
'{inviter} for {permissions} access.'.format(**context),
|
||||
end=line_end)
|
||||
if cancel:
|
||||
# Deletion not directly supported, so hack url &
|
||||
# use send delete verb directly
|
||||
delete_url = repo.url + "/invitations/" + str(invite.id)
|
||||
success = repo._delete(delete_url)
|
||||
if success:
|
||||
print("Cancelled")
|
||||
else:
|
||||
print("FAILED to cancel")
|
||||
logger.warning("Couldn't cancel invite for {login} "
|
||||
"from {created_at}".format(**context))
|
||||
except (github3.exceptions.NotFoundError,
|
||||
github3.exceptions.ConnectionError) as e:
|
||||
# just report
|
||||
logger.warning("Got 404 for invitation in {}, may be unhandled inviations. '{}'".format(repo.name,
|
||||
str(e)))
|
||||
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(description=__doc__, epilog=_epilog)
|
||||
# from
|
||||
# https://stackoverflow.com/questions/18462610/argumentparser-epilog-and-description-formatting-in-conjunction-with-argumentdef
|
||||
class CustomFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
|
||||
pass
|
||||
parser = argparse.ArgumentParser(description=__doc__,
|
||||
epilog=_epilog, formatter_class=CustomFormatter)
|
||||
parser.add_argument('--cancel', action='store_true',
|
||||
help='Cancel stale invitations')
|
||||
parser.add_argument('--cutoff', help='When invitations go stale '
|
||||
|
@ -100,4 +151,7 @@ def main():
|
|||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.WARN, format='%(asctime)s %(message)s')
|
||||
main()
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
raise SystemExit("\nCancelled by user")
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
jsonstreams==0.4.1
|
||||
# we need a version of github3.py with
|
||||
# https://github.com/sigmavirus24/github3.py/pull/675 landed
|
||||
# github3.py==1.0.0a4
|
||||
git+https://github.com/hwine/github3.py.git@invitations
|
||||
github3.py==1.3.0
|
||||
PyYaml==5.1
|
||||
tinydb==3.2.1
|
||||
# until up on pypa
|
||||
|
|
Загрузка…
Ссылка в новой задаче