-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from github/prs
Add main and pr env vars
- Loading branch information
Showing
7 changed files
with
357 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
[isort] | ||
[settings] | ||
profile = black |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
"""A GitHub Action to suggest removal of non-organization members from CODEOWNERS files.""" | ||
|
||
import uuid | ||
|
||
import auth | ||
import env | ||
import github3 | ||
|
||
|
||
def main(): # pragma: no cover | ||
"""Run the main program""" | ||
|
||
# Get the environment variables | ||
( | ||
organization, | ||
repository_list, | ||
token, | ||
ghe, | ||
exempt_repositories_list, | ||
dry_run, | ||
title, | ||
body, | ||
commit_message, | ||
) = env.get_env_vars() | ||
|
||
# Auth to GitHub.com or GHE | ||
github_connection = auth.auth_to_github(token, ghe) | ||
pull_count = 0 | ||
eligble_for_pr_count = 0 | ||
|
||
# Get the repositories from the organization or list of repositories | ||
repos = get_repos_iterator(organization, repository_list, github_connection) | ||
|
||
for repo in repos: | ||
# Check if the repository is in the exempt_repositories_list | ||
if repo.full_name in exempt_repositories_list: | ||
print(f"Skipping {repo.full_name} as it is in the exempt_repositories_list") | ||
continue | ||
|
||
# Check to see if repository is archived | ||
if repo.archived: | ||
print(f"Skipping {repo.full_name} as it is archived") | ||
continue | ||
|
||
# Check to see if repository has a CODEOWNERS file | ||
file_changed = False | ||
codeowners_file_contents = None | ||
codeowners_filepath = None | ||
try: | ||
if repo.file_contents(".github/CODEOWNERS").size > 0: | ||
codeowners_file_contents = repo.file_contents(".github/CODEOWNERS") | ||
codeowners_filepath = ".github/CODEOWNERS" | ||
except github3.exceptions.NotFoundError: | ||
pass | ||
try: | ||
if repo.file_contents("CODEOWNERS").size > 0: | ||
codeowners_file_contents = repo.file_contents("CODEOWNERS") | ||
codeowners_filepath = "CODEOWNERS" | ||
except github3.exceptions.NotFoundError: | ||
pass | ||
try: | ||
if repo.file_contents("docs/CODEOWNERS").size > 0: | ||
codeowners_file_contents = repo.file_contents("docs/CODEOWNERS") | ||
codeowners_filepath = "docs/CODEOWNERS" | ||
except github3.exceptions.NotFoundError: | ||
pass | ||
|
||
if not codeowners_file_contents: | ||
print(f"Skipping {repo.full_name} as it does not have a CODEOWNERS file") | ||
continue | ||
|
||
# Extract the usernames from the CODEOWNERS file | ||
usernames = get_usernames_from_codeowners(codeowners_file_contents) | ||
|
||
for username in usernames: | ||
# Check to see if the username is a member of the organization | ||
if not github_connection.organization(organization).has_member(username): | ||
print( | ||
f"\t{username} is not a member of {organization}. Suggest removing them from {repo.full_name}" | ||
) | ||
if not dry_run: | ||
# Remove that username from the codeowners_file_contents | ||
file_changed = True | ||
codeowners_file_contents = codeowners_file_contents.decoded.replace( | ||
f"@{username}", "" | ||
) | ||
|
||
# Update the CODEOWNERS file if usernames were removed | ||
if file_changed: | ||
eligble_for_pr_count += 1 | ||
try: | ||
pull = commit_changes( | ||
title, | ||
body, | ||
repo, | ||
codeowners_file_contents, | ||
commit_message, | ||
codeowners_filepath, | ||
) | ||
pull_count += 1 | ||
print(f"\tCreated pull request {pull.html_url}") | ||
except github3.exceptions.NotFoundError: | ||
print("\tFailed to create pull request. Check write permissions.") | ||
continue | ||
|
||
# Report the statistics from this run | ||
print(f"Found {eligble_for_pr_count} users to remove") | ||
print(f"Created {pull_count} pull requests successfully") | ||
|
||
|
||
def get_repos_iterator(organization, repository_list, github_connection): | ||
"""Get the repositories from the organization or list of repositories""" | ||
repos = [] | ||
if organization and not repository_list: | ||
repos = github_connection.organization(organization).repositories() | ||
else: | ||
# Get the repositories from the repository_list | ||
for repo in repository_list: | ||
repos.append( | ||
github_connection.repository(repo.split("/")[0], repo.split("/")[1]) | ||
) | ||
|
||
return repos | ||
|
||
|
||
def get_usernames_from_codeowners(codeowners_file_contents): | ||
"""Extract the usernames from the CODEOWNERS file""" | ||
usernames = [] | ||
for line in codeowners_file_contents.decoded.splitlines(): | ||
# skip comments | ||
if line.startswith("#"): | ||
continue | ||
# skip empty lines | ||
if not line.strip(): | ||
continue | ||
# If the line has an @ symbol, grab the word with the @ in it and add it to the list | ||
if "@" in line: | ||
usernames.append(line.split("@")[1].split()[0]) | ||
return usernames | ||
|
||
|
||
def commit_changes( | ||
title, body, repo, codeowners_file_contents, commit_message, codeowners_filepath | ||
): | ||
"""Commit the changes to the repo and open a pull reques and return the pull request object""" | ||
default_branch = repo.default_branch | ||
# Get latest commit sha from default branch | ||
default_branch_commit = repo.ref("heads/" + default_branch).object.sha | ||
front_matter = "refs/heads/" | ||
branch_name = "codeowners-" + str(uuid.uuid4()) | ||
repo.create_ref(front_matter + branch_name, default_branch_commit) | ||
repo.create_file( | ||
path=codeowners_filepath, | ||
message=commit_message, | ||
content=codeowners_file_contents.encode(), # Convert to bytes object | ||
branch=branch_name, | ||
) | ||
|
||
pull = repo.create_pull( | ||
title=title, body=body, head=branch_name, base=repo.default_branch | ||
) | ||
return pull | ||
|
||
|
||
if __name__ == "__main__": # pragma: no cover | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
"""Test the functions in the cleanowners module.""" | ||
|
||
import unittest | ||
import uuid | ||
from unittest.mock import MagicMock, patch | ||
|
||
from cleanowners import ( | ||
commit_changes, | ||
get_repos_iterator, | ||
get_usernames_from_codeowners, | ||
) | ||
|
||
|
||
class TestCommitChanges(unittest.TestCase): | ||
"""Test the commit_changes function in cleanowners.py""" | ||
|
||
@patch("uuid.uuid4") | ||
def test_commit_changes(self, mock_uuid): | ||
"""Test the commit_changes function.""" | ||
mock_uuid.return_value = uuid.UUID( | ||
"12345678123456781234567812345678" | ||
) # Mock UUID generation | ||
mock_repo = MagicMock() # Mock repo object | ||
mock_repo.default_branch = "main" | ||
mock_repo.ref.return_value.object.sha = "abc123" # Mock SHA for latest commit | ||
mock_repo.create_ref.return_value = True | ||
mock_repo.create_file.return_value = True | ||
mock_repo.create_pull.return_value = "MockPullRequest" | ||
|
||
title = "Test Title" | ||
body = "Test Body" | ||
dependabot_file = "testing!" | ||
branch_name = "codeowners-12345678-1234-5678-1234-567812345678" | ||
commit_message = "Test commit message" | ||
result = commit_changes( | ||
title, | ||
body, | ||
mock_repo, | ||
dependabot_file, | ||
commit_message, | ||
"CODEOWNERS", | ||
) | ||
|
||
# Assert that the methods were called with the correct arguments | ||
mock_repo.create_ref.assert_called_once_with( | ||
f"refs/heads/{branch_name}", "abc123" | ||
) | ||
mock_repo.create_file.assert_called_once_with( | ||
path="CODEOWNERS", | ||
message=commit_message, | ||
content=dependabot_file.encode(), | ||
branch=branch_name, | ||
) | ||
mock_repo.create_pull.assert_called_once_with( | ||
title=title, | ||
body=body, | ||
head=branch_name, | ||
base="main", | ||
) | ||
|
||
# Assert that the function returned the expected result | ||
self.assertEqual(result, "MockPullRequest") | ||
|
||
|
||
class TestGetUsernamesFromCodeowners(unittest.TestCase): | ||
"""Test the get_usernames_from_codeowners function in cleanowners.py""" | ||
|
||
def test_get_usernames_from_codeowners(self): | ||
"""Test the get_usernames_from_codeowners function.""" | ||
codeowners_file_contents = MagicMock() | ||
codeowners_file_contents.decoded = """ | ||
# Comment | ||
@user1 | ||
@user2 | ||
# Another comment | ||
@user3 | ||
""" | ||
expected_usernames = ["user1", "user2", "user3"] | ||
|
||
result = get_usernames_from_codeowners(codeowners_file_contents) | ||
|
||
self.assertEqual(result, expected_usernames) | ||
|
||
|
||
class TestGetReposIterator(unittest.TestCase): | ||
"""Test the get_repos_iterator function in evergreen.py""" | ||
|
||
@patch("github3.login") | ||
def test_get_repos_iterator_with_organization(self, mock_github): | ||
"""Test the get_repos_iterator function with an organization""" | ||
organization = "my_organization" | ||
repository_list = [] | ||
github_connection = mock_github.return_value | ||
|
||
mock_organization = MagicMock() | ||
mock_repositories = MagicMock() | ||
mock_organization.repositories.return_value = mock_repositories | ||
github_connection.organization.return_value = mock_organization | ||
|
||
result = get_repos_iterator(organization, repository_list, github_connection) | ||
|
||
# Assert that the organization method was called with the correct argument | ||
github_connection.organization.assert_called_once_with(organization) | ||
|
||
# Assert that the repositories method was called on the organization object | ||
mock_organization.repositories.assert_called_once() | ||
|
||
# Assert that the function returned the expected result | ||
self.assertEqual(result, mock_repositories) | ||
|
||
@patch("github3.login") | ||
def test_get_repos_iterator_with_repository_list(self, mock_github): | ||
"""Test the get_repos_iterator function with a repository list""" | ||
organization = None | ||
repository_list = ["org/repo1", "org/repo2"] | ||
github_connection = mock_github.return_value | ||
|
||
mock_repository = MagicMock() | ||
mock_repository_list = [mock_repository, mock_repository] | ||
github_connection.repository.side_effect = mock_repository_list | ||
|
||
result = get_repos_iterator(organization, repository_list, github_connection) | ||
|
||
# Assert that the repository method was called with the correct arguments for each repository in the list | ||
expected_calls = [ | ||
unittest.mock.call("org", "repo1"), | ||
unittest.mock.call("org", "repo2"), | ||
] | ||
github_connection.repository.assert_has_calls(expected_calls) | ||
|
||
# Assert that the function returned the expected result | ||
self.assertEqual(result, mock_repository_list) |
Oops, something went wrong.