From: Aric Gardner Date: Thu, 18 Jul 2019 20:42:51 +0000 (-0400) Subject: Changes needed in lftools for github orgs X-Git-Tag: v0.26.0^2 X-Git-Url: https://gerrit.linuxfoundation.org/infra/gitweb?a=commitdiff_plain;h=refs%2Fchanges%2F62%2F16262%2F15;p=releng%2Flftools.git Changes needed in lftools for github orgs Part of the effort for self serve user management check-votes now works against a a githubpr Note: You can't vote on your own pr so I add the pr owner as a voter automatically. infofile match-ldap-to-info now works against github groups github submit-pr will submit a pr if mergable github votes will count votes on a pr Fixes a bug in github team invitation. Teams needs to be array of :class:`github.Team.Team` adds: --add_team and --remove_team to github update-repo so that the replication user can be added to each new repo in an automated way ISSUE: RELENG-1864 Signed-off-by: Aric Gardner Change-Id: I65d30dab1d1638123fdb46bc705ec6190cb3182a --- diff --git a/docs/commands/github.rst b/docs/commands/github.rst index 37bd08e1..c64df6ad 100644 --- a/docs/commands/github.rst +++ b/docs/commands/github.rst @@ -23,12 +23,33 @@ list .. program-output:: lftools github list --help + +update-repo +----------- + +.. program-output:: lftools github update-repo --help + + +submit-pr +--------- + +.. program-output:: lftools github submit-pr --help + + user ---- .. program-output:: lftools github user --help -API requires an [github] section in ~/.config/lftools/lftools.ini: + +votes +----- + +.. program-output:: lftools github votes --help + + + +API requires a [github] section in ~/.config/lftools/lftools.ini: .. code-block:: bash diff --git a/docs/commands/infofile.rst b/docs/commands/infofile.rst index 33f02ccd..227254d4 100644 --- a/docs/commands/infofile.rst +++ b/docs/commands/infofile.rst @@ -21,3 +21,12 @@ sync-committers --------------- .. program-output:: lftools infofile sync-committers --help + + +API for check votes requires a [github] section in ~/.config/lftools/lftools.ini: + +.. code-block:: bash + + [github] + token = REDACTED + diff --git a/lftools/cli/github_cli.py b/lftools/cli/github_cli.py index 4a2d7824..53b08409 100644 --- a/lftools/cli/github_cli.py +++ b/lftools/cli/github_cli.py @@ -18,6 +18,9 @@ from github import Github from github import GithubException from lftools import config +from lftools.github_helper import helper_list +from lftools.github_helper import helper_user_github +from lftools.github_helper import prvotes @click.group() @@ -27,6 +30,43 @@ def github_cli(ctx): pass +@click.command(name='submit-pr') +@click.argument('organization') +@click.argument('repo') +@click.argument('pr', type=int) +@click.pass_context +def submit_pr(ctx, organization, repo, pr): + """Submit a pr if mergeable.""" + token = config.get_setting("github", "token") + g = Github(token) + orgName = organization + try: + org = g.get_organization(orgName) + except GithubException as ghe: + print(ghe) + + repo = org.get_repo(repo) + pr_mergable = repo.get_pull(pr).mergeable + + if pr_mergable: + print(pr_mergable) + repo.get_pull(pr).merge(commit_message="Vote Completed, merging INFO file") + else: + print("PR NOT MERGABLE {}".format(pr_mergable)) + sys.exit(1) + + +@click.command(name='votes') +@click.argument('organization') +@click.argument('repo') +@click.argument('pr', type=int) +@click.pass_context +def votes(ctx, organization, repo, pr): + """Helper for votes.""" + approval_list = prvotes(organization, repo, pr) + print("Approvals:", approval_list) + + @click.command(name='list') @click.argument('organization') @click.option('--audit', is_flag=True, required=False, @@ -37,90 +77,14 @@ def github_cli(ctx): help='All members and their respective teams') @click.option('--teams', is_flag=True, required=False, help='List avaliable teams') +@click.option('--team', type=str, required=False, + help='List members of a team') @click.option('--repofeatures', is_flag=True, required=False, help='List enabled features for repos in an org') @click.pass_context -def list(ctx, organization, repos, audit, full, teams, repofeatures): - """List an Organization's GitHub repos.""" - token = config.get_setting("github", "token") - g = Github(token) - orgName = organization - - try: - org = g.get_organization(orgName) - except GithubException as ghe: - print(ghe) - - if repos: - print("All repos for organization: ", orgName) - repos = org.get_repos() - for repo in repos: - print(repo.name) - - if audit: - print("{} members without 2fa:".format(orgName)) - try: - members = org.get_members(filter_="2fa_disabled") - except GithubException as ghe: - print(ghe) - for member in members: - print(member.login) - print("{} outside collaborators without 2fa:".format(orgName)) - try: - collaborators = org.get_outside_collaborators(filter_="2fa_disabled") - except GithubException as ghe: - print(ghe) - for collaborator in collaborators: - print(collaborator.login) - - if repofeatures: - repos = org.get_repos() - for repo in repos: - print("{} wiki:{} issues:{}".format(repo.name, repo.has_wiki, repo.has_issues)) - issues = repo.get_issues - for issue in issues(): - print("{}".format(issue)) - - if full: - print("---") - print("# All owners for {}:".format(orgName)) - print("{}-owners:".format(orgName)) - - try: - members = org.get_members(role="admin") - except GithubException as ghe: - print(ghe) - for member in members: - print(" - '{}'".format(member.login)) - print("# All members for {}".format(orgName)) - print("{}-members:".format(orgName)) - - try: - members = org.get_members() - except GithubException as ghe: - print(ghe) - for member in members: - print(" - '{}'".format(member.login)) - print("# All members and all teams for {}".format(orgName)) - - try: - teams = org.get_teams - except GithubException as ghe: - print(ghe) - for team in teams(): - print("{}:".format(team.name)) - for user in team.get_members(): - print(" - '{}'".format(user.login)) - print("") - teams = None - - if teams: - try: - teams = org.get_teams - except GithubException as ghe: - print(ghe) - for team in teams(): - print("{}".format(team.name)) +def list(ctx, organization, repos, audit, full, teams, team, repofeatures): + """List options for github org repos.""" + helper_list(ctx, organization, repos, audit, full, teams, team, repofeatures) @click.command(name='create-repo') @@ -171,7 +135,7 @@ def createrepo(ctx, organization, repository, description, has_issues, has_proje print(ghe) -@click.command(name='modify-repo') +@click.command(name='update-repo') @click.argument('organization') @click.argument('repository') @click.option('--has_issues', is_flag=True, required=False, @@ -180,9 +144,13 @@ def createrepo(ctx, organization, repository, description, has_issues, has_proje help='Repo should have projects') @click.option('--has_wiki', is_flag=True, required=False, help='Repo should have wiki') +@click.option('--add_team', type=str, required=False, + help='Add team to repo') +@click.option('--remove_team', type=str, required=False, + help='remove team from repo') @click.pass_context -def modifyrepo(ctx, organization, repository, has_issues, has_projects, has_wiki): - """Modify a Github repo within an Organization. +def updaterepo(ctx, organization, repository, has_issues, has_projects, has_wiki, add_team, remove_team): + """Update a Github repo within an Organization. By default has_issues has_wiki and has_projects is set to false. See --help to use this command to enable these options. @@ -202,11 +170,28 @@ def modifyrepo(ctx, organization, repository, has_issues, has_projects, has_wiki repos = org.get_repos() - for repo in repos: - if repo.name == repository: - repo.edit(has_issues=has_issues) - repo.edit(has_wiki=has_wiki) - repo.edit(has_wiki=has_projects) + if has_wiki or has_issues or has_projects: + for repo in repos: + if repo.name == repository: + repo.edit(has_issues=has_issues) + repo.edit(has_wiki=has_wiki) + repo.edit(has_wiki=has_projects) + + if add_team or remove_team: + teams = org.get_teams + + for repo in repos: + if repo.name == repository: + repo_actual = (repo) + + for team in teams(): + if team.name == add_team: + print(team.id) + team.add_to_repos(repo_actual) + team.set_repo_permission(repo_actual, "write") + if team.name == remove_team: + print(team.id) + team.remove_from_repos(repo_actual) @click.command(name='create-team') @@ -272,74 +257,13 @@ def createteam(ctx, organization, name, repo, privacy): @click.pass_context def user(ctx, organization, user, team, delete, admin): """Add and Remove users from an org team.""" - token = config.get_setting("github", "token") - g = Github(token) - orgName = organization - try: - org = g.get_organization(orgName) - except GithubException as ghe: - print(ghe) - try: - user_object = g.get_user(user) - print(user_object) - except GithubException as ghe: - print(ghe) - print("user {} not found".format(user)) - sys.exit(1) - # check if user is a member - try: - is_member = org.has_in_members(user_object) - print("Is {} a member of org {}".format(user, is_member)) - except GithubException as ghe: - print(ghe) - # get teams - try: - teams = org.get_teams - except GithubException as ghe: - print(ghe) - - my_teams = [team] - teams = [team for team in teams() if team.name in my_teams] - - if delete: - if is_member: - for t in teams: - team_id = (t.id) - try: - team = org.get_team(team_id) - team.remove_membership(user_object) - except GithubException as ghe: - print(ghe) - else: - print("{} is not a member of org cannot delete".format(user)) - - if user and not delete: - if admin and is_member: - try: - team.add_membership(member=user_object, role="maintainer") - except GithubException as ghe: - print(ghe) - if admin and not is_member: - try: - org.invite_user(user=user_object, role="admin", teams=teams) - except GithubException as ghe: - print(ghe) - - if not admin and is_member: - try: - team.add_membership(member=user_object, role="member") - except GithubException as ghe: - print(ghe) - - if not admin and not is_member: - try: - org.invite_user(user=user_object, teams=teams) - except GithubException as ghe: - print(ghe) + helper_user_github(ctx, organization, user, team, delete, admin) +github_cli.add_command(submit_pr) +github_cli.add_command(votes) github_cli.add_command(list) github_cli.add_command(createteam) github_cli.add_command(createrepo) -github_cli.add_command(modifyrepo) +github_cli.add_command(updaterepo) github_cli.add_command(user) diff --git a/lftools/cli/infofile.py b/lftools/cli/infofile.py index 31fcf548..5d4dc918 100644 --- a/lftools/cli/infofile.py +++ b/lftools/cli/infofile.py @@ -18,6 +18,8 @@ from pygerrit2 import GerritRestAPI import ruamel.yaml import yaml +from lftools.github_helper import prvotes + log = logging.getLogger(__name__) @@ -130,18 +132,29 @@ def sync_committers(ctx, id, info_file, ldap_file, repo): @click.command(name='check-votes') @click.argument('info_file') -@click.argument('gerrit_url') -@click.argument('change_number') +@click.argument('endpoint', type=str) +@click.argument('change_number', type=int) @click.option('--tsc', type=str, required=False, help='path to TSC INFO file') +@click.option('--github_repo', type=str, required=False, + help='Provide github repo to Check against a Github Change') @click.pass_context -def check_votes(ctx, info_file, gerrit_url, change_number, tsc): +def check_votes(ctx, info_file, endpoint, change_number, tsc, github_repo): """Check votes on an INFO.yaml change. - Check for Majority of votes on a gerrit patchset + Check for Majority of votes on a gerrit or github patchset which changes an INFO.yaml file. + + For Gerrit endpoint is the gerrit url + For Github the enpoint is the organization name + + Examples: + lftools infofile check-votes /tmp/test/INFO.yaml lfit-sandbox 18 --github_repo test + + lftools infofile check-votes ~/lf/allrepos/onosfw/INFO.yaml https://gerrit.opnfv.org/gerrit/ 67302 + """ - def main(ctx, info_file, gerrit_url, change_number, tsc, majority_of_committers): + def main(ctx, info_file, endpoint, change_number, tsc, github_repo, majority_of_committers): """Function so we can iterate into TSC members after commiter vote has happend.""" with open(info_file) as file: try: @@ -150,32 +163,36 @@ def check_votes(ctx, info_file, gerrit_url, change_number, tsc): log.error(exc) committer_info = info_data['committers'] - info_committers = [] - for count, item in enumerate(committer_info): - committer = committer_info[count]['id'] - info_committers.append(committer) - - rest = GerritRestAPI(url=gerrit_url) - changes = rest.get("changes/{}/reviewers".format(change_number)) info_change = [] - for change in changes: - line = (change['username'], change['approvals']['Code-Review']) - if '+1' in line[1] or '+2' in line[1]: - info_change.append(change['username']) + if github_repo: + id = 'github_id' + githubvotes = prvotes(endpoint, github_repo, change_number) + for vote in githubvotes: + info_change.append(vote) + + else: + id = 'id' + rest = GerritRestAPI(url=endpoint) + changes = rest.get("changes/{}/reviewers".format(change_number)) + for change in changes: + line = (change['username'], change['approvals']['Code-Review']) + if '+1' in line[1] or '+2' in line[1]: + info_change.append(change['username']) + + for count, item in enumerate(committer_info): + committer = committer_info[count][id] + info_committers.append(committer) have_not_voted = [item for item in info_committers if item not in info_change] have_not_voted_length = (len(have_not_voted)) - have_voted = [item for item in info_committers if item in info_change] have_voted_length = (len(have_voted)) - log.info("Number of Committers:") log.info(len(info_committers)) committer_lenght = (len(info_committers)) - log.info("Committers that have voted:") log.info(have_voted) log.info(have_voted_length) @@ -189,8 +206,7 @@ def check_votes(ctx, info_file, gerrit_url, change_number, tsc): if (have_voted_length != 0): majority = (committer_lenght / have_voted_length) - - if (majority == 1): + if (majority >= 1): log.info("Majority committer vote reached") if (tsc): log.info("Need majority of tsc") @@ -199,13 +215,12 @@ def check_votes(ctx, info_file, gerrit_url, change_number, tsc): if majority_of_committers == 2: log.info("TSC majority reached auto merging commit") else: - main(ctx, info_file, gerrit_url, change_number, tsc, majority_of_committers) + main(ctx, info_file, endpoint, change_number, tsc, github_repo, majority_of_committers) else: log.info("majority not yet reached") sys.exit(1) - majority_of_committers = 0 - main(ctx, info_file, gerrit_url, change_number, tsc, majority_of_committers) + main(ctx, info_file, endpoint, change_number, tsc, github_repo, majority_of_committers) infofile.add_command(get_committers) diff --git a/lftools/cli/lfidapi.py b/lftools/cli/lfidapi.py index f13eecdd..1f54cf59 100755 --- a/lftools/cli/lfidapi.py +++ b/lftools/cli/lfidapi.py @@ -71,12 +71,14 @@ def create_group(ctx, group): @click.command() @click.argument('info_file') @click.argument('group') +@click.option('--githuborg', type=str, required=False, + help='github org name') @click.option('--noop', is_flag=True, required=False, help='show what would be changed') @click.pass_context -def match_ldap_to_info(ctx, info_file, group, noop): - """Match an LDAP groups membership to an INFO.yaml file.""" - helper_match_ldap_to_info(info_file, group, noop) +def match_ldap_to_info(ctx, info_file, group, githuborg, noop): + """Match an LDAP or GITHUB group membership to an INFO.yaml file.""" + helper_match_ldap_to_info(info_file, group, githuborg, noop) lfidapi.add_command(search_members) diff --git a/lftools/github_helper.py b/lftools/github_helper.py new file mode 100755 index 00000000..4b73775b --- /dev/null +++ b/lftools/github_helper.py @@ -0,0 +1,223 @@ +# SPDX-License-Identifier: EPL-1.0 +############################################################################## +# Copyright (c) 2019 The Linux Foundation and others. +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Eclipse Public License v1.0 +# which accompanies this distribution, and is available at +# http://www.eclipse.org/legal/epl-v10.html +############################################################################## +"""Github stub.""" + +from __future__ import print_function + +import logging +import sys + +from github import Github +from github import GithubException + +from lftools import config + +log = logging.getLogger(__name__) + + +def helper_list(ctx, organization, repos, audit, full, teams, team, repofeatures): + """List options for github org repos.""" + token = config.get_setting("github", "token") + g = Github(token) + orgName = organization + + try: + org = g.get_organization(orgName) + except GithubException as ghe: + log.error(ghe) + + if repos: + log.info("All repos for organization: ", orgName) + repos = org.get_repos() + for repo in repos: + log.info(repo.name) + + if audit: + log.info("{} members without 2fa:".format(orgName)) + try: + members = org.get_members(filter_="2fa_disabled") + except GithubException as ghe: + log.error(ghe) + for member in members: + log.info(member.login) + log.info("{} outside collaborators without 2fa:".format(orgName)) + try: + collaborators = org.get_outside_collaborators(filter_="2fa_disabled") + except GithubException as ghe: + log.error(ghe) + for collaborator in collaborators: + log.info(collaborator.login) + + if repofeatures: + repos = org.get_repos() + for repo in repos: + log.info("{} wiki:{} issues:{}".format(repo.name, repo.has_wiki, repo.has_issues)) + issues = repo.get_issues + for issue in issues(): + log.info("{}".format(issue)) + + if full: + log.info("---") + log.info("# All owners for {}:".format(orgName)) + log.info("{}-owners:".format(orgName)) + + try: + members = org.get_members(role="admin") + except GithubException as ghe: + log.error(ghe) + for member in members: + log.info(" - '{}'".format(member.login)) + log.info("# All members for {}".format(orgName)) + log.info("{}-members:".format(orgName)) + + try: + members = org.get_members() + except GithubException as ghe: + log.error(ghe) + for member in members: + log.info(" - '{}'".format(member.login)) + log.info("# All members and all teams for {}".format(orgName)) + + try: + teams = org.get_teams + except GithubException as ghe: + log.error(ghe) + for team in teams(): + log.info("{}:".format(team.name)) + for user in team.get_members(): + log.info(" - '{}'".format(user.login)) + log.info("") + teams = None + + if teams: + try: + teams = org.get_teams + except GithubException as ghe: + log.error(ghe) + for team in teams(): + log.info("{}".format(team.name)) + + if team: + try: + teams = org.get_teams + except GithubException as ghe: + log.error(ghe) + + team_members = [] + + for t in teams(): + if t.name == team: + log.info("{}".format(t.name)) + for user in t.get_members(): + team_members.append(user.login) + log.info(" - '{}'".format(user.login)) + + return(team_members) + + +def prvotes(organization, repo, pr): + """Get votes on a github pr.""" + token = config.get_setting("github", "token") + g = Github(token) + orgName = organization + try: + org = g.get_organization(orgName) + except GithubException as ghe: + log.error(ghe) + + repo = org.get_repo(repo) + approval_list = [] + author = repo.get_pull(pr).user.login + approval_list.append(author) + + pr_mergable = repo.get_pull(pr).mergeable + log.info("MERGEABLE: {}".format(pr_mergable)) + + approvals = repo.get_pull(pr).get_reviews() + for approve in approvals: + if approve.state == ("APPROVED"): + approval_list.append(approve.user.login) + return(approval_list) + + +def helper_user_github(ctx, organization, user, team, delete, admin): + """Add and Remove users from an org team.""" + token = config.get_setting("github", "token") + g = Github(token) + orgName = organization + try: + org = g.get_organization(orgName) + except GithubException as ghe: + log.error(ghe) + try: + user_object = g.get_user(user) + log.info(user_object) + except GithubException as ghe: + log.error(ghe) + log.info("user {} not found".format(user)) + sys.exit(1) + # check if user is a member + try: + is_member = org.has_in_members(user_object) + log.info("Is {} a member of org {}".format(user, is_member)) + except GithubException as ghe: + log.error(ghe) + # get teams + try: + teams = org.get_teams + except GithubException as ghe: + log.error(ghe) + + # set team to proper object + my_teams = [team] + this_team = [team for team in teams() if team.name in my_teams] + for t in this_team: + team_id = (t.id) + team = org.get_team(team_id) + teams = [] + teams.append(team) + + if delete: + if is_member: + try: + team.remove_membership(user_object) + except GithubException as ghe: + log.error(ghe) + log.info("Removing user {} from {}".format(user_object, team)) + else: + log.info("{} is not a member of org cannot delete".format(user)) + # TODO add revoke invite + log.info("Code does not handle revoking invitations.") + + if user and not delete: + if admin and is_member: + try: + team.add_membership(member=user_object, role="maintainer") + except GithubException as ghe: + log.error(ghe) + if admin and not is_member: + try: + org.invite_user(user=user_object, role="admin", teams=teams) + except GithubException as ghe: + log.error(ghe) + log.info("Sending Admin invite to {} for {}".format(user_object, team)) + + if not admin and is_member: + try: + team.add_membership(member=user_object, role="member") + except GithubException as ghe: + log.error(ghe) + + if not admin and not is_member: + try: + org.invite_user(user=user_object, teams=teams) + except GithubException as ghe: + log.error(ghe) + log.info("Sending invite to {} for {}".format(user_object, team)) diff --git a/lftools/lfidapi.py b/lftools/lfidapi.py index 1b69759b..2bdeccc0 100755 --- a/lftools/lfidapi.py +++ b/lftools/lfidapi.py @@ -18,6 +18,8 @@ import requests from six.moves import urllib import yaml +from lftools.github_helper import helper_list +from lftools.github_helper import helper_user_github from lftools.oauth2_helper import oauth_helper log = logging.getLogger(__name__) @@ -110,42 +112,72 @@ def helper_create_group(group): log.debug(json.dumps(result, indent=4, sort_keys=True)) -def helper_match_ldap_to_info(info_file, group, noop): - """Helper only to be used in automation.""" +def helper_match_ldap_to_info(info_file, group, githuborg, noop): + """Helper matches ldap or github group to users in an info file. + + Used in automation. + """ with open(info_file) as file: try: info_data = yaml.safe_load(file) except yaml.YAMLError as exc: print(exc) + id = 'id' + if githuborg: + id = 'github_id' + ldap_data = helper_list(ctx=False, organization=githuborg, repos=False, audit=False, + full=False, teams=False, repofeatures=False, team=group) + else: + ldap_data = helper_search_members(group) - ldap_data = helper_search_members(group) committer_info = info_data['committers'] info_committers = [] for count, item in enumerate(committer_info): - committer = committer_info[count]['id'] + committer = committer_info[count][id] info_committers.append(committer) ldap_committers = [] - for count, item in enumerate(ldap_data): - committer = ldap_data[count]['username'] - ldap_committers.append(committer) + if githuborg: + for x in ldap_data: + committer = x + ldap_committers.append(committer) + + else: + for count, item in enumerate(ldap_data): + committer = ldap_data[count]['username'] + ldap_committers.append(committer) all_users = ldap_committers + info_committers - all_users.remove("lfservices_releng") + + if not githuborg: + all_users.remove("lfservices_releng") + + log.info("All users in org group") all_users = sorted(set(all_users)) + for x in all_users: + log.info(x) for user in all_users: removed_by_patch = [item for item in ldap_committers if item not in info_committers] if (user in removed_by_patch): log.info("%s found in group %s " % (user, group)) if noop is False: - log.info(" removing user %s from group %s" % (user, group)) - helper_user(user, group, "--delete") + log.info("Removing user %s from group %s" % (user, group)) + if githuborg: + helper_user_github(ctx=False, organization=githuborg, user=user, + team=group, delete=True, admin=False) + else: + helper_user(user, group, "--delete") added_by_patch = [item for item in info_committers if item not in ldap_committers] if (user in added_by_patch): log.info("%s not found in group %s" % (user, group)) if noop is False: - log.info(" adding user %s to group %s" % (user, group)) - helper_user(user, group, "") + log.info("Adding user %s to group %s" % (user, group)) + if githuborg: + helper_user_github(ctx=False, organization=githuborg, user=user, + team=group, delete=False, admin=False) + + else: + helper_user(user, group, "") diff --git a/releasenotes/notes/github-1e99906af8ef75ac.yaml b/releasenotes/notes/github-1e99906af8ef75ac.yaml new file mode 100644 index 00000000..15f8d441 --- /dev/null +++ b/releasenotes/notes/github-1e99906af8ef75ac.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + --team now lists members of a specific team + +fixes: + - | + Fixes invite to team + +adds: + - | + prvotes counts votes on a github pull request diff --git a/releasenotes/notes/infofile-4dec08c571b39df8.yaml b/releasenotes/notes/infofile-4dec08c571b39df8.yaml new file mode 100644 index 00000000..c3655bf6 --- /dev/null +++ b/releasenotes/notes/infofile-4dec08c571b39df8.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + check_votes now takes click.option('--github_repo') + Used in automation to determine is 50% of committers + have voted on an INFO.yaml change diff --git a/releasenotes/notes/lfidapi-d597e5ef08fda9f7.yaml b/releasenotes/notes/lfidapi-d597e5ef08fda9f7.yaml new file mode 100644 index 00000000..eec7bdc4 --- /dev/null +++ b/releasenotes/notes/lfidapi-d597e5ef08fda9f7.yaml @@ -0,0 +1,7 @@ +--- +adds: + - | + --githuborg + lfidapi info match-ldap-to-info now works for github groups. + expects group to be called $reponame-committers + id field for github users is github_id