From 58247d4c799248321ce019ab350de54b768eab16 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 29 Jun 2023 17:13:13 -0700 Subject: [PATCH 1/5] feat: add JSON output Signed-off-by: Zack Koppert --- .gitignore | 1 + README.md | 29 +++++++++++ issue_metrics.py | 21 +++++--- json_writer.py | 105 ++++++++++++++++++++++++++++++++++++++++ test_markdown_writer.py | 2 + 5 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 json_writer.py diff --git a/.gitignore b/.gitignore index 28fbcfb..de4c02c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Output files issue_metrics.md +issue_metrics.json # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/README.md b/README.md index 5cb410c..b300e20 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,35 @@ Here is the output with all hidable columns hidden: ``` +## Example using the JSON output instead of the markdown output + +There is JSON output available as well. You could use it for any number of possibilities, but here is one example that demonstrates retreiving the JSON output and then printing it out. + +```yaml +name: Monthly issue metrics +on: + workflow_dispatch: + schedule: + - cron: '3 2 1 * *' + +jobs: + build: + name: issue metrics + runs-on: ubuntu-latest + + steps: + - name: Run issue-metrics tool + id: issue-metrics + uses: github/issue-metrics@v2 + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + SEARCH_QUERY: 'repo:owner/repo is:issue created:2023-05-01..2023-05-31 -reason:"not planned"' + + - name: Print output of issue metrics tool + run: echo "${{ steps.issue-metrics.outputs.metrics }}" + +``` + ## Local usage without Docker 1. Copy `.env-example` to `.env` diff --git a/issue_metrics.py b/issue_metrics.py index 66dc088..789d6c6 100644 --- a/issue_metrics.py +++ b/issue_metrics.py @@ -29,15 +29,16 @@ import github3 from dotenv import load_dotenv +from classes import IssueWithMetrics from discussions import get_discussions -from time_to_close import measure_time_to_close, get_average_time_to_close +from json_writer import write_to_json +from markdown_writer import write_to_markdown +from time_to_answer import get_average_time_to_answer, measure_time_to_answer +from time_to_close import get_average_time_to_close, measure_time_to_close from time_to_first_response import ( - measure_time_to_first_response, get_average_time_to_first_response, + measure_time_to_first_response, ) -from time_to_answer import measure_time_to_answer, get_average_time_to_answer -from markdown_writer import write_to_markdown -from classes import IssueWithMetrics def get_env_vars() -> tuple[str, str]: @@ -274,7 +275,15 @@ def main(): average_time_to_answer = get_average_time_to_answer(issues_with_metrics) - # Write the results to a markdown file + # Write the results to json and a markdown file + write_to_json( + issues_with_metrics, + average_time_to_first_response, + average_time_to_close, + average_time_to_answer, + num_issues_open, + num_issues_closed, + ) write_to_markdown( issues_with_metrics, average_time_to_first_response, diff --git a/json_writer.py b/json_writer.py new file mode 100644 index 0000000..323eac5 --- /dev/null +++ b/json_writer.py @@ -0,0 +1,105 @@ +"""A module for writing GitHub issue metrics to a json file. + +Functions: + write_to_json( + issues_with_metrics: List[IssueWithMetrics], + average_time_to_first_response: timedelta, + average_time_to_close: timedelta, + average_time_to_answer: timedelta, + num_issues_opened: int, + num_issues_closed: int, + ) -> str: + Write the issues with metrics to a json file. + +""" + + +import json +from datetime import timedelta +import os +from typing import List, Union + +from classes import IssueWithMetrics + + +def write_to_json( + issues_with_metrics: Union[List[IssueWithMetrics], None], + average_time_to_first_response: Union[timedelta, None], + average_time_to_close: Union[timedelta, None], + average_time_to_answer: Union[timedelta, None], + num_issues_opened: Union[int, None], + num_issues_closed: Union[int, None], +) -> str: + """ + Write the issues with metrics to a JSON file called issue_metrics.json. + + json structure is like following + { + "average_time_to_first_response": "2 days, 12:00:00", + "average_time_to_close": "5 days, 0:00:00", + "average_time_to_answer": "1 day, 0:00:00", + "num_items_opened": 2, + "num_items_closed": 1, + "total_item_count": 2, + "issues": [ + { + "title": "Issue 1", + "html_url": "https://github.com/owner/repo/issues/1", + "time_to_first_response": "3 days, 0:00:00", + "time_to_close": "6 days, 0:00:00", + "time_to_answer": "None", + }, + { + "title": "Issue 2", + "html_url": "https://github.com/owner/repo/issues/2", + "time_to_first_response": "2 days, 0:00:00", + "time_to_close": "4 days, 0:00:00", + "time_to_answer": "1 day, 0:00:00", + }, + ], + } + + """ + + # Ensure issues_with_metrics is not None + if not issues_with_metrics: + raise ValueError("issues_with_metrics cannot be None") + + # Create a dictionary with the metrics + metrics = { + "average_time_to_first_response": str(average_time_to_first_response), + "average_time_to_close": str(average_time_to_close), + "average_time_to_answer": str(average_time_to_answer), + "num_items_opened": num_issues_opened, + "num_items_closed": num_issues_closed, + "total_item_count": len(issues_with_metrics), + } + + # Create a list of dictionaries with the issues and metrics + issues = [] + for issue in issues_with_metrics: + issues.append( + { + "title": issue.title, + "html_url": issue.html_url, + "time_to_first_response": str(issue.time_to_first_response), + "time_to_close": str(issue.time_to_close), + "time_to_answer": str(issue.time_to_answer), + } + ) + + # Add the issues to the metrics dictionary + metrics["issues"] = issues + + # add output to github action output + # pylint: disable=unspecified-encoding + metrics_json = json.dumps(metrics, indent=4) + if os.environ.get("GITHUB_OUTPUT"): + with open(os.environ["GITHUB_OUTPUT"], "a") as file_handle: + print(f"metrics={metrics_json}", file=file_handle) + + # Write the metrics to a JSON file + with open("issue_metrics.json", "w", encoding="utf-8") as file: + json.dump(metrics, file, indent=4) + + return metrics_json diff --git a/test_markdown_writer.py b/test_markdown_writer.py index 0e64ff5..75a03f5 100644 --- a/test_markdown_writer.py +++ b/test_markdown_writer.py @@ -2,6 +2,8 @@ Classes: TestWriteToMarkdown: A class to test the write_to_markdown function with mock data. + TestWriteToMarkdownWithEnv: A class to test the write_to_markdown function with + environment variables set. """ import os From c49a10b948956102763128b001243fb215e3f5ed Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 29 Jun 2023 17:17:30 -0700 Subject: [PATCH 2/5] skip json output if no issues are found Signed-off-by: Zack Koppert --- json_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json_writer.py b/json_writer.py index 323eac5..01ad8c4 100644 --- a/json_writer.py +++ b/json_writer.py @@ -63,7 +63,7 @@ def write_to_json( # Ensure issues_with_metrics is not None if not issues_with_metrics: - raise ValueError("issues_with_metrics cannot be None") + return "" # Create a dictionary with the metrics metrics = { From 9c447db4fb743ff1dc389c1ff8033a0ee9053229 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Thu, 29 Jun 2023 17:18:03 -0700 Subject: [PATCH 3/5] add tests Signed-off-by: Zack Koppert --- test_json_writer.py | 77 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 test_json_writer.py diff --git a/test_json_writer.py b/test_json_writer.py new file mode 100644 index 0000000..006f280 --- /dev/null +++ b/test_json_writer.py @@ -0,0 +1,77 @@ +"""Tests for the write_to_json function in json_writer.py.""" + +import json +import unittest +from datetime import timedelta +from classes import IssueWithMetrics +from json_writer import write_to_json + + +class TestWriteToJson(unittest.TestCase): + """Tests for the write_to_json function.""" + + def test_write_to_json(self): + """Test that write_to_json writes the correct JSON file.""" + issues_with_metrics = [ + IssueWithMetrics( + title="Issue 1", + html_url="https://github.com/owner/repo/issues/1", + time_to_first_response=timedelta(days=3), + time_to_close=timedelta(days=6), + time_to_answer=None, + ), + IssueWithMetrics( + title="Issue 2", + html_url="https://github.com/owner/repo/issues/2", + time_to_first_response=timedelta(days=2), + time_to_close=timedelta(days=4), + time_to_answer=timedelta(days=1), + ), + ] + average_time_to_first_response = timedelta(days=2.5) + average_time_to_close = timedelta(days=5) + average_time_to_answer = timedelta(days=1) + num_issues_opened = 2 + num_issues_closed = 1 + + expected_output = { + "average_time_to_first_response": "2 days, 12:00:00", + "average_time_to_close": "5 days, 0:00:00", + "average_time_to_answer": "1 day, 0:00:00", + "num_items_opened": 2, + "num_items_closed": 1, + "total_item_count": 2, + "issues": [ + { + "title": "Issue 1", + "html_url": "https://github.com/owner/repo/issues/1", + "time_to_first_response": "3 days, 0:00:00", + "time_to_close": "6 days, 0:00:00", + "time_to_answer": "None", + }, + { + "title": "Issue 2", + "html_url": "https://github.com/owner/repo/issues/2", + "time_to_first_response": "2 days, 0:00:00", + "time_to_close": "4 days, 0:00:00", + "time_to_answer": "1 day, 0:00:00", + }, + ], + } + + # Call the function and check the output + self.assertEqual( + write_to_json( + issues_with_metrics, + average_time_to_first_response, + average_time_to_close, + average_time_to_answer, + num_issues_opened, + num_issues_closed, + ), + json.dumps(expected_output, indent=4), + ) + + +if __name__ == "__main__": + unittest.main() From 3d1b1b698f0089112bcc096da687cf4bee55b26f Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 30 Jun 2023 09:31:30 -0700 Subject: [PATCH 4/5] try to format json on single line for actions env variable. Signed-off-by: Zack Koppert --- json_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/json_writer.py b/json_writer.py index 01ad8c4..53a8758 100644 --- a/json_writer.py +++ b/json_writer.py @@ -93,7 +93,7 @@ def write_to_json( # add output to github action output # pylint: disable=unspecified-encoding - metrics_json = json.dumps(metrics, indent=4) + metrics_json = json.dumps(metrics) if os.environ.get("GITHUB_OUTPUT"): with open(os.environ["GITHUB_OUTPUT"], "a") as file_handle: print(f"metrics={metrics_json}", file=file_handle) From d1818bcc7c0d950ee1dd04d30e757675869c37e8 Mon Sep 17 00:00:00 2001 From: Zack Koppert Date: Fri, 30 Jun 2023 09:34:12 -0700 Subject: [PATCH 5/5] update test to match json format in function Signed-off-by: Zack Koppert --- test_json_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_json_writer.py b/test_json_writer.py index 006f280..474883f 100644 --- a/test_json_writer.py +++ b/test_json_writer.py @@ -69,7 +69,7 @@ def test_write_to_json(self): num_issues_opened, num_issues_closed, ), - json.dumps(expected_output, indent=4), + json.dumps(expected_output), )