diff --git a/.github/workflows/use-action.yml b/.github/workflows/use-action.yml index 564436c..617c926 100644 --- a/.github/workflows/use-action.yml +++ b/.github/workflows/use-action.yml @@ -29,3 +29,10 @@ jobs: GH_TOKEN: ${{ secrets.GH_TOKEN }} ORGANIZATION: github INACTIVE_DAYS: 1 + + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Stale repository report + content-filepath: ./stale_repos.md + assignees: zkoppert diff --git a/README.md b/README.md index 816bea3..1f65c73 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,25 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_TOKEN }} ORGANIZATION: ${{ secrets.ORGANIZATION }} - INACTIVE_DAYS: 365 + INACTIVE_DAYS: 365 + + - name: Create issue + uses: peter-evans/create-issue-from-file@v4 + with: + title: Stale repository report + content-filepath: ./stale_repos.md + assignees: + +``` + +### Example stale_repos.md output + +```markdown +# Inactive Repositories + +| Repository URL | Days Inactive | +| --- | ---: | +| https://github.com/github/.github | 5 | ``` ## Local usage without Docker diff --git a/stale_repos.py b/stale_repos.py index 22aa8e4..8fc2cf7 100755 --- a/stale_repos.py +++ b/stale_repos.py @@ -2,7 +2,7 @@ """ Find stale repositories in a GitHub organization. """ import os -from datetime import datetime +from datetime import datetime, timezone from os.path import dirname, join import github3 @@ -46,17 +46,26 @@ def main(): # Iterate over repos in the org, acquire inactive days, # and print out the repo url and days inactive if it's over the threshold (inactive_days) - print_inactive_repos(github_connection, inactive_days_threshold, organization) + inactive_repos = get_inactive_repos( + github_connection, inactive_days_threshold, organization + ) + # Write the list of inactive repos to a csv file + write_to_markdown(inactive_repos) -def print_inactive_repos(github_connection, inactive_days_threshold, organization): - """Print out the repo url and days inactive if it's over the threshold (inactive_days). + +def get_inactive_repos(github_connection, inactive_days_threshold, organization): + """Return and print out the repo url and days inactive if it's over + the threshold (inactive_days). Args: github_connection: The GitHub connection object. inactive_days_threshold: The threshold (in days) for considering a repo as inactive. organization: The name of the organization to retrieve repositories from. + Returns: + A list of tuples containing the repo and days inactive. + """ inactive_repos = [] for repo in github_connection.repositories_by(organization): @@ -64,11 +73,30 @@ def print_inactive_repos(github_connection, inactive_days_threshold, organizatio if last_push_str is None: continue last_push = parse(last_push_str) - days_inactive = (datetime.now() - last_push).days - if days_inactive > int(inactive_days_threshold): - inactive_repos.append((repo, days_inactive)) + days_inactive = (datetime.now(timezone.utc) - last_push).days + if days_inactive > int(inactive_days_threshold) and not repo.archived: + inactive_repos.append((repo.html_url, days_inactive)) print(f"{repo.html_url}: {days_inactive} days inactive") # type: ignore print(f"Found {len(inactive_repos)} stale repos in {organization}") + return inactive_repos + + +def write_to_markdown(inactive_repos, file=None): + """Write the list of inactive repos to a markdown file. + + Args: + inactive_repos: A list of tuples containing the repo and days inactive. + file: A file object to write to. If None, a new file will be created. + + """ + inactive_repos.sort(key=lambda x: x[1], reverse=True) + with file or open("stale_repos.md", "w", encoding="utf-8") as file: + file.write("# Inactive Repositories\n\n") + file.write("| Repository URL | Days Inactive |\n") + file.write("| --- | ---: |\n") + for repo_url, days_inactive in inactive_repos: + file.write(f"| {repo_url} | {days_inactive} |\n") + print("Wrote stale repos to stale_repos.md") def auth_to_github(): diff --git a/test_stale_repos.py b/test_stale_repos.py index b36a53a..caa14c9 100644 --- a/test_stale_repos.py +++ b/test_stale_repos.py @@ -19,11 +19,11 @@ import io import os import unittest -from datetime import datetime, timedelta -from unittest.mock import MagicMock, patch +from datetime import datetime, timedelta, timezone +from unittest.mock import MagicMock, call, patch import github3.github -from stale_repos import auth_to_github, print_inactive_repos +from stale_repos import auth_to_github, get_inactive_repos, write_to_markdown class AuthToGithubTestCase(unittest.TestCase): @@ -149,23 +149,25 @@ def test_print_inactive_repos_with_inactive_repos(self): github_connection = MagicMock() # Create a mock repository object with a last push time of 30 days ago - thirty_days_ago = datetime.now() - timedelta(days=30) + thirty_days_ago = datetime.now(timezone.utc) - timedelta(days=30) mock_repo = MagicMock() mock_repo.pushed_at = thirty_days_ago.isoformat() mock_repo.html_url = "https://github.com/example/repo" + mock_repo.archived = False github_connection.repositories_by.return_value = [mock_repo] # Call the function with a threshold of 20 days inactive_days_threshold = 20 organization = "example" with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: - print_inactive_repos( - github_connection, inactive_days_threshold, organization - ) + get_inactive_repos(github_connection, inactive_days_threshold, organization) output = mock_stdout.getvalue() # Check that the output contains the expected repo URL and days inactive - expected_output = f"{mock_repo.html_url}: 30 days inactive\nFound 1 stale repos in {organization}\n" + expected_output = ( + f"{mock_repo.html_url}: 30 days inactive\n" + f"Found 1 stale repos in {organization}\n" + ) self.assertEqual(expected_output, output) def test_print_inactive_repos_with_no_inactive_repos(self): @@ -179,7 +181,7 @@ def test_print_inactive_repos_with_no_inactive_repos(self): github_connection = MagicMock() # Create a mock repository object with a last push time of 30 days ago - thirty_days_ago = datetime.now() - timedelta(days=30) + thirty_days_ago = datetime.now(timezone.utc) - timedelta(days=30) mock_repo = MagicMock() mock_repo.pushed_at = thirty_days_ago.isoformat() mock_repo.html_url = "https://github.com/example/repo" @@ -189,9 +191,7 @@ def test_print_inactive_repos_with_no_inactive_repos(self): inactive_days_threshold = 40 organization = "example" with patch("sys.stdout", new_callable=io.StringIO) as mock_stdout: - print_inactive_repos( - github_connection, inactive_days_threshold, organization - ) + get_inactive_repos(github_connection, inactive_days_threshold, organization) output = mock_stdout.getvalue() # Check that the output contains the expected repo URL and days inactive @@ -199,5 +199,43 @@ def test_print_inactive_repos_with_no_inactive_repos(self): self.assertEqual(expected_output, output) +class WriteToMarkdownTestCase(unittest.TestCase): + """ + Unit test case for the write_to_markdown() function. + """ + + def test_write_to_markdown(self): + """Test that the write_to_markdown function writes the expected data to a file. + + This test creates a list of inactive repos and a mock file object using MagicMock. + It then calls the write_to_markdown function with the list of inactive repos and + the mock file object. Finally, it uses the assert_has_calls method to check that + the mock file object was called with the expected data. + + """ + + # Create a list of inactive repos + inactive_repos = [ + ("https://github.com/example/repo2", 40), + ("https://github.com/example/repo1", 30), + ] + + # Create a mock file object + mock_file = MagicMock() + + # Call the write_to_markdown function with the mock file object + write_to_markdown(inactive_repos, file=mock_file) + + # Check that the mock file object was called with the expected data + expected_calls = [ + call.write("# Inactive Repositories\n\n"), + call.write("| Repository URL | Days Inactive |\n"), + call.write("| --- | ---: |\n"), + call.write("| https://github.com/example/repo2 | 40 |\n"), + call.write("| https://github.com/example/repo1 | 30 |\n"), + ] + mock_file.__enter__.return_value.assert_has_calls(expected_calls) + + if __name__ == "__main__": unittest.main()