From 2b5a3d139d28b2e264ff010c4c479db591602b80 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:40:52 +0000 Subject: [PATCH 01/11] Initial plan From c7c4b166ac97ec0be7f3212271f3fa66a85b3094 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:48:21 +0000 Subject: [PATCH 02/11] Add GitHub Actions Job Summary support Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- markdown.py | 85 ++++++++++++++++++++++++++++------- test_markdown.py | 112 +++++++++++++++++++++++++++++++++++++---------- 2 files changed, 158 insertions(+), 39 deletions(-) diff --git a/markdown.py b/markdown.py index 5d5e419..adc0deb 100644 --- a/markdown.py +++ b/markdown.py @@ -1,6 +1,8 @@ # pylint: disable=too-many-locals """This module contains the functions needed to write the output to markdown files.""" +import os + def write_to_markdown( collaborators, @@ -54,6 +56,70 @@ def write_to_markdown( filename, start_date, end_date, organization, repository, table, summary_table ) + # Also write to GitHub Actions Step Summary if available + write_to_github_summary( + start_date, end_date, organization, repository, table, summary_table + ) + + +def write_to_github_summary( + start_date, end_date, organization, repository, table, summary_table +): + """ + Write markdown content to GitHub Actions Step Summary. + + Args: + start_date (str): The start date of the date range for the contributor list. + end_date (str): The end date of the date range for the contributor list. + organization (str): The organization for which the contributors are being listed. + repository (str): The repository for which the contributors are being listed. + table (str): A string containing a markdown table of the contributors and the total contribution count. + summary_table (str): A string containing a markdown table of the summary statistics. + + Returns: + None + """ + # Only write to GitHub Step Summary if we're running in a GitHub Actions environment + github_step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if github_step_summary: + content = generate_markdown_content( + start_date, end_date, organization, repository, table, summary_table + ) + with open(github_step_summary, "a", encoding="utf-8") as summary_file: + summary_file.write(content) + + +def generate_markdown_content( + start_date, end_date, organization, repository, table, summary_table +): + """ + This function generates markdown content as a string. + + Args: + start_date (str): The start date of the date range for the contributor list. + end_date (str): The end date of the date range for the contributor list. + organization (str): The organization for which the contributors are being listed. + repository (str): The repository for which the contributors are being listed. + table (str): A string containing a markdown table of the contributors and the total contribution count. + summary_table (str): A string containing a markdown table of the summary statistics. + + Returns: + str: The complete markdown content as a string. + + """ + content = "# Contributors\n\n" + if start_date and end_date: + content += f"- Date range for contributor list: {start_date} to {end_date}\n" + if organization: + content += f"- Organization: {organization}\n" + if repository: + content += f"- Repository: {repository}\n" + content += "\n" + content += summary_table + content += table + content += "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + return content + def write_markdown_file( filename, start_date, end_date, organization, repository, table, summary_table @@ -74,22 +140,11 @@ def write_markdown_file( None """ + content = generate_markdown_content( + start_date, end_date, organization, repository, table, summary_table + ) with open(filename, "w", encoding="utf-8") as markdown_file: - markdown_file.write("# Contributors\n\n") - if start_date and end_date: - markdown_file.write( - f"- Date range for contributor list: {start_date} to {end_date}\n" - ) - if organization: - markdown_file.write(f"- Organization: {organization}\n") - if repository: - markdown_file.write(f"- Repository: {repository}\n") - markdown_file.write("\n") - markdown_file.write(summary_table) - markdown_file.write(table) - markdown_file.write( - "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" - ) + markdown_file.write(content) def get_summary_table(collaborators, start_date, end_date, total_contributions): diff --git a/test_markdown.py b/test_markdown.py index 54a7337..37ff321 100644 --- a/test_markdown.py +++ b/test_markdown.py @@ -12,8 +12,9 @@ class TestMarkdown(unittest.TestCase): Test case for the markdown module. """ + @patch("markdown.os.environ.get", return_value=None) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) - def test_write_to_markdown(self, mock_file): + def test_write_to_markdown(self, mock_file, mock_env_get): """ Test the write_to_markdown function. """ @@ -54,22 +55,25 @@ def test_write_to_markdown(self, mock_file): ) mock_file.assert_called_once_with("filename", "w", encoding="utf-8") - mock_file().write.assert_any_call("# Contributors\n\n") - mock_file().write.assert_any_call( + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" - ) - mock_file().write.assert_any_call( - "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" - ) - mock_file().write.assert_any_call( + "- Repository: org/repo\n\n" + "| Total Contributors | Total Contributions | % New Contributors |\n" + "| --- | --- | --- |\n" + "| 2 | 300 | 50.0% |\n\n" "| Username | All Time Contribution Count | New Contributor | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- |\n" "| @user1 | 100 | False | commit url |\n" "| @user2 | 200 | True | commit url2 |\n" + "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" ) + mock_file().write.assert_called_once_with(expected_content) + @patch("markdown.os.environ.get", return_value=None) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) - def test_write_to_markdown_with_sponsors(self, mock_file): + def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): """ Test the write_to_markdown function with sponsors info turned on. """ @@ -110,22 +114,25 @@ def test_write_to_markdown_with_sponsors(self, mock_file): ) mock_file.assert_called_once_with("filename", "w", encoding="utf-8") - mock_file().write.assert_any_call("# Contributors\n\n") - mock_file().write.assert_any_call( + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" - ) - mock_file().write.assert_any_call( - "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" - ) - mock_file().write.assert_any_call( + "- Repository: org/repo\n\n" + "| Total Contributors | Total Contributions | % New Contributors |\n" + "| --- | --- | --- |\n" + "| 2 | 300 | 50.0% |\n\n" "| Username | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- | --- |\n" "| @user1 | 100 | False | [Sponsor Link](sponsor_url_1) | commit url |\n" "| @user2 | 200 | True | not sponsorable | commit url2 |\n" + "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" ) + mock_file().write.assert_called_once_with(expected_content) + @patch("markdown.os.environ.get", return_value=None) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) - def test_write_to_markdown_without_link_to_profile(self, mock_file): + def test_write_to_markdown_without_link_to_profile(self, mock_file, mock_env_get): """ Test the write_to_markdown function with link to profile turned off. """ @@ -166,19 +173,76 @@ def test_write_to_markdown_without_link_to_profile(self, mock_file): ) mock_file.assert_called_once_with("filename", "w", encoding="utf-8") - mock_file().write.assert_any_call("# Contributors\n\n") - mock_file().write.assert_any_call( + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" - ) - mock_file().write.assert_any_call( - "| Total Contributors | Total Contributions | % New Contributors |\n| --- | --- | --- |\n| 2 | 300 | 50.0% |\n\n" - ) - mock_file().write.assert_any_call( + "- Repository: org/repo\n\n" + "| Total Contributors | Total Contributions | % New Contributors |\n" + "| --- | --- | --- |\n" + "| 2 | 300 | 50.0% |\n\n" "| Username | All Time Contribution Count | New Contributor | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- |\n" "| user1 | 100 | False | commit url |\n" "| user2 | 200 | True | commit url2 |\n" + "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + ) + mock_file().write.assert_called_once_with(expected_content) + + @patch("markdown.os.environ.get", return_value="/tmp/step_summary") + @patch("builtins.open", new_callable=mock_open) + def test_write_to_github_summary(self, mock_file, mock_env_get): + """ + Test that markdown content is written to GitHub Step Summary when environment variable is set. + """ + person1 = contributor_stats.ContributorStats( + "user1", + False, + "url", + 100, + "commit url", + "sponsor_url_1", + ) + person2 = contributor_stats.ContributorStats( + "user2", + False, + "url2", + 200, + "commit url2", + "sponsor_url_2", ) + # Set person2 as a new contributor since this cannot be set on initiatization of the object + person2.new_contributor = True + collaborators = [ + person1, + person2, + ] + ghe = "" + + write_to_markdown( + collaborators, + "filename", + "2023-01-01", + "2023-01-02", + None, + "org/repo", + "false", + True, + ghe, + ) + + # Verify that open was called twice - once for the markdown file and once for the step summary + self.assertEqual(mock_file.call_count, 2) + + # Verify the first call is for the markdown file + first_call = mock_file.call_args_list[0] + self.assertEqual(first_call[0], ("filename", "w")) + self.assertEqual(first_call[1]["encoding"], "utf-8") + + # Verify the second call is for the step summary file + second_call = mock_file.call_args_list[1] + self.assertEqual(second_call[0], ("/tmp/step_summary", "a")) + self.assertEqual(second_call[1]["encoding"], "utf-8") if __name__ == "__main__": From be2d2d2a130279382dc5bc038e01550f8bef0f95 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 04:50:48 +0000 Subject: [PATCH 03/11] Add documentation for GitHub Actions Job Summary feature Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- README.md | 6 ++++++ test_markdown.py | 16 +++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index c60d65b..46b9cb3 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,12 @@ jobs: | @zkoppert | 1913 | False | [Sponsor Link](https://github.com/sponsors/zkoppert) | [super-linter/super-linter](https://github.com/super-linter/super-linter/commits?author=zkoppert&since=2021-09-01&until=2023-09-30) | ``` +## GitHub Actions Job Summary + +When running as a GitHub Action, the contributors report is automatically displayed in the [GitHub Actions Job Summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). This provides immediate visibility of the results directly in the workflow run interface without needing to check separate files or issues. + +The job summary contains the same markdown content that is written to the `contributors.md` file, making it easy to view contributor information right in the GitHub Actions UI. + ## Local usage without Docker 1. Make sure you have at least Python3.11 installed diff --git a/test_markdown.py b/test_markdown.py index 37ff321..dee3672 100644 --- a/test_markdown.py +++ b/test_markdown.py @@ -12,7 +12,9 @@ class TestMarkdown(unittest.TestCase): Test case for the markdown module. """ - @patch("markdown.os.environ.get", return_value=None) # Mock GITHUB_STEP_SUMMARY to None + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) def test_write_to_markdown(self, mock_file, mock_env_get): """ @@ -71,7 +73,9 @@ def test_write_to_markdown(self, mock_file, mock_env_get): ) mock_file().write.assert_called_once_with(expected_content) - @patch("markdown.os.environ.get", return_value=None) # Mock GITHUB_STEP_SUMMARY to None + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): """ @@ -130,7 +134,9 @@ def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): ) mock_file().write.assert_called_once_with(expected_content) - @patch("markdown.os.environ.get", return_value=None) # Mock GITHUB_STEP_SUMMARY to None + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) def test_write_to_markdown_without_link_to_profile(self, mock_file, mock_env_get): """ @@ -233,12 +239,12 @@ def test_write_to_github_summary(self, mock_file, mock_env_get): # Verify that open was called twice - once for the markdown file and once for the step summary self.assertEqual(mock_file.call_count, 2) - + # Verify the first call is for the markdown file first_call = mock_file.call_args_list[0] self.assertEqual(first_call[0], ("filename", "w")) self.assertEqual(first_call[1]["encoding"], "utf-8") - + # Verify the second call is for the step summary file second_call = mock_file.call_args_list[1] self.assertEqual(second_call[0], ("/tmp/step_summary", "a")) From 6405638cb54a01b97752876c495cedb565fc924c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 05:57:03 +0000 Subject: [PATCH 04/11] Fix linting issues in GitHub Actions Job Summary implementation Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- markdown.py | 138 +++++++++++++++++++++++++++++++++-------------- test_markdown.py | 4 ++ 2 files changed, 102 insertions(+), 40 deletions(-) diff --git a/markdown.py b/markdown.py index adc0deb..ac52274 100644 --- a/markdown.py +++ b/markdown.py @@ -17,18 +17,27 @@ def write_to_markdown( ): """ This function writes a list of collaborators to a markdown file in table format. - Each collaborator is represented as a dictionary with keys 'username', 'contribution_count', 'new_contributor', and 'commits'. + Each collaborator is represented as a dictionary with keys 'username', + 'contribution_count', 'new_contributor', and 'commits'. Args: - collaborators (list): A list of dictionaries, where each dictionary represents a collaborator. - Each dictionary should have the keys 'username', 'contribution_count', and 'commits'. - filename (str): The path of the markdown file to which the table will be written. - start_date (str): The start date of the date range for the contributor list. + collaborators (list): A list of dictionaries, where each dictionary + represents a collaborator. Each dictionary should + have the keys 'username', 'contribution_count', + and 'commits'. + filename (str): The path of the markdown file to which the table will + be written. + start_date (str): The start date of the date range for the contributor + list. end_date (str): The end date of the date range for the contributor list. - organization (str): The organization for which the contributors are being listed. - repository (str): The repository for which the contributors are being listed. - sponsor_info (str): True if the user wants the sponsor_url shown in the report - link_to_profile (str): True if the user wants the username linked to Github profile in the report + organization (str): The organization for which the contributors are + being listed. + repository (str): The repository for which the contributors are being + listed. + sponsor_info (str): True if the user wants the sponsor_url shown in + the report + link_to_profile (str): True if the user wants the username linked to + Github profile in the report Returns: None @@ -46,14 +55,21 @@ def write_to_markdown( ghe, ) - # Put together the summary table including # of new contributions, # of new contributors, % new contributors, % returning contributors + # Put together the summary table including # of new contributions, + # # of new contributors, % new contributors, % returning contributors summary_table = get_summary_table( collaborators, start_date, end_date, total_contributions ) # Write the markdown file write_markdown_file( - filename, start_date, end_date, organization, repository, table, summary_table + filename, + start_date, + end_date, + organization, + repository, + table, + summary_table, ) # Also write to GitHub Actions Step Summary if available @@ -69,21 +85,32 @@ def write_to_github_summary( Write markdown content to GitHub Actions Step Summary. Args: - start_date (str): The start date of the date range for the contributor list. + start_date (str): The start date of the date range for the contributor + list. end_date (str): The end date of the date range for the contributor list. - organization (str): The organization for which the contributors are being listed. - repository (str): The repository for which the contributors are being listed. - table (str): A string containing a markdown table of the contributors and the total contribution count. - summary_table (str): A string containing a markdown table of the summary statistics. + organization (str): The organization for which the contributors are + being listed. + repository (str): The repository for which the contributors are being + listed. + table (str): A string containing a markdown table of the contributors + and the total contribution count. + summary_table (str): A string containing a markdown table of the + summary statistics. Returns: None """ - # Only write to GitHub Step Summary if we're running in a GitHub Actions environment + # Only write to GitHub Step Summary if we're running in a GitHub Actions + # environment github_step_summary = os.environ.get("GITHUB_STEP_SUMMARY") if github_step_summary: content = generate_markdown_content( - start_date, end_date, organization, repository, table, summary_table + start_date, + end_date, + organization, + repository, + table, + summary_table, ) with open(github_step_summary, "a", encoding="utf-8") as summary_file: summary_file.write(content) @@ -96,12 +123,17 @@ def generate_markdown_content( This function generates markdown content as a string. Args: - start_date (str): The start date of the date range for the contributor list. + start_date (str): The start date of the date range for the contributor + list. end_date (str): The end date of the date range for the contributor list. - organization (str): The organization for which the contributors are being listed. - repository (str): The repository for which the contributors are being listed. - table (str): A string containing a markdown table of the contributors and the total contribution count. - summary_table (str): A string containing a markdown table of the summary statistics. + organization (str): The organization for which the contributors are + being listed. + repository (str): The repository for which the contributors are being + listed. + table (str): A string containing a markdown table of the contributors + and the total contribution count. + summary_table (str): A string containing a markdown table of the + summary statistics. Returns: str: The complete markdown content as a string. @@ -109,7 +141,10 @@ def generate_markdown_content( """ content = "# Contributors\n\n" if start_date and end_date: - content += f"- Date range for contributor list: {start_date} to {end_date}\n" + content += ( + f"- Date range for contributor list: {start_date} to " + f"{end_date}\n" + ) if organization: content += f"- Organization: {organization}\n" if repository: @@ -117,24 +152,41 @@ def generate_markdown_content( content += "\n" content += summary_table content += table - content += "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + content += ( + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" + ) return content def write_markdown_file( - filename, start_date, end_date, organization, repository, table, summary_table + filename, + start_date, + end_date, + organization, + repository, + table, + summary_table, ): """ - This function writes all the tables and data to a markdown file with tables to organizae the information. + This function writes all the tables and data to a markdown file with + tables to organize the information. Args: - filename (str): The path of the markdown file to which the table will be written. - start_date (str): The start date of the date range for the contributor list. + filename (str): The path of the markdown file to which the table will + be written. + start_date (str): The start date of the date range for the contributor + list. end_date (str): The end date of the date range for the contributor list. - organization (str): The organization for which the contributors are being listed. - repository (str): The repository for which the contributors are being listed. - table (str): A string containing a markdown table of the contributors and the total contribution count. - summary_table (str): A string containing a markdown table of the summary statistics. + organization (str): The organization for which the contributors are + being listed. + repository (str): The repository for which the contributors are being + listed. + table (str): A string containing a markdown table of the contributors + and the total contribution count. + summary_table (str): A string containing a markdown table of the + summary statistics. Returns: None @@ -147,7 +199,9 @@ def write_markdown_file( markdown_file.write(content) -def get_summary_table(collaborators, start_date, end_date, total_contributions): +def get_summary_table( + collaborators, start_date, end_date, total_contributions +): """ This function returns a string containing a markdown table of the summary statistics. @@ -175,7 +229,9 @@ def get_summary_table(collaborators, start_date, end_date, total_contributions): new_contributors_percentage = 0 summary_table += f"| {str(len(collaborators))} | {str(total_contributions)} | {str(new_contributors_percentage)}% |\n\n" else: - summary_table = "| Total Contributors | Total Contributions |\n| --- | --- |\n" + summary_table = ( + "| Total Contributors | Total Contributions |\n| --- | --- |\n" + ) summary_table += ( f"| {str(len(collaborators))} | {str(total_contributions)} |\n\n" ) @@ -240,15 +296,17 @@ def get_contributor_table( for url in commit_url_list: url = url.strip() # get the organization and repository name from the url ie. org1/repo2 from https://github.com/org1/repo2/commits?author-zkoppert - endpoint = ghe.removeprefix("https://") if ghe else "github.com" - org_repo_link_name = url.split("/commits")[0].split(f"{endpoint}/")[1] + endpoint = ( + ghe.removeprefix("https://") if ghe else "github.com" + ) + org_repo_link_name = url.split("/commits")[0].split( + f"{endpoint}/" + )[1] url = f"[{org_repo_link_name}]({url})" commit_urls += f"{url}, " new_contributor = collaborator.new_contributor - row = ( - f"| {'' if not link_to_profile else '@'}{username} | {contribution_count} |" - ) + row = f"| {'' if not link_to_profile else '@'}{username} | {contribution_count} |" if "New Contributor" in columns: row += f" {new_contributor} |" if "Sponsor URL" in columns: diff --git a/test_markdown.py b/test_markdown.py index dee3672..dc9792d 100644 --- a/test_markdown.py +++ b/test_markdown.py @@ -16,6 +16,7 @@ class TestMarkdown(unittest.TestCase): "markdown.os.environ.get", return_value=None ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) + # pylint: disable=unused-argument def test_write_to_markdown(self, mock_file, mock_env_get): """ Test the write_to_markdown function. @@ -77,6 +78,7 @@ def test_write_to_markdown(self, mock_file, mock_env_get): "markdown.os.environ.get", return_value=None ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) + # pylint: disable=unused-argument def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): """ Test the write_to_markdown function with sponsors info turned on. @@ -138,6 +140,7 @@ def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): "markdown.os.environ.get", return_value=None ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) + # pylint: disable=unused-argument def test_write_to_markdown_without_link_to_profile(self, mock_file, mock_env_get): """ Test the write_to_markdown function with link to profile turned off. @@ -197,6 +200,7 @@ def test_write_to_markdown_without_link_to_profile(self, mock_file, mock_env_get @patch("markdown.os.environ.get", return_value="/tmp/step_summary") @patch("builtins.open", new_callable=mock_open) + # pylint: disable=unused-argument def test_write_to_github_summary(self, mock_file, mock_env_get): """ Test that markdown content is written to GitHub Step Summary when environment variable is set. From 78b07ef4c22f6def852e6c4948b583cb88965a0c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:23:26 +0000 Subject: [PATCH 05/11] Fix pylint unused-argument warnings in test_markdown.py Co-authored-by: zkoppert <6935431+zkoppert@users.noreply.github.com> --- markdown.py | 23 ++++++++--------------- test_markdown.py | 20 ++++++++++++-------- 2 files changed, 20 insertions(+), 23 deletions(-) diff --git a/markdown.py b/markdown.py index ac52274..b1a1aad 100644 --- a/markdown.py +++ b/markdown.py @@ -142,8 +142,7 @@ def generate_markdown_content( content = "# Contributors\n\n" if start_date and end_date: content += ( - f"- Date range for contributor list: {start_date} to " - f"{end_date}\n" + f"- Date range for contributor list: {start_date} to " f"{end_date}\n" ) if organization: content += f"- Organization: {organization}\n" @@ -199,9 +198,7 @@ def write_markdown_file( markdown_file.write(content) -def get_summary_table( - collaborators, start_date, end_date, total_contributions -): +def get_summary_table(collaborators, start_date, end_date, total_contributions): """ This function returns a string containing a markdown table of the summary statistics. @@ -229,9 +226,7 @@ def get_summary_table( new_contributors_percentage = 0 summary_table += f"| {str(len(collaborators))} | {str(total_contributions)} | {str(new_contributors_percentage)}% |\n\n" else: - summary_table = ( - "| Total Contributors | Total Contributions |\n| --- | --- |\n" - ) + summary_table = "| Total Contributors | Total Contributions |\n| --- | --- |\n" summary_table += ( f"| {str(len(collaborators))} | {str(total_contributions)} |\n\n" ) @@ -296,17 +291,15 @@ def get_contributor_table( for url in commit_url_list: url = url.strip() # get the organization and repository name from the url ie. org1/repo2 from https://github.com/org1/repo2/commits?author-zkoppert - endpoint = ( - ghe.removeprefix("https://") if ghe else "github.com" - ) - org_repo_link_name = url.split("/commits")[0].split( - f"{endpoint}/" - )[1] + endpoint = ghe.removeprefix("https://") if ghe else "github.com" + org_repo_link_name = url.split("/commits")[0].split(f"{endpoint}/")[1] url = f"[{org_repo_link_name}]({url})" commit_urls += f"{url}, " new_contributor = collaborator.new_contributor - row = f"| {'' if not link_to_profile else '@'}{username} | {contribution_count} |" + row = ( + f"| {'' if not link_to_profile else '@'}{username} | {contribution_count} |" + ) if "New Contributor" in columns: row += f" {new_contributor} |" if "Sponsor URL" in columns: diff --git a/test_markdown.py b/test_markdown.py index dc9792d..37a3ea6 100644 --- a/test_markdown.py +++ b/test_markdown.py @@ -16,8 +16,9 @@ class TestMarkdown(unittest.TestCase): "markdown.os.environ.get", return_value=None ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) - # pylint: disable=unused-argument - def test_write_to_markdown(self, mock_file, mock_env_get): + def test_write_to_markdown( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument """ Test the write_to_markdown function. """ @@ -78,8 +79,9 @@ def test_write_to_markdown(self, mock_file, mock_env_get): "markdown.os.environ.get", return_value=None ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) - # pylint: disable=unused-argument - def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): + def test_write_to_markdown_with_sponsors( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument """ Test the write_to_markdown function with sponsors info turned on. """ @@ -140,8 +142,9 @@ def test_write_to_markdown_with_sponsors(self, mock_file, mock_env_get): "markdown.os.environ.get", return_value=None ) # Mock GITHUB_STEP_SUMMARY to None @patch("builtins.open", new_callable=mock_open) - # pylint: disable=unused-argument - def test_write_to_markdown_without_link_to_profile(self, mock_file, mock_env_get): + def test_write_to_markdown_without_link_to_profile( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument """ Test the write_to_markdown function with link to profile turned off. """ @@ -200,8 +203,9 @@ def test_write_to_markdown_without_link_to_profile(self, mock_file, mock_env_get @patch("markdown.os.environ.get", return_value="/tmp/step_summary") @patch("builtins.open", new_callable=mock_open) - # pylint: disable=unused-argument - def test_write_to_github_summary(self, mock_file, mock_env_get): + def test_write_to_github_summary( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument """ Test that markdown content is written to GitHub Step Summary when environment variable is set. """ From a2119f7692440a25df8a73a73aca5f082d6b9d4f Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 22 Aug 2025 14:10:00 -0700 Subject: [PATCH 06/11] fix: use single f-string Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Zack Koppert --- markdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/markdown.py b/markdown.py index b1a1aad..ac7b9f4 100644 --- a/markdown.py +++ b/markdown.py @@ -142,7 +142,7 @@ def generate_markdown_content( content = "# Contributors\n\n" if start_date and end_date: content += ( - f"- Date range for contributor list: {start_date} to " f"{end_date}\n" + f"- Date range for contributor list: {start_date} to {end_date}\n" ) if organization: content += f"- Organization: {organization}\n" From d17776d3152cf60f645e7627a787f0034203cccb Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 22 Aug 2025 21:18:45 +0000 Subject: [PATCH 07/11] fix: black formatting Signed-off-by: Zack Koppert --- markdown.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/markdown.py b/markdown.py index ac7b9f4..3176f60 100644 --- a/markdown.py +++ b/markdown.py @@ -141,9 +141,7 @@ def generate_markdown_content( """ content = "# Contributors\n\n" if start_date and end_date: - content += ( - f"- Date range for contributor list: {start_date} to {end_date}\n" - ) + content += f"- Date range for contributor list: {start_date} to {end_date}\n" if organization: content += f"- Organization: {organization}\n" if repository: From 19315cc458a1045ad623da3ef89ff7c798e87351 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 22 Aug 2025 21:20:58 +0000 Subject: [PATCH 08/11] fix: add missing arg in docstring Signed-off-by: Zack Koppert --- markdown.py | 1 + 1 file changed, 1 insertion(+) diff --git a/markdown.py b/markdown.py index 3176f60..e4a614e 100644 --- a/markdown.py +++ b/markdown.py @@ -38,6 +38,7 @@ def write_to_markdown( the report link_to_profile (str): True if the user wants the username linked to Github profile in the report + ghe (str): The GitHub Enterprise instance URL, if applicable. Returns: None From 29985e59aaa9125e65e81c7f7b7635137f277912 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 22 Aug 2025 21:43:00 +0000 Subject: [PATCH 09/11] perf: call generate_markdown_content only once, use DRY principle Signed-off-by: Zack Koppert --- markdown.py | 73 ++++++++++------------------------------------------- 1 file changed, 13 insertions(+), 60 deletions(-) diff --git a/markdown.py b/markdown.py index e4a614e..8ac6e44 100644 --- a/markdown.py +++ b/markdown.py @@ -62,41 +62,24 @@ def write_to_markdown( collaborators, start_date, end_date, total_contributions ) - # Write the markdown file - write_markdown_file( - filename, - start_date, - end_date, - organization, - repository, - table, - summary_table, + # Generate the markdown content once + content = generate_markdown_content( + start_date, end_date, organization, repository, table, summary_table ) + # Write the markdown file + write_markdown_file(filename, content) + # Also write to GitHub Actions Step Summary if available - write_to_github_summary( - start_date, end_date, organization, repository, table, summary_table - ) + write_to_github_summary(content) -def write_to_github_summary( - start_date, end_date, organization, repository, table, summary_table -): +def write_to_github_summary(content): """ Write markdown content to GitHub Actions Step Summary. Args: - start_date (str): The start date of the date range for the contributor - list. - end_date (str): The end date of the date range for the contributor list. - organization (str): The organization for which the contributors are - being listed. - repository (str): The repository for which the contributors are being - listed. - table (str): A string containing a markdown table of the contributors - and the total contribution count. - summary_table (str): A string containing a markdown table of the - summary statistics. + content (str): The pre-generated markdown content to write. Returns: None @@ -105,14 +88,6 @@ def write_to_github_summary( # environment github_step_summary = os.environ.get("GITHUB_STEP_SUMMARY") if github_step_summary: - content = generate_markdown_content( - start_date, - end_date, - organization, - repository, - table, - summary_table, - ) with open(github_step_summary, "a", encoding="utf-8") as summary_file: summary_file.write(content) @@ -158,41 +133,19 @@ def generate_markdown_content( return content -def write_markdown_file( - filename, - start_date, - end_date, - organization, - repository, - table, - summary_table, -): +def write_markdown_file(filename, content): """ - This function writes all the tables and data to a markdown file with - tables to organize the information. + This function writes the pre-generated markdown content to a file. Args: - filename (str): The path of the markdown file to which the table will + filename (str): The path of the markdown file to which the content will be written. - start_date (str): The start date of the date range for the contributor - list. - end_date (str): The end date of the date range for the contributor list. - organization (str): The organization for which the contributors are - being listed. - repository (str): The repository for which the contributors are being - listed. - table (str): A string containing a markdown table of the contributors - and the total contribution count. - summary_table (str): A string containing a markdown table of the - summary statistics. + content (str): The pre-generated markdown content to write. Returns: None """ - content = generate_markdown_content( - start_date, end_date, organization, repository, table, summary_table - ) with open(filename, "w", encoding="utf-8") as markdown_file: markdown_file.write(content) From 7c59b4f9f26ea12914628781308d732bb4be2cb1 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 22 Aug 2025 21:53:45 +0000 Subject: [PATCH 10/11] test: increase test coverage of markdown writing to improve confidence Signed-off-by: Zack Koppert --- test_markdown.py | 255 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 247 insertions(+), 8 deletions(-) diff --git a/test_markdown.py b/test_markdown.py index 37a3ea6..98c5e1a 100644 --- a/test_markdown.py +++ b/test_markdown.py @@ -67,11 +67,14 @@ def test_write_to_markdown( "| Total Contributors | Total Contributions | % New Contributors |\n" "| --- | --- | --- |\n" "| 2 | 300 | 50.0% |\n\n" - "| Username | All Time Contribution Count | New Contributor | Commits between 2023-01-01 and 2023-01-02 |\n" + "| Username | All Time Contribution Count | New Contributor | " + "Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- |\n" "| @user1 | 100 | False | commit url |\n" "| @user2 | 200 | True | commit url2 |\n" - "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" ) mock_file().write.assert_called_once_with(expected_content) @@ -130,11 +133,14 @@ def test_write_to_markdown_with_sponsors( "| Total Contributors | Total Contributions | % New Contributors |\n" "| --- | --- | --- |\n" "| 2 | 300 | 50.0% |\n\n" - "| Username | All Time Contribution Count | New Contributor | Sponsor URL | Commits between 2023-01-01 and 2023-01-02 |\n" + "| Username | All Time Contribution Count | New Contributor | " + "Sponsor URL | Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- | --- |\n" "| @user1 | 100 | False | [Sponsor Link](sponsor_url_1) | commit url |\n" "| @user2 | 200 | True | not sponsorable | commit url2 |\n" - "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" ) mock_file().write.assert_called_once_with(expected_content) @@ -193,11 +199,14 @@ def test_write_to_markdown_without_link_to_profile( "| Total Contributors | Total Contributions | % New Contributors |\n" "| --- | --- | --- |\n" "| 2 | 300 | 50.0% |\n\n" - "| Username | All Time Contribution Count | New Contributor | Commits between 2023-01-01 and 2023-01-02 |\n" + "| Username | All Time Contribution Count | New Contributor | " + "Commits between 2023-01-01 and 2023-01-02 |\n" "| --- | --- | --- | --- |\n" "| user1 | 100 | False | commit url |\n" "| user2 | 200 | True | commit url2 |\n" - "\n _this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" ) mock_file().write.assert_called_once_with(expected_content) @@ -207,7 +216,8 @@ def test_write_to_github_summary( self, mock_file, mock_env_get ): # pylint: disable=unused-argument """ - Test that markdown content is written to GitHub Step Summary when environment variable is set. + Test that markdown content is written to GitHub Step Summary + when environment variable is set. """ person1 = contributor_stats.ContributorStats( "user1", @@ -245,7 +255,8 @@ def test_write_to_github_summary( ghe, ) - # Verify that open was called twice - once for the markdown file and once for the step summary + # Verify that open was called twice - once for the markdown file + # and once for the step summary self.assertEqual(mock_file.call_count, 2) # Verify the first call is for the markdown file @@ -258,6 +269,234 @@ def test_write_to_github_summary( self.assertEqual(second_call[0], ("/tmp/step_summary", "a")) self.assertEqual(second_call[1]["encoding"], "utf-8") + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None + @patch("builtins.open", new_callable=mock_open) + def test_write_to_markdown_with_organization( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument + """ + Test the write_to_markdown function with organization specified. + """ + person1 = contributor_stats.ContributorStats( + "user1", + False, + "url", + 100, + "https://github.com/org1/repo1/commits?author=user1", + "sponsor_url_1", + ) + person2 = contributor_stats.ContributorStats( + "user2", + False, + "url2", + 200, + "https://github.com/org2/repo2/commits?author=user2, " + "https://github.com/org3/repo3/commits?author=user2", + "sponsor_url_2", + ) + # Set person2 as a new contributor since this cannot be set on initiatization of the object + person2.new_contributor = True + collaborators = [ + person1, + person2, + ] + ghe = "" + + write_to_markdown( + collaborators, + "filename", + "2023-01-01", + "2023-01-02", + "test-org", + None, + "false", + True, + ghe, + ) + + mock_file.assert_called_once_with("filename", "w", encoding="utf-8") + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" + "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" + "- Organization: test-org\n\n" + "| Total Contributors | Total Contributions | % New Contributors |\n" + "| --- | --- | --- |\n" + "| 2 | 300 | 50.0% |\n\n" + "| Username | All Time Contribution Count | New Contributor | " + "Commits between 2023-01-01 and 2023-01-02 |\n" + "| --- | --- | --- | --- |\n" + "| @user1 | 100 | False | " + "[org1/repo1](https://github.com/org1/repo1/commits?author=user1), |\n" + "| @user2 | 200 | True | " + "[org2/repo2](https://github.com/org2/repo2/commits?author=user2), " + "[org3/repo3](https://github.com/org3/repo3/commits?author=user2), |\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" + ) + mock_file().write.assert_called_once_with(expected_content) + + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None + @patch("builtins.open", new_callable=mock_open) + def test_write_to_markdown_empty_collaborators( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument + """ + Test the write_to_markdown function with empty collaborators list. + """ + collaborators = [] + ghe = "" + + write_to_markdown( + collaborators, + "filename", + "2023-01-01", + "2023-01-02", + None, + "org/repo", + "false", + True, + ghe, + ) + + mock_file.assert_called_once_with("filename", "w", encoding="utf-8") + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" + "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" + "- Repository: org/repo\n\n" + "| Total Contributors | Total Contributions | % New Contributors |\n" + "| --- | --- | --- |\n" + "| 0 | 0 | 0% |\n\n" + "| Username | All Time Contribution Count | New Contributor | " + "Commits between 2023-01-01 and 2023-01-02 |\n" + "| --- | --- | --- | --- |\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" + ) + mock_file().write.assert_called_once_with(expected_content) + + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None + @patch("builtins.open", new_callable=mock_open) + def test_write_to_markdown_no_dates( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument + """ + Test the write_to_markdown function without start and end dates. + """ + person1 = contributor_stats.ContributorStats( + "user1", + False, + "url", + 100, + "commit url", + "sponsor_url_1", + ) + person2 = contributor_stats.ContributorStats( + "user2", + False, + "url2", + 200, + "commit url2", + "sponsor_url_2", + ) + collaborators = [ + person1, + person2, + ] + ghe = "" + + write_to_markdown( + collaborators, + "filename", + None, + None, + None, + "org/repo", + "false", + True, + ghe, + ) + + mock_file.assert_called_once_with("filename", "w", encoding="utf-8") + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" + "- Repository: org/repo\n\n" + "| Total Contributors | Total Contributions |\n" + "| --- | --- |\n" + "| 2 | 300 |\n\n" + "| Username | All Time Contribution Count | All Commits |\n" + "| --- | --- | --- |\n" + "| @user1 | 100 | commit url |\n" + "| @user2 | 200 | commit url2 |\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" + ) + mock_file().write.assert_called_once_with(expected_content) + + @patch( + "markdown.os.environ.get", return_value=None + ) # Mock GITHUB_STEP_SUMMARY to None + @patch("builtins.open", new_callable=mock_open) + def test_write_to_markdown_with_ghe( + self, mock_file, mock_env_get + ): # pylint: disable=unused-argument + """ + Test the write_to_markdown function with GitHub Enterprise URL. + """ + person1 = contributor_stats.ContributorStats( + "user1", + False, + "url", + 100, + "https://github.example.com/org1/repo1/commits?author=user1", + "sponsor_url_1", + ) + collaborators = [person1] + ghe = "https://github.example.com" + + write_to_markdown( + collaborators, + "filename", + "2023-01-01", + "2023-01-02", + "test-org", + None, + "false", + True, + ghe, + ) + + mock_file.assert_called_once_with("filename", "w", encoding="utf-8") + # With the new implementation, content is written as a single string + expected_content = ( + "# Contributors\n\n" + "- Date range for contributor list: 2023-01-01 to 2023-01-02\n" + "- Organization: test-org\n\n" + "| Total Contributors | Total Contributions | % New Contributors |\n" + "| --- | --- | --- |\n" + "| 1 | 100 | 0.0% |\n\n" + "| Username | All Time Contribution Count | New Contributor | " + "Commits between 2023-01-01 and 2023-01-02 |\n" + "| --- | --- | --- | --- |\n" + "| @user1 | 100 | False | " + "[org1/repo1](https://github.example.com/org1/repo1/commits?author=user1), |\n" + "\n _this file was generated by the " + "[Contributors GitHub Action]" + "(https://github.com/github/contributors)_\n" + ) + mock_file().write.assert_called_once_with(expected_content) + if __name__ == "__main__": unittest.main() From 62504d0d18a862e1da4cd2d7ee55ec9c5ae38070 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 22 Aug 2025 22:11:32 +0000 Subject: [PATCH 11/11] docs: update comments to reflect new functionality Signed-off-by: Zack Koppert --- markdown.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/markdown.py b/markdown.py index 8ac6e44..ab3d852 100644 --- a/markdown.py +++ b/markdown.py @@ -16,7 +16,8 @@ def write_to_markdown( ghe, ): """ - This function writes a list of collaborators to a markdown file in table format. + This function writes a list of collaborators to a markdown file in table format + and optionally to GitHub Actions Job Summary if running in a GitHub Actions environment. Each collaborator is represented as a dictionary with keys 'username', 'contribution_count', 'new_contributor', and 'commits'.